PPulse.Observability.AspNet 1.0.3

dotnet add package PPulse.Observability.AspNet --version 1.0.3
                    
NuGet\Install-Package PPulse.Observability.AspNet -Version 1.0.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PPulse.Observability.AspNet" Version="1.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PPulse.Observability.AspNet" Version="1.0.3" />
                    
Directory.Packages.props
<PackageReference Include="PPulse.Observability.AspNet" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PPulse.Observability.AspNet --version 1.0.3
                    
#r "nuget: PPulse.Observability.AspNet, 1.0.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PPulse.Observability.AspNet@1.0.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PPulse.Observability.AspNet&version=1.0.3
                    
Install as a Cake Addin
#tool nuget:?package=PPulse.Observability.AspNet&version=1.0.3
                    
Install as a Cake Tool

PPulse.Observability.AspNet

Policy Pulse observability integration for ASP.NET Framework (4.6.2+) applications. Provides tracing, metrics, and structured logging export to the PPulse collector.

Installation

dotnet add package PPulse.Observability.AspNet

PackageReference projects need two web.config entries

NuGet only edits your source web.config for packages.config projects. If your project restores with PackageReference — an SDK-style project, or a legacy .csproj with <RestoreProjectStyle>PackageReference</RestoreProjectStyle> — you must add two things by hand.

The build checks your web.config and tells you which is missing: PPOBS001 for the module registration, PPOBS002 for the binding redirects. Both go quiet once the entry is present, so a clean build means you're done.

Migrating to an SDK-style project does not avoid this; SDK-style projects also restore via PackageReference.

1. Register the OpenTelemetry HTTP module. This is the one that fails quietly: without it the application builds, starts, and serves traffic normally while producing no telemetry and no error.

<system.web>
  <httpModules>
    <add name="TelemetryHttpModule"
         type="OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule, OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule" />
  </httpModules>
</system.web>

<system.webServer>
  <modules>
    <remove name="TelemetryHttpModule" />
    <add name="TelemetryHttpModule"
         type="OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule, OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule"
         preCondition="managedHandler" />
  </modules>
</system.webServer>

<httpModules> covers the classic pipeline and <modules> covers integrated mode; add both. The <remove> makes the block safe to apply more than once.

You do not need to register our own module — ResponseFaultHttpModule self-registers via PreApplicationStartMethod as long as the assembly is in bin/.

2. Add binding redirects to your source web.config. NuGet writes these for you only on packages.config projects.

Do not expect AutoGenerateBindingRedirects to cover this. It only emits when GenerateBindingRedirectsOutputType is true, which the SDK sets for Exe output — and a web application is a Library, so nothing is generated anywhere.

The reliable way to obtain the correct set is a throwaway console project — console specifically, because that is the only output type the SDK generates redirects for:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net462</TargetFramework>  
    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
    <GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="PPulse.Observability.AspNet" Version="<same as yours>" />
  </ItemGroup>
</Project>

Build it, then copy the <runtime><assemblyBinding> block from bin/Debug/<tfm>/<name>.exe.config into your web app's source web.config.

The set is specific to your target framework. It is not a fixed list you can copy between projects. On net462 it is 8 entries; on net48 it is 7, because System.ValueTuple is in-box from 4.7 and NuGet therefore stops copying it to bin/. A redirect naming an assembly that is not in your bin/ fails at runtime with FileNotFoundException — so reusing the wrong framework's block is worse than having none.

Skipping this raises FileLoadException — though not necessarily at startup, so a clean-looking start does not mean it is done.

Redo this on every dependency upgrade. packages.config projects get their redirects rewritten by NuGet on each update; PackageReference projects do not, so an upgrade that moves any transitive version leaves the block stale. Re-run the console project and re-copy.

packages.config projects need none of the above; NuGet applies both at install time.

Setup

Call the builder from your application startup (e.g. Global.asax.cs). Keep the returned ObservabilityProvider alive for the application lifetime — it holds the TracerProvider and MeterProvider singletons.

Global.asax.cs

public class WebApiApplication : System.Web.HttpApplication
{
    public static ILoggerFactory LogFactory { get; private set; }
    private static ObservabilityProvider observabilityProvider;

