Skip to content

Commit cafef2a

Browse files
committed
Allow requiring OpenTelemetry protocol if needed
For some scenarios (mine is runnin it ASP.NET framework applications), gRPC is not supported. This change adds an overload for AddOtlpExporter APIs to have a required protocol. Since there's not a default http endpoint if one is not set, this will throw an exception pointing the user to set that variable.
1 parent ebea942 commit cafef2a

File tree

4 files changed

+181
-19
lines changed

4 files changed

+181
-19
lines changed

src/Aspire.Hosting/ApplicationModel/OtlpExporterAnnotation.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ namespace Aspire.Hosting.ApplicationModel;
1111
[DebuggerDisplay("Type = {GetType().Name,nq}")]
1212
public class OtlpExporterAnnotation : IResourceAnnotation
1313
{
14-
}
14+
/// <summary>
15+
/// Gets or sets the default protocol for the OTLP exporter.
16+
/// </summary>
17+
public OtlpProtocol? RequiredProtocol { get; init; }
18+
}

src/Aspire.Hosting/OtlpConfigurationExtensions.cs

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,30 @@ public static void AddOtlpEnvironment(IResource resource, IConfiguration configu
3232
// Add annotation to mark this resource as having OTLP exporter configured
3333
resource.Annotations.Add(new OtlpExporterAnnotation());
3434

35+
RegisterOtlpEnvironment(resource, configuration, environment);
36+
}
37+
38+
/// <summary>
39+
/// Configures OpenTelemetry in projects using environment variables.
40+
/// </summary>
41+
/// <param name="resource">The resource to add annotations to.</param>
42+
/// <param name="configuration">The configuration to use for the OTLP exporter endpoint URL.</param>
43+
/// <param name="environment">The host environment to check if the application is running in development mode.</param>
44+
/// <param name="protocol">The protocol to use for the OTLP exporter. If not set, it will try gRPC then Http.</param>
45+
public static void AddOtlpEnvironment(IResource resource, IConfiguration configuration, IHostEnvironment environment, OtlpProtocol protocol)
46+
{
47+
ArgumentNullException.ThrowIfNull(resource);
48+
ArgumentNullException.ThrowIfNull(configuration);
49+
ArgumentNullException.ThrowIfNull(environment);
50+
51+
// Add annotation to mark this resource as having OTLP exporter configured
52+
resource.Annotations.Add(new OtlpExporterAnnotation { RequiredProtocol = protocol });
53+
54+
RegisterOtlpEnvironment(resource, configuration, environment);
55+
}
56+
57+
private static void RegisterOtlpEnvironment(IResource resource, IConfiguration configuration, IHostEnvironment environment)
58+
{
3559
// Configure OpenTelemetry in projects using environment variables.
3660
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md
3761

@@ -43,26 +67,13 @@ public static void AddOtlpEnvironment(IResource resource, IConfiguration configu
4367
return;
4468
}
4569

46-
var dashboardOtlpGrpcUrl = configuration.GetString(KnownConfigNames.DashboardOtlpGrpcEndpointUrl, KnownConfigNames.Legacy.DashboardOtlpGrpcEndpointUrl);
47-
var dashboardOtlpHttpUrl = configuration.GetString(KnownConfigNames.DashboardOtlpHttpEndpointUrl, KnownConfigNames.Legacy.DashboardOtlpHttpEndpointUrl);
48-
49-
// The dashboard can support OTLP/gRPC and OTLP/HTTP endpoints at the same time, but it can
50-
// only tell resources about one of the endpoints via environment variables.
51-
// If both OTLP/gRPC and OTLP/HTTP are available then prefer gRPC.
52-
if (dashboardOtlpGrpcUrl != null)
53-
{
54-
SetOtelEndpointAndProtocol(context.EnvironmentVariables, dashboardOtlpGrpcUrl, "grpc");
55-
}
56-
else if (dashboardOtlpHttpUrl != null)
57-
{
58-
SetOtelEndpointAndProtocol(context.EnvironmentVariables, dashboardOtlpHttpUrl, "http/protobuf");
59-
}
60-
else
70+
if (!resource.TryGetLastAnnotation<OtlpExporterAnnotation>(out var otlpExporterAnnotation))
6171
{
62-
// No endpoints provided to host. Use default value for URL.
63-
SetOtelEndpointAndProtocol(context.EnvironmentVariables, DashboardOtlpUrlDefaultValue, "grpc");
72+
return;
6473
}
6574

75+
SetOtelEndpointAndProtocol(context, configuration, otlpExporterAnnotation);
76+
6677
// Set the service name and instance id to the resource name and UID. Values are injected by DCP.
6778
var dcpDependencyCheckService = context.ExecutionContext.ServiceProvider.GetRequiredService<IDcpDependencyCheckService>();
6879
var dcpInfo = await dcpDependencyCheckService.GetDcpInfoAsync(cancellationToken: context.CancellationToken).ConfigureAwait(false);
@@ -91,6 +102,42 @@ public static void AddOtlpEnvironment(IResource resource, IConfiguration configu
91102
}
92103
}));
93104

