Skip to content
Last9
Book demo

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

LibraryWhat’s captured
ASP.NET CoreIncoming request spans — controllers and Minimal API endpoints
IHttpClientFactory / HttpClientOutbound HTTP call spans with W3C traceparent header injection
ADO.NET (DbCommand)SQL query spans with db.statement (SqlClient, SQLite, Npgsql, etc.)
.NET CLRGC, 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

  1. 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" -UseBasicParsing
    Invoke-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
    New-Item -Path $otelHome -ItemType Directory -Force | Out-Null
    Expand-Archive -Path "$env:TEMP\otel-dotnet.zip" -DestinationPath $otelHome -Force
    Copy-Item "$env:TEMP\OpenTelemetry.DotNet.Auto.psm1" "$otelHome\" -Force
  2. 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-OpenTelemetryCore
    Register-OpenTelemetryForIIS

    This restarts IIS. The CLR profiler is now enabled for all app pools.

  3. 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 managedRuntimeVersion is set to v4.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)
  4. 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' }
    )
  5. Restart the app pool

    Restart-WebAppPool -Name "YourCoreAppPool"
  6. 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.

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 traceparent header continue the upstream trace
  • Outgoing requests via HttpClient or IHttpClientFactory carry the traceparent to downstream services

This works the same for both controller-based routes and Minimal API endpoints.

Sampling Configuration

SamplerOTEL_TRACES_SAMPLER valueUse case
Always onalways_onDevelopment, debugging
Always offalways_offDisable temporarily
Fixed percentagetraceidratioProduction — set OTEL_TRACES_SAMPLER_ARG=0.1 for 10%
Respect parent, ratio for rootparentbased_traceidratioProduction 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 app
receivers:
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.ps1 for both IIS and Kestrel modes (handles managedRuntimeVersion automatically)
  • OTel Collector config with CLR + IIS metrics

Troubleshooting

No spans — IIS mode

  1. Check managedRuntimeVersion — must be empty, not v4.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"
  2. Verify the profiler loaded in the w3wp process:

    Get-Process w3wp | % { $_.Modules | Where ModuleName -like "*OpenTelemetry*" }
  3. 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 .dll

Two 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" -Wait
Register-OpenTelemetryForIIS

Still stuck?

Please get in touch with us on Discord or Email if you have any questions.