diff --git a/src/Aspire.Hosting/ApplicationModel/OtlpExporterAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/OtlpExporterAnnotation.cs
index c2698c7f188..abe67a95231 100644
--- a/src/Aspire.Hosting/ApplicationModel/OtlpExporterAnnotation.cs
+++ b/src/Aspire.Hosting/ApplicationModel/OtlpExporterAnnotation.cs
@@ -11,4 +11,8 @@ namespace Aspire.Hosting.ApplicationModel;
[DebuggerDisplay("Type = {GetType().Name,nq}")]
public class OtlpExporterAnnotation : IResourceAnnotation
{
-}
\ No newline at end of file
+ ///
+ /// Gets or sets the default protocol for the OTLP exporter.
+ ///
+ public OtlpProtocol? RequiredProtocol { get; init; }
+}
diff --git a/src/Aspire.Hosting/OtlpConfigurationExtensions.cs b/src/Aspire.Hosting/OtlpConfigurationExtensions.cs
index 024cf9407fb..1c3be620536 100644
--- a/src/Aspire.Hosting/OtlpConfigurationExtensions.cs
+++ b/src/Aspire.Hosting/OtlpConfigurationExtensions.cs
@@ -32,6 +32,30 @@ public static void AddOtlpEnvironment(IResource resource, IConfiguration configu
// Add annotation to mark this resource as having OTLP exporter configured
resource.Annotations.Add(new OtlpExporterAnnotation());
+ RegisterOtlpEnvironment(resource, configuration, environment);
+ }
+
+ ///
+ /// Configures OpenTelemetry in projects using environment variables.
+ ///
+ /// The resource to add annotations to.
+ /// The configuration to use for the OTLP exporter endpoint URL.
+ /// The host environment to check if the application is running in development mode.
+ /// The protocol to use for the OTLP exporter. If not set, it will try gRPC then Http.
+ public static void AddOtlpEnvironment(IResource resource, IConfiguration configuration, IHostEnvironment environment, OtlpProtocol protocol)
+ {
+ ArgumentNullException.ThrowIfNull(resource);
+ ArgumentNullException.ThrowIfNull(configuration);
+ ArgumentNullException.ThrowIfNull(environment);
+
+ // Add annotation to mark this resource as having OTLP exporter configured with a required protocol
+ resource.Annotations.Add(new OtlpExporterAnnotation { RequiredProtocol = protocol });
+
+ RegisterOtlpEnvironment(resource, configuration, environment);
+ }
+
+ private static void RegisterOtlpEnvironment(IResource resource, IConfiguration configuration, IHostEnvironment environment)
+ {
// Configure OpenTelemetry in projects using environment variables.
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md
@@ -43,26 +67,13 @@ public static void AddOtlpEnvironment(IResource resource, IConfiguration configu
return;
}
- var dashboardOtlpGrpcUrl = configuration.GetString(KnownConfigNames.DashboardOtlpGrpcEndpointUrl, KnownConfigNames.Legacy.DashboardOtlpGrpcEndpointUrl);
- var dashboardOtlpHttpUrl = configuration.GetString(KnownConfigNames.DashboardOtlpHttpEndpointUrl, KnownConfigNames.Legacy.DashboardOtlpHttpEndpointUrl);
-
- // The dashboard can support OTLP/gRPC and OTLP/HTTP endpoints at the same time, but it can
- // only tell resources about one of the endpoints via environment variables.
- // If both OTLP/gRPC and OTLP/HTTP are available then prefer gRPC.
- if (dashboardOtlpGrpcUrl != null)
- {
- SetOtelEndpointAndProtocol(context.EnvironmentVariables, dashboardOtlpGrpcUrl, "grpc");
- }
- else if (dashboardOtlpHttpUrl != null)
+ if (!resource.TryGetLastAnnotation(out var otlpExporterAnnotation))
{
- SetOtelEndpointAndProtocol(context.EnvironmentVariables, dashboardOtlpHttpUrl, "http/protobuf");
- }
- else
- {
- // No endpoints provided to host. Use default value for URL.
- SetOtelEndpointAndProtocol(context.EnvironmentVariables, DashboardOtlpUrlDefaultValue, "grpc");
+ return;
}
+ SetOtel(context, configuration, otlpExporterAnnotation.RequiredProtocol);
+
// Set the service name and instance id to the resource name and UID. Values are injected by DCP.
var dcpDependencyCheckService = context.ExecutionContext.ServiceProvider.GetRequiredService();
var dcpInfo = await dcpDependencyCheckService.GetDcpInfoAsync(cancellationToken: context.CancellationToken).ConfigureAwait(false);
@@ -91,6 +102,42 @@ public static void AddOtlpEnvironment(IResource resource, IConfiguration configu
}
}));
+ static void SetOtel(EnvironmentCallbackContext context, IConfiguration configuration, OtlpProtocol? requiredProtocol)
+ {
+ var dashboardOtlpGrpcUrl = configuration.GetString(KnownConfigNames.DashboardOtlpGrpcEndpointUrl, KnownConfigNames.Legacy.DashboardOtlpGrpcEndpointUrl);
+ var dashboardOtlpHttpUrl = configuration.GetString(KnownConfigNames.DashboardOtlpHttpEndpointUrl, KnownConfigNames.Legacy.DashboardOtlpHttpEndpointUrl);
+
+ // Check if a specific protocol is required by the annotation
+ if (requiredProtocol is OtlpProtocol.Grpc)
+ {
+ SetOtelEndpointAndProtocol(context.EnvironmentVariables, dashboardOtlpGrpcUrl ?? DashboardOtlpUrlDefaultValue, "grpc");
+ }
+ else if (requiredProtocol is OtlpProtocol.HttpProtobuf)
+ {
+ 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");
+ }
+ else
+ {
+ // No specific protocol required, use the existing preference logic
+ // The dashboard can support OTLP/gRPC and OTLP/HTTP endpoints at the same time, but it can
+ // only tell resources about one of the endpoints via environment variables.
+ // If both OTLP/gRPC and OTLP/HTTP are available then prefer gRPC.
+ if (dashboardOtlpGrpcUrl is not null)
+ {
+ SetOtelEndpointAndProtocol(context.EnvironmentVariables, dashboardOtlpGrpcUrl, "grpc");
+ }
+ else if (dashboardOtlpHttpUrl is not null)
+ {
+ SetOtelEndpointAndProtocol(context.EnvironmentVariables, dashboardOtlpHttpUrl, "http/protobuf");
+ }
+ else
+ {
+ // No endpoints provided to host. Use default value for URL.
+ SetOtelEndpointAndProtocol(context.EnvironmentVariables, DashboardOtlpUrlDefaultValue, "grpc");
+ }
+ }
+ }
+
static void SetOtelEndpointAndProtocol(Dictionary environmentVariables, string url, string protocol)
{
environmentVariables["OTEL_EXPORTER_OTLP_ENDPOINT"] = new HostUrl(url);
@@ -117,4 +164,25 @@ public static IResourceBuilder WithOtlpExporter(this IResourceBuilder b
return builder;
}
+
+ ///
+ /// Injects the appropriate environment variables to allow the resource to enable sending telemetry to the dashboard.
+ ///
+ /// - It sets the OTLP endpoint to the value of the ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL environment variable.
+ /// - It sets the service name and instance id to the resource name and UID. Values are injected by the orchestrator.
+ /// - 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.
+ ///
+ ///
+ /// The resource type.
+ /// The resource builder.
+ /// The protocol to use for the OTLP exporter. If not set, it will try gRPC then Http.
+ /// The .
+ public static IResourceBuilder WithOtlpExporter(this IResourceBuilder builder, OtlpProtocol protocol) where T : IResourceWithEnvironment
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ AddOtlpEnvironment(builder.Resource, builder.ApplicationBuilder.Configuration, builder.ApplicationBuilder.Environment, protocol);
+
+ return builder;
+ }
}
diff --git a/src/Aspire.Hosting/OtlpProtocol.cs b/src/Aspire.Hosting/OtlpProtocol.cs
new file mode 100644
index 00000000000..1f02e7b7251
--- /dev/null
+++ b/src/Aspire.Hosting/OtlpProtocol.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting;
+
+///
+/// Protocols available for OTLP exporters.
+///
+public enum OtlpProtocol
+{
+ ///
+ /// A gRPC-based OTLP exporter.
+ ///
+ Grpc,
+
+ ///
+ /// Http/Protobuf-based OTLP exporter.
+ ///
+ HttpProtobuf
+}
diff --git a/tests/Aspire.Hosting.Tests/WithOtlpExporterTests.cs b/tests/Aspire.Hosting.Tests/WithOtlpExporterTests.cs
new file mode 100644
index 00000000000..f6a966e0099
--- /dev/null
+++ b/tests/Aspire.Hosting.Tests/WithOtlpExporterTests.cs
@@ -0,0 +1,72 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.Tests.Utils;
+using Aspire.Hosting.Utils;
+using Microsoft.AspNetCore.InternalTesting;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Aspire.Hosting.Tests;
+
+public class WithOtlpExporterTests
+{
+ [InlineData(default, "http://localhost:8889", null, "http://localhost:8889", "grpc")]
+ [InlineData(default, "http://localhost:8889", "http://localhost:8890", "http://localhost:8889", "grpc")]
+ [InlineData(default, null, "http://localhost:8890", "http://localhost:8890", "http/protobuf")]
+ [InlineData(OtlpProtocol.HttpProtobuf, "http://localhost:8889", "http://localhost:8890", "http://localhost:8890", "http/protobuf")]
+ [InlineData(OtlpProtocol.Grpc, "http://localhost:8889", "http://localhost:8890", "http://localhost:8889", "grpc")]
+ [InlineData(OtlpProtocol.Grpc, null, null, "http://localhost:18889", "grpc")]
+ [Theory]
+ public async Task OtlpEndpointSet(OtlpProtocol? protocol, string? grpcEndpoint, string? httpEndpoint, string expectedUrl, string expectedProtocol)
+ {
+ using var builder = TestDistributedApplicationBuilder.Create();
+
+ builder.Configuration["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = grpcEndpoint;
+ builder.Configuration["ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"] = httpEndpoint;
+
+ var container = builder.AddResource(new ContainerResource("testSource"));
+
+ if (protocol is { } value)
+ {
+ container = container.WithOtlpExporter(value);
+ }
+ else
+ {
+ container = container.WithOtlpExporter();
+ }
+
+ using var app = builder.Build();
+
+ var serviceProvider = app.Services.GetRequiredService();
+
+ var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
+ container.Resource,
+ serviceProvider: serviceProvider
+ ).DefaultTimeout();
+
+ Assert.Equal(expectedUrl, config["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+ Assert.Equal(expectedProtocol, config["OTEL_EXPORTER_OTLP_PROTOCOL"]);
+ }
+
+ [Fact]
+ public async Task RequiredHttpOtlpThrowsExceptionIfNotRegistered()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create();
+
+ builder.Configuration["ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"] = null;
+
+ var container = builder.AddResource(new ContainerResource("testSource"))
+ .WithOtlpExporter(OtlpProtocol.HttpProtobuf);
+
+ using var app = builder.Build();
+
+ var serviceProvider = app.Services.GetRequiredService();
+
+ await Assert.ThrowsAsync(() =>
+ EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
+ container.Resource,
+ serviceProvider: serviceProvider
+ ).DefaultTimeout()
+ );
+ }
+}