105+
static void SetOtelEndpointAndProtocol(EnvironmentCallbackContext context, IConfiguration configuration, OtlpExporterAnnotation otlpExporterAnnotation)
106+
{
107+
var dashboardOtlpGrpcUrl = configuration.GetString(KnownConfigNames.DashboardOtlpGrpcEndpointUrl, KnownConfigNames.Legacy.DashboardOtlpGrpcEndpointUrl);
108+
var dashboardOtlpHttpUrl = configuration.GetString(KnownConfigNames.DashboardOtlpHttpEndpointUrl, KnownConfigNames.Legacy.DashboardOtlpHttpEndpointUrl);
109+
110+
// Check if a specific protocol is required by the annotation
111+
if (otlpExporterAnnotation.RequiredProtocol is OtlpProtocol.Grpc)
112+
{
113+
SetOtelEndpointAndProtocol(context.EnvironmentVariables, dashboardOtlpGrpcUrl ?? DashboardOtlpUrlDefaultValue, "grpc");
114+
}
115+
else if (otlpExporterAnnotation.RequiredProtocol is OtlpProtocol.HttpProtobuf)
116+
{
117+
SetOtelEndpointAndProtocol(context.EnvironmentVariables, dashboardOtlpHttpUrl ?? throw new InvalidOperationException("OtlpExporter is configured to require http/protobuf, but no endpoint was configured for ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"), "http/protobuf");
118+
}
119+
else
120+
{
121+
// No specific protocol required, use the existing preference logic
122+
// The dashboard can support OTLP/gRPC and OTLP/HTTP endpoints at the same time, but it can
123+
// only tell resources about one of the endpoints via environment variables.
124+
// If both OTLP/gRPC and OTLP/HTTP are available then prefer gRPC.
125+
if (dashboardOtlpGrpcUrl is not null)
126+
{
127+
SetOtelEndpointAndProtocol(context.EnvironmentVariables, dashboardOtlpGrpcUrl, "grpc");
128+
}
129+
else if (dashboardOtlpHttpUrl is not null)
130+
{
131+
SetOtelEndpointAndProtocol(context.EnvironmentVariables, dashboardOtlpHttpUrl, "http/protobuf");
132+
}
133+
else
134+
{
135+
// No endpoints provided to host. Use default value for URL.
136+
SetOtelEndpointAndProtocol(context.EnvironmentVariables, DashboardOtlpUrlDefaultValue, "grpc");
137+
}
138+
}
139+
}
140+
94141
static void SetOtelEndpointAndProtocol(Dictionary<string, object> environmentVariables, string url, string protocol)
95142
{
96143
environmentVariables["OTEL_EXPORTER_OTLP_ENDPOINT"] = new HostUrl(url);
@@ -112,7 +159,26 @@ public static IResourceBuilder<T> WithOtlpExporter<T>(this IResourceBuilder<T> b
112159
ArgumentNullException.ThrowIfNull(builder);
113160

114161
AddOtlpEnvironment(builder.Resource, builder.ApplicationBuilder.Configuration, builder.ApplicationBuilder.Environment);
115-
162+
163+
return builder;
164+
}
165+
166+
/// <summary>
167+
/// Injects the appropriate environment variables to allow the resource to enable sending telemetry to the dashboard.
168+
/// 1. It sets the OTLP endpoint to the value of the ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL environment variable.
169+
/// 2. It sets the service name and instance id to the resource name and UID. Values are injected by the orchestrator.
170+
/// 3. It sets a small batch schedule delay in development. This reduces the delay that OTLP exporter waits to sends telemetry and makes the dashboard telemetry pages responsive.
171+
/// </summary>
172+
/// <typeparam name="T">The resource type.</typeparam>
173+
/// <param name="builder">The resource builder.</param>
174+
/// <param name="protocol">The protocol to use for the OTLP exporter. If not set, it will try gRPC then Http.</param>
175+
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
176+
public static IResourceBuilder<T> WithOtlpExporter<T>(this IResourceBuilder<T> builder, OtlpProtocol protocol) where T : IResourceWithEnvironment
177+
{
178+
ArgumentNullException.ThrowIfNull(builder);
179+
180+
AddOtlpEnvironment(builder.Resource, builder.ApplicationBuilder.Configuration, builder.ApplicationBuilder.Environment, protocol);
181+
116182
return builder;
117183
}
118184
}

