From cb57bf122eec3bf4dca35f5b6a40d0d83338957a Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 4 Dec 2024 15:44:56 +0800 Subject: [PATCH 1/9] Update metrics sample to use OTLP and OTEL collector --- .../MetricsApp.AppHost.csproj | 2 +- .../CollectorLifecycleHook.cs | 54 +++++++++++++++++++ .../CollectorResource.cs | 7 +++ .../CollectorResourceBuilderExtensions.cs | 39 ++++++++++++++ .../CollectorServiceExtensions.cs | 13 +++++ .../OpenTelemetryCollector/README.md | 3 ++ samples/Metrics/MetricsApp.AppHost/Program.cs | 17 ++++-- .../Properties/launchSettings.json | 3 +- samples/Metrics/ServiceDefaults/Extensions.cs | 8 --- .../provisioning/datasources/default.yaml | 2 +- samples/Metrics/otelcollector/config.yaml | 42 +++++++++++++++ samples/Metrics/prometheus/prometheus.yml | 10 ++-- 12 files changed, 178 insertions(+), 22 deletions(-) create mode 100644 samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorLifecycleHook.cs create mode 100644 samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResource.cs create mode 100644 samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResourceBuilderExtensions.cs create mode 100644 samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorServiceExtensions.cs create mode 100644 samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/README.md create mode 100644 samples/Metrics/otelcollector/config.yaml diff --git a/samples/Metrics/MetricsApp.AppHost/MetricsApp.AppHost.csproj b/samples/Metrics/MetricsApp.AppHost/MetricsApp.AppHost.csproj index 79d53ee1..96992275 100644 --- a/samples/Metrics/MetricsApp.AppHost/MetricsApp.AppHost.csproj +++ b/samples/Metrics/MetricsApp.AppHost/MetricsApp.AppHost.csproj @@ -1,4 +1,4 @@ - + diff --git a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorLifecycleHook.cs b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorLifecycleHook.cs new file mode 100644 index 00000000..85265b0f --- /dev/null +++ b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorLifecycleHook.cs @@ -0,0 +1,54 @@ +using Aspire.Hosting.Lifecycle; +using Microsoft.Extensions.Logging; + +namespace MetricsApp.AppHost.OpenTelemetryCollector; + +internal sealed class CollectorLifecycleHook : IDistributedApplicationLifecycleHook +{ + private readonly ILogger _logger; + + public CollectorLifecycleHook(ILogger logger) + { + _logger = logger; + } + + public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) + { + var resources = appModel.GetProjectResources(); + var collectorResource = appModel.Resources.OfType().FirstOrDefault(); + + if (collectorResource == null) + { + _logger.LogWarning("No collector resource found"); + return Task.CompletedTask; + } + + var endpoint = collectorResource.GetEndpoint(CollectorResource.OtlpGrpcEndpointName); + if (endpoint == null) + { + _logger.LogWarning("No endpoint for the collector"); + return Task.CompletedTask; + } + + if (resources.Count() == 0) + { + _logger.LogInformation("No resources to add Environment Variables to"); + } + + foreach (var resourceItem in resources) + { + _logger.LogDebug($"Forwarding Telemetry for {resourceItem.Name} to the collector"); + if (resourceItem == null) + { + continue; + } + + resourceItem.Annotations.Add(new EnvironmentCallbackAnnotation((EnvironmentCallbackContext context) => + { + context.EnvironmentVariables["OTEL_EXPORTER_OTLP_ENDPOINT"] = endpoint; + })); + } + + return Task.CompletedTask; + } +} diff --git a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResource.cs b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResource.cs new file mode 100644 index 00000000..f1901ed2 --- /dev/null +++ b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResource.cs @@ -0,0 +1,7 @@ +namespace MetricsApp.AppHost.OpenTelemetryCollector; + +public class CollectorResource(string name) : ContainerResource(name) +{ + internal const string OtlpGrpcEndpointName = "grpc"; + internal const string OtlpHttpEndpointName = "http"; +} diff --git a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResourceBuilderExtensions.cs b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResourceBuilderExtensions.cs new file mode 100644 index 00000000..383a3385 --- /dev/null +++ b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResourceBuilderExtensions.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Configuration; + +namespace MetricsApp.AppHost.OpenTelemetryCollector; + +public static class CollectorResourceBuilderExtensions +{ + private const string DashboardOtlpUrlVariableName = "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"; + private const string DashboardOtlpApiKeyVariableName = "AppHost:OtlpApiKey"; + private const string DashboardOtlpUrlDefaultValue = "http://localhost:18889"; + + public static IResourceBuilder AddCollector(this IDistributedApplicationBuilder builder, string name, string configFileLocation) + { + builder.AddCollectorInfrastructure(); + + var url = builder.Configuration[DashboardOtlpUrlVariableName] ?? DashboardOtlpUrlDefaultValue; + + var dashboardOtlpEndpoint = ReplaceLocalhostWithContainerHost(url, builder.Configuration); + var dashboardInsecure = url.StartsWith("https", StringComparison.OrdinalIgnoreCase) ? "false" : "true"; + + var resource = new CollectorResource(name); + return builder.AddResource(resource) + .WithImage("ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib", "latest") + .WithEndpoint(port: 4317, targetPort: 4317, name: CollectorResource.OtlpGrpcEndpointName, scheme: "http") + .WithEndpoint(port: 4318, targetPort: 4318, name: CollectorResource.OtlpHttpEndpointName, scheme: "http") + .WithBindMount(configFileLocation, "/etc/otelcol-contrib/config.yaml") + .WithEnvironment("ASPIRE_ENDPOINT", dashboardOtlpEndpoint) + .WithEnvironment("ASPIRE_API_KEY", builder.Configuration[DashboardOtlpApiKeyVariableName]) + .WithEnvironment("ASPIRE_INSECURE", dashboardInsecure); + } + + 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); + } +} diff --git a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorServiceExtensions.cs b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorServiceExtensions.cs new file mode 100644 index 00000000..e110f517 --- /dev/null +++ b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorServiceExtensions.cs @@ -0,0 +1,13 @@ +using Aspire.Hosting.Lifecycle; + +namespace MetricsApp.AppHost.OpenTelemetryCollector; + +internal static class CollectorServiceExtensions +{ + public static IDistributedApplicationBuilder AddCollectorInfrastructure(this IDistributedApplicationBuilder builder) + { + builder.Services.TryAddLifecycleHook(); + + return builder; + } +} diff --git a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/README.md b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/README.md new file mode 100644 index 00000000..14274dc8 --- /dev/null +++ b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/README.md @@ -0,0 +1,3 @@ +# Aspire Hosting extension for the OpenTelemetry Collector + +Based on source from https://github.com/practical-otel/opentelemetry-aspire-collector by @martinjt. diff --git a/samples/Metrics/MetricsApp.AppHost/Program.cs b/samples/Metrics/MetricsApp.AppHost/Program.cs index 947bb53a..6615a754 100644 --- a/samples/Metrics/MetricsApp.AppHost/Program.cs +++ b/samples/Metrics/MetricsApp.AppHost/Program.cs @@ -1,17 +1,24 @@ -var builder = DistributedApplication.CreateBuilder(args); +using MetricsApp.AppHost.OpenTelemetryCollector; + +var builder = DistributedApplication.CreateBuilder(args); + +var prometheus = builder.AddContainer("prometheus", "prom/prometheus") + .WithBindMount("../prometheus", "/etc/prometheus", isReadOnly: true) + .WithArgs("--web.enable-otlp-receiver", "--config.file=/etc/prometheus/prometheus.yml") + .WithHttpEndpoint(targetPort: 9090, name: "http"); var grafana = builder.AddContainer("grafana", "grafana/grafana") .WithBindMount("../grafana/config", "/etc/grafana", isReadOnly: true) .WithBindMount("../grafana/dashboards", "/var/lib/grafana/dashboards", isReadOnly: true) + .WithEnvironment(c => c.EnvironmentVariables["PROMETHEUS_PORT"] = $"{prometheus.GetEndpoint("http").Port}") .WithHttpEndpoint(targetPort: 3000, name: "http"); +builder.AddCollector("otelcollector", "../otelcollector/config.yaml") + .WithEnvironment("PROMETHEUS_ENDPOINT", $"{prometheus.GetEndpoint("http")}/api/v1/otlp"); + builder.AddProject("app") .WithEnvironment("GRAFANA_URL", grafana.GetEndpoint("http")); -builder.AddContainer("prometheus", "prom/prometheus") - .WithBindMount("../prometheus", "/etc/prometheus", isReadOnly: true) - .WithHttpEndpoint(/* This port is fixed as it's referenced from the Grafana config */ port: 9090, targetPort: 9090); - using var app = builder.Build(); await app.RunAsync(); diff --git a/samples/Metrics/MetricsApp.AppHost/Properties/launchSettings.json b/samples/Metrics/MetricsApp.AppHost/Properties/launchSettings.json index 4003981e..39ee6038 100644 --- a/samples/Metrics/MetricsApp.AppHost/Properties/launchSettings.json +++ b/samples/Metrics/MetricsApp.AppHost/Properties/launchSettings.json @@ -22,7 +22,8 @@ "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19032", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20132" + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20132", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true" } }, "generate-manifest": { diff --git a/samples/Metrics/ServiceDefaults/Extensions.cs b/samples/Metrics/ServiceDefaults/Extensions.cs index 022f785f..9865e709 100644 --- a/samples/Metrics/ServiceDefaults/Extensions.cs +++ b/samples/Metrics/ServiceDefaults/Extensions.cs @@ -66,11 +66,6 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli builder.Services.AddOpenTelemetry().UseOtlpExporter(); } - // The following lines enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - builder.Services.AddOpenTelemetry() - // BUG: Part of the workaround for https://github.com/open-telemetry/opentelemetry-dotnet-contrib/issues/1617 - .WithMetrics(metrics => metrics.AddPrometheusExporter(options => options.DisableTotalNameSuffixForCounters = true)); - return builder; } @@ -85,9 +80,6 @@ public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicati public static WebApplication MapDefaultEndpoints(this WebApplication app) { - // The following line enables the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - app.MapPrometheusScrapingEndpoint(); - // Adding health checks endpoints to applications in non-development environments has security implications. // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. if (app.Environment.IsDevelopment()) diff --git a/samples/Metrics/grafana/config/provisioning/datasources/default.yaml b/samples/Metrics/grafana/config/provisioning/datasources/default.yaml index 76457f38..9fd8f066 100644 --- a/samples/Metrics/grafana/config/provisioning/datasources/default.yaml +++ b/samples/Metrics/grafana/config/provisioning/datasources/default.yaml @@ -5,5 +5,5 @@ datasources: type: prometheus access: proxy # Access mode - proxy (server in the UI) or direct (browser in the UI). - url: http://host.docker.internal:9090 + url: http://host.docker.internal:$PROMETHEUS_PORT uid: PBFA97CFB590B2093 diff --git a/samples/Metrics/otelcollector/config.yaml b/samples/Metrics/otelcollector/config.yaml new file mode 100644 index 00000000..74999aa7 --- /dev/null +++ b/samples/Metrics/otelcollector/config.yaml @@ -0,0 +1,42 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + +exporters: + debug: + verbosity: detailed + otlp/aspire: + endpoint: ${env:ASPIRE_ENDPOINT} + headers: + x-otlp-api-key: ${env:ASPIRE_API_KEY} + tls: + insecure: ${env:ASPIRE_INSECURE} + insecure_skip_verify: true + otlphttp/prometheus: + endpoint: ${env:PROMETHEUS_ENDPOINT} + tls: + insecure: true + +service: + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [otlp/aspire] + metrics: + receivers: [otlp] + processors: [batch] + exporters: + - otlp/aspire + - otlphttp/prometheus + logs: + receivers: [otlp] + processors: [batch] + exporters: [otlp/aspire] diff --git a/samples/Metrics/prometheus/prometheus.yml b/samples/Metrics/prometheus/prometheus.yml index 89b3561a..899a64d1 100644 --- a/samples/Metrics/prometheus/prometheus.yml +++ b/samples/Metrics/prometheus/prometheus.yml @@ -1,7 +1,5 @@ -global: - scrape_interval: 1s # makes for a good demo +storage: + tsdb: + out_of_order_time_window: 30m -scrape_configs: - - job_name: 'metricsapp' - static_configs: - - targets: ['host.docker.internal:5048'] # hard-coded port matches launchSettings.json +otlp: From e1d0e5cc03458495479956ce8502b33b7c6c3144 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 4 Dec 2024 18:16:28 +0800 Subject: [PATCH 2/9] PR feedback --- .../CollectorResourceBuilderExtensions.cs | 13 ++----------- samples/Metrics/MetricsApp.AppHost/Program.cs | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResourceBuilderExtensions.cs b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResourceBuilderExtensions.cs index 383a3385..0b62c97d 100644 --- a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResourceBuilderExtensions.cs +++ b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResourceBuilderExtensions.cs @@ -14,7 +14,7 @@ public static IResourceBuilder AddCollector(this IDistributed var url = builder.Configuration[DashboardOtlpUrlVariableName] ?? DashboardOtlpUrlDefaultValue; - var dashboardOtlpEndpoint = ReplaceLocalhostWithContainerHost(url, builder.Configuration); + var dashboardOtlpEndpoint = new HostUrl(url); var dashboardInsecure = url.StartsWith("https", StringComparison.OrdinalIgnoreCase) ? "false" : "true"; var resource = new CollectorResource(name); @@ -23,17 +23,8 @@ public static IResourceBuilder AddCollector(this IDistributed .WithEndpoint(port: 4317, targetPort: 4317, name: CollectorResource.OtlpGrpcEndpointName, scheme: "http") .WithEndpoint(port: 4318, targetPort: 4318, name: CollectorResource.OtlpHttpEndpointName, scheme: "http") .WithBindMount(configFileLocation, "/etc/otelcol-contrib/config.yaml") - .WithEnvironment("ASPIRE_ENDPOINT", dashboardOtlpEndpoint) + .WithEnvironment("ASPIRE_ENDPOINT", $"{dashboardOtlpEndpoint}") .WithEnvironment("ASPIRE_API_KEY", builder.Configuration[DashboardOtlpApiKeyVariableName]) .WithEnvironment("ASPIRE_INSECURE", dashboardInsecure); } - - 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); - } } diff --git a/samples/Metrics/MetricsApp.AppHost/Program.cs b/samples/Metrics/MetricsApp.AppHost/Program.cs index 6615a754..6d267e1d 100644 --- a/samples/Metrics/MetricsApp.AppHost/Program.cs +++ b/samples/Metrics/MetricsApp.AppHost/Program.cs @@ -10,7 +10,7 @@ var grafana = builder.AddContainer("grafana", "grafana/grafana") .WithBindMount("../grafana/config", "/etc/grafana", isReadOnly: true) .WithBindMount("../grafana/dashboards", "/var/lib/grafana/dashboards", isReadOnly: true) - .WithEnvironment(c => c.EnvironmentVariables["PROMETHEUS_PORT"] = $"{prometheus.GetEndpoint("http").Port}") + .WithEnvironment("PROMETHEUS_PORT", $"{prometheus.GetEndpoint("http").Property(EndpointProperty.Port)}") .WithHttpEndpoint(targetPort: 3000, name: "http"); builder.AddCollector("otelcollector", "../otelcollector/config.yaml") From 5ce58f338c76d8a054eba05a8e83a62ec655b5f7 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 4 Dec 2024 18:19:00 +0800 Subject: [PATCH 3/9] PR feedback --- .../CollectorResourceBuilderExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResourceBuilderExtensions.cs b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResourceBuilderExtensions.cs index 0b62c97d..37f40aac 100644 --- a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResourceBuilderExtensions.cs +++ b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResourceBuilderExtensions.cs @@ -20,8 +20,8 @@ public static IResourceBuilder AddCollector(this IDistributed var resource = new CollectorResource(name); return builder.AddResource(resource) .WithImage("ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib", "latest") - .WithEndpoint(port: 4317, targetPort: 4317, name: CollectorResource.OtlpGrpcEndpointName, scheme: "http") - .WithEndpoint(port: 4318, targetPort: 4318, name: CollectorResource.OtlpHttpEndpointName, scheme: "http") + .WithEndpoint(targetPort: 4317, name: CollectorResource.OtlpGrpcEndpointName, scheme: "http") + .WithEndpoint(targetPort: 4318, name: CollectorResource.OtlpHttpEndpointName, scheme: "http") .WithBindMount(configFileLocation, "/etc/otelcol-contrib/config.yaml") .WithEnvironment("ASPIRE_ENDPOINT", $"{dashboardOtlpEndpoint}") .WithEnvironment("ASPIRE_API_KEY", builder.Configuration[DashboardOtlpApiKeyVariableName]) From 91c3b08426227810405f28384a0f91ebf8ed4b8c Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 4 Dec 2024 18:27:19 +0800 Subject: [PATCH 4/9] Remove Prometheus OTEL package --- samples/Metrics/ServiceDefaults/ServiceDefaults.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/samples/Metrics/ServiceDefaults/ServiceDefaults.csproj b/samples/Metrics/ServiceDefaults/ServiceDefaults.csproj index 033e7df7..139fc228 100644 --- a/samples/Metrics/ServiceDefaults/ServiceDefaults.csproj +++ b/samples/Metrics/ServiceDefaults/ServiceDefaults.csproj @@ -13,8 +13,6 @@ - - From a7fb5ffd6f21acbb3aa37c76f2eabd08c5b43384 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 5 Dec 2024 07:11:29 +0800 Subject: [PATCH 5/9] Update packages --- samples/Metrics/MetricsApp/MetricsApp.csproj | 4 ++-- samples/Metrics/ServiceDefaults/ServiceDefaults.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/Metrics/MetricsApp/MetricsApp.csproj b/samples/Metrics/MetricsApp/MetricsApp.csproj index cf92622e..d9390539 100644 --- a/samples/Metrics/MetricsApp/MetricsApp.csproj +++ b/samples/Metrics/MetricsApp/MetricsApp.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -11,7 +11,7 @@ - + diff --git a/samples/Metrics/ServiceDefaults/ServiceDefaults.csproj b/samples/Metrics/ServiceDefaults/ServiceDefaults.csproj index 139fc228..aff55813 100644 --- a/samples/Metrics/ServiceDefaults/ServiceDefaults.csproj +++ b/samples/Metrics/ServiceDefaults/ServiceDefaults.csproj @@ -15,7 +15,7 @@ - + From ab93b6c344977593a94196d1fc17e4a32ea9d8f6 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 5 Dec 2024 07:19:36 +0800 Subject: [PATCH 6/9] PR feedback --- .../CollectorLifecycleHook.cs | 54 ------------------- .../CollectorServiceExtensions.cs | 13 ----- .../OpenTelemetryCollectorLifecycleHook.cs | 43 +++++++++++++++ ...e.cs => OpenTelemetryCollectorResource.cs} | 2 +- ...etryCollectorResourceBuilderExtensions.cs} | 16 +++--- ...OpenTelemetryCollectorServiceExtensions.cs | 13 +++++ samples/Metrics/MetricsApp.AppHost/Program.cs | 2 +- 7 files changed, 65 insertions(+), 78 deletions(-) delete mode 100644 samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorLifecycleHook.cs delete mode 100644 samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorServiceExtensions.cs create mode 100644 samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorLifecycleHook.cs rename samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/{CollectorResource.cs => OpenTelemetryCollectorResource.cs} (67%) rename samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/{CollectorResourceBuilderExtensions.cs => OpenTelemetryCollectorResourceBuilderExtensions.cs} (61%) create mode 100644 samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorServiceExtensions.cs diff --git a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorLifecycleHook.cs b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorLifecycleHook.cs deleted file mode 100644 index 85265b0f..00000000 --- a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorLifecycleHook.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Aspire.Hosting.Lifecycle; -using Microsoft.Extensions.Logging; - -namespace MetricsApp.AppHost.OpenTelemetryCollector; - -internal sealed class CollectorLifecycleHook : IDistributedApplicationLifecycleHook -{ - private readonly ILogger _logger; - - public CollectorLifecycleHook(ILogger logger) - { - _logger = logger; - } - - public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) - { - var resources = appModel.GetProjectResources(); - var collectorResource = appModel.Resources.OfType().FirstOrDefault(); - - if (collectorResource == null) - { - _logger.LogWarning("No collector resource found"); - return Task.CompletedTask; - } - - var endpoint = collectorResource.GetEndpoint(CollectorResource.OtlpGrpcEndpointName); - if (endpoint == null) - { - _logger.LogWarning("No endpoint for the collector"); - return Task.CompletedTask; - } - - if (resources.Count() == 0) - { - _logger.LogInformation("No resources to add Environment Variables to"); - } - - foreach (var resourceItem in resources) - { - _logger.LogDebug($"Forwarding Telemetry for {resourceItem.Name} to the collector"); - if (resourceItem == null) - { - continue; - } - - resourceItem.Annotations.Add(new EnvironmentCallbackAnnotation((EnvironmentCallbackContext context) => - { - context.EnvironmentVariables["OTEL_EXPORTER_OTLP_ENDPOINT"] = endpoint; - })); - } - - return Task.CompletedTask; - } -} diff --git a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorServiceExtensions.cs b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorServiceExtensions.cs deleted file mode 100644 index e110f517..00000000 --- a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorServiceExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Aspire.Hosting.Lifecycle; - -namespace MetricsApp.AppHost.OpenTelemetryCollector; - -internal static class CollectorServiceExtensions -{ - public static IDistributedApplicationBuilder AddCollectorInfrastructure(this IDistributedApplicationBuilder builder) - { - builder.Services.TryAddLifecycleHook(); - - return builder; - } -} diff --git a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorLifecycleHook.cs b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorLifecycleHook.cs new file mode 100644 index 00000000..2e25348f --- /dev/null +++ b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorLifecycleHook.cs @@ -0,0 +1,43 @@ +using Aspire.Hosting.Lifecycle; +using Microsoft.Extensions.Logging; + +namespace MetricsApp.AppHost.OpenTelemetryCollector; + +internal sealed class OpenTelemetryCollectorLifecycleHook : IDistributedApplicationLifecycleHook +{ + private readonly ILogger _logger; + + public OpenTelemetryCollectorLifecycleHook(ILogger logger) + { + _logger = logger; + } + + public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) + { + var collectorResource = appModel.Resources.OfType().FirstOrDefault(); + if (collectorResource == null) + { + _logger.LogWarning($"No {nameof(OpenTelemetryCollectorResource)} resource found."); + return Task.CompletedTask; + } + + var endpoint = collectorResource.GetEndpoint(OpenTelemetryCollectorResource.OtlpGrpcEndpointName); + if (!endpoint.Exists) + { + _logger.LogWarning($"No {OpenTelemetryCollectorResource.OtlpGrpcEndpointName} endpoint for the collector."); + return Task.CompletedTask; + } + + foreach (var resource in appModel.GetProjectResources()) + { + _logger.LogDebug("Forwarding telemetry for {ResourceName} to the collector.", resource.Name); + + resource.Annotations.Add(new EnvironmentCallbackAnnotation((EnvironmentCallbackContext context) => + { + context.EnvironmentVariables["OTEL_EXPORTER_OTLP_ENDPOINT"] = endpoint; + })); + } + + return Task.CompletedTask; + } +} diff --git a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResource.cs b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorResource.cs similarity index 67% rename from samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResource.cs rename to samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorResource.cs index f1901ed2..ef54a542 100644 --- a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResource.cs +++ b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorResource.cs @@ -1,6 +1,6 @@ namespace MetricsApp.AppHost.OpenTelemetryCollector; -public class CollectorResource(string name) : ContainerResource(name) +public class OpenTelemetryCollectorResource(string name) : ContainerResource(name) { internal const string OtlpGrpcEndpointName = "grpc"; internal const string OtlpHttpEndpointName = "http"; diff --git a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResourceBuilderExtensions.cs b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorResourceBuilderExtensions.cs similarity index 61% rename from samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResourceBuilderExtensions.cs rename to samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorResourceBuilderExtensions.cs index 37f40aac..b9ee4e1d 100644 --- a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/CollectorResourceBuilderExtensions.cs +++ b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorResourceBuilderExtensions.cs @@ -1,27 +1,25 @@ -using Microsoft.Extensions.Configuration; +namespace MetricsApp.AppHost.OpenTelemetryCollector; -namespace MetricsApp.AppHost.OpenTelemetryCollector; - -public static class CollectorResourceBuilderExtensions +public static class OpenTelemetryCollectorResourceBuilderExtensions { private const string DashboardOtlpUrlVariableName = "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL"; private const string DashboardOtlpApiKeyVariableName = "AppHost:OtlpApiKey"; private const string DashboardOtlpUrlDefaultValue = "http://localhost:18889"; - public static IResourceBuilder AddCollector(this IDistributedApplicationBuilder builder, string name, string configFileLocation) + public static IResourceBuilder AddOpenTelemetryCollector(this IDistributedApplicationBuilder builder, string name, string configFileLocation) { - builder.AddCollectorInfrastructure(); + builder.AddOpenTelemetryCollectorInfrastructure(); var url = builder.Configuration[DashboardOtlpUrlVariableName] ?? DashboardOtlpUrlDefaultValue; var dashboardOtlpEndpoint = new HostUrl(url); var dashboardInsecure = url.StartsWith("https", StringComparison.OrdinalIgnoreCase) ? "false" : "true"; - var resource = new CollectorResource(name); + var resource = new OpenTelemetryCollectorResource(name); return builder.AddResource(resource) .WithImage("ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib", "latest") - .WithEndpoint(targetPort: 4317, name: CollectorResource.OtlpGrpcEndpointName, scheme: "http") - .WithEndpoint(targetPort: 4318, name: CollectorResource.OtlpHttpEndpointName, scheme: "http") + .WithEndpoint(targetPort: 4317, name: OpenTelemetryCollectorResource.OtlpGrpcEndpointName, scheme: "http") + .WithEndpoint(targetPort: 4318, name: OpenTelemetryCollectorResource.OtlpHttpEndpointName, scheme: "http") .WithBindMount(configFileLocation, "/etc/otelcol-contrib/config.yaml") .WithEnvironment("ASPIRE_ENDPOINT", $"{dashboardOtlpEndpoint}") .WithEnvironment("ASPIRE_API_KEY", builder.Configuration[DashboardOtlpApiKeyVariableName]) diff --git a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorServiceExtensions.cs b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorServiceExtensions.cs new file mode 100644 index 00000000..aae6d246 --- /dev/null +++ b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorServiceExtensions.cs @@ -0,0 +1,13 @@ +using Aspire.Hosting.Lifecycle; + +namespace MetricsApp.AppHost.OpenTelemetryCollector; + +internal static class OpenTelemetryCollectorServiceExtensions +{ + public static IDistributedApplicationBuilder AddOpenTelemetryCollectorInfrastructure(this IDistributedApplicationBuilder builder) + { + builder.Services.TryAddLifecycleHook(); + + return builder; + } +} diff --git a/samples/Metrics/MetricsApp.AppHost/Program.cs b/samples/Metrics/MetricsApp.AppHost/Program.cs index 6d267e1d..76814e0f 100644 --- a/samples/Metrics/MetricsApp.AppHost/Program.cs +++ b/samples/Metrics/MetricsApp.AppHost/Program.cs @@ -13,7 +13,7 @@ .WithEnvironment("PROMETHEUS_PORT", $"{prometheus.GetEndpoint("http").Property(EndpointProperty.Port)}") .WithHttpEndpoint(targetPort: 3000, name: "http"); -builder.AddCollector("otelcollector", "../otelcollector/config.yaml") +builder.AddOpenTelemetryCollector("otelcollector", "../otelcollector/config.yaml") .WithEnvironment("PROMETHEUS_ENDPOINT", $"{prometheus.GetEndpoint("http")}/api/v1/otlp"); builder.AddProject("app") From c217f0fc5642547853af332a83894ae3db1c8dc3 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 5 Dec 2024 09:16:21 +0800 Subject: [PATCH 7/9] Update --- .../MetricsApp.AppHost.csproj | 4 ++- ...metryCollectorResourceBuilderExtensions.cs | 29 +++++++++++++++---- samples/Metrics/otelcollector/config.yaml | 2 +- samples/Shared/DevCertHostingExtensions.cs | 4 +-- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/samples/Metrics/MetricsApp.AppHost/MetricsApp.AppHost.csproj b/samples/Metrics/MetricsApp.AppHost/MetricsApp.AppHost.csproj index 96992275..78c4688e 100644 --- a/samples/Metrics/MetricsApp.AppHost/MetricsApp.AppHost.csproj +++ b/samples/Metrics/MetricsApp.AppHost/MetricsApp.AppHost.csproj @@ -11,9 +11,11 @@ + + - + diff --git a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorResourceBuilderExtensions.cs b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorResourceBuilderExtensions.cs index b9ee4e1d..899f631e 100644 --- a/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorResourceBuilderExtensions.cs +++ b/samples/Metrics/MetricsApp.AppHost/OpenTelemetryCollector/OpenTelemetryCollectorResourceBuilderExtensions.cs @@ -1,4 +1,6 @@ -namespace MetricsApp.AppHost.OpenTelemetryCollector; +using Microsoft.Extensions.Hosting; + +namespace MetricsApp.AppHost.OpenTelemetryCollector; public static class OpenTelemetryCollectorResourceBuilderExtensions { @@ -11,18 +13,33 @@ public static IResourceBuilder AddOpenTelemetryC builder.AddOpenTelemetryCollectorInfrastructure(); var url = builder.Configuration[DashboardOtlpUrlVariableName] ?? DashboardOtlpUrlDefaultValue; + var isHttpsEnabled = url.StartsWith("https", StringComparison.OrdinalIgnoreCase); var dashboardOtlpEndpoint = new HostUrl(url); - var dashboardInsecure = url.StartsWith("https", StringComparison.OrdinalIgnoreCase) ? "false" : "true"; var resource = new OpenTelemetryCollectorResource(name); - return builder.AddResource(resource) + var resourceBuilder = builder.AddResource(resource) .WithImage("ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib", "latest") - .WithEndpoint(targetPort: 4317, name: OpenTelemetryCollectorResource.OtlpGrpcEndpointName, scheme: "http") - .WithEndpoint(targetPort: 4318, name: OpenTelemetryCollectorResource.OtlpHttpEndpointName, scheme: "http") + .WithEndpoint(targetPort: 4317, name: OpenTelemetryCollectorResource.OtlpGrpcEndpointName, scheme: isHttpsEnabled ? "https" : "http") + .WithEndpoint(targetPort: 4318, name: OpenTelemetryCollectorResource.OtlpHttpEndpointName, scheme: isHttpsEnabled ? "https" : "http") .WithBindMount(configFileLocation, "/etc/otelcol-contrib/config.yaml") .WithEnvironment("ASPIRE_ENDPOINT", $"{dashboardOtlpEndpoint}") .WithEnvironment("ASPIRE_API_KEY", builder.Configuration[DashboardOtlpApiKeyVariableName]) - .WithEnvironment("ASPIRE_INSECURE", dashboardInsecure); + .WithEnvironment("ASPIRE_INSECURE", isHttpsEnabled ? "false" : "true"); + + if (isHttpsEnabled && builder.ExecutionContext.IsRunMode && builder.Environment.IsDevelopment()) + { + DevCertHostingExtensions.RunWithHttpsDevCertificate(resourceBuilder, "HTTPS_CERT_FILE", "HTTPS_CERT_KEY_FILE", (certFilePath, certKeyPath) => + { + // Set TLS details using YAML path via the command line. This allows the values to be added to the existing config file. + // Setting the values in the config file doesn't work because adding the "tls" section always enables TLS, even if there is no cert provided. + resourceBuilder.WithArgs( + @"--config=yaml:receivers::otlp::protocols::grpc::tls::cert_file: ""dev-certs/dev-cert.pem""", + @"--config=yaml:receivers::otlp::protocols::grpc::tls::key_file: ""dev-certs/dev-cert.key""", + @"--config=/etc/otelcol-contrib/config.yaml"); + }); + } + + return resourceBuilder; } } diff --git a/samples/Metrics/otelcollector/config.yaml b/samples/Metrics/otelcollector/config.yaml index 74999aa7..8f182f19 100644 --- a/samples/Metrics/otelcollector/config.yaml +++ b/samples/Metrics/otelcollector/config.yaml @@ -18,7 +18,7 @@ exporters: x-otlp-api-key: ${env:ASPIRE_API_KEY} tls: insecure: ${env:ASPIRE_INSECURE} - insecure_skip_verify: true + insecure_skip_verify: true # Required in local development because cert is localhost and the endpoint is host.docker.internal otlphttp/prometheus: endpoint: ${env:PROMETHEUS_ENDPOINT} tls: diff --git a/samples/Shared/DevCertHostingExtensions.cs b/samples/Shared/DevCertHostingExtensions.cs index 862e69d6..ae9706a0 100644 --- a/samples/Shared/DevCertHostingExtensions.cs +++ b/samples/Shared/DevCertHostingExtensions.cs @@ -46,8 +46,8 @@ public static IResourceBuilder RunWithHttpsDevCertificate( var bindSource = Path.GetDirectoryName(certPath) ?? throw new UnreachableException(); - var certFileDest = Path.Combine(DEV_CERT_BIND_MOUNT_DEST_DIR, certFileName); - var certKeyFileDest = Path.Combine(DEV_CERT_BIND_MOUNT_DEST_DIR, certKeyFileName); + var certFileDest = $"{DEV_CERT_BIND_MOUNT_DEST_DIR}/{certFileName}"; + var certKeyFileDest = $"{DEV_CERT_BIND_MOUNT_DEST_DIR}/{certKeyFileName}"; builder.ApplicationBuilder.CreateResourceBuilder(containerResource) .WithBindMount(bindSource, DEV_CERT_BIND_MOUNT_DEST_DIR, isReadOnly: true) From 95569f2e22bc99de710ccc232c1ccbc3bb5e8e46 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 5 Dec 2024 09:17:49 +0800 Subject: [PATCH 8/9] Update --- samples/Metrics/MetricsApp.AppHost/MetricsApp.AppHost.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/Metrics/MetricsApp.AppHost/MetricsApp.AppHost.csproj b/samples/Metrics/MetricsApp.AppHost/MetricsApp.AppHost.csproj index 78c4688e..ac270e58 100644 --- a/samples/Metrics/MetricsApp.AppHost/MetricsApp.AppHost.csproj +++ b/samples/Metrics/MetricsApp.AppHost/MetricsApp.AppHost.csproj @@ -15,7 +15,7 @@ - + From c0027c51b5d71facff165b1ac00f16b04c1fa354 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 6 Dec 2024 07:11:11 +0800 Subject: [PATCH 9/9] PR feedback --- samples/Metrics/MetricsApp.AppHost/Program.cs | 2 +- .../grafana/config/provisioning/datasources/default.yaml | 2 +- samples/Metrics/otelcollector/config.yaml | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/Metrics/MetricsApp.AppHost/Program.cs b/samples/Metrics/MetricsApp.AppHost/Program.cs index 76814e0f..fff02d00 100644 --- a/samples/Metrics/MetricsApp.AppHost/Program.cs +++ b/samples/Metrics/MetricsApp.AppHost/Program.cs @@ -10,7 +10,7 @@ var grafana = builder.AddContainer("grafana", "grafana/grafana") .WithBindMount("../grafana/config", "/etc/grafana", isReadOnly: true) .WithBindMount("../grafana/dashboards", "/var/lib/grafana/dashboards", isReadOnly: true) - .WithEnvironment("PROMETHEUS_PORT", $"{prometheus.GetEndpoint("http").Property(EndpointProperty.Port)}") + .WithEnvironment("PROMETHEUS_ENDPOINT", prometheus.GetEndpoint("http")) .WithHttpEndpoint(targetPort: 3000, name: "http"); builder.AddOpenTelemetryCollector("otelcollector", "../otelcollector/config.yaml") diff --git a/samples/Metrics/grafana/config/provisioning/datasources/default.yaml b/samples/Metrics/grafana/config/provisioning/datasources/default.yaml index 9fd8f066..18278481 100644 --- a/samples/Metrics/grafana/config/provisioning/datasources/default.yaml +++ b/samples/Metrics/grafana/config/provisioning/datasources/default.yaml @@ -5,5 +5,5 @@ datasources: type: prometheus access: proxy # Access mode - proxy (server in the UI) or direct (browser in the UI). - url: http://host.docker.internal:$PROMETHEUS_PORT + url: $PROMETHEUS_ENDPOINT uid: PBFA97CFB590B2093 diff --git a/samples/Metrics/otelcollector/config.yaml b/samples/Metrics/otelcollector/config.yaml index 8f182f19..68680466 100644 --- a/samples/Metrics/otelcollector/config.yaml +++ b/samples/Metrics/otelcollector/config.yaml @@ -30,13 +30,13 @@ service: receivers: [otlp] processors: [batch] exporters: [otlp/aspire] + logs: + receivers: [otlp] + processors: [batch] + exporters: [otlp/aspire] metrics: receivers: [otlp] processors: [batch] exporters: - otlp/aspire - otlphttp/prometheus - logs: - receivers: [otlp] - processors: [batch] - exporters: [otlp/aspire]