|
| 1 | +// Copyright 2025 Carnegie Mellon University. All Rights Reserved. |
| 2 | +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. |
| 3 | + |
| 4 | +using System; |
| 5 | +using System.Reflection; |
| 6 | +using Microsoft.Extensions.Configuration; |
| 7 | +using Microsoft.Extensions.DependencyInjection; |
| 8 | +using Microsoft.Extensions.Hosting; |
| 9 | +using Microsoft.Extensions.Logging; |
| 10 | +using OpenTelemetry.Logs; |
| 11 | +using OpenTelemetry.Metrics; |
| 12 | +using OpenTelemetry.Resources; |
| 13 | +using OpenTelemetry.Trace; |
| 14 | + |
| 15 | +namespace Crucible.Common.ServiceDefaults.OpenTelemetry; |
| 16 | + |
| 17 | +public static class OpenTelemetryExtensions |
| 18 | +{ |
| 19 | + /// <summary> |
| 20 | + /// Call to configure default configuration for OpenTelemetry-enhanced logging. |
| 21 | + /// |
| 22 | + /// NOTE: This function is exposed primarily for apps created before .NET Core 8 that bootstrap with IHostBuilder rather than the newer IHostApplicationBuilder. |
| 23 | + /// If your app uses IHostApplicationBuilder, you shouldn't need to call this function directly. |
| 24 | + /// </summary> |
| 25 | + /// <param name="logging"></param> |
| 26 | + /// <returns></returns> |
| 27 | + public static ILoggingBuilder AddOpenTelemetryLogging(this ILoggingBuilder logging) |
| 28 | + { |
| 29 | + AddLogging(logging, ResolveServiceIdentity()); |
| 30 | + return logging; |
| 31 | + } |
| 32 | + |
| 33 | + /// <summary> |
| 34 | + /// Call to configure default OpenTelemetry services. Customizable with the <cref>optionsBuilder</cref> parameter. See its properties for details. |
| 35 | + /// |
| 36 | + /// NOTE: This function is exposed primarily for apps created before .NET Core 8 that bootstrap with IHostBuilder rather than the newer IHostApplicationBuilder. |
| 37 | + /// If your app uses IHostApplicationBuilder, you shouldn't need to call this function directly. |
| 38 | + /// </summary> |
| 39 | + /// <param name="services">Your app's service collection.</param> |
| 40 | + /// <param name="hostEnvironment">The hosting environment in which your app is starting up.</param> |
| 41 | + /// <param name="configuration">Your app's configuration.</param> |
| 42 | + /// <param name="optionsBuilder">A builder used to customize OpenTelemetry configuration.</param> |
| 43 | + /// <returns></returns> |
| 44 | + public static IServiceCollection AddOpenTelemetryServiceDefaults(this IServiceCollection services, IHostEnvironment hostEnvironment, IConfiguration configuration, Action<OpenTelemetryOptions>? optionsBuilder = null) |
| 45 | + { |
| 46 | + var options = BuildOptions(optionsBuilder); |
| 47 | + var identity = ResolveServiceIdentity(hostEnvironment); |
| 48 | + |
| 49 | + AddServices(services, options, identity); |
| 50 | + AddExporters(services, configuration["OTEL_EXPORTER_OTLP_ENDPOINT"], options); |
| 51 | + |
| 52 | + return services; |
| 53 | + } |
| 54 | + |
| 55 | + /// <summary> |
| 56 | + /// Add default service and logging configuration for OpenTelemetry. Customizable with the <cref>optionsBuilder</cref> parameter. See its properties for details. |
| 57 | + /// </summary> |
| 58 | + /// <param name="builder">Your app's </param> |
| 59 | + /// <param name="optionsBuilder"></param> |
| 60 | + /// <returns></returns> |
| 61 | + public static IHostApplicationBuilder AddOpenTelemetryServiceDefaults(this IHostApplicationBuilder builder, Action<OpenTelemetryOptions>? optionsBuilder = null) |
| 62 | + { |
| 63 | + var options = BuildOptions(optionsBuilder); |
| 64 | + var identity = ResolveServiceIdentity(builder.Environment); |
| 65 | + |
| 66 | + builder.ConfigureOpenTelemetry(options, identity); |
| 67 | + |
| 68 | + return builder; |
| 69 | + } |
| 70 | + |
| 71 | + private static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder, OpenTelemetryOptions options, ServiceIdentity identity) |
| 72 | + { |
| 73 | + AddServices(builder.Services, options, identity); |
| 74 | + AddExporters(builder.Services, builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"], options); |
| 75 | + |
| 76 | + return builder; |
| 77 | + } |
| 78 | + |
| 79 | + private static void AddLogging(this ILoggingBuilder logging, ServiceIdentity identity) |
| 80 | + { |
| 81 | + logging.AddOpenTelemetry(x => |
| 82 | + { |
| 83 | + x.IncludeScopes = true; |
| 84 | + x.IncludeFormattedMessage = true; |
| 85 | + x.ParseStateValues = true; |
| 86 | + x.SetResourceBuilder( |
| 87 | + ResourceBuilder |
| 88 | + .CreateDefault() |
| 89 | + .AddService( |
| 90 | + serviceName: identity.ServiceName, |
| 91 | + serviceVersion: identity.ServiceVersion, |
| 92 | + serviceInstanceId: identity.ServiceInstanceId)); |
| 93 | + }); |
| 94 | + } |
| 95 | + |
| 96 | + private static void AddServices(this IServiceCollection services, OpenTelemetryOptions options, ServiceIdentity identity) |
| 97 | + { |
| 98 | + services.AddLogging(logging => AddLogging(logging, identity)); |
| 99 | + services |
| 100 | + .AddOpenTelemetry() |
| 101 | + .ConfigureResource(resource => |
| 102 | + resource.AddService( |
| 103 | + serviceName: identity.ServiceName, |
| 104 | + serviceVersion: identity.ServiceVersion, |
| 105 | + serviceInstanceId: identity.ServiceInstanceId)) |
| 106 | + .WithMetrics(x => |
| 107 | + { |
| 108 | + x.AddRuntimeInstrumentation(); |
| 109 | + x.AddProcessInstrumentation(); |
| 110 | + x.AddHttpClientInstrumentation(); |
| 111 | + x.AddAspNetCoreInstrumentation(); |
| 112 | + |
| 113 | + if (options.IncludeDefaultMeters) |
| 114 | + { |
| 115 | + x.AddMeter |
| 116 | + ( |
| 117 | + "Microsoft.AspNetCore.Hosting", |
| 118 | + "Microsoft.AspNetCore.Server.Kestrel", |
| 119 | + "Microsoft.EntityFrameworkCore", |
| 120 | + "System.Net.Http", |
| 121 | + "System.Net.NameResolution" |
| 122 | + ); |
| 123 | + } |
| 124 | + |
| 125 | + if (options.CustomMeters.Any()) |
| 126 | + { |
| 127 | + x.AddMeter([.. options.CustomMeters]); |
| 128 | + } |
| 129 | + }) |
| 130 | + .WithTracing(x => |
| 131 | + { |
| 132 | + if (options.IncludeDefaultActivitySources) |
| 133 | + { |
| 134 | + x.AddSource("Microsoft.AspNetCore"); |
| 135 | + x.AddSource("Microsoft.EntityFrameworkCore"); |
| 136 | + x.AddSource("System.Net.Http"); |
| 137 | + } |
| 138 | + |
| 139 | + if (options.CustomActivitySources.Any()) |
| 140 | + { |
| 141 | + x.AddSource([.. options.CustomActivitySources]); |
| 142 | + } |
| 143 | + |
| 144 | + if (options.AddAlwaysOnTracingSampler) |
| 145 | + { |
| 146 | + x.SetSampler<AlwaysOnSampler>(); |
| 147 | + } |
| 148 | + |
| 149 | + x |
| 150 | + // record structured logs for traces |
| 151 | + .AddAspNetCoreInstrumentation(o => o.RecordException = true) |
| 152 | + .AddHttpClientInstrumentation() |
| 153 | + .AddEntityFrameworkCoreInstrumentation(); |
| 154 | + }); |
| 155 | + } |
| 156 | + |
| 157 | + private static void AddExporters(this IServiceCollection services, string? otelExporterEndpoint, OpenTelemetryOptions options) |
| 158 | + { |
| 159 | + var isOtlpEndpointConfigured = !string.IsNullOrWhiteSpace(otelExporterEndpoint); |
| 160 | + |
| 161 | + services.Configure<OpenTelemetryLoggerOptions>(logging => |
| 162 | + { |
| 163 | + if (isOtlpEndpointConfigured) |
| 164 | + { |
| 165 | + logging.AddOtlpExporter(); |
| 166 | + } |
| 167 | + |
| 168 | + if (options.AddConsoleExporter) |
| 169 | + { |
| 170 | + logging.AddConsoleExporter(); |
| 171 | + } |
| 172 | + }); |
| 173 | + |
| 174 | + services.ConfigureOpenTelemetryMeterProvider(metrics => |
| 175 | + { |
| 176 | + if (isOtlpEndpointConfigured) |
| 177 | + { |
| 178 | + metrics.AddOtlpExporter(); |
| 179 | + } |
| 180 | + |
| 181 | + if (options.AddConsoleExporter) |
| 182 | + { |
| 183 | + metrics.AddConsoleExporter(); |
| 184 | + } |
| 185 | + |
| 186 | + if (options.AddPrometheusExporter) |
| 187 | + { |
| 188 | + metrics.AddPrometheusExporter(); |
| 189 | + } |
| 190 | + }); |
| 191 | + |
| 192 | + services.ConfigureOpenTelemetryTracerProvider(tracing => |
| 193 | + { |
| 194 | + if (isOtlpEndpointConfigured) |
| 195 | + { |
| 196 | + tracing.AddOtlpExporter(); |
| 197 | + } |
| 198 | + |
| 199 | + if (options.AddConsoleExporter) |
| 200 | + { |
| 201 | + tracing.AddConsoleExporter(); |
| 202 | + } |
| 203 | + }); |
| 204 | + } |
| 205 | + |
| 206 | + private static OpenTelemetryOptions BuildOptions(Action<OpenTelemetryOptions>? optionsBuilder = null) |
| 207 | + { |
| 208 | + var options = new OpenTelemetryOptions(); |
| 209 | + |
| 210 | + if (optionsBuilder is not null) |
| 211 | + { |
| 212 | + optionsBuilder(options); |
| 213 | + } |
| 214 | + |
| 215 | + return options; |
| 216 | + } |
| 217 | + |
| 218 | + private static ServiceIdentity ResolveServiceIdentity(IHostEnvironment? environment = null) |
| 219 | + { |
| 220 | + var assembly = Assembly.GetEntryAssembly(); |
| 221 | + var serviceName = !string.IsNullOrWhiteSpace(environment?.ApplicationName) |
| 222 | + ? environment!.ApplicationName |
| 223 | + : assembly?.GetName().Name ?? AppDomain.CurrentDomain.FriendlyName; |
| 224 | + |
| 225 | + var serviceVersion = assembly?.GetName().Version?.ToString(); |
| 226 | + var serviceInstanceId = Environment.MachineName; |
| 227 | + |
| 228 | + return new ServiceIdentity(serviceName, serviceVersion, serviceInstanceId); |
| 229 | + } |
| 230 | + |
| 231 | + private readonly record struct ServiceIdentity(string ServiceName, string? ServiceVersion, string ServiceInstanceId); |
| 232 | +} |
0 commit comments