    protected void Application_Start()
    {
        // 1. Configure PPulse — call WithILoggingBuilder() to prepare log export,
        //    but the binding is applied below when ILoggerFactory is created.
        observabilityProvider = ObservabilityProviderBuilder.Create()
            .UsePolicyPulse(opts =>
            {
                opts.AppCode = "MyApp";
                opts.AppName = "My Application";
                opts.Environment = "Production";
                opts.CollectorBaseUri = new Uri("https://your-collector/");
                opts.TenantCode = "your-tenant-code";
                opts.ApiKey = "your-api-key";
            })
            .WithMetrics().AddAspNetMetrics()
            .WithTracing().AddAspNetTracing()
            .WithILoggingBuilder()   // prepares log export; applied below
            .AddAspNetExceptionCapture()
            .Build();

        // 2. Create the logger factory and wire in the PPulse logging config.
        LogFactory = LoggerFactory.Create(lb =>
        {
            // add any other providers here (e.g. Serilog)
            observabilityProvider.LoggingBinder(lb);  // <-- applies PPulse log export
        });
    }

    // 3. Start a PPulse logging scope for each request so logs are correlated
    //    to the trace via the PPulse tenant/correlation/session headers.
    protected void Application_BeginRequest()
    {
        Request.StartPolicyPulseLoggingScope(LogFactory);
    }
}

Logging: WithILoggingBuilder and LoggingBinder

ASP.NET Framework has no built-in DI-hosted ILoggingBuilder, so logging setup is split into two steps:

  1. .WithILoggingBuilder() on the builder — captures the log export configuration as a delegate, stored on the returned ObservabilityProvider.
  2. observabilityProvider.LoggingBinder(lb) inside LoggerFactory.Create(...) — applies that configuration to whichever ILoggingBuilder you create. Call this alongside any other log providers (Serilog, Console, etc.).

If you omit WithILoggingBuilder(), LoggingBinder will throw InvalidOperationException when accessed.

What is instrumented

  • Inbound HTTP requests (via AddAspNetTracing)
  • Outbound HTTP calls
  • SQL client operations
  • Runtime and process metrics (via AddAspNetMetrics)
  • PPulse metadata headers (tenant, correlation, session) extracted from inbound requests and attached to traces and logs
  • First-chance exceptions captured and attached to the active trace — see PPulse.Observability.AspNetCore README for ConfigureException filtering options (namespace lists and predicate); the same options apply here

Verifying it works

Request a page served by managed code. TelemetryHttpModule is registered with preCondition="managedHandler", so it does not run for static files, directory listings, or 404s. Hitting a static home page produces no spans even when everything is configured correctly — which looks identical to the module being missing.

The quickest unambiguous check is a generic handler that reports the ambient activity:

// ping.ashx
var a = System.Diagnostics.Activity.Current;
context.Response.Write(a == null ? "activity=NONE" : "trace=" + a.TraceId);

activity=NONE on a managed endpoint means the module is not registered — that is PPOBS001. A trace id means the module is running, independent of whether export has succeeded yet.

Troubleshooting

No telemetry, no errors, and the module is registered. Check the GAC:

Get-ChildItem 'C:\Windows\Microsoft.NET\assembly' -Recurse -Filter '*.dll' |
  Where-Object { $_.Name -match 'OpenTelemetry|Microsoft.Extensions|DiagnosticSource' }

Anything returned will shadow the assemblies in your bin/. OpenTelemetry freezes its core AssemblyVersion at 1.0.0.0, so a GAC copy has an identical strong name to the one you shipped — and the GAC always wins. The usual source is an APM agent; the OpenTelemetry .NET AutoInstrumentation installer places about 46 assemblies there. Symptoms range from silence to FileLoadException naming a version neither you nor your bin/ asked for. No web.config change can override this; the assemblies have to be removed from the GAC.

FileLoadException mentioning Microsoft.Extensions.*. This package requires Microsoft.Extensions.* 10.0.0, which OpenTelemetry 1.17.0 floors on .NET Framework. An application carrying its own lower version has a genuine conflict — an AppDomain resolves one version per identity, and a binding redirect cannot serve two. That conflict has to be settled before deployment.

SDK-style project builds but fails at runtime with type load errors. ASP.NET loads from bin/ only, while the SDK writes to bin/<Config>/<TFM>/. Add:

<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>bin\</OutputPath>

Install fails on .NET Framework 4.6.1 or lower. This package targets net462. There is no lower asset; 4.6.2 is a hard floor.

Product Compatible and additional computed target framework versions.
.NET Framework net462 is compatible.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.3 59 8/18/2026