Jede Chat-Completion kostet Geld und braucht Zeit — und standardmäßig taucht beides nirgendwo auf. Keine Token-Zahlen, keine Latenzwerte, keine Spur davon, was die App tatsächlich nach draußen geschickt hat. Wenn zum ersten Mal jemand fragt „Was kostet unser AI-Feature pro Tag?" oder „Warum war dieser Request langsam?", lautet die ehrliche Antwort: Das weiß niemand.
Dieser Walkthrough ändert das mit OpenTelemetry und Application Insights: verteilte Traces über den gesamten Request, Token-Verbrauch als abfragbare Custom Metrics und eine optionale Kostenschätzung pro Request. Er baut direkt auf dem schlüssellosen Setup aus dem vorherigen Tutorial auf — eine .NET-Minimal-API, die eine Microsoft-Foundry-Ressource ohne API-Keys aufruft. Dieser Teil ist hier die Baseline; ein Diff der beiden Repos zeigt exakt, was Observability zusätzlich braucht.
Die drei Signale
Jeder /chat-Request erzeugt drei Arten von Telemetrie in Application Insights:
- Ein verteilter Trace — der eingehende HTTP-Request, ein eigener
chat completion-Span mit Deployment-Name und Token-Zahlen als Tags, und der ausgehende Azure-OpenAI-HTTP-Aufruf als dessen Kind. Ein Klick beantwortet die Frage „Wo ist die Zeit geblieben?". - Custom Metrics —
chat.tokens.prompt,chat.tokens.completionundchat.tokens.totalals Counter,chat.request.durationals Latenz-Histogramm. Alle mit dem Deployment-Namen getaggt, sodass sich jedes Chart nach Modell aufteilen lässt. - Eine Kostenschätzung —
chat.request.costin USD pro Request, nur aufgezeichnet, wenn Preise konfiguriert sind.
OpenTelemetry verdrahten
Die Azure-Monitor-Distro übernimmt die Schwerarbeit — ein Aufruf verdrahtet Traces, Metriken und Logs, inklusive ASP.NET-Core- und HttpClient-Instrumentierung, sodass die ausgehenden Azure-OpenAI-Aufrufe automatisch in den Traces auftauchen. Nur die eigenen Instrumente müssen zusätzlich registriert werden:
builder.Services.AddOpenTelemetry().UseAzureMonitor()// Unsere eigenen Instrumente (Token-Zahlen, Latenz) aus Telemetry.cs..WithMetrics(metrics => metrics.AddMeter(Telemetry.SourceName))// Unsere eigenen Spans um jeden Modell-Aufruf..WithTracing(tracing => tracing.AddSource(Telemetry.SourceName));
UseAzureMonitor() liest per Konvention APPLICATIONINSIGHTS_CONNECTION_STRING — lokal aus einer gitignorierten appsettings.local.json, auf App Service aus einem App Setting, das Terraform anlegt. Ohne Connection String läuft die App einfach unbeobachtet weiter.
Den Modell-Aufruf instrumentieren
Der interessante Teil ist, was rund um den eigentlichen Modell-Aufruf passiert. Eine eigene Activity umschließt ihn, und der Token-Verbrauch, den jede Non-Streaming-Completion ohnehin mitbringt, wird ausgelesen und aufgezeichnet:
using var activity = Telemetry.ActivitySource.StartActivity("chat completion");activity?.SetTag("gen_ai.request.model", deployment);var stopwatch = Stopwatch.StartNew();ChatCompletion completion = await chatClient.CompleteChatAsync(request.Message);stopwatch.Stop();// Jede Non-Streaming-Completion bringt ihren Token-Verbrauch mit —// er muss nur ausgelesen und aufgezeichnet werden.var tags = new TagList { { "gen_ai.request.model", deployment } };Telemetry.PromptTokens.Add(completion.Usage.InputTokenCount, tags);Telemetry.CompletionTokens.Add(completion.Usage.OutputTokenCount, tags);Telemetry.TotalTokens.Add(completion.Usage.TotalTokenCount, tags);Telemetry.RequestDuration.Record(stopwatch.Elapsed.TotalMilliseconds, tags);
gen_ai.request.model stammt aus den OpenTelemetry-GenAI-Konventionen. Wer sich daran hält, dessen Dashboards und Queries funktionieren weiter, wenn ein zweites Deployment dazukommt — und bleiben kompatibel mit Tooling, das die Konvention versteht.
Die Kostenschätzung ist bewusst einfach: Input- und Output-Preise pro einer Million Tokens in appsettings.json (Pricing:InputPer1MTokens / Pricing:OutputPer1MTokens), multipliziert mit den Token-Zahlen jedes Requests. Preise ändern sich, deshalb stehen beide standardmäßig auf 0 — die Metrik bleibt aus, bis man die aktuellen Preise für Modell und Region nachgeschlagen hat. Ist sie aktiviert, zeichnet jeder Request chat.request.cost auf und taggt seinen Span mit chat.estimated_cost_usd.
Abfragen in Application Insights
Die Custom Metrics landen in der Tabelle customMetrics, die eigenen Spans in dependencies — beides per KQL abfragbar. Tokens pro Tag:
customMetrics| where name == "chat.tokens.total"| summarize tokens = sum(valueSum) by bin(timestamp, 1d)| render columnchart
Und die zehn teuersten Requests, direkt aus den Span-Tags:
dependencies| where name == "chat completion"| extend cost_usd = todouble(customDimensions["chat.estimated_cost_usd"])| top 10 by cost_usd| project timestamp, duration, cost_usd
Die Dashboards muss man nicht selbst bauen: Das Terraform deployt auch ein Azure Workbook („Azure OpenAI Observability", unter Monitor > Workbooks) mit Tokens über Zeit, Latenz p50/p95, geschätzten Kosten, Requests pro Modell und der Tabelle der teuersten Requests.
Ausführen
az logincd infracp terraform.tfvars.example terraform.tfvars # Subscription + eigene Object-ID eintragenterraform initterraform apply
Das provisioniert alles aus dem schlüssellosen Setup plus einen Log-Analytics-Workspace, Application Insights und das Workbook. Die App über appsettings.local.json auf die Outputs openai_v1_endpoint und application_insights_connection_string zeigen lassen, dann dotnet run und einen Request schicken.
Ein einzelner Request ergibt allerdings leere Charts. Das Repo bringt einen kleinen Load Generator mit, der einen Schwung unterschiedlicher Prompts schickt, damit die Token-, Latenz- und Kosten-Charts etwas zu zeigen haben:
cd tools/load-generatordotnet run # 40 Requests an http://localhost:5002
Die Application-Insights-Ingestion braucht ein bis zwei Minuten — Telemetrie ist nicht Echtzeit. Und ein 401 direkt nach terraform apply ist weiterhin normal: Rollenzuweisungen brauchen ein paar Minuten, bis sie greifen.
Das Deployment ist ein dotnet publish und ein az webapp deploy; die genauen Befehle stehen im README des Repos. Auf App Service meldet derselbe Code über den Connection String, den Terraform bereits gesetzt hat — keine Code-Änderung, keine Config-Anpassung.
Was Sie brauchen
- Ein Azure-Abonnement
- Azure CLI, eingeloggt mit
az login - .NET 10 SDK
- Terraform >= 1.9
In einem echten Enterprise-Setup kommt noch mehr dazu — Alerts auf Kosten- und Latenz-Schwellwerte, Sampling-Strategien bei hohem Traffic und Dashboards pro Team — aber Traces, Token-Metriken und Kosten pro Request sind das Fundament, auf dem all das aufbaut.


