Native OpenTelemetry-Tags in ASP.NET Core

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.

Quelle: ASP.NET Core metrics, Microsoft Learn