Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/release-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
type: choice
options:
- Crucible.Common.Authentication
- Crucible.Common.ServiceDefaults
- Crucible.Common.Utilities
versionNumber:
description: "A semver version string (e.g. 1.0.0)"
Expand Down
2 changes: 2 additions & 0 deletions Crucible.Common.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
</Configurations>
<Folder Name="/src/">
<Project Path="src/Crucible.Common.Authentication/Crucible.Common.Authentication.csproj" />
<Project Path="src/Crucible.Common.ServiceDefaults/Crucible.Common.ServiceDefaults.csproj" />
<Project Path="src/Crucible.Common.Utilities/Crucible.Common.Utilities.csproj" />
</Folder>
<Folder Name="/test/">
<Project Path="test/Crucible.Common.Tests/Crucible.Common.Tests.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<!--README config-->
<PropertyGroup>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>

<!--LICENSE config-->
<PropertyGroup>
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
</PropertyGroup>

<ItemGroup>
<None Include="../../LICENSE.md" Pack="true" PackagePath="/" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.13.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.13.0-beta.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" Version="1.12.0-beta.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
</ItemGroup>
</Project>
3 changes: 3 additions & 0 deletions src/Crucible.Common.ServiceDefaults/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Crucible.Common.ServiceDefaults

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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// Copyright 2025 Carnegie Mellon University. All Rights Reserved.
// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;

namespace Crucible.Common.ServiceDefaults.OpenTelemetry;

