How to Configure Application Performance Monitoring on Windows Server 2012 R2

Application Performance Monitoring (APM) on Windows Server 2012 R2 encompasses a set of tools and techniques that measure how well application workloads perform, identify bottlenecks, and alert on degradation. Unlike infrastructure monitoring which focuses on CPU, memory, and disk, APM targets application-specific metrics: response times, transaction throughput, error rates, thread pool saturation, and application-specific counters exposed by the .NET CLR, IIS, COM+ services, and installed applications. This guide covers configuring APM using built-in Windows tools, the .NET CLR performance counters, IIS application monitoring, and custom performance counter creation.

Prerequisites

Administrator privileges are required. The target applications must be running on the server. For .NET application monitoring, the appropriate .NET Framework version must be installed. Performance Monitor and the Windows Server Backup tools should be installed. For IIS application monitoring, the IIS Management Console and IIS logging must be enabled. If using third-party APM tools such as New Relic or AppDynamics agents, .NET Framework 4.5 is required.

Step 1: Monitor .NET CLR Performance Counters

The .NET CLR exposes a comprehensive set of performance counters covering garbage collection, exceptions, lock contention, and JIT compilation. Add these to a Performance Monitor Data Collector Set for .NET applications:

# .NET CLR critical performance counters for APM
# Memory and GC
\.NET CLR Memory(*)# Gen 0 Collections
\.NET CLR Memory(*)# Gen 1 Collections
\.NET CLR Memory(*)# Gen 2 Collections
\.NET CLR Memory(*)Large Object Heap size
\.NET CLR Memory(*)# Bytes in all Heaps
\.NET CLR Memory(*)% Time in GC

# Exception handling (high values indicate application errors)
\.NET CLR Exceptions(*)# of Exceps Thrown / sec
\.NET CLR Exceptions(*)# of Filters / sec

# Lock contention (indicates threading issues)
\.NET CLR LocksAndThreads(*)Total # of Contentions
\.NET CLR LocksAndThreads(*)Contention Rate / sec
\.NET CLR LocksAndThreads(*)Current Queue Length
\.NET CLR LocksAndThreads(*)# of current logical Threads

# JIT compilation (spikes indicate cold start or dynamic compilation)
\.NET CLR Jit(*)# of Methods Jitted
\.NET CLR Jit(*)% Time in Jit

Create a DCS capturing these counters every 15 seconds:

$dotnetCounters = @(
    '.NET CLR Memory(_Total)% Time in GC',
    '.NET CLR Memory(_Total)# Bytes in all Heaps',
    '.NET CLR Exceptions(_Total)# of Exceps Thrown / sec',
    '.NET CLR LocksAndThreads(_Total)Contention Rate / sec'
)
$dotnetCounters | Out-File "C:PerfDataDotNetCounters.txt" -Encoding ASCII
logman create counter "DotNET APM" -cf "C:PerfDataDotNetCounters.txt" -si 15 -f bincirc -max 200 -o "C:PerfDataDotNetAPM"

Step 2: Monitor IIS Application Performance

IIS exposes performance counters for web application monitoring including request queue length, active requests, and response times:

# IIS application pool and worker process counters
W3SVC_W3WP(*)Active Requests
W3SVC_W3WP(*)Total Requests
W3SVC_W3WP(*)Requests / Sec
W3SVC_W3WP(*)Request Execution Time
W3SVC_W3WP(*)Request Wait Time
ASP.NET Applications(__Total__)Requests/Sec
ASP.NET Applications(__Total__)Errors Total/Sec
ASP.NET Applications(__Total__)Request Execution Time
ASP.NET Applications(__Total__)Requests In Application Queue
ASP.NETRequests Queued
ASP.NETRequests Rejected
ASP.NETRequest Wait Time
Web Service(*)Current Connections
Web Service(*)Total Bytes Sent
Web Service(*)Total Bytes Received

Use PowerShell to take an immediate sample of IIS performance metrics:

$iisCounters = @(
    "ASP.NETRequests Queued",
    "ASP.NETRequest Wait Time",
    "Web Service(_Total)Current Connections",
    "Web Service(_Total)Total Bytes Sent"
)
Get-Counter -Counter $iisCounters | Select-Object -ExpandProperty CounterSamples | Select-Object Path, CookedValue | Format-Table -AutoSize

Step 3: Configure IIS Failed Request Tracing

IIS Failed Request Tracing (FREB) captures detailed traces of slow or failed requests, providing visibility into exactly what happened inside IIS for each problematic request:

# Enable Failed Request Tracing at site level
Import-Module WebAdministration

# Enable FREB logging for default web site
$site = "Default Web Site"
Set-WebConfigurationProperty -pspath "MACHINE/WEBROOT/APPHOST/$site" -filter "system.webServer/tracing" -name "enabled" -value "true"

# Configure trace rules: capture requests over 3000ms or with HTTP 500 errors
Add-WebConfigurationProperty -pspath "MACHINE/WEBROOT/APPHOST/$site" -filter "system.webServer/tracing/traceFailedRequests" -name "." -value @{path="*"}
Set-WebConfigurationProperty -pspath "MACHINE/WEBROOT/APPHOST/$site" -filter "system.webServer/tracing/traceFailedRequests/add[@path='*']/traceAreas" -name "." -value @{provider="ASPNET"; verbosity="Verbose"}
Set-WebConfigurationProperty -pspath "MACHINE/WEBROOT/APPHOST/$site" -filter "system.webServer/tracing/traceFailedRequests/add[@path='*']/failureDefinitions" -name "statusCodes" -value "500"
Set-WebConfigurationProperty -pspath "MACHINE/WEBROOT/APPHOST/$site" -filter "system.webServer/tracing/traceFailedRequests/add[@path='*']/failureDefinitions" -name "timeTaken" -value "00:00:03"

