Skip to content
Merged
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
34 changes: 34 additions & 0 deletions dotnet/src/SmooAI.Observability/Otel/ObservabilitySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,24 @@ public sealed class SetupOtelOptions
/// metrics from, beyond the SDK's default meter.
/// </summary>
public IReadOnlyList<string>? AdditionalMeterNames { get; set; }

/// <summary>
/// Opt-in: collect ASP.NET Core HTTP <em>server</em> spans + metrics via
/// <c>OpenTelemetry.Instrumentation.AspNetCore</c>. Off by default to keep the
/// SDK lean — consumers who serve HTTP set this to <c>true</c> and get
/// incoming-request traces/metrics without hand-wiring
/// <see cref="AdditionalActivitySources"/>. Only takes effect when the
/// matching exporter endpoint is configured.
/// </summary>
public bool EnableAspNetCoreInstrumentation { get; set; }

/// <summary>
/// Opt-in: collect outbound <see cref="System.Net.Http.HttpClient"/>
/// <em>client</em> spans + metrics via <c>OpenTelemetry.Instrumentation.Http</c>.
/// Off by default. Only takes effect when the matching exporter endpoint is
/// configured.
/// </summary>
public bool EnableHttpInstrumentation { get; set; }
}

/// <summary>
Expand Down Expand Up @@ -184,6 +202,14 @@ private static OtelSdkHandle Build(SetupOtelOptions options)
{
tracerBuilder.AddSource(options.AdditionalActivitySources.ToArray());
}
if (options.EnableAspNetCoreInstrumentation)
{
tracerBuilder.AddAspNetCoreInstrumentation();
}
if (options.EnableHttpInstrumentation)
{
tracerBuilder.AddHttpClientInstrumentation();
}
tracerBuilder.AddOtlpExporter(o => ConfigureExporter(o, options.OtlpTracesEndpoint!, options));
tracerProvider = tracerBuilder.Build();
}
Expand All @@ -198,6 +224,14 @@ private static OtelSdkHandle Build(SetupOtelOptions options)
{
meterBuilder.AddMeter(options.AdditionalMeterNames.ToArray());
}
if (options.EnableAspNetCoreInstrumentation)
{
meterBuilder.AddAspNetCoreInstrumentation();
}
if (options.EnableHttpInstrumentation)
{
meterBuilder.AddHttpClientInstrumentation();
}
meterBuilder.AddOtlpExporter((exporterOptions, readerOptions) =>
{
ConfigureExporter(exporterOptions, options.OtlpMetricsEndpoint!, options);
Expand Down
9 changes: 9 additions & 0 deletions dotnet/src/SmooAI.Observability/SmooAI.Observability.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@
<ItemGroup>
<PackageReference Include="OpenTelemetry" Version="1.16.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.16.0" />
<!--
Opt-in auto-instrumentation. Wired into ObservabilitySdk.Setup behind
SetupOtelOptions.EnableAspNetCoreInstrumentation / EnableHttpInstrumentation
(both default off). The instrumentation extension methods register the
relevant ActivitySources/Meters so consumers get HTTP server + client spans
without hand-wiring AdditionalActivitySources.
-->
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
</ItemGroup>

Expand Down
136 changes: 136 additions & 0 deletions dotnet/tests/SmooAI.Observability.Tests/OtelInstrumentationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.Diagnostics;
using SmooAI.Observability.Otel;

namespace SmooAI.Observability.Tests;

/// <summary>
/// Verifies the opt-in ASP.NET Core + HttpClient auto-instrumentation wiring in
/// <see cref="ObservabilitySdk.Setup"/>. We assert on subscription rather than on
/// emitted spans: an <see cref="ActivitySource"/> reports
/// <see cref="ActivitySource.HasListeners"/> == true only when a tracer provider
/// is subscribed to it, so it's a reliable proxy for "the instrumentation
/// registered its source".
/// </summary>
public class OtelInstrumentationTests
{
// Source names the OTel instrumentation packages subscribe to. Stable across
// the 1.x line: the HttpClient instrumentation listens to System.Net.Http and
// the ASP.NET Core instrumentation listens to Microsoft.AspNetCore.
private const string HttpClientSourceName = "System.Net.Http";
private const string AspNetCoreSourceName = "Microsoft.AspNetCore";

[Fact]
public void Default_DoesNotSubscribeToInstrumentationSources()
{
ObservabilitySdk.ResetForTests();
using var http = new ActivitySource(HttpClientSourceName);
using var aspnet = new ActivitySource(AspNetCoreSourceName);

// Sanity: no listeners before setup.
Assert.False(http.HasListeners());
Assert.False(aspnet.HasListeners());

var handle = ObservabilitySdk.Setup(new SetupOtelOptions
{
ServiceName = "svc",
OtlpTracesEndpoint = "https://ingest.test/v1/traces",
// EnableAspNetCoreInstrumentation / EnableHttpInstrumentation default to false.
});

Assert.False(http.HasListeners());
Assert.False(aspnet.HasListeners());

handle.Dispose();
ObservabilitySdk.ResetForTests();
}

[Fact]
public void EnableHttpInstrumentation_SubscribesToHttpClientSource()
{
ObservabilitySdk.ResetForTests();
using var http = new ActivitySource(HttpClientSourceName);
using var aspnet = new ActivitySource(AspNetCoreSourceName);

var handle = ObservabilitySdk.Setup(new SetupOtelOptions
{
ServiceName = "svc",
OtlpTracesEndpoint = "https://ingest.test/v1/traces",
EnableHttpInstrumentation = true,
});

Assert.True(http.HasListeners());
// Only HTTP requested — ASP.NET Core stays off.
Assert.False(aspnet.HasListeners());

handle.Dispose();
ObservabilitySdk.ResetForTests();
}

[Fact]
public void EnableAspNetCoreInstrumentation_SubscribesToAspNetCoreSource()
{
ObservabilitySdk.ResetForTests();
using var http = new ActivitySource(HttpClientSourceName);
using var aspnet = new ActivitySource(AspNetCoreSourceName);

var handle = ObservabilitySdk.Setup(new SetupOtelOptions
{
ServiceName = "svc",
OtlpTracesEndpoint = "https://ingest.test/v1/traces",
EnableAspNetCoreInstrumentation = true,
});

Assert.True(aspnet.HasListeners());
// Only ASP.NET Core requested — HTTP client stays off.
Assert.False(http.HasListeners());

handle.Dispose();
ObservabilitySdk.ResetForTests();
}

[Fact]
public void EnableBoth_SubscribesToBothSources()
{
ObservabilitySdk.ResetForTests();
using var http = new ActivitySource(HttpClientSourceName);
using var aspnet = new ActivitySource(AspNetCoreSourceName);

var handle = ObservabilitySdk.Setup(new SetupOtelOptions
{
ServiceName = "svc",
OtlpTracesEndpoint = "https://ingest.test/v1/traces",
OtlpMetricsEndpoint = "https://ingest.test/v1/metrics",
EnableAspNetCoreInstrumentation = true,
EnableHttpInstrumentation = true,
});

Assert.True(http.HasListeners());
Assert.True(aspnet.HasListeners());

handle.Dispose();
ObservabilitySdk.ResetForTests();
}

[Fact]
public void Enable_WithoutTracesEndpoint_DoesNotBuildTracerOrSubscribe()
{
ObservabilitySdk.ResetForTests();
using var http = new ActivitySource(HttpClientSourceName);
using var aspnet = new ActivitySource(AspNetCoreSourceName);

// No traces endpoint -> no tracer provider -> instrumentation cannot subscribe.
var handle = ObservabilitySdk.Setup(new SetupOtelOptions
{
ServiceName = "svc",
OtlpMetricsEndpoint = "https://ingest.test/v1/metrics",
EnableAspNetCoreInstrumentation = true,
EnableHttpInstrumentation = true,
});

Assert.False(http.HasListeners());
Assert.False(aspnet.HasListeners());

handle.Dispose();
ObservabilitySdk.ResetForTests();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,15 @@
<ProjectReference Include="..\..\src\SmooAI.Observability\SmooAI.Observability.csproj" />
</ItemGroup>

<!--
OpenTelemetry.Instrumentation.AspNetCore only subscribes to the
"Microsoft.AspNetCore" ActivitySource when the ASP.NET Core shared framework
is present. Without this reference the EnableAspNetCoreInstrumentation test
passes locally (framework on the dev box) but fails on a clean CI runner.
Reference it so the test exercises the instrumentation as a real host would.
-->
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

</Project>
Loading