Skip to content
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

Update metrics sample to use OTLP and OTEL collector #606

Merged
merged 9 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
6 changes: 4 additions & 2 deletions samples/Metrics/MetricsApp.AppHost/MetricsApp.AppHost.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" />

Expand All @@ -11,9 +11,11 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="..\..\Shared\DevCertHostingExtensions.cs" Link="DevCertHostingExtensions.cs" />

<PackageReference Include="Aspire.Hosting.AppHost" Version="9.0.0" />

<ProjectReference Include="..\MetricsApp\MetricsApp.csproj" />
<ProjectReference Include="..\MetricsApp\MetricsApp.csproj" />
</ItemGroup>
DamianEdwards marked this conversation as resolved.
Show resolved Hide resolved

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Logging;

namespace MetricsApp.AppHost.OpenTelemetryCollector;

internal sealed class OpenTelemetryCollectorLifecycleHook : IDistributedApplicationLifecycleHook
{
private readonly ILogger<OpenTelemetryCollectorLifecycleHook> _logger;

public OpenTelemetryCollectorLifecycleHook(ILogger<OpenTelemetryCollectorLifecycleHook> logger)
{
_logger = logger;
}

public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken)
{
var collectorResource = appModel.Resources.OfType<OpenTelemetryCollectorResource>().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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this check all resources? In the callback it would then check if OTEL_EXPORTER_OTLP_ENDPOINT is set and replace it with this value.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that OTEL_EXPORTER_OTLP_ENDPOINT env var is added by a callback. At the resource level we don't know exactly what env vars there are from a callback.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do work in the callback to look at what has been set.

{
_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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace MetricsApp.AppHost.OpenTelemetryCollector;

public class OpenTelemetryCollectorResource(string name) : ContainerResource(name)
{
internal const string OtlpGrpcEndpointName = "grpc";
internal const string OtlpHttpEndpointName = "http";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Microsoft.Extensions.Hosting;

namespace MetricsApp.AppHost.OpenTelemetryCollector;

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<OpenTelemetryCollectorResource> AddOpenTelemetryCollector(this IDistributedApplicationBuilder builder, string name, string configFileLocation)
{
builder.AddOpenTelemetryCollectorInfrastructure();

var url = builder.Configuration[DashboardOtlpUrlVariableName] ?? DashboardOtlpUrlDefaultValue;
var isHttpsEnabled = url.StartsWith("https", StringComparison.OrdinalIgnoreCase);

var dashboardOtlpEndpoint = new HostUrl(url);

var resource = new OpenTelemetryCollectorResource(name);
var resourceBuilder = builder.AddResource(resource)
.WithImage("ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib", "latest")
.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", 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<OpenTelemetryCollectorLifecycleHook>();

return builder;
}
}
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 12 additions & 5 deletions samples/Metrics/MetricsApp.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -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("PROMETHEUS_PORT", $"{prometheus.GetEndpoint("http").Property(EndpointProperty.Port)}")
.WithHttpEndpoint(targetPort: 3000, name: "http");

builder.AddOpenTelemetryCollector("otelcollector", "../otelcollector/config.yaml")
.WithEnvironment("PROMETHEUS_ENDPOINT", $"{prometheus.GetEndpoint("http")}/api/v1/otlp");

builder.AddProject<Projects.MetricsApp>("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();
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 2 additions & 2 deletions samples/Metrics/MetricsApp/MetricsApp.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
Expand All @@ -11,7 +11,7 @@
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.11" />

<PackageReference Include="Swashbuckle.AspNetCore" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.1.0" />
</ItemGroup>

<ItemGroup>
Expand Down
8 changes: 0 additions & 8 deletions samples/Metrics/ServiceDefaults/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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())
Expand Down
4 changes: 1 addition & 3 deletions samples/Metrics/ServiceDefaults/ServiceDefaults.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.0.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.10.0" />
<!-- BUG: 1.8.0-rc.1 breaks prometheus exporter so sticking to 1.8.0-beta.1 for now https://github.com/open-telemetry/opentelemetry-dotnet-contrib/issues/1617 -->
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.8.0-rc.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.10.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.10.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.9.0" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
uid: PBFA97CFB590B2093
42 changes: 42 additions & 0 deletions samples/Metrics/otelcollector/config.yaml
Original file line number Diff line number Diff line change
@@ -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 # Required in local development because cert is localhost and the endpoint is host.docker.internal
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]
10 changes: 4 additions & 6 deletions samples/Metrics/prometheus/prometheus.yml
Original file line number Diff line number Diff line change
@@ -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:
4 changes: 2 additions & 2 deletions samples/Shared/DevCertHostingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ public static IResourceBuilder<TResource> RunWithHttpsDevCertificate<TResource>(

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}";
Comment on lines -49 to +50
Copy link
Member Author

@JamesNK JamesNK Dec 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI Path.Combine would add backslash when app host is run on Windows.

e.g. /dev-cert\dev-cert.pem

The OpenTelemetry collector errored when given the bad path. Seems best to hardcode to forward slash.


builder.ApplicationBuilder.CreateResourceBuilder(containerResource)
.WithBindMount(bindSource, DEV_CERT_BIND_MOUNT_DEST_DIR, isReadOnly: true)
Expand Down
Loading