A patra parte din seria despre observabilitate. Trace-urile explică incidente individuale; metricile le detectează înainte să devină incidente. Aici definim metrici de business, le punem pe dashboards și legăm alertele.
Metrici de infrastructură vs. metrici de business
CPU, memorie, request rate, latență — le primești gratuit din instrumentarea automată. Dar întrebările care contează pentru produs sunt de business: care e rata de cache hit a cache-ului semantic? Câte mesaje consumă fiecare tenant din limita lui? Cât costă în RU un request de chat?
Acestea nu există până nu le definești. Instrumentul standard: System.Diagnostics.Metrics — nativ în .NET, colectat de OpenTelemetry prin AddMeter (configurarea din partea 3 le prinde deja).
Meter, Counter, Histogram
public class ChatMetrics
{
// Numele Meter-ului corespunde cu AddMeter("ChatApi.*")
private static readonly Meter Meter = new("ChatApi.Chat");
private readonly Counter<long> _messagesProcessed;
private readonly Counter<long> _cacheLookups;
private readonly Histogram<double> _openAiDuration;
private readonly Histogram<double> _requestCharge;
public ChatMetrics()
{
_messagesProcessed = Meter.CreateCounter<long>(
"chat.messages.processed",
description: "Mesaje de chat procesate");
_cacheLookups = Meter.CreateCounter<long>(
"chat.cache.lookups",
description: "Cautari in cache-ul semantic");
_openAiDuration = Meter.CreateHistogram<double>(
"chat.openai.duration",
unit: "ms",
description: "Durata apelurilor OpenAI");
_requestCharge = Meter.CreateHistogram<double>(
"chat.cosmos.request_charge",
unit: "RU",
description: "RU consumate per operatie Cosmos");
}
public void MessageProcessed(string tenantTier) =>
_messagesProcessed.Add(1,
new KeyValuePair<string, object?>("tenant.tier", tenantTier));
public void CacheLookup(bool hit) =>
_cacheLookups.Add(1,
new KeyValuePair<string, object?>("cache.result", hit ? "hit" : "miss"));
public void OpenAiCallCompleted(double durationMs, string model) =>
_openAiDuration.Record(durationMs,
new KeyValuePair<string, object?>("openai.model", model));
public void CosmosOperation(double requestCharge, string operation) =>
_requestCharge.Record(requestCharge,
new KeyValuePair<string, object?>("db.operation", operation));
}
// Program.cs -- singleton, injectat unde e nevoie
builder.Services.AddSingleton<ChatMetrics>();
Alegerea instrumentului:
- Counter — valori care doar cresc (mesaje procesate, erori); agregarea naturală e rata
- Histogram — distribuții (durate, RU, dimensiuni); îți dă percentilele p50/p95/p99 din partea 1
- ObservableGauge — valori-instantaneu citite la colectare (conexiuni active, elemente în memorie); definit cu callback, nu apelat de tine
Capcana cardinalitații
Fiecare combinație de valori de dimensiuni creează o serie de timp separată. tenant.tier cu 3 valori = 3 serii. tenant.id cu 5000 de tenanți = 5000 de serii per metrică — cost exploziv și dashboards inutilizabile.
Regula: dimensiunile au cardinalitate mică și finită (tier, model, operație, rezultat). Identificatorii individuali (tenant id, session id, user id) trăiesc în log-uri și trace-uri, unde contextul individual e punctul forte — nu în metrici, unde agregarea e punctul forte. Exact diviziunea muncii din partea 1.
Dashboards cu Azure Workbooks
Metricile custom ajung în tabela customMetrics. Workbooks combină KQL, grafice și parametri interactivi într-un dashboard partajabil. Query-urile de bază pentru un dashboard de chat:
// Rata de cache hit, pe ore
customMetrics
| where name == "chat.cache.lookups"
| extend result = tostring(customDimensions["cache.result"])
| summarize lookups = sum(valueSum) by result, bin(timestamp, 1h)
| evaluate pivot(result, sum(lookups))
| extend hitRate = todouble(hit) / (hit + miss) * 100
// p95 durata OpenAI, per model
customMetrics
| where name == "chat.openai.duration"
| extend model = tostring(customDimensions["openai.model"])
| summarize p95 = percentile(valueSum / valueCount, 95) by model, bin(timestamp, 15m)
| render timechart
Structura de dashboard care s-a dovedit utilă: un rând de „semne vitale” (request rate, error rate, p95 global), un rând de business (cache hit rate, mesaje per tier, cost RU), un rând de dependențe (durata OpenAI, durata Cosmos, adâncimea cozii Service Bus). Sub 10 grafice — dashboardul care arată tot nu arată nimic.
Alertele care contează
Un dashboard e util când te uiți la el; alerta te caută ea. Principiul de selecție: alertezi pe simptome resimțite de utilizatori, nu pe cauze interne. CPU la 90% nu e alertă (poate e doar load normal); p95 peste 3 secunde e.
# Alerta pe log-based query -- rata de erori peste 2%
az monitor scheduled-query create \
--name "chat-error-rate" \
--resource-group my-rg \
--scopes $APP_INSIGHTS_ID \
--condition "count > 0" \
--condition-query 'requests
| where timestamp > ago(10m)
| summarize errorRate = countif(success == false) * 100.0 / count()
| where errorRate > 2' \
--evaluation-frequency 5m \
--window-size 10m \
--action-groups $ACTION_GROUP_ID
Setul minim pentru aplicația din serie: error rate peste prag, p95 peste prag, adâncimea DLQ peste zero (din seria Service Bus), daily cap de ingestie atins (din partea 2) și — specific business — cache hit rate sub prag (degradarea cache-ului semantic costă bani direct, în apeluri OpenAI).
Igiena alertelor e la fel de importantă ca alertele: fiecare alertă care se dovedește zgomot se ajustează sau se șterge imediat. O echipă care ignoră alertele pentru că „mereu țipă aia” nu mai are alerte deloc.
Ce urmează
Avem log-uri structurate, trace-uri distribuite și metrici pe dashboards. În ultima parte, limbajul care le leagă la investigație: KQL — de la alertă la cauză, pas cu pas, pe un incident realist.
Întrebări? Scrie-mi la contact@ludoprogramming.com.