ASP.NET Core (.NET 6+)
Instrument ASP.NET Core applications with OpenTelemetry auto-instrumentation and send traces and metrics to Last9
Use the OpenTelemetry .NET Automatic Instrumentation agent to instrument ASP.NET Core applications without adding any OTel SDK packages to your project. The CLR profiler intercepts incoming requests, outbound HTTP calls, and SQL queries at the process level.
What Gets Auto-Instrumented
| Library | What’s captured |
|---|---|
| ASP.NET Core | Incoming request spans — controllers and Minimal API endpoints |
| IHttpClientFactory / HttpClient | Outbound HTTP call spans with W3C traceparent header injection |
| ADO.NET (DbCommand) | SQL query spans with db.statement (SqlClient, SQLite, Npgsql, etc.) |
| .NET CLR | GC, heap, thread pool, exception rate metrics (via Windows perf counters) |
Prerequisites
- .NET 6.0 or later runtime installed
- For IIS mode: Windows Server 2016+ with IIS 8.5+
- Administrator access on the server
- Windows PowerShell 5.1 (Desktop edition — not PowerShell 7/pwsh) — required for setup only
- Last9 Account with OpenTelemetry integration credentials
Installation
-
Download the auto-instrumentation package
Open an elevated Windows PowerShell 5.1 session (not PowerShell 7):
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12$version = "v1.14.1"$otelHome = "C:\Program Files\OpenTelemetry .NET AutoInstrumentation"Invoke-WebRequest `-Uri "https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/download/$version/opentelemetry-dotnet-instrumentation-windows.zip" `-OutFile "$env:TEMP\otel-dotnet.zip" -UseBasicParsingInvoke-WebRequest `-Uri "https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/download/$version/OpenTelemetry.DotNet.Auto.psm1" `-OutFile "$env:TEMP\OpenTelemetry.DotNet.Auto.psm1" -UseBasicParsingNew-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\" -Force -
Register the profiler for IIS
$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. The CLR profiler is now enabled for all app pools.
-
Set the app pool CLR mode to “No Managed Code”
ASP.NET Core runs the .NET runtime in-process — the app pool must not be configured as a Classic .NET 4.x pool. If
managedRuntimeVersionis set tov4.0, the profiler will fail silently.Import-Module WebAdministration$poolName = "YourCoreAppPool" # Replace with your app pool name# Must be empty string ("") — not "v4.0"Set-ItemProperty "IIS:\AppPools\$poolName" -Name managedRuntimeVersion -Value ""# Verify(Get-ItemProperty "IIS:\AppPools\$poolName").managedRuntimeVersion# Should return: "" (empty) -
Configure environment variables on the app pool
Import-Module WebAdministration$poolName = "YourCoreAppPool"Set-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 = 'none' },@{ name = 'OTEL_EXPORTER_OTLP_ENDPOINT'; value = 'http://localhost:4317' },@{ name = 'OTEL_EXPORTER_OTLP_PROTOCOL'; value = 'grpc' },@{ name = 'OTEL_PROPAGATORS'; value = 'tracecontext,baggage' },@{ name = 'OTEL_TRACES_SAMPLER'; value = 'parentbased_traceidratio' },@{ name = 'OTEL_TRACES_SAMPLER_ARG'; value = '1.0' })Import-Module WebAdministration$poolName = "YourCoreAppPool"Set-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 = 'none' },@{ 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 = 'parentbased_traceidratio' },@{ name = 'OTEL_TRACES_SAMPLER_ARG'; value = '1.0' }) -
Restart the app pool
Restart-WebAppPool -Name "YourCoreAppPool" -
Verify instrumentation
Make a request to your application, then check 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.Native. If the process list is empty, the app pool has not received a request yet — make one first.
For development or self-hosted deployments where the process runs directly (not through IIS), set the environment variables before starting dotnet run or dotnet YourApp.dll.
-
Download and install the profiler
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12$version = "v1.14.1"$otelHome = "C:\Program Files\OpenTelemetry .NET AutoInstrumentation"Invoke-WebRequest `-Uri "https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/download/$version/opentelemetry-dotnet-instrumentation-windows.zip" `-OutFile "$env:TEMP\otel-dotnet.zip" -UseBasicParsingInvoke-WebRequest `-Uri "https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/download/$version/OpenTelemetry.DotNet.Auto.psm1" `-OutFile "$env:TEMP\OpenTelemetry.DotNet.Auto.psm1" -UseBasicParsingNew-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\" -Force$env:OTEL_DOTNET_AUTO_HOME = $otelHomeImport-Module "$otelHome\OpenTelemetry.DotNet.Auto.psm1"Install-OpenTelemetryCore -
Set environment variables and run
All
CORECLR_*,DOTNET_*, andOTEL_*variables must be set in the same shell session that runsdotnet. Setting them in a separate window has no effect.$otelHome = "C:\Program Files\OpenTelemetry .NET AutoInstrumentation"# Profiler registration$env:CORECLR_ENABLE_PROFILING = "1"$env:CORECLR_PROFILER = "{918728DD-259F-4A6A-AC2B-B85E1B658318}"$env:CORECLR_PROFILER_PATH_64 = "$otelHome\win-x64\OpenTelemetry.AutoInstrumentation.Native.dll"$env:DOTNET_ADDITIONAL_DEPS = "$otelHome\AdditionalDeps"$env:DOTNET_SHARED_STORE = "$otelHome\store"$env:DOTNET_STARTUP_HOOKS = "$otelHome\net\OpenTelemetry.AutoInstrumentation.StartupHook.dll"$env:OTEL_DOTNET_AUTO_HOME = $otelHome# Service identity$env:OTEL_SERVICE_NAME = "your-service-name"$env:OTEL_RESOURCE_ATTRIBUTES = "deployment.environment=production,host.name=$env:COMPUTERNAME"# Exporter — via OTel Collector (recommended)$env:OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4317"$env:OTEL_EXPORTER_OTLP_PROTOCOL = "grpc"# Signals$env:OTEL_TRACES_EXPORTER = "otlp"$env:OTEL_METRICS_EXPORTER = "otlp"$env:OTEL_LOGS_EXPORTER = "none"# Propagation + sampling$env:OTEL_PROPAGATORS = "tracecontext,baggage"$env:OTEL_TRACES_SAMPLER = "parentbased_traceidratio"$env:OTEL_TRACES_SAMPLER_ARG = "1.0"# Run the appdotnet run
Distributed Trace Propagation
The profiler automatically reads and writes W3C traceparent headers on all HTTP traffic. No code changes are needed:
- Incoming requests with a
traceparentheader continue the upstream trace - Outgoing requests via
HttpClientorIHttpClientFactorycarry thetraceparentto downstream services
This works the same for both controller-based routes and Minimal API endpoints.
Sampling Configuration
| Sampler | OTEL_TRACES_SAMPLER value | Use case |
|---|---|---|
| Always on | always_on | Development, debugging |
| Always off | always_off | Disable temporarily |
| Fixed percentage | traceidratio | Production — set OTEL_TRACES_SAMPLER_ARG=0.1 for 10% |
| Respect parent, ratio for root | parentbased_traceidratio | Production with distributed tracing |
For production with a gateway collector, use parentbased_traceidratio. This respects tail-sampling decisions made upstream and applies the percentage only to root traces.
PII and SQL Statement Redaction
On .NET Core, the CLR profiler captures db.query.text (new OTel semantic conventions name) by default for every SQL query executed via ADO.NET. Unlike .NET Framework — where SQL text capture requires explicit opt-in via OTEL_DOTNET_AUTO_SQLCLIENT_NETFX_ILREWRITE_ENABLED — there is no env var to disable it on .NET Core.
The correct control point is the OTel Collector. Add a transform/redact_pii processor to strip both the old and new attribute names before spans leave your network:
processors: transform/redact_pii: trace_statements: - context: span statements: - delete_key(attributes, "db.statement") # SDK < 1.6 (old semconv) - delete_key(attributes, "db.query.text") # SDK ≥ 1.6 (new semconv)Wire it into the traces pipeline before the exporter:
service: pipelines: traces: receivers: [otlp] processors: [memory_limiter, transform/redact_pii, batch] exporters: [otlp]The otelcol-dotnet.yaml in the sample app already includes this processor.
IIS + CLR Metrics
To collect IIS request metrics and .NET CLR runtime metrics (GC, heap, threads), deploy an OTel Collector with the iis and windowsperfcounters receivers on the same host.
# otelcol-dotnet.yaml — deploy alongside your appreceivers: iis: collection_interval: 60s windowsperfcounters/dotnet: collection_interval: 60s perfcounters: - object: ".NET CLR Memory" instances: ["_Global_"] counters: - name: "# Bytes in all Heaps" metric: dotnet.clr.heap_bytes - name: "% Time in GC" metric: dotnet.clr.gc.time_percent - name: "# Gen 0 Collections" metric: dotnet.clr.gc.gen0_collections - name: "# Gen 1 Collections" metric: dotnet.clr.gc.gen1_collections - name: "# Gen 2 Collections" metric: dotnet.clr.gc.gen2_collections - object: ".NET CLR Exceptions" instances: ["_Global_"] counters: - name: "# of Exceps Thrown / sec" metric: dotnet.clr.exceptions.per_second - object: ".NET CLR LocksAndThreads" instances: ["_Global_"] counters: - name: "# of current logical Threads" metric: dotnet.clr.threads.logical
processors: batch: timeout: 10s memory_limiter: limit_mib: 256 check_interval: 5s resourcedetection: detectors: [system]
exporters: otlp: endpoint: "$last9_otlp_endpoint" headers: Authorization: "$last9_otlp_auth_header"
service: pipelines: metrics/dotnet: receivers: [iis, windowsperfcounters/dotnet] processors: [memory_limiter, resourcedetection, batch] exporters: [otlp]Sample Application
A complete working example is available in the Last9 OTel examples repository. It includes:
- Controller-based and Minimal API endpoints
- ADO.NET SQL calls and outbound HTTP calls (all auto-instrumented)
setup-otel.ps1for both IIS and Kestrel modes (handlesmanagedRuntimeVersionautomatically)- OTel Collector config with CLR + IIS metrics
Troubleshooting
No spans — IIS mode
-
Check
managedRuntimeVersion— must be empty, notv4.0:(Get-ItemProperty "IIS:\AppPools\YourCoreAppPool").managedRuntimeVersion# Must return: "" (empty string)# If it returns "v4.0", run:Set-ItemProperty "IIS:\AppPools\YourCoreAppPool" -Name managedRuntimeVersion -Value ""Restart-WebAppPool -Name "YourCoreAppPool" -
Verify the profiler loaded in the w3wp process:
Get-Process w3wp | % { $_.Modules | Where ModuleName -like "*OpenTelemetry*" } -
Check the auto-instrumentation log:
Get-ChildItem $env:TEMP -Filter "otel-dotnet-auto-*" |Sort LastWriteTime -Desc | Select -First 1 | Get-Content | Select -Last 30
No spans — Kestrel mode
All CORECLR_* and DOTNET_* environment variables must be set in the same PowerShell session that runs dotnet. If you set them in one window and run the app in another, the profiler does not load.
Verify they are present before running:
$env:CORECLR_ENABLE_PROFILING # Should be: 1$env:DOTNET_STARTUP_HOOKS # Should be a path ending in .dllTwo services showing the same service.name
Missing Visual C++ Redistributable
If setup logs show HRESULT: 0x80004005, install the Visual C++ Redistributable:
Invoke-WebRequest -Uri "https://aka.ms/vs/17/release/vc_redist.x64.exe" -OutFile "$env:TEMP\vc_redist.x64.exe"Start-Process "$env:TEMP\vc_redist.x64.exe" -ArgumentList "/install /quiet /norestart" -WaitRegister-OpenTelemetryForIISStill stuck?
Please get in touch with us on Discord or Email if you have any questions.