Native OpenTelemetry-Tags in ASP.NET Core
- Matt
Das Grafana-Dashboard zeigte mir die Anfragedauer sauber nach Route und
Statuscode aufgeschlüsselt, aus dem eingebauten Metrik
http.server.request.duration. Was fehlte, war unsere eigene Dimension. Ich
wollte dieselbe Kurve nach Mandant getrennt sehen, ohne dafür einen zweiten
Counter zu bauen, der am Ende nur die Hälfte der Anfragen erwischt. Genau dafür
gibt es seit .NET 8 eine Stelle, an der man sich in das schon vorhandene Metrik
einklinkt.
Die Feature am HttpContext
Der Einstieg heißt IHttpMetricsTagsFeature aus
Microsoft.AspNetCore.Http.Features. ASP.NET Core liest die Tags aus dieser
Feature genau dann aus, wenn es die Anfragedauer aufzeichnet. Man holt sie sich
in einer Middleware und hängt Schlüssel-Wert-Paare an:
using Microsoft.AspNetCore.Http.Features;
app.Use(async (context, next) =>
{
var tagsFeature = context.Features.Get<IHttpMetricsTagsFeature>();
if (tagsFeature != null)
{
var mandant = context.Request.Headers["X-Tenant"].ToString() switch
{
"" => "unbekannt",
var t => t
};
tagsFeature.Tags.Add(new KeyValuePair<string, object?>("tenant", mandant));
}
await next.Invoke();
});
Der Tag tenant taucht danach neben http.route und http.response.status_code
am selben Histogramm auf. Kein eigener Meter, kein IMeterFactory, keine
zweite Zeitmessung.
Der Haken mit dem null-Check
Die Zeile, die in keiner schnellen Erklärung steht: Die Feature liegt nur dann
am HttpContext, wenn überhaupt jemand auf das Metrik hört. In einem Testlauf
ohne konfigurierten Exporter oder ohne AddMeter("Microsoft.AspNetCore.Hosting")
ist tagsFeature schlicht null. Ich habe das erst gemerkt, als die Middleware
lokal ohne OpenTelemetry-Pipeline lief und der Zugriff auf .Tags mit einer
NullReferenceException abbrach. Der if-Block ist also kein Schmuck, sondern
Pflicht.
Der Preis heißt Kardinalität
Der Reiz ist zugleich die Falle. Jeder zusätzliche Tag-Wert erzeugt eine neue
Zeitreihe. tenant mit fünf Kunden ist harmlos. Dieselbe Mechanik mit einer
Benutzer-ID oder einem Zeitstempel sprengt die Dimensionen, und
Sammelwerkzeuge wie Prometheus fangen irgendwann an zu filtern oder fressen
Speicher. Die Regel, die ich mir gemerkt habe: Tags nur für Werte, deren
Wertebereich ich vorher aufzählen kann.
Wenn eine Route gar keine Metrik braucht, etwa der Health-Check-Endpunkt, den
ich in einem früheren Beitrag
beschrieben habe, dann ist der umgekehrte Weg der bessere: DisableHttpMetrics()
beim Mappen des Endpunkts. Ab .NET 9 lässt sich dasselbe pro Anfrage über
IHttpMetricsTagsFeature.MetricsDisabled umschalten, wenn eine Anfrage auf gar
keinen Endpunkt trifft.
Für alles dazwischen ist das Anreichern der bessere Deal, weil ich die bereits gemessene Dauer weiterverwende, statt sie ein zweites Mal zu nehmen.