1818using Microsoft . EntityFrameworkCore ;
1919using Microsoft . Extensions . Diagnostics . HealthChecks ;
2020using OpenTelemetry ;
21+ using OpenTelemetry . Instrumentation . AspNetCore ;
2122using OpenTelemetry . Metrics ;
2223using OpenTelemetry . Trace ;
2324
@@ -33,37 +34,55 @@ private static void Main(string[] args)
3334 builder . Services . AddHealthChecks ( )
3435 . AddCheck ( "self" , ( ) => HealthCheckResult . Healthy ( ) , [ "live" ] ) ;
3536
36- // OpenTelemetry — Aspire injects OTEL_EXPORTER_OTLP_ENDPOINT when hosted.
37- // Azure Monitor is enabled in production when APPLICATIONINSIGHTS_CONNECTION_STRING is set.
37+ // OpenTelemetry — two mutually exclusive export paths:
38+ // Production: Azure Monitor (Application Insights) via APPLICATIONINSIGHTS_CONNECTION_STRING
39+ // Local/Aspire: OTLP to Aspire Dashboard via OTEL_EXPORTER_OTLP_ENDPOINT
40+ // Never both simultaneously — that would cause duplicate telemetry in App Insights.
3841 bool useAzureMonitor = ! string . IsNullOrEmpty ( builder . Configuration [ "APPLICATIONINSIGHTS_CONNECTION_STRING" ] ) ;
42+ bool useOtlp = ! string . IsNullOrWhiteSpace ( builder . Configuration [ "OTEL_EXPORTER_OTLP_ENDPOINT" ] ) ;
43+
3944 builder . Logging . AddOpenTelemetry ( logging =>
4045 {
4146 logging . IncludeFormattedMessage = true ;
4247 logging . IncludeScopes = true ;
4348 } ) ;
44- builder . Services . AddOpenTelemetry ( )
45- . WithMetrics ( metrics => metrics
46- . AddAspNetCoreInstrumentation ( )
47- . AddHttpClientInstrumentation ( )
48- . AddRuntimeInstrumentation ( ) )
49+
50+ // Health probe paths excluded from tracing unconditionally — applies to both
51+ // manual instrumentation and Azure Monitor's auto-instrumentation.
52+ builder . Services . Configure < AspNetCoreTraceInstrumentationOptions > ( options =>
53+ options . Filter = ctx =>
54+ ! ctx . Request . Path . StartsWithSegments ( "/health" )
55+ && ! ctx . Request . Path . StartsWithSegments ( "/alive" ) ) ;
56+
57+ var otel = builder . Services . AddOpenTelemetry ( )
58+ . WithMetrics ( metrics =>
59+ {
60+ // Azure Monitor auto-instruments ASP.NET Core + HttpClient metrics; only add
61+ // them manually when using OTLP so we don't register duplicate meter listeners.
62+ if ( ! useAzureMonitor )
63+ {
64+ metrics . AddAspNetCoreInstrumentation ( )
65+ . AddHttpClientInstrumentation ( ) ;
66+ }
67+ // Runtime metrics are not included in the Azure Monitor distro.
68+ metrics . AddRuntimeInstrumentation ( ) ;
69+ } )
4970 . WithTracing ( tracing =>
5071 {
5172 tracing . AddSource ( builder . Environment . ApplicationName ) ;
73+ // Azure Monitor distro auto-instruments tracing; add manually only for OTLP path.
5274 if ( ! useAzureMonitor )
5375 {
54- tracing
55- . AddAspNetCoreInstrumentation ( t =>
56- t . Filter = ctx =>
57- ! ctx . Request . Path . StartsWithSegments ( "/health" )
58- && ! ctx . Request . Path . StartsWithSegments ( "/alive" ) )
59- . AddHttpClientInstrumentation ( )
60- . AddSqlClientInstrumentation ( ) ;
76+ tracing . AddAspNetCoreInstrumentation ( )
77+ . AddHttpClientInstrumentation ( )
78+ . AddSqlClientInstrumentation ( ) ;
6179 }
6280 } ) ;
63- if ( ! string . IsNullOrWhiteSpace ( builder . Configuration [ "OTEL_EXPORTER_OTLP_ENDPOINT" ] ) )
64- builder . Services . AddOpenTelemetry ( ) . UseOtlpExporter ( ) ;
81+
6582 if ( useAzureMonitor )
66- builder . Services . AddOpenTelemetry ( ) . UseAzureMonitor ( ) ;
83+ otel . UseAzureMonitor ( ) ;
84+ else if ( useOtlp )
85+ otel . UseOtlpExporter ( ) ;
6786
6887 // HttpClient defaults — standard retry/circuit breaker for all named clients.
6988 builder . Services . ConfigureHttpClientDefaults ( http => http . AddStandardResilienceHandler ( ) ) ;
0 commit comments