Skip to content

Commit ae997d7

Browse files
Task/add service defaults (#2)
Adds a ServiceDefaults package that configures OpenTelemetry and can be extended to configure other common services in the future. Co-authored-by: Jarrett Booz <[email protected]>
1 parent f221621 commit ae997d7

File tree

7 files changed

+400
-0
lines changed

7 files changed

+400
-0
lines changed

.github/workflows/release-package.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
type: choice
1010
options:
1111
- Crucible.Common.Authentication
12+
- Crucible.Common.ServiceDefaults
1213
- Crucible.Common.Utilities
1314
versionNumber:
1415
description: "A semver version string (e.g. 1.0.0)"

Crucible.Common.slnx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
</Configurations>
77
<Folder Name="/src/">
88
<Project Path="src/Crucible.Common.Authentication/Crucible.Common.Authentication.csproj" />
9+
<Project Path="src/Crucible.Common.ServiceDefaults/Crucible.Common.ServiceDefaults.csproj" />
10+
<Project Path="src/Crucible.Common.Utilities/Crucible.Common.Utilities.csproj" />
911
</Folder>
1012
<Folder Name="/test/">
1113
<Project Path="test/Crucible.Common.Tests/Crucible.Common.Tests.csproj" />
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
<!--README config-->
10+
<PropertyGroup>
11+
<PackageReadmeFile>README.md</PackageReadmeFile>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<None Include="README.md" Pack="true" PackagePath="\" />
16+
</ItemGroup>
17+
18+
<!--LICENSE config-->
19+
<PropertyGroup>
20+
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
21+
</PropertyGroup>
22+
23+
<ItemGroup>
24+
<None Include="../../LICENSE.md" Pack="true" PackagePath="/" />
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
29+
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.13.0" />
30+
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.0" />
31+
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.13.0-beta.1" />
32+
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.0" />
33+
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
34+
<PackageReference Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" Version="1.13.0-beta.1" />
35+
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
36+
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" />
37+
<PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="1.13.0-beta.1" />
38+
</ItemGroup>
39+
</Project>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Crucible.Common.ServiceDefaults
2+
3+
Default service configuration for Crucible API apps. Right now, this is mostly just configuration for [OpenTelemetry](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-with-otel), but may involve more stuff as we get going.
4+
5+
## Quick start
6+
7+
```csharp
8+
var builder = Host.CreateApplicationBuilder(args);
9+
10+
builder.AddServiceDefaults(openTelemetryOptions =>
11+
{
12+
openTelemetryOptions.AddConsoleExporter = builder.Environment.IsDevelopment();
13+
openTelemetryOptions.CustomActivitySources = ["Player.Api"];
14+
});
15+
```
16+
17+
For `Startup`-style apps, call `services.AddServiceDefaults(env, Configuration);` during service registration.
18+
19+
## Required environment variables
20+
21+
- `OTEL_EXPORTER_OTLP_ENDPOINT`
22+
Endpoint for the OTLP collector that receives logs, metrics, and traces. Use an `http://` or `https://` URL such as `http://otel-collector:4317`. If this is omitted the SDK still wires instrumentation, but nothing is sent to an external collector unless an optional exporter is enabled.
23+
24+
## Optional environment variables
25+
26+
The OpenTelemetry SDK honors additional standard variables (for example `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_EXPORTER_OTLP_HEADERS`, `OTEL_EXPORTER_OTLP_TIMEOUT`), although this library does not set defaults for them. Configure these when you need to pass authentication headers, tweak timeouts, or add extra resource attributes.
27+
28+
## Optional library settings
29+
30+
Pass an `Action<OpenTelemetryOptions>` when calling `AddServiceDefaults` (or the specific `AddOpenTelemetryServiceDefaults` overloads) to customize:
31+
32+
- `AddAlwaysOnTracingSampler` – force sampling of every trace span.
33+
- `AddConsoleExporter` – write telemetry to console for local debugging.
34+
- `AddPrometheusExporter` – surface metrics through the built-in Prometheus endpoint.
35+
- `CustomActivitySources` – register additional activity sources to trace.
36+
- `CustomMeters` – register additional meters to collect metrics from.
37+
- `IncludeDefaultActivitySources` – toggle the built-in sources (`Microsoft.AspNetCore`, `Microsoft.EntityFrameworkCore`, `System.Net.Http`).
38+
- `IncludeDefaultMeters` – toggle the default meters described below.
39+
40+
## What is enabled by default
41+
42+
- Structured logging with OTLP or console exporters.
43+
- Metrics from the .NET runtime, the hosting stack, ASP.NET Core, HttpClient, Entity Framework Core, and process CPU/memory usage.
44+
- Traces for ASP.NET Core, HttpClient, and Entity Framework Core operations.
45+
46+
Adjust the options or supply additional instrumentation packages to tailor telemetry for your service.
47+
48+
### Default metrics and meters
49+
50+
The metrics pipeline wires several OpenTelemetry instrumentations:
51+
52+
- [`AddRuntimeInstrumentation`](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/main/src/OpenTelemetry.Instrumentation.Runtime) publishes `System.Runtime` metrics for garbage collection, thread pool saturation, and exception rates.
53+
- [`AddProcessInstrumentation`](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/main/src/OpenTelemetry.Instrumentation.Process) reports process CPU usage, working set, and other host-level resource counters.
54+
- [`AddHttpClientInstrumentation`](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/main/src/OpenTelemetry.Instrumentation.Http) records dependency call duration, size, and error metrics for outgoing HTTP requests.
55+
- [`AddAspNetCoreInstrumentation`](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/tree/main/src/OpenTelemetry.Instrumentation.AspNetCore) captures request duration, size, and other server metrics for incoming ASP.NET Core traffic.
56+
57+
When `IncludeDefaultMeters` remains `true`, the provider also subscribes to these built-in meters:
58+
59+
- [`Microsoft.AspNetCore.Hosting`](https://learn.microsoft.com/en-us/aspnet/core/diagnostics/metrics) – high-level hosting metrics such as request queue depth and current requests.
60+
- [`Microsoft.AspNetCore.Server.Kestrel`](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/diagnostics) – Kestrel transport metrics for connections, TLS handshakes, and request/response throughput.
61+
- [`Microsoft.EntityFrameworkCore`](https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/metrics) – database command execution counts and timings.
62+
- [`System.Net.Http`](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/telemetry/metrics) – outgoing request rate, duration, and failure counts from the .NET networking stack.
63+
- [`System.Net.NameResolution`](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/telemetry/metrics) – DNS cache and lookup timings surfaced by the .NET networking stack
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
namespace Crucible.Common.ServiceDefaults.OpenTelemetry;
5+
6+
public sealed class OpenTelemetryOptions
7+
{
8+
public bool AddAlwaysOnTracingSampler { get; set; } = false;
9+
public bool AddConsoleExporter { get; set; } = false;
10+
public bool AddPrometheusExporter { get; set; } = false;
11+
public IEnumerable<string> CustomActivitySources { get; set; } = [];
12+
public bool IncludeDefaultActivitySources { get; set; } = true;
13+
public IEnumerable<string> CustomMeters { get; set; } = [];
14+
public bool IncludeDefaultMeters { get; set; } = true;
15+
}

0 commit comments

Comments
 (0)