Skip to content
Last9
Book demo

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

LibraryWhat’s captured
ASP.NET / ASP.NET MVCIncoming HTTP request spans with method, URL, status code, duration
System.Net.Http / WebClientOutbound 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

  1. 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 package
    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
    # Download the PowerShell management module
    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
    # Extract
    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

    Verify the installation:

    Test-Path "$otelHome\win-x64\OpenTelemetry.AutoInstrumentation.Native.dll"
    # Should return: True
  2. 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-OpenTelemetryCore
    Register-OpenTelemetryForIIS

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

  3. 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 name
    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 = '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' }
    )
  4. Restart the app pool

    Restart-WebAppPool -Name "YourAppPool"
  5. 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.Native in the module list.

    Check Last9 Traces and filter by your service.name to 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 traceparent header (from upstream services, mobile apps, or load balancers) will be continued as part of the same trace
  • Outgoing requests (via HttpClient, WebClient, or HttpWebRequest) will automatically include the traceparent header, propagating the trace to downstream services

No configuration needed — this works by default when OTEL_PROPAGATORS=tracecontext,baggage is set.

Sampling Configuration

SamplerValueUse case
always_onCapture all tracesDevelopment, debugging
always_offCapture no tracesDisable temporarily
traceidratioSample by percentageProduction (set OTEL_TRACES_SAMPLER_ARG=0.1 for 10%)
parentbased_traceidratioRespect parent’s sampling decision, ratio for new tracesProduction 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.ps1 script 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, Message

Common errors:

  • HRESULT: 0x80004005 — Missing Visual C++ Redistributable. Install from Microsoft
  • CoCreateInstance failed — Run Install-OpenTelemetryCore and Register-OpenTelemetryForIIS again

No spans appearing

  1. Verify the app pool has the correct environment variables:

    (Get-ItemProperty "IIS:\AppPools\YourAppPool").environmentVariables.Collection | Select name, value
  2. Check the auto-instrumentation log:

    Get-ChildItem "$env:TEMP" -Filter "otel-dotnet-auto-*" |
    Sort LastWriteTime -Descending | Select -First 1 | Get-Content | Select -Last 30
  3. 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.