// 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.ApplicationModel;
using Aspire.Hosting.Dcp.Model;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Aspire.Hosting;
/// <summary>
/// Provides extension methods for configuring OpenTelemetry in projects using environment variables.
/// </summary>
public static class OtlpConfigurationExtensions
{
/// <summary>
/// Configures OpenTelemetry in projects using environment variables.
/// </summary>
/// <param name="resource">The resource to add annotations to.</param>
/// <param name="configuration">The configuration to use for the OTLP exporter endpoint URL.</param>
/// <param name="environment">The host environment to check if the application is running in development mode.</param>
public static void AddOtlpEnvironment(IResource resource, IConfiguration configuration, IHostEnvironment environment)
{
ArgumentNullException.ThrowIfNull(resource);
ArgumentNullException.ThrowIfNull(configuration);
ArgumentNullException.ThrowIfNull(environment);
// Add annotation to mark this resource as having OTLP exporter configured
resource.Annotations.Add(new OtlpExporterAnnotation());
RegisterOtlpEnvironment(resource, configuration, environment);
}
/// <summary>
/// Configures OpenTelemetry in projects using environment variables.
/// </summary>
/// <param name="resource">The resource to add annotations to.</param>
/// <param name="configuration">The configuration to use for the OTLP exporter endpoint URL.</param>
/// <param name="environment">The host environment to check if the application is running in development mode.</param>
/// <param name="protocol">The protocol to use for the OTLP exporter. If not set, it will try gRPC then Http.</param>
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
resource.Annotations.Add(new EnvironmentCallbackAnnotation(async context =>
{
if (context.ExecutionContext.IsPublishMode)
{
// REVIEW: Do we want to set references to an imaginary otlp provider as a requirement?
return;
}
if (!resource.TryGetLastAnnotation<OtlpExporterAnnotation>(out var otlpExporterAnnotation))
{
return;
}
var dashboardEndpoint = ResolveOtlpEndpointFromDashboard(context, otlpExporterAnnotation.RequiredProtocol);
if (dashboardEndpoint is not null)
{
// Use the dashboard endpoint reference directly. This resolves to the actual allocated URL,
// including when ports are randomized (e.g. isolated mode).
context.EnvironmentVariables[KnownOtelConfigNames.ExporterOtlpEndpoint] = dashboardEndpoint.Value.Endpoint;
context.EnvironmentVariables[KnownOtelConfigNames.ExporterOtlpProtocol] = dashboardEndpoint.Value.Protocol;
}
else
{
// Fall back to resolving from configuration. This is the case when the dashboard resource
// is not in the model (e.g. in tests or publish mode).
var (url, protocol) = OtlpEndpointResolver.ResolveOtlpEndpoint(configuration, otlpExporterAnnotation.RequiredProtocol);
context.EnvironmentVariables[KnownOtelConfigNames.ExporterOtlpEndpoint] = new HostUrl(url);
context.EnvironmentVariables[KnownOtelConfigNames.ExporterOtlpProtocol] = protocol;
}
// Set the service name and instance id to the resource name and UID. Values are injected by DCP.
context.EnvironmentVariables[KnownOtelConfigNames.ResourceAttributes] = "service.instance.id={{- index .Annotations \"" + CustomResource.OtelServiceInstanceIdAnnotation + "\" -}}";
context.EnvironmentVariables[KnownOtelConfigNames.ServiceName] = "{{- index .Annotations \"" + CustomResource.OtelServiceNameAnnotation + "\" -}}";
if (configuration["AppHost:OtlpApiKey"] is { } otlpApiKey)
{
context.EnvironmentVariables[KnownOtelConfigNames.ExporterOtlpHeaders] = $"x-otlp-api-key={otlpApiKey}";
}
// Configure OTLP to quickly provide all data with a small delay in development.
if (environment.IsDevelopment())
{
// Set 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.
var value = "1000"; // milliseconds
context.EnvironmentVariables[KnownOtelConfigNames.BlrpScheduleDelay] = value;
context.EnvironmentVariables[KnownOtelConfigNames.BspScheduleDelay] = value;
context.EnvironmentVariables[KnownOtelConfigNames.MetricExportInterval] = value;
// Configure trace sampler to send all traces to the dashboard.
context.EnvironmentVariables[KnownOtelConfigNames.TracesSampler] = "always_on";
// Configure metrics to include exemplars.
context.EnvironmentVariables[KnownOtelConfigNames.MetricsExemplarFilter] = "trace_based";
// Output sensitive message content for GenAI.
// A convention for libraries that output GenAI telemetry is to use `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` env var.
// See:
// - https://opentelemetry.io/blog/2024/otel-generative-ai/
// - https://github.com/search?q=org%3Aopen-telemetry+OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT&type=code
context.EnvironmentVariables[KnownOtelConfigNames.InstrumentationGenAiCaptureMessageContent] = "true";
}
}));
}
/// <summary>
/// Injects the appropriate environment variables to allow the resource to enable sending telemetry to the dashboard.
/// <list type="number">
/// <item>It sets the OTLP endpoint to the value of the <c>ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL</c> environment variable.</item>
/// <item>It sets the service name and instance id to the resource name and UID. Values are injected by the orchestrator.</item>
/// <item>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.</item>
/// </list>
/// </summary>
/// <typeparam name="T">The resource type.</typeparam>
/// <param name="builder">The resource builder.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
[AspireExportIgnore(Reason = "Polyglot AppHosts use the internal withOtlpExporter dispatcher export.")]
public static IResourceBuilder<T> WithOtlpExporter<T>(this IResourceBuilder<T> builder) where T : IResourceWithEnvironment
{
ArgumentNullException.ThrowIfNull(builder);
AddOtlpEnvironment(builder.Resource, builder.ApplicationBuilder.Configuration, builder.ApplicationBuilder.Environment);
return builder;
}
/// <summary>
/// Configures OTLP telemetry export
/// </summary>
[AspireExport("withOtlpExporter")]
internal static IResourceBuilder<T> WithOtlpExporterForPolyglot<T>(
this IResourceBuilder<T> builder,
OtlpProtocol? protocol = null) where T : IResourceWithEnvironment
{
ArgumentNullException.ThrowIfNull(builder);
return protocol is null
? builder.WithOtlpExporter()
: builder.WithOtlpExporter(protocol.Value);
}
/// <summary>
/// Injects the appropriate environment variables to allow the resource to enable sending telemetry to the dashboard.
/// <list type="number">
/// <item>It sets the OTLP endpoint to the value of the <c>ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL</c> environment variable.</item>
/// <item>It sets the service name and instance id to the resource name and UID. Values are injected by the orchestrator.</item>
/// <item>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.</item>
/// </list>
/// </summary>
/// <typeparam name="T">The resource type.</typeparam>
/// <param name="builder">The resource builder.</param>
/// <param name="protocol">The protocol to use for the OTLP exporter. If not set, it will try gRPC then Http.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
[AspireExportIgnore(Reason = "Polyglot AppHosts use the internal withOtlpExporter dispatcher export.")]
public static IResourceBuilder<T> WithOtlpExporter<T>(this IResourceBuilder<T> builder, OtlpProtocol protocol) where T : IResourceWithEnvironment
{
ArgumentNullException.ThrowIfNull(builder);
AddOtlpEnvironment(builder.Resource, builder.ApplicationBuilder.Configuration, builder.ApplicationBuilder.Environment, protocol);
return builder;
}
/// <summary>
/// Tries to resolve the OTLP endpoint from the dashboard resource in the distributed application model.
/// This ensures that when ports are randomized (e.g. isolated mode), resources use the actual
/// allocated endpoint rather than the statically configured port.
/// </summary>
/// <remarks>
/// The returned <see cref="EndpointReference"/> has no network context baked in, so it resolves
/// using the calling resource's network at evaluation time. This means containers automatically
/// get container-network URLs and non-containers get localhost URLs.
/// </remarks>
private static (EndpointReference Endpoint, string Protocol)? ResolveOtlpEndpointFromDashboard(EnvironmentCallbackContext context, OtlpProtocol? requiredProtocol)
{
DistributedApplicationModel? model;
try
{
model = context.ExecutionContext.Services.GetService<DistributedApplicationModel>();
}
catch (InvalidOperationException)
{
// ServiceProvider may not be available if the container hasn't been built yet
// (e.g. env var evaluation during testing without a fully built host).
return null;
}
if (model is null)
{
return null;
}
if (!model.Resources.TryGetByName(KnownResourceNames.AspireDashboard, out var resource) || resource is not IResourceWithEndpoints dashboardResource)
{
return null;
}
var grpcEndpoint = dashboardResource.GetEndpoint(KnownEndpointNames.OtlpGrpcEndpointName);
var httpEndpoint = dashboardResource.GetEndpoint(KnownEndpointNames.OtlpHttpEndpointName);
return (requiredProtocol, grpcEndpoint.Exists, httpEndpoint.Exists) switch
{
(OtlpProtocol.Grpc, true, _) => (grpcEndpoint, "grpc"),
(OtlpProtocol.HttpProtobuf, _, true) => (httpEndpoint, "http/protobuf"),
(OtlpProtocol.HttpJson, _, true) => (httpEndpoint, "http/json"),
(_, true, _) => (grpcEndpoint, "grpc"),
(_, _, true) => (httpEndpoint, "http/protobuf"),
_ => null
};
}
}