FREB trace files are stored in C:inetpublogsFailedReqLogFiles as XML files viewable in Internet Explorer or transformed with the built-in XSLT stylesheet.

Step 4: Create Custom Application Performance Counters

For custom applications that do not expose performance counters natively, create custom counters that can be populated by the application or by monitoring scripts:

# Create a custom performance counter category for an order processing application
$counterCreationData = New-Object System.Diagnostics.CounterCreationDataCollection

$ordersPerSec = New-Object System.Diagnostics.CounterCreationData
$ordersPerSec.CounterName = "Orders Processed/sec"
$ordersPerSec.CounterType = [System.Diagnostics.PerformanceCounterType]::RateOfCountsPerSecond32
$counterCreationData.Add($ordersPerSec)

$avgProcessingTime = New-Object System.Diagnostics.CounterCreationData
$avgProcessingTime.CounterName = "Average Order Processing Time"
$avgProcessingTime.CounterType = [System.Diagnostics.PerformanceCounterType]::AverageTimer32
$counterCreationData.Add($avgProcessingTime)

[System.Diagnostics.PerformanceCounterCategory]::Create("OrderProcessingApp", "Custom counters for order processing application", [System.Diagnostics.PerformanceCounterCategoryType]::SingleInstance, $counterCreationData)
Write-Host "Custom performance counters created"

Step 5: Monitor Application Event Logs for Errors

Application errors are logged to the Windows Application event log. Create a monitoring script that alerts on elevated error rates:

# Count application errors in the last 15 minutes
$since = (Get-Date).AddMinutes(-15)
$errors = Get-WinEvent -LogName Application | Where-Object {$_.LevelDisplayName -eq "Error" -and $_.TimeCreated -gt $since}
$criticalErrors = $errors | Where-Object {$_.LevelDisplayName -eq "Critical"}

$errorRate = $errors.Count
$threshold = 10

if ($errorRate -gt $threshold) {
    $report = "APPLICATION ERROR ALERT`n"
    $report += "Server: $env:COMPUTERNAME`n"
    $report += "Time: $(Get-Date)`n"
    $report += "Errors in last 15 minutes: $errorRate (threshold: $threshold)`n`n"
    $report += "Recent Errors:`n"
    $report += ($errors | Select-Object -First 5 TimeCreated, Source, Message | Format-List | Out-String)
    
    Send-MailMessage -SmtpServer "smtp.yourdomain.com" -From "[email protected]" -To "[email protected]" -Subject "APM ALERT: $errorRate Application Errors on $env:COMPUTERNAME" -Body $report
}

Step 6: Monitor ASP.NET Application Pools with PowerShell

Monitor IIS application pool health and recycling events:

Import-Module WebAdministration

# Get all application pool states
Get-ChildItem IIS:AppPools | Select-Object Name, State, @{N="WorkerProcesses";E={(Get-WebConfiguration system.applicationHost/sites/*/application/virtualDirectory -PipelineVariable app | Where-Object {$_ -ne $null}).Count}} | Format-Table -AutoSize

# Check for recently recycled app pools (event log)
Get-WinEvent -LogName "System" | Where-Object {$_.Id -in @(5079, 5080, 5117) -and $_.TimeCreated -gt (Get-Date).AddHours(-24)} | Select-Object TimeCreated, Message | Format-List

Step 7: Baseline and Alert on Response Time Degradation

Create a synthetic transaction monitoring script that measures application response time and alerts if it exceeds acceptable thresholds:

# Measure HTTP response time for an application endpoint
$url = "http://localhost/health"
$maxResponseMs = 2000  # Alert if over 2000ms

try {
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $response = Invoke-WebRequest -Uri $url -TimeoutSec 10 -UseBasicParsing
    $sw.Stop()
    $responseMs = $sw.ElapsedMilliseconds
    $statusCode = $response.StatusCode
    
    Write-Host "Response time: $responseMs ms, Status: $statusCode"
    
    if ($responseMs -gt $maxResponseMs) {
        Send-MailMessage -SmtpServer "smtp.yourdomain.com" -From "[email protected]" -To "[email protected]" -Subject "SLOW RESPONSE: $url took ${responseMs}ms" -Body "Response time $responseMs ms exceeds threshold of $maxResponseMs ms."
    }
} catch {
    Send-MailMessage -SmtpServer "smtp.yourdomain.com" -From "[email protected]" -To "[email protected]" -Subject "APPLICATION DOWN: $url is not responding" -Body "Error: $_"
}

Summary

Application performance monitoring on Windows Server 2012 R2 requires combining .NET CLR performance counters, IIS application metrics, custom event log monitoring, and synthetic transaction testing into a cohesive monitoring strategy. By establishing performance baselines, configuring threshold-based alerts for response time degradation and elevated error rates, and enabling IIS Failed Request Tracing for deep diagnostic capability, operations teams can detect and diagnose application performance issues before they escalate into customer-facing outages. Regular review of APM data and trending analysis enables proactive capacity planning and code quality improvements.