src/Aspire.Hosting/OtlpProtocol.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Aspire.Hosting;
5+
6+
/// <summary>
7+
/// Protocols available for OTLP exporters.
8+
/// </summary>
9+
public enum OtlpProtocol
10+
{
11+
/// <summary>
12+
/// A gRPC-based OTLP exporter.
13+
/// </summary>
14+
Grpc,
15+
16+
/// <summary>
17+
/// Http/Protobuf-based OTLP exporter.
18+
/// </summary>
19+
HttpProtobuf
20+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.Tests.Utils;
5+
using Aspire.Hosting.Utils;
6+
using Microsoft.AspNetCore.InternalTesting;
7+
using Microsoft.Extensions.DependencyInjection;
8+
9+
namespace Aspire.Hosting.Tests;
10+
11+
public class WithOtlpExporterTests
12+
{
13+
[InlineData(default, "http://localhost:8889", null, "http://localhost:8889", "grpc")]
14+
[InlineData(default, "http://localhost:8889", "http://localhost:8890", "http://localhost:8889", "grpc")]
15+
[InlineData(default, null, "http://localhost:8890", "http://localhost:8890", "http/protobuf")]
16+
[InlineData(OtlpProtocol.HttpProtobuf, "http://localhost:8889", "http://localhost:8890", "http://localhost:8890", "http/protobuf")]
17+
[InlineData(OtlpProtocol.Grpc, "http://localhost:8889", "http://localhost:8890", "http://localhost:8889", "grpc")]
18+
[InlineData(OtlpProtocol.Grpc, null, null, "http://localhost:18889", "grpc")]
19+
[Theory]
20+
public async Task OtlpEndpointSet(OtlpProtocol? protocol, string? grpcEndpoint, string? httpEndpoint, string expectedUrl, string expectedProtocol)
21+
{
22+
using var builder = TestDistributedApplicationBuilder.Create();
23+
24+
builder.Configuration["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = grpcEndpoint;
25+
builder.Configuration["ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"] = httpEndpoint;
26+
27+
var container = builder.AddResource(new ContainerResource("testSource"));
28+
29+
if (protocol is { } value)
30+
{
31+
container = container.WithOtlpExporter(value);
32+
}
33+
else
34+
{
35+
container = container.WithOtlpExporter();
36+
}
37+
38+
using var app = builder.Build();
39+
40+
var serviceProvider = app.Services.GetRequiredService<IServiceProvider>();
41+
42+
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
43+
container.Resource,
44+
serviceProvider: serviceProvider
45+
).DefaultTimeout();
46+
47+
Assert.Equal(expectedUrl, config["OTEL_EXPORTER_OTLP_ENDPOINT"]);
48+
Assert.Equal(expectedProtocol, config["OTEL_EXPORTER_OTLP_PROTOCOL"]);
49+
}
50+
51+
[Fact]
52+
public async Task RequiredHttpOtlpThrowsExceptionIfNotRegistered()
53+
{
54+
using var builder = TestDistributedApplicationBuilder.Create();
55+
56+
builder.Configuration["ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"] = null;
57+
58+
var container = builder.AddResource(new ContainerResource("testSource"))
59+
.WithOtlpExporter(OtlpProtocol.HttpProtobuf);
60+
61+
using var app = builder.Build();
62+
63+
var serviceProvider = app.Services.GetRequiredService<IServiceProvider>();
64+
65+
await Assert.ThrowsAsync<InvalidOperationException>(() =>
66+
EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
67+
container.Resource,
68+
serviceProvider: serviceProvider
69+
).DefaultTimeout()
70+
);
71+
}
72+
}

0 commit comments

Comments
 (0)