Improve telemetry (#1337) (Closes #1335)

* Improve telemetry (Closes #1335)

* Fix CD pipeline
This commit is contained in:
2026-01-23 16:02:04 +01:00
parent 1f8d4c126e
commit 26b02ac94d
5 changed files with 13 additions and 117 deletions
+2 -1
View File
@@ -69,7 +69,8 @@ jobs:
run: |
dotnet user-secrets --project Daybreak\Daybreak.csproj set AadApplicationId "${{ secrets.AadApplicationId }}"
dotnet user-secrets --project Daybreak\Daybreak.csproj set AadTenantId "${{ secrets.AadTenantId }}"
dotnet user-secrets --project Daybreak\Daybreak.csproj set ApmApiKey "${{ secrets.ApmApiKey }}"
dotnet user-secrets --project Daybreak\Daybreak.csproj set ApmServiceAccount "${{ secrets.ApmServiceAccount }}"
dotnet user-secrets --project Daybreak\Daybreak.csproj set ApmServiceKey "${{ secrets.ApmServiceKey }}"
dotnet user-secrets --project Daybreak\Daybreak.csproj set ApmUri "${{ secrets.ApmUri }}"
- name: Restore project
@@ -1,8 +1,5 @@
using Daybreak.Shared.Services.ApplicationArguments;
using Daybreak.Shared.Services.FileProviders;
using Daybreak.Shared.Services.FileProviders;
using Daybreak.Shared.Services.Initialization;
using Daybreak.Shared.Services.Metrics;
using Daybreak.Shared.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http.Logging;
using Microsoft.Extensions.Logging;
@@ -35,12 +32,7 @@ public abstract class PluginConfigurationBase
public static HttpMessageHandler SetupLoggingAndMetrics<T>(IServiceProvider serviceProvider)
{
var logger = serviceProvider.GetRequiredService<ILogger<T>>();
var metricsService = serviceProvider.GetRequiredService<IMetricsService>();
return new MetricsHttpMessageHandler<T>(
metricsService,
new LoggingHttpMessageHandler(logger!) { InnerHandler = new HttpClientHandler() });
return new LoggingHttpMessageHandler(logger) { InnerHandler = new HttpClientHandler() };
}
public static void SetupDaybreakUserAgent(HttpRequestHeaders httpRequestHeaders)
@@ -1,57 +0,0 @@
using Daybreak.Shared.Services.Metrics;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Text.RegularExpressions;
namespace Daybreak.Shared.Utils;
public sealed partial class MetricsHttpMessageHandler<T> : DelegatingHandler
{
private const string LatencyUnitsName = "ms";
private const string LatencyDescription = "Http Request Latency";
private readonly string name;
private readonly Histogram<double> latencyHistogram;
public MetricsHttpMessageHandler(IMetricsService metricsService, HttpMessageHandler innerHandler) : base(innerHandler)
{
// Sanitize the type name to be OpenTelemetry compliant
// OpenTelemetry instrument names must match: [a-zA-Z][a-zA-Z0-9_.\-/]*
var typeName = typeof(T).Name;
// Remove generic arity suffix (e.g., `1, `2) and replace invalid characters
var sanitizedName = InvalidCharsRegex().Replace(typeName, "").ToLower();
this.name = $"http.client.latency.{sanitizedName}";
this.latencyHistogram = metricsService.CreateHistogram<double>(this.name, LatencyUnitsName, LatencyDescription, Models.Metrics.AggregationTypes.P95);
}
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
{
var sw = Stopwatch.StartNew();
try
{
var response = base.Send(request, cancellationToken);
return response;
}
finally
{
this.latencyHistogram.Record(sw.ElapsedMilliseconds);
}
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var sw = Stopwatch.StartNew();
try
{
var response = await base.SendAsync(request, cancellationToken);
return response;
}
finally
{
this.latencyHistogram.Record(sw.ElapsedMilliseconds);
}
}
[GeneratedRegex(@"[^a-zA-Z0-9_.\-/]")]
private static partial Regex InvalidCharsRegex();
}
+2 -1
View File
@@ -4,8 +4,9 @@ public class SecretKeys
{
public static readonly SecretKeys AadApplicationId = new() { Key = "AadApplicationId" };
public static readonly SecretKeys AadTenantId = new() { Key = "AadTenantId" };
public static readonly SecretKeys ApmApiKey = new() { Key = "ApmApiKey" };
public static readonly SecretKeys ApmUri = new() { Key = "ApmUri" };
public static readonly SecretKeys ApmServiceAccount = new() { Key = "ApmServiceAccount" };
public static readonly SecretKeys ApmServiceKey = new() { Key = "ApmServiceKey" };
public string Key { get; private set; } = string.Empty;
private SecretKeys()
+7 -48
View File
@@ -14,7 +14,6 @@ using System.Core.Extensions;
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Extensions.Core;
using System.Xml.Linq;
using TrailBlazr.Services;
namespace Daybreak.Services.Telemetry;
@@ -26,7 +25,8 @@ internal sealed class TelemetryHost : IDisposable, IHostedService
private const string MaskedValue = "[REDACTED]";
private static readonly string ApmEndpoint = SecretManager.GetSecret(SecretKeys.ApmUri);
private static readonly string ApmApiKey = SecretManager.GetSecret(SecretKeys.ApmApiKey);
private static readonly string ApmServiceAccount = SecretManager.GetSecret(SecretKeys.ApmServiceAccount);
private static readonly string ApmServiceKey = SecretManager.GetSecret(SecretKeys.ApmServiceKey);
// Headers that should be masked for security
private static readonly HashSet<string> SensitiveHeaders = new(StringComparer.OrdinalIgnoreCase)
@@ -50,7 +50,6 @@ internal sealed class TelemetryHost : IDisposable, IHostedService
private readonly ILoggerFactory loggerFactory;
private readonly ILogger<TelemetryHost> logger;
private TracerProvider? tracer;
private MeterProvider? meter;
private OpenTelemetryLoggerProvider? otlpLoggerProvider;
private ILogger? otlpLogger;
@@ -89,10 +88,8 @@ internal sealed class TelemetryHost : IDisposable, IHostedService
public void Dispose()
{
TelemetryLogSink.Instance.LoggingHandler = default;
this.tracer?.Dispose();
this.meter?.Dispose();
this.otlpLoggerProvider?.Dispose();
this.tracer = default;
this.meter = default;
this.otlpLoggerProvider = default;
this.otlpLogger = default;
@@ -145,9 +142,7 @@ internal sealed class TelemetryHost : IDisposable, IHostedService
{
var scopedLogger = this.logger.CreateScopedLogger();
TelemetryLogSink.Instance.LoggingHandler = default;
this.tracer?.Dispose();
this.meter?.Dispose();
this.tracer = default;
this.meter = default;
this.otlpLoggerProvider?.Dispose();
this.otlpLoggerProvider = default;
@@ -158,50 +153,14 @@ internal sealed class TelemetryHost : IDisposable, IHostedService
return;
}
if (string.IsNullOrWhiteSpace(ApmApiKey) || string.IsNullOrWhiteSpace(ApmEndpoint))
if (string.IsNullOrWhiteSpace(ApmServiceKey) || string.IsNullOrWhiteSpace(ApmEndpoint) || string.IsNullOrWhiteSpace(ApmServiceAccount))
{
scopedLogger.LogError("ApmApiKey or ApmEndpoint is not configured. Telemetry is disabled.");
return;
}
var apmUri = new Uri(ApmEndpoint);
this.tracer = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(this.resourceBuilder)
.AddSource(ServiceName)
.ConfigureResource(r => r.AddService(
serviceName: ServiceName,
serviceVersion: ProjectConfiguration.CurrentVersion.ToString()))
.AddHttpClientInstrumentation(o =>
{
o.RecordException = true;
o.EnrichWithHttpRequestMessage = (activity, request) =>
{
activity.DisplayName = request.RequestUri?.IdnHost ?? UnknownHost;
activity.SetTag("http.url", request.RequestUri?.ToString() ?? string.Empty);
activity.SetTag("http.method", request.Method.Method);
AddRequestHeaders(activity, request);
};
o.EnrichWithHttpResponseMessage = (activity, response) =>
{
activity.SetTag("http.status_code", (int)response.StatusCode);
activity.SetStatus(response.IsSuccessStatusCode ? ActivityStatusCode.Ok : ActivityStatusCode.Error);
AddResponseHeaders(activity, response);
};
o.EnrichWithException = (activity, exception) =>
{
activity.SetTag("exception.type", exception.GetType().FullName);
activity.SetTag("exception.message", exception.Message);
activity.SetTag("exception.stacktrace", exception.StackTrace);
};
})
.AddSqlClientInstrumentation()
.AddOtlpExporter(o =>
{
o.Endpoint = new Uri(apmUri, "v1/traces");
o.Headers = $"Authorization=ApiKey {ApmApiKey}";
o.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf;
})
.Build();
var creds = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"{ApmServiceAccount}:{ApmServiceKey}"));
this.meter = Sdk.CreateMeterProviderBuilder()
.SetResourceBuilder(this.resourceBuilder)
@@ -214,9 +173,9 @@ internal sealed class TelemetryHost : IDisposable, IHostedService
.AddOtlpExporter((exporterOptions, readerOptions) =>
{
exporterOptions.Endpoint = new Uri(apmUri, "v1/metrics");
exporterOptions.Headers = $"Authorization=ApiKey {ApmApiKey}";
exporterOptions.Headers = $"Authorization=Basic {creds}";
exporterOptions.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf;
readerOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 300_000;
readerOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 10_000;
readerOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta;
})
.Build();
@@ -231,7 +190,7 @@ internal sealed class TelemetryHost : IDisposable, IHostedService
logOpts.AddOtlpExporter(exp =>
{
exp.Endpoint = new Uri(apmUri, "v1/logs");
exp.Headers = $"Authorization=ApiKey {ApmApiKey}";
exp.Headers = $"Authorization=Basic {creds}";
exp.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf;
});