Skip to content

Commit 7c26b63

Browse files
committed
Added OpenTelemetry Collector component
1 parent 8410e56 commit 7c26b63

File tree

7 files changed

+421
-0
lines changed

7 files changed

+421
-0
lines changed

CommunityToolkit.Aspire.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@
172172
<Project Path="src/CommunityToolkit.Aspire.Hosting.Ngrok/CommunityToolkit.Aspire.Hosting.Ngrok.csproj" />
173173
<Project Path="src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.csproj" />
174174
<Project Path="src/CommunityToolkit.Aspire.Hosting.Ollama/CommunityToolkit.Aspire.Hosting.Ollama.csproj" />
175+
<Project Path="src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector.csproj" />
175176
<Project Path="src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/CommunityToolkit.Aspire.Hosting.PapercutSmtp.csproj" />
176177
<Project Path="src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.csproj" />
177178
<Project Path="src/CommunityToolkit.Aspire.Hosting.PowerShell/CommunityToolkit.Aspire.Hosting.PowerShell.csproj" />
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using Aspire.Hosting.ApplicationModel;
2+
using Aspire.Hosting.Lifecycle;
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.Hosting;
5+
6+
namespace Aspire.Hosting;
7+
8+
/// <summary>
9+
/// Extension methods to add the collector resource
10+
/// </summary>
11+
public static class CollectorExtensions
12+
{
13+
private const string DashboardOtlpUrlVariableNameLegacy = "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL";
14+
private const string DashboardOtlpUrlVariableName = "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL";
15+
private const string DashboardOtlpApiKeyVariableName = "AppHost:OtlpApiKey";
16+
private const string DashboardOtlpUrlDefaultValue = "http://localhost:18889";
17+
18+
/// <summary>
19+
/// Adds an OpenTelemetry Collector into the Aspire AppHost
20+
/// </summary>
21+
/// <param name="builder"></param>
22+
/// <param name="name"></param>
23+
/// <param name="configureSettings"></param>
24+
/// <returns></returns>
25+
public static IResourceBuilder<CollectorResource> AddOpenTelemetryCollector(this IDistributedApplicationBuilder builder,
26+
string name,
27+
Action<OpenTelemetryCollectorSettings> configureSettings = null!)
28+
{
29+
var url = builder.Configuration[DashboardOtlpUrlVariableName] ??
30+
builder.Configuration[DashboardOtlpUrlVariableNameLegacy] ??
31+
DashboardOtlpUrlDefaultValue;
32+
33+
var settings = new OpenTelemetryCollectorSettings();
34+
configureSettings?.Invoke(settings);
35+
36+
var isHttpsEnabled = !settings.ForceNonSecureReceiver && url.StartsWith("https", StringComparison.OrdinalIgnoreCase);
37+
38+
var dashboardOtlpEndpoint = ReplaceLocalhostWithContainerHost(url, builder.Configuration);
39+
40+
var resource = new CollectorResource(name);
41+
var resourceBuilder = builder.AddResource(resource)
42+
.WithImage(settings.CollectorImage, settings.CollectorVersion)
43+
.WithEnvironment("ASPIRE_ENDPOINT", dashboardOtlpEndpoint)
44+
.WithEnvironment("ASPIRE_API_KEY", builder.Configuration[DashboardOtlpApiKeyVariableName]);
45+
46+
if (settings.EnableGrpcEndpoint)
47+
resourceBuilder.WithEndpoint(targetPort: 4317, name: CollectorResource.GRPCEndpointName, scheme: isHttpsEnabled ? "https" : "http");
48+
if (settings.EnableHttpEndpoint)
49+
resourceBuilder.WithEndpoint(targetPort: 4318, name: CollectorResource.HTTPEndpointName, scheme: isHttpsEnabled ? "https" : "http");
50+
51+
52+
if (!settings.ForceNonSecureReceiver && isHttpsEnabled && builder.ExecutionContext.IsRunMode && builder.Environment.IsDevelopment())
53+
{
54+
DevCertHostingExtensions.RunWithHttpsDevCertificate(resourceBuilder, "HTTPS_CERT_FILE", "HTTPS_CERT_KEY_FILE", (certFilePath, certKeyPath) =>
55+
{
56+
if (settings.EnableHttpEndpoint)
57+
{
58+
resourceBuilder.WithArgs(
59+
$@"--config=yaml:receivers::otlp::protocols::http::tls::cert_file: ""{certFilePath}""",
60+
$@"--config=yaml:receivers::otlp::protocols::http::tls::key_file: ""{certKeyPath}""");
61+
}
62+
if (settings.EnableGrpcEndpoint)
63+
{
64+
resourceBuilder.WithArgs(
65+
$@"--config=yaml:receivers::otlp::protocols::grpc::tls::cert_file: ""{certFilePath}""",
66+
$@"--config=yaml:receivers::otlp::protocols::grpc::tls::key_file: ""{certKeyPath}""");
67+
}
68+
});
69+
}
70+
return resourceBuilder;
71+
}
72+
73+
/// <summary>
74+
/// Force all apps to forward to the collector instead of the dashboard directly
75+
/// </summary>
76+
/// <param name="builder"></param>
77+
/// <returns></returns>
78+
public static IResourceBuilder<CollectorResource> WithAppForwarding(this IResourceBuilder<CollectorResource> builder)
79+
{
80+
builder.ApplicationBuilder.Services.TryAddLifecycleHook<EnvironmentVariableHook>();
81+
return builder;
82+
}
83+
84+
private static string ReplaceLocalhostWithContainerHost(string value, IConfiguration configuration)
85+
{
86+
var hostName = configuration["AppHost:ContainerHostname"] ?? "host.docker.internal";
87+
88+
return value.Replace("localhost", hostName, StringComparison.OrdinalIgnoreCase)
89+
.Replace("127.0.0.1", hostName)
90+
.Replace("[::1]", hostName);
91+
}
92+
93+
/// <summary>
94+
/// Adds a config file to the collector
95+
/// </summary>
96+
/// <param name="builder"></param>
97+
/// <param name="configPath"></param>
98+
/// <returns></returns>
99+
public static IResourceBuilder<CollectorResource> WithConfig(this IResourceBuilder<CollectorResource> builder, string configPath)
100+
{
101+
var configFileInfo = new FileInfo(configPath);
102+
return builder.WithBindMount(configPath, $"/config/{configFileInfo.Name}")
103+
.WithArgs($"--config=/config/{configFileInfo.Name}");
104+
}
105+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using Aspire.Hosting.ApplicationModel;
2+
3+
namespace Aspire.Hosting;
4+
5+
/// <summary>
6+
/// The collector resource
7+
/// </summary>
8+
/// <param name="name">Name of the resource</param>
9+
public class CollectorResource(string name) : ContainerResource(name)
10+
{
11+
internal static string GRPCEndpointName = "grpc";
12+
internal static string HTTPEndpointName = "http";
13+
14+
/// <summary>
15+
/// gRPC Endpoint
16+
/// </summary>
17+
public EndpointReference GRPCEndpoint => new(this, GRPCEndpointName);
18+
19+
/// <summary>
20+
/// HTTP Endpoint
21+
/// </summary>
22+
public EndpointReference HTTPEndpoint => new(this, HTTPEndpointName);
23+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<Description>An Aspire component to add an OpenTelemetry Collector into the OTLP pipeline</Description>
5+
<AdditionalPackageTags>opentelemetry observability</AdditionalPackageTags>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Aspire.Hosting" />
10+
</ItemGroup>
11+
</Project>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using Aspire.Hosting.ApplicationModel;
2+
using Aspire.Hosting.Lifecycle;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace Aspire.Hosting;
6+
7+
/// <summary>
8+
/// Hooks to add the OTLP environment variables to the various containers
9+
/// </summary>
10+
/// <param name="logger"></param>
11+
public class EnvironmentVariableHook(ILogger<EnvironmentVariableHook> logger) : IDistributedApplicationLifecycleHook
12+
{
13+
/// <inheritdoc />
14+
public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken)
15+
{
16+
var resources = appModel.GetProjectResources();
17+
var collectorResource = appModel.Resources.OfType<CollectorResource>().FirstOrDefault();
18+
19+
if (collectorResource is null)
20+
{
21+
logger.LogWarning("No collector resource found");
22+
return Task.CompletedTask;
23+
}
24+
25+
var grpcEndpoint = collectorResource!.GetEndpoint(collectorResource!.GRPCEndpoint.EndpointName);
26+
var httpEndpoint = collectorResource!.GetEndpoint(collectorResource!.HTTPEndpoint.EndpointName);
27+
28+
if (!resources.Any())
29+
{
30+
logger.LogInformation("No resources to add Environment Variables to");
31+
}
32+
33+
foreach (var resourceItem in resources)
34+
{
35+
logger.LogDebug("Forwarding Telemetry for {name} to the collector", resourceItem.Name);
36+
if (resourceItem is null) continue;
37+
38+
resourceItem.Annotations.Add(new EnvironmentCallbackAnnotation((EnvironmentCallbackContext context) =>
39+
{
40+
var protocol = context.EnvironmentVariables.GetValueOrDefault("OTEL_EXPORTER_OTLP_PROTOCOL", "");
41+
var endpoint = protocol.ToString() == "http/protobuf" ? httpEndpoint : grpcEndpoint;
42+
43+
if (endpoint == null)
44+
{
45+
logger.LogWarning("No {protocol} endpoint on the collector for {resourceName} to use",
46+
protocol, resourceItem.Name);
47+
return;
48+
}
49+
50+
if (context.EnvironmentVariables.ContainsKey("OTEL_EXPORTER_OTLP_ENDPOINT"))
51+
context.EnvironmentVariables.Remove("OTEL_EXPORTER_OTLP_ENDPOINT");
52+
context.EnvironmentVariables.Add("OTEL_EXPORTER_OTLP_ENDPOINT", endpoint.Url);
53+
}));
54+
}
55+
56+
return Task.CompletedTask;
57+
}
58+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
namespace Aspire.Hosting;
2+
3+
/// <summary>
4+
/// Settings for the OpenTelemetry Collector
5+
/// </summary>
6+
public class OpenTelemetryCollectorSettings
7+
{
8+
/// <summary>
9+
/// The version of the collector, defaults to latest
10+
/// </summary>
11+
public string CollectorVersion { get; set; } = "latest";
12+
13+
/// <summary>
14+
/// The image of the collector, defaults to ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib
15+
/// </summary>
16+
public string CollectorImage { get; set; } = "ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib";
17+
18+
/// <summary>
19+
/// Force the default OTLP receivers in the collector to use HTTP even if Aspire is set to HTTPS
20+
/// </summary>
21+
public bool ForceNonSecureReceiver { get; set; } = false;
22+
23+
/// <summary>
24+
/// Enable the gRPC endpoint on the collector container (requires the relevant collector config)
25+
///
26+
/// Note: this will also setup SSL if Aspire is configured for HTTPS
27+
/// </summary>
28+
public bool EnableGrpcEndpoint { get; set; } = true;
29+
30+
/// <summary>
31+
/// Enable the HTTP endpoint on the collector container (requires the relevant collector config)
32+
///
33+
/// Note: this will also setup SSL if Aspire is configured for HTTPS
34+
/// </summary>
35+
public bool EnableHttpEndpoint { get; set; } = true;
36+
}

0 commit comments

Comments
 (0)