-
Notifications
You must be signed in to change notification settings - Fork 110
Add OpenTelemetry Collector extension/component #603
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
7c26b63
e6c40fe
f347af6
aafae22
ef21d6b
b3a8d93
fe14d9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
using Aspire.Hosting.ApplicationModel; | ||
using Aspire.Hosting.Lifecycle; | ||
using Microsoft.Extensions.Configuration; | ||
using Microsoft.Extensions.Hosting; | ||
|
||
namespace Aspire.Hosting; | ||
|
||
/// <summary> | ||
/// Extension methods to add the collector resource | ||
/// </summary> | ||
public static class CollectorExtensions | ||
{ | ||
private const string DashboardOtlpUrlVariableNameLegacy = "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"; | ||
private const string DashboardOtlpUrlVariableName = "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"; | ||
private const string DashboardOtlpApiKeyVariableName = "AppHost:OtlpApiKey"; | ||
private const string DashboardOtlpUrlDefaultValue = "http://localhost:18889"; | ||
|
||
/// <summary> | ||
/// Adds an OpenTelemetry Collector into the Aspire AppHost | ||
/// </summary> | ||
/// <param name="builder"></param> | ||
/// <param name="name"></param> | ||
/// <param name="configureSettings"></param> | ||
/// <returns></returns> | ||
public static IResourceBuilder<CollectorResource> AddOpenTelemetryCollector(this IDistributedApplicationBuilder builder, | ||
string name, | ||
Action<OpenTelemetryCollectorSettings> configureSettings = null!) | ||
martinjt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
var url = builder.Configuration[DashboardOtlpUrlVariableName] ?? | ||
builder.Configuration[DashboardOtlpUrlVariableNameLegacy] ?? | ||
DashboardOtlpUrlDefaultValue; | ||
|
||
var settings = new OpenTelemetryCollectorSettings(); | ||
configureSettings?.Invoke(settings); | ||
|
||
var isHttpsEnabled = !settings.ForceNonSecureReceiver && url.StartsWith("https", StringComparison.OrdinalIgnoreCase); | ||
|
||
var dashboardOtlpEndpoint = ReplaceLocalhostWithContainerHost(url, builder.Configuration); | ||
|
||
var resource = new CollectorResource(name); | ||
var resourceBuilder = builder.AddResource(resource) | ||
.WithImage(settings.CollectorImage, settings.CollectorVersion) | ||
.WithEnvironment("ASPIRE_ENDPOINT", dashboardOtlpEndpoint) | ||
.WithEnvironment("ASPIRE_API_KEY", builder.Configuration[DashboardOtlpApiKeyVariableName]); | ||
|
||
if (settings.EnableGrpcEndpoint) | ||
resourceBuilder.WithEndpoint(targetPort: 4317, name: CollectorResource.GRPCEndpointName, scheme: isHttpsEnabled ? "https" : "http"); | ||
if (settings.EnableHttpEndpoint) | ||
resourceBuilder.WithEndpoint(targetPort: 4318, name: CollectorResource.HTTPEndpointName, scheme: isHttpsEnabled ? "https" : "http"); | ||
|
||
|
||
if (!settings.ForceNonSecureReceiver && isHttpsEnabled && builder.ExecutionContext.IsRunMode && builder.Environment.IsDevelopment()) | ||
{ | ||
DevCertHostingExtensions.RunWithHttpsDevCertificate(resourceBuilder, "HTTPS_CERT_FILE", "HTTPS_CERT_KEY_FILE", (certFilePath, certKeyPath) => | ||
{ | ||
if (settings.EnableHttpEndpoint) | ||
{ | ||
resourceBuilder.WithArgs( | ||
$@"--config=yaml:receivers::otlp::protocols::http::tls::cert_file: ""{certFilePath}""", | ||
$@"--config=yaml:receivers::otlp::protocols::http::tls::key_file: ""{certKeyPath}"""); | ||
} | ||
if (settings.EnableGrpcEndpoint) | ||
{ | ||
resourceBuilder.WithArgs( | ||
$@"--config=yaml:receivers::otlp::protocols::grpc::tls::cert_file: ""{certFilePath}""", | ||
$@"--config=yaml:receivers::otlp::protocols::grpc::tls::key_file: ""{certKeyPath}"""); | ||
} | ||
}); | ||
} | ||
return resourceBuilder; | ||
} | ||
|
||
/// <summary> | ||
/// Force all apps to forward to the collector instead of the dashboard directly | ||
/// </summary> | ||
/// <param name="builder"></param> | ||
/// <returns></returns> | ||
public static IResourceBuilder<CollectorResource> WithAppForwarding(this IResourceBuilder<CollectorResource> builder) | ||
{ | ||
builder.ApplicationBuilder.Services.TryAddLifecycleHook<EnvironmentVariableHook>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lifecycle hooks are mostly considered legacy with the event system now in place. The hook should be replacable with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm trying to gather from the documentation whether the |
||
return builder; | ||
} | ||
|
||
private static string ReplaceLocalhostWithContainerHost(string value, IConfiguration configuration) | ||
{ | ||
var hostName = configuration["AppHost:ContainerHostname"] ?? "host.docker.internal"; | ||
|
||
return value.Replace("localhost", hostName, StringComparison.OrdinalIgnoreCase) | ||
.Replace("127.0.0.1", hostName) | ||
.Replace("[::1]", hostName); | ||
} | ||
|
||
/// <summary> | ||
/// Adds a config file to the collector | ||
/// </summary> | ||
/// <param name="builder"></param> | ||
/// <param name="configPath"></param> | ||
/// <returns></returns> | ||
public static IResourceBuilder<CollectorResource> WithConfig(this IResourceBuilder<CollectorResource> builder, string configPath) | ||
{ | ||
var configFileInfo = new FileInfo(configPath); | ||
return builder.WithBindMount(configPath, $"/config/{configFileInfo.Name}") | ||
.WithArgs($"--config=/config/{configFileInfo.Name}"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
using Aspire.Hosting.ApplicationModel; | ||
|
||
namespace Aspire.Hosting; | ||
|
||
/// <summary> | ||
/// The collector resource | ||
/// </summary> | ||
/// <param name="name">Name of the resource</param> | ||
public class CollectorResource(string name) : ContainerResource(name) | ||
martinjt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
internal static string GRPCEndpointName = "grpc"; | ||
internal static string HTTPEndpointName = "http"; | ||
|
||
/// <summary> | ||
/// gRPC Endpoint | ||
/// </summary> | ||
public EndpointReference GRPCEndpoint => new(this, GRPCEndpointName); | ||
martinjt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// <summary> | ||
/// HTTP Endpoint | ||
/// </summary> | ||
public EndpointReference HTTPEndpoint => new(this, HTTPEndpointName); | ||
martinjt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<Description>An Aspire component to add an OpenTelemetry Collector into the OTLP pipeline</Description> | ||
<AdditionalPackageTags>opentelemetry observability</AdditionalPackageTags> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Aspire.Hosting" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<Compile Include="$(SharedDir)\DevCertHostingExtensions.cs" Link="Utils\DevCertHostingExtensions.cs" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
using Aspire.Hosting.ApplicationModel; | ||
using Aspire.Hosting.Lifecycle; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace Aspire.Hosting; | ||
|
||
/// <summary> | ||
/// Hooks to add the OTLP environment variables to the various containers | ||
/// </summary> | ||
/// <param name="logger"></param> | ||
public class EnvironmentVariableHook(ILogger<EnvironmentVariableHook> logger) : IDistributedApplicationLifecycleHook | ||
martinjt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
/// <inheritdoc /> | ||
public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) | ||
{ | ||
var resources = appModel.GetProjectResources(); | ||
var collectorResource = appModel.Resources.OfType<CollectorResource>().FirstOrDefault(); | ||
|
||
if (collectorResource is null) | ||
{ | ||
logger.LogWarning("No collector resource found"); | ||
return Task.CompletedTask; | ||
} | ||
|
||
var grpcEndpoint = collectorResource!.GetEndpoint(collectorResource!.GRPCEndpoint.EndpointName); | ||
martinjt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var httpEndpoint = collectorResource!.GetEndpoint(collectorResource!.HTTPEndpoint.EndpointName); | ||
martinjt marked this conversation as resolved.
Show resolved
Hide resolved
martinjt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if (!resources.Any()) | ||
{ | ||
logger.LogInformation("No resources to add Environment Variables to"); | ||
} | ||
|
||
foreach (var resourceItem in resources) | ||
{ | ||
logger.LogDebug("Forwarding Telemetry for {name} to the collector", resourceItem.Name); | ||
if (resourceItem is null) continue; | ||
|
||
resourceItem.Annotations.Add(new EnvironmentCallbackAnnotation((EnvironmentCallbackContext context) => | ||
{ | ||
var protocol = context.EnvironmentVariables.GetValueOrDefault("OTEL_EXPORTER_OTLP_PROTOCOL", ""); | ||
var endpoint = protocol.ToString() == "http/protobuf" ? httpEndpoint : grpcEndpoint; | ||
|
||
if (endpoint == null) | ||
martinjt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
logger.LogWarning("No {protocol} endpoint on the collector for {resourceName} to use", | ||
protocol, resourceItem.Name); | ||
return; | ||
} | ||
|
||
if (context.EnvironmentVariables.ContainsKey("OTEL_EXPORTER_OTLP_ENDPOINT")) | ||
context.EnvironmentVariables.Remove("OTEL_EXPORTER_OTLP_ENDPOINT"); | ||
context.EnvironmentVariables.Add("OTEL_EXPORTER_OTLP_ENDPOINT", endpoint.Url); | ||
})); | ||
} | ||
|
||
return Task.CompletedTask; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,36 @@ | ||||||
namespace Aspire.Hosting; | ||||||
|
||||||
/// <summary> | ||||||
/// Settings for the OpenTelemetry Collector | ||||||
/// </summary> | ||||||
public class OpenTelemetryCollectorSettings | ||||||
{ | ||||||
/// <summary> | ||||||
/// The version of the collector, defaults to latest | ||||||
/// </summary> | ||||||
public string CollectorVersion { get; set; } = "latest"; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should pin to a version so it's known stable per the package version There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not convinced that pinning is the right approach for that, especially since there isn't a v1 yet. It's generally accepted that people don't pin even in production. |
||||||
|
||||||
/// <summary> | ||||||
/// The image of the collector, defaults to ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib | ||||||
/// </summary> | ||||||
public string CollectorImage { get; set; } = "ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib"; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like this should be split to registry/image like we do with other container resources. And is there a reason you're using ghcr.io not Docker Hub? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ghcr is the recommended default since the limits went into dockerhub. Do you have an example of the pattern other integrations use for the registry split. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aspire/src/CommunityToolkit.Aspire.Hosting.Ollama/OllamaContainerImageTags.cs Lines 5 to 6 in 0c0afa1
|
||||||
|
||||||
/// <summary> | ||||||
/// Force the default OTLP receivers in the collector to use HTTP even if Aspire is set to HTTPS | ||||||
/// </summary> | ||||||
public bool ForceNonSecureReceiver { get; set; } = false; | ||||||
|
||||||
/// <summary> | ||||||
/// Enable the gRPC endpoint on the collector container (requires the relevant collector config) | ||||||
/// | ||||||
/// Note: this will also setup SSL if Aspire is configured for HTTPS | ||||||
/// </summary> | ||||||
public bool EnableGrpcEndpoint { get; set; } = true; | ||||||
|
||||||
/// <summary> | ||||||
/// Enable the HTTP endpoint on the collector container (requires the relevant collector config) | ||||||
/// | ||||||
/// Note: this will also setup SSL if Aspire is configured for HTTPS | ||||||
/// </summary> | ||||||
public bool EnableHttpEndpoint { get; set; } = true; | ||||||
} |
Uh oh!
There was an error while loading. Please reload this page.