File: Dcp\OtlpEndpointReferenceGatherer.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.Dcp;
 
/// <summary>
/// For containers, it replaces OTLP endpoint environemnt variable value with a reference to dashboard OTLP ingestion endpoint.
/// </summary>
/// <remarks>
/// In run mode, the dashboard plays the role of an OTLP collector, but the dashboard resouce is added dynamically,
/// just before the application started. That is why the OTLP configuration extension methods use configuration only.
/// OTOH, DCP has full model to work with, and can replace the OTLP endpoint environment variables with references
/// to the dashboard OTLP ingestion endpoint. For containers this allows DCP to tunnel these properly into container networks.
/// </remarks>
internal class OtlpEndpointReferenceGatherer : IExecutionConfigurationGatherer
{
    public async ValueTask GatherAsync(IExecutionConfigurationGathererContext context, IResource resource, ILogger resourceLogger, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken = default)
    {
        if (!resource.IsContainer() || !resource.TryGetLastAnnotation<OtlpExporterAnnotation>(out var oea))
        {
            // This gatherer is only relevant for container resources that emit OTEL telemetry.
            return;
        }
 
        if (!context.EnvironmentVariables.TryGetValue(OtlpConfigurationExtensions.OtlpEndpointEnvironmentVariableName, out _))
        {
            // If the OTLP endpoint is not set, do not try to set it.
            return;
        }
 
        var model = executionContext.ServiceProvider.GetService<DistributedApplicationModel>();
        if (model is null)
        {
            // Tests may not have a full model
            return;
        }
 
        var dashboardResource = model.Resources.SingleOrDefault(r => StringComparers.ResourceName.Equals(r.Name, KnownResourceNames.AspireDashboard)) as IResourceWithEndpoints;
        if (dashboardResource == null)
        {
            // Most test runs do not include the dashboard, and that's ok. If the dashboard is not present, do not try to set the OTLP endpoint.
            return;
        }
 
        if (!dashboardResource.TryGetEndpoints(out var dashboardEndpoints))
        {
            Debug.Fail("Dashboard does not have any endpoints??");
            return;
        }
 
        var grpcEndpoint = dashboardEndpoints.FirstOrDefault(e => e.Name == KnownEndpointNames.OtlpGrpcEndpointName);
        var httpEndpoint = dashboardEndpoints.FirstOrDefault(e => e.Name == KnownEndpointNames.OtlpHttpEndpointName);
        var resourceNetwork = resource.GetDefaultResourceNetwork();
 
        var endpointReference = (oea.RequiredProtocol, grpcEndpoint, httpEndpoint) switch
        {
            (OtlpProtocol.Grpc, not null, _) => new EndpointReference(dashboardResource, grpcEndpoint, resourceNetwork),
            (OtlpProtocol.HttpProtobuf or OtlpProtocol.HttpJson, _, not null) => new EndpointReference(dashboardResource, httpEndpoint, resourceNetwork),
            (_, not null, _) => new EndpointReference(dashboardResource, grpcEndpoint, resourceNetwork),
            (_, _, not null) => new EndpointReference(dashboardResource, httpEndpoint, resourceNetwork),
            _ => null
        };
        Debug.Assert(endpointReference != null, "Dashboard should have at least one matching OTLP endpoint");
 
        if (endpointReference is not null)
        {
            ValueProviderContext vpc = new() { ExecutionContext = executionContext, Caller = resource, Network = resourceNetwork };
            var url = await endpointReference.GetValueAsync(vpc, cancellationToken).ConfigureAwait(false);
            Debug.Assert(url is not null, $"We should be able to get a URL value from the reference dashboard endpoint '{endpointReference.EndpointName}'");
            if (url is not null)
            {
                context.EnvironmentVariables[OtlpConfigurationExtensions.OtlpEndpointEnvironmentVariableName] = url;
            }
        }
    }
}