public static class CrucibleOpenTelemetryExtensions
{
/// <summary>
/// Call to configure default configuration for OpenTelemetry-enhanced logging.
///
/// NOTE: This function is exposed primarily for apps created before .NET Core 8 that bootstrap with IHostBuilder rather than the newer IHostApplicationBuilder.
/// If your app uses IHostApplicationBuilder, you shouldn't need to call this function directly.
/// </summary>
/// <param name="logging"></param>
/// <returns></returns>
public static ILoggingBuilder AddCrucibleOpenTelemetryLogging(this ILoggingBuilder logging)
{
AddLogging(logging);
return logging;
}

/// <summary>
/// Call to configure default OpenTelemetry services. Customizable with the <cref>optionsBuilder</cref> parameter. See its properties for details.
///
/// NOTE: This function is exposed primarily for apps created before .NET Core 8 that bootstrap with IHostBuilder rather than the newer IHostApplicationBuilder.
/// If your app uses IHostApplicationBuilder, you shouldn't need to call this function directly.
/// </summary>
/// <param name="services">Your app's service collection.</param>
/// <param name="hostEnvironment">The hosting environment in which your app is starting up.</param>
/// <param name="configuration">Your app's configuration.</param>
/// <param name="optionsBuilder">A builder used to customize OpenTelemetry configuration.</param>
/// <returns></returns>
public static IServiceCollection AddCrucibleOpenTelemetryServices(this IServiceCollection services, IHostEnvironment hostEnvironment, IConfiguration configuration, Action<CrucibleOpenTelemetryOptions>? optionsBuilder = null)
{
var options = BuildOptions(optionsBuilder);

AddServices(services, hostEnvironment, options);
AddExporters(services, configuration["OTEL_EXPORTER_OTLP_ENDPOINT"], options);

return services;
}

/// <summary>
/// Add default service and logging configuration for OpenTelemetry. Customizable with the <cref>optionsBuilder</cref> parameter. See its properties for details.
/// </summary>
/// <param name="builder">Your app's </param>
/// <param name="optionsBuilder"></param>
/// <returns></returns>
public static IHostApplicationBuilder AddCrucibleOpenTelemetryServiceDefaults(this IHostApplicationBuilder builder, Action<CrucibleOpenTelemetryOptions>? optionsBuilder = null)
{
var options = BuildOptions(optionsBuilder);

builder.ConfigureOpenTelemetry(options);
// builder.AddDefaultHealthChecks();
// builder.Services.AddServiceDiscovery();

// builder.Services.ConfigureHttpClientDefaults(http =>
// {
// // Turn on resilience by default
// http.AddStandardResilienceHandler();

// // Turn on service discovery by default
// http.UseServiceDiscovery();
// });

return builder;
}

private static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder, CrucibleOpenTelemetryOptions options)
{
// configure logging
AddLogging(builder.Logging);
AddServices(builder.Services, builder.Environment, options);
AddExporters(builder.Services, builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"], options);

return builder;
}

private static void AddLogging(this ILoggingBuilder logging)
{
logging.AddOpenTelemetry(x =>
{
x.IncludeScopes = true;
x.IncludeFormattedMessage = true;

// Not doing this yet, but protects against "unknown_service" in traces/metrics, maybe?
// x.SetResourceBuilder
// (
// ResourceBuilder
// .CreateDefault()
// .AddService
// (
// serviceName: builder.Environment.ApplicationName,
// serviceVersion: typeof(Program).Assembly.GetName().Version?.ToString(),
// serviceInstanceId: Environment.MachineName
// )
// );
});
}

private static void AddServices(this IServiceCollection services, IHostEnvironment env, CrucibleOpenTelemetryOptions options)
{
services
.AddOpenTelemetry()
.WithMetrics(x =>
{
x.AddRuntimeInstrumentation();

if (options.IncludeDefaultMeters)
{
x.AddMeter
(
"Microsoft.AspNetCore.Hosting",
"Microsoft.AspNetCore.Server.Kestrel",
"Microsoft.EntityFrameworkCore",
"System.Net.Http"
);
}

if (options.CustomMeters.Any())
{
x.AddMeter([.. options.CustomMeters]);
}
})
.WithTracing(x =>
{
if (options.IncludeDefaultActivitySources)
{
x.AddSource("Microsoft.AspNetCore");
x.AddSource("Microsoft.EntityFrameworkCore");
x.AddSource("System.Net.Http");
}

if (options.CustomActivitySources.Any())
{
x.AddSource([.. options.CustomActivitySources]);
}

if (options.AddAlwaysOnTracingSampler)
{
x.SetSampler<AlwaysOnSampler>();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this have performance impacts in dev? Should it be optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this, yeah. This is for sure not a thing you want to do in production, according to multiple sources, but most examples have it on in dev. I don't mind parameterizing it like we do the other config. I'll add this.

}

x
// record structured logs for traces
.AddAspNetCoreInstrumentation(o => o.RecordException = true)
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation();
});
}

private static void AddExporters(this IServiceCollection services, string? otelExporterEndpoint, CrucibleOpenTelemetryOptions options)
{
var isOtlpEndpointConfigured = !string.IsNullOrWhiteSpace(otelExporterEndpoint);

services.Configure<OpenTelemetryLoggerOptions>(logging =>
{
if (isOtlpEndpointConfigured)
{
logging.AddOtlpExporter();
}

if (options.AddConsoleExporter)
{
logging.AddConsoleExporter();
}
});

services.ConfigureOpenTelemetryMeterProvider(metrics =>
{
if (isOtlpEndpointConfigured)
{
metrics.AddOtlpExporter();
}

if (options.AddConsoleExporter)
{
metrics.AddConsoleExporter();
}

if (options.AddPrometheusExporter)
{
metrics.AddPrometheusExporter();
}
});

services.ConfigureOpenTelemetryTracerProvider(tracing =>
{
if (isOtlpEndpointConfigured)
{
tracing.AddOtlpExporter();
}

if (options.AddConsoleExporter)
{
tracing.AddConsoleExporter();
}
});
}

private static CrucibleOpenTelemetryOptions BuildOptions(Action<CrucibleOpenTelemetryOptions>? optionsBuilder = null)
{
var options = new CrucibleOpenTelemetryOptions();

if (optionsBuilder is not null)
{
optionsBuilder(options);
}

return options;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2025 Carnegie Mellon University. All Rights Reserved.
// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information.

namespace Crucible.Common.ServiceDefaults.OpenTelemetry;

public sealed class CrucibleOpenTelemetryOptions
{
public bool AddAlwaysOnTracingSampler { get; set; } = false;
public bool AddConsoleExporter { get; set; } = false;
public bool AddPrometheusExporter { get; set; } = false;
public IEnumerable<string> CustomActivitySources { get; set; } = [];
public bool IncludeDefaultActivitySources { get; set; } = true;
public IEnumerable<string> CustomMeters { get; set; } = [];
public bool IncludeDefaultMeters { get; set; } = true;
}