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
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,10 @@ public static IResourceBuilder<OpenTelemetryCollectorResource> AddOpenTelemetryC

var isHttpsEnabled = !settings.ForceNonSecureReceiver && url.StartsWith("https", StringComparison.OrdinalIgnoreCase);

var dashboardOtlpEndpoint = ReplaceLocalhostWithContainerHost(url, builder.Configuration);

var resource = new OpenTelemetryCollectorResource(name);
var resourceBuilder = builder.AddResource(resource)
.WithImage(settings.CollectorImage, settings.CollectorTag)
.WithEnvironment("ASPIRE_ENDPOINT", dashboardOtlpEndpoint)
.WithEnvironment("ASPIRE_ENDPOINT", new HostUrl(url))
.WithEnvironment("ASPIRE_API_KEY", builder.Configuration[DashboardOtlpApiKeyVariableName]);

if (settings.EnableGrpcEndpoint)
Expand All @@ -53,8 +51,11 @@ public static IResourceBuilder<OpenTelemetryCollectorResource> AddOpenTelemetryC
if (!settings.ForceNonSecureReceiver && isHttpsEnabled && builder.ExecutionContext.IsRunMode && builder.Environment.IsDevelopment())
{
resourceBuilder.RunWithHttpsDevCertificate();
var certFilePath = Path.Combine(DevCertHostingExtensions.DEV_CERT_BIND_MOUNT_DEST_DIR, DevCertHostingExtensions.CERT_FILE_NAME);
var certKeyPath = Path.Combine(DevCertHostingExtensions.DEV_CERT_BIND_MOUNT_DEST_DIR, DevCertHostingExtensions.CERT_KEY_FILE_NAME);

// Not using `Path.Combine` as we MUST use unix style paths in the container
var certFilePath = $"{DevCertHostingExtensions.DEV_CERT_BIND_MOUNT_DEST_DIR}/{DevCertHostingExtensions.CERT_FILE_NAME}";
var certKeyPath = $"{DevCertHostingExtensions.DEV_CERT_BIND_MOUNT_DEST_DIR}/{DevCertHostingExtensions.CERT_KEY_FILE_NAME}";

if (settings.EnableHttpEndpoint)
{
resourceBuilder.WithArgs(
Expand All @@ -78,19 +79,23 @@ public static IResourceBuilder<OpenTelemetryCollectorResource> AddOpenTelemetryC
/// <returns></returns>
public static IResourceBuilder<OpenTelemetryCollectorResource> WithAppForwarding(this IResourceBuilder<OpenTelemetryCollectorResource> builder)
{
builder.AddEnvironmentVariablesEventHook()
.WithFirstStartup();
builder.ApplicationBuilder.Eventing.Subscribe<BeforeStartEvent>((evt, ct) =>
{
var logger = evt.Services.GetRequiredService<ResourceLoggerService>().GetLogger(builder.Resource);
var otelSenders = evt.Model.Resources
.OfType<IResourceWithEnvironment>()
.Where(x => x.HasAnnotationOfType<OtlpExporterAnnotation>());

return builder;
}
foreach (var otelSender in otelSenders)
{
var otelSenderBuilder = builder.ApplicationBuilder.CreateResourceBuilder(otelSender);
otelSenderBuilder.WithOpenTelemetryCollectorRouting(builder);
}

private static string ReplaceLocalhostWithContainerHost(string value, IConfiguration configuration)
{
var hostName = configuration["AppHost:ContainerHostname"] ?? "host.docker.internal";
return Task.CompletedTask;
});

return value.Replace("localhost", hostName, StringComparison.OrdinalIgnoreCase)
.Replace("127.0.0.1", hostName)
.Replace("[::1]", hostName);
return builder;
}

/// <summary>
Expand All @@ -106,73 +111,4 @@ public static IResourceBuilder<OpenTelemetryCollectorResource> WithConfig(this I
.WithArgs($"--config=/config/{configFileInfo.Name}");
}

/// <summary>
/// Sets up the OnBeforeResourceStarted event to add a wait annotation to all resources that have the OtlpExporterAnnotation
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
private static IResourceBuilder<OpenTelemetryCollectorResource> WithFirstStartup(this IResourceBuilder<OpenTelemetryCollectorResource> builder)
{
builder.OnBeforeResourceStarted((collectorResource, beforeStartedEvent, cancellationToken) =>
{
var logger = beforeStartedEvent.Services.GetRequiredService<ResourceLoggerService>().GetLogger(collectorResource);
var appModel = beforeStartedEvent.Services.GetRequiredService<DistributedApplicationModel>();
var resources = appModel.GetProjectResources();

foreach (var resourceItem in resources.Where(r => r.HasAnnotationOfType<OtlpExporterAnnotation>()))
{
resourceItem.Annotations.Add(new WaitAnnotation(collectorResource, WaitType.WaitUntilHealthy));
}
return Task.CompletedTask;
});
return builder;
}

/// <summary>
/// Sets up the OnResourceEndpointsAllocated event to add/update the OTLP environment variables for the collector to the various resources
/// </summary>
/// <param name="builder"></param>
private static IResourceBuilder<OpenTelemetryCollectorResource> AddEnvironmentVariablesEventHook(this IResourceBuilder<OpenTelemetryCollectorResource> builder)
{
builder.OnResourceEndpointsAllocated((collectorResource, allocatedEvent, cancellationToken) =>
{
var logger = allocatedEvent.Services.GetRequiredService<ResourceLoggerService>().GetLogger(collectorResource);
var appModel = allocatedEvent.Services.GetRequiredService<DistributedApplicationModel>();
var resources = appModel.GetProjectResources();

var grpcEndpoint = collectorResource.GetEndpoint(collectorResource.GrpcEndpoint.EndpointName);
var httpEndpoint = collectorResource.GetEndpoint(collectorResource.HttpEndpoint.EndpointName);

if (!resources.Any())
{
logger.LogInformation("No resources to add Environment Variables to");
}

foreach (var resourceItem in resources.Where(r => r.HasAnnotationOfType<OtlpExporterAnnotation>()))
{
logger.LogDebug("Forwarding Telemetry for {name} to the collector", resourceItem.Name);
if (resourceItem is null) continue;

resourceItem.Annotations.Add(new EnvironmentCallbackAnnotation(context =>
{
var protocol = context.EnvironmentVariables.GetValueOrDefault("OTEL_EXPORTER_OTLP_PROTOCOL", "");
var endpoint = protocol.ToString() == "http/protobuf" ? httpEndpoint : grpcEndpoint;

if (endpoint is null)
{
logger.LogWarning("No {protocol} endpoint on the collector for {resourceName} to use",
protocol, resourceItem.Name);
return;
}

context.EnvironmentVariables.Remove("OTEL_EXPORTER_OTLP_ENDPOINT");
context.EnvironmentVariables.Add("OTEL_EXPORTER_OTLP_ENDPOINT", endpoint.Url);
}));
}

return Task.CompletedTask;
});

return builder;
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
using Aspire.Components.Common.Tests;
using Aspire.Hosting;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Xunit.Abstractions;

namespace CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.Tests;

public class ResourceCreationTests
public class ResourceCreationTests(ITestOutputHelper testOutputHelper)
{
[Fact]
public void CanCreateTheCollectorResource()
{
var builder = DistributedApplication.CreateBuilder();
var builder = TestDistributedApplicationBuilder.Create();

builder.AddOpenTelemetryCollector("collector")
.WithConfig("./config.yaml")
Expand Down Expand Up @@ -150,31 +150,33 @@ public void CanDisableBothEndpoints()
}

[Fact]
public void ContainerHasAspireEnvironmentVariables()
[RequiresDocker]
public async Task ContainerHasAspireEnvironmentVariables()
{
var builder = DistributedApplication.CreateBuilder();
using var builder = TestDistributedApplicationBuilder.Create()
.WithTestAndResourceLogging(testOutputHelper);
builder.Configuration["APPHOST:ContainerHostname"] = "what.ever";

builder.AddOpenTelemetryCollector("collector")
var collector = builder.AddOpenTelemetryCollector("collector")
.WithAppForwarding();

using var app = builder.Build();
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var collectorResource = appModel.Resources.OfType<OpenTelemetryCollectorResource>().SingleOrDefault();
Assert.NotNull(collectorResource);

var envs = collectorResource.Annotations.OfType<EnvironmentCallbackAnnotation>().ToList();
Assert.NotEmpty(envs);
var resourceNotificationService = app.Services.GetRequiredService<ResourceNotificationService>();

var context = new EnvironmentCallbackContext(new DistributedApplicationExecutionContext(new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run)));
foreach (var env in envs)
{
env.Callback(context);
}
await app.StartAsync();
await resourceNotificationService.WaitForResourceHealthyAsync(collector.Resource.Name);

Assert.True(resourceNotificationService.TryGetCurrentState(collector.Resource.Name, out var resourceEvent));

var envVars = resourceEvent.Snapshot.EnvironmentVariables.ToDictionary(k => k.Name, v => v.Value);

var endpoint = Assert.Contains("ASPIRE_ENDPOINT", envVars);
var apiKey = Assert.Contains("ASPIRE_API_KEY", envVars);

Assert.Contains("ASPIRE_ENDPOINT", context.EnvironmentVariables.Keys);
Assert.Contains("ASPIRE_API_KEY", context.EnvironmentVariables.Keys);
Assert.Equal("http://host.docker.internal:18889", context.EnvironmentVariables["ASPIRE_ENDPOINT"]);
Assert.NotNull(context.EnvironmentVariables["ASPIRE_API_KEY"]);
Assert.Equal($"http://what.ever:18889", endpoint);
Assert.NotNull(apiKey);
}

[Fact]
Expand Down
Loading