ASP.NET on IIS (.NET Framework)
Instrument ASP.NET applications on IIS with OpenTelemetry to send traces, metrics, and logs to Last9
Use the OpenTelemetry .NET Automatic Instrumentation agent to instrument ASP.NET applications running on IIS and send telemetry data to Last9. The agent uses the .NET CLR profiler to capture traces from incoming HTTP requests, outbound HTTP calls, SQL queries, and WCF services.
What Gets Auto-Instrumented
| Library | What’s captured |
|---|---|
| ASP.NET / ASP.NET MVC | Incoming HTTP request spans with method, URL, status code, duration |
| System.Net.Http / WebClient | Outbound HTTP call spans |
| ADO.NET (System.Data) | SQL query spans with db.statement |
| WCF (client) | Service call spans |
Prerequisites
- Windows Server 2016 or later with IIS 8.5+
- .NET Framework 4.6.2–4.8 installed — versions 4.5.x and below are not supported by the CLR profiler
- Administrator access on the server
- IIS app pool name — find it with:
Get-WebApplication | Select ApplicationPool - Last9 Account with OpenTelemetry integration credentials
Installation
-
Download the auto-instrumentation package
Open an elevated PowerShell session (Windows PowerShell 5.1, not PowerShell 7):
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12$version = "v1.14.1"$otelHome = "C:\Program Files\OpenTelemetry .NET AutoInstrumentation"# Download the instrumentation packageInvoke-WebRequest `-Uri "https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/download/$version/opentelemetry-dotnet-instrumentation-windows.zip" `-OutFile "$env:TEMP\otel-dotnet.zip" -UseBasicParsing# Download the PowerShell management moduleInvoke-WebRequest `-Uri "https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/download/$version/OpenTelemetry.DotNet.Auto.psm1" `-OutFile "$env:TEMP\OpenTelemetry.DotNet.Auto.psm1" -UseBasicParsing# ExtractNew-Item -Path $otelHome -ItemType Directory -Force | Out-NullExpand-Archive -Path "$env:TEMP\otel-dotnet.zip" -DestinationPath $otelHome -ForceCopy-Item "$env:TEMP\OpenTelemetry.DotNet.Auto.psm1" "$otelHome\" -ForceVerify the installation:
Test-Path "$otelHome\win-x64\OpenTelemetry.AutoInstrumentation.Native.dll"# Should return: True -
Register the profiler for IIS
The official PowerShell module registers the CLR profiler and configures IIS globally:
$env:OTEL_DOTNET_AUTO_HOME = "C:\Program Files\OpenTelemetry .NET AutoInstrumentation"Import-Module "$env:OTEL_DOTNET_AUTO_HOME\OpenTelemetry.DotNet.Auto.psm1"Install-OpenTelemetryCoreRegister-OpenTelemetryForIISThis restarts IIS automatically. The profiler is now enabled for all app pools.
-
Configure your app pool
Set environment variables on the specific app pool to control service name, exporter, and sampling:
Import-Module WebAdministration$poolName = "YourAppPool" # Replace with your app pool nameSet-ItemProperty "IIS:\AppPools\$poolName" -Name environmentVariables -Value @(@{ name = 'OTEL_SERVICE_NAME'; value = 'your-service-name' },@{ name = 'OTEL_RESOURCE_ATTRIBUTES'; value = "deployment.environment=production,host.name=$env:COMPUTERNAME" },@{ name = 'OTEL_TRACES_EXPORTER'; value = 'otlp' },@{ name = 'OTEL_METRICS_EXPORTER'; value = 'otlp' },@{ name = 'OTEL_LOGS_EXPORTER'; value = 'otlp' },@{ name = 'OTEL_EXPORTER_OTLP_ENDPOINT'; value = '$last9_otlp_endpoint' },@{ name = 'OTEL_EXPORTER_OTLP_PROTOCOL'; value = 'http/protobuf' },@{ name = 'OTEL_EXPORTER_OTLP_HEADERS'; value = 'Authorization=$last9_otlp_auth_header' },@{ name = 'OTEL_PROPAGATORS'; value = 'tracecontext,baggage' },@{ name = 'OTEL_TRACES_SAMPLER'; value = 'always_on' })Import-Module WebAdministration$poolName = "YourAppPool" # Replace with your app pool nameSet-ItemProperty "IIS:\AppPools\$poolName" -Name environmentVariables -Value @(@{ name = 'OTEL_SERVICE_NAME'; value = 'your-service-name' },@{ name = 'OTEL_RESOURCE_ATTRIBUTES'; value = "deployment.environment=production,host.name=$env:COMPUTERNAME" },@{ name = 'OTEL_TRACES_EXPORTER'; value = 'otlp' },@{ name = 'OTEL_METRICS_EXPORTER'; value = 'otlp' },@{ name = 'OTEL_LOGS_EXPORTER'; value = 'otlp' },@{ name = 'OTEL_EXPORTER_OTLP_ENDPOINT'; value = 'http://localhost:4318' },@{ name = 'OTEL_EXPORTER_OTLP_PROTOCOL'; value = 'http/protobuf' },@{ name = 'OTEL_PROPAGATORS'; value = 'tracecontext,baggage' },@{ name = 'OTEL_TRACES_SAMPLER'; value = 'always_on' }) -
Restart the app pool
Restart-WebAppPool -Name "YourAppPool" -
Verify instrumentation
Make a request to your application, then verify the profiler loaded:
Get-Process w3wp | ForEach-Object {$modules = $_.Modules | Where-Object { $_.ModuleName -like "*OpenTelemetry*" }if ($modules) {Write-Host "PID $($_.Id): OTel profiler loaded"$modules | Select ModuleName}}You should see
OpenTelemetry.AutoInstrumentation.Nativein the module list.Check Last9 Traces and filter by your
service.nameto see incoming and outgoing HTTP spans.
Distributed Trace Propagation
The auto-instrumentation automatically reads and writes W3C traceparent headers on all HTTP requests. This means:
- Incoming requests with a
traceparentheader (from upstream services, mobile apps, or load balancers) will be continued as part of the same trace - Outgoing requests (via
HttpClient,WebClient, orHttpWebRequest) will automatically include thetraceparentheader, propagating the trace to downstream services
No configuration needed — this works by default when OTEL_PROPAGATORS=tracecontext,baggage is set.
Sampling Configuration
| Sampler | Value | Use case |
|---|---|---|
always_on | Capture all traces | Development, debugging |
always_off | Capture no traces | Disable temporarily |
traceidratio | Sample by percentage | Production (set OTEL_TRACES_SAMPLER_ARG=0.1 for 10%) |
parentbased_traceidratio | Respect parent’s sampling decision, ratio for new traces | Production with distributed tracing |
For production, use parentbased_traceidratio to respect upstream sampling decisions while controlling the sampling rate for traces originating at this service.
Multi-App Pool Configuration
To disable instrumentation for a specific app pool without affecting others:
Import-Module "$env:OTEL_DOTNET_AUTO_HOME\OpenTelemetry.DotNet.Auto.psm1"Disable-OpenTelemetryForIISAppPool -AppPoolName "PoolToDisable"PII and SQL Statement Redaction
Even with OTEL_DOTNET_AUTO_SQLCLIENT_NETFX_ILREWRITE_ENABLED=false, add a transform/redact_pii processor to your OTel Collector as a defence-in-depth layer. This removes db.statement and db.query.text from all spans before they leave your network:
processors: transform/redact_pii: trace_statements: - context: span statements: - delete_key(attributes, "db.statement") - delete_key(attributes, "db.query.text") # new semconv name (SDK ≥1.6)Wire it into the traces pipeline:
service: pipelines: traces: receivers: [otlp] processors: [memory_limiter, transform/redact_pii, batch] exporters: [otlp]The delete_key OTTL function is a no-op when the key is absent, so this is safe to deploy unconditionally.
Sample Application
A complete working example is available in the Last9 OTel examples repository. It includes:
- Web API and MVC controllers with ADO.NET SQL calls
- Outbound HTTP calls via
HttpClient - A one-shot
setup-otel.ps1script that runs all four steps above - An OTel Collector config with IIS + CLR Windows perf counter metrics
Troubleshooting
Profiler not loading
Check the .NET Runtime event log:
Get-WinEvent -LogName Application -MaxEvents 20 | Where-Object { $_.ProviderName -eq '.NET Runtime' -and $_.Message -like '*profiler*'} | Format-List TimeCreated, MessageCommon errors:
HRESULT: 0x80004005— Missing Visual C++ Redistributable. Install from MicrosoftCoCreateInstance failed— RunInstall-OpenTelemetryCoreandRegister-OpenTelemetryForIISagain
No spans appearing
-
Verify the app pool has the correct environment variables:
(Get-ItemProperty "IIS:\AppPools\YourAppPool").environmentVariables.Collection | Select name, value -
Check the auto-instrumentation log:
Get-ChildItem "$env:TEMP" -Filter "otel-dotnet-auto-*" |Sort LastWriteTime -Descending | Select -First 1 | Get-Content | Select -Last 30 -
Ensure the app pool is running in 64-bit mode (32-bit mode uses a different profiler path):
(Get-ItemProperty "IIS:\AppPools\YourAppPool").enable32BitAppOnWin64# Should be False for 64-bit
SQL query spans missing
ADO.NET instrumentation works automatically for System.Data.SqlClient. If using a third-party data access layer, ensure it ultimately uses ADO.NET under the hood.
Still stuck?
Please get in touch with us on Discord or Email if you have any questions.