Ultima parte din seria despre observabilitate. Toată telemetria din părțile anterioare — log-uri Serilog, trace-uri OpenTelemetry, metrici custom — stă în Application Insights și așteaptă întrebări. Limbajul întrebărilor e KQL. Îl învățăm pe un incident realist, de la alertă la cauză.
Harta tabelelor
Ce am instrumentat în serie și unde a ajuns:
| Tabela | Conține | Sursa din seria noastră |
|---|---|---|
| requests | Request-urile HTTP primite | Instrumentarea automată ASP.NET Core |
| dependencies | Apeluri ieșite: Cosmos, Service Bus, HTTP, OpenAI | Instrumentarea automată Azure SDK |
| traces | Log-urile (da, log-urile!) | Serilog cu TelemetryConverter.Traces |
| exceptions | Excepțiile cu stack trace | Automată + LogError cu excepție |
| customMetrics | Metricile din Meter | ChatMetrics din partea 4 |
Coloana care le leagă pe toate: operation_Id — trace id-ul distribuit. Toate log-urile, dependențele și excepțiile unui request partajează același operation_Id. E cheia oricărei corelări.
Operatorii esențiali în 60 de secunde
requests
| where timestamp > ago(1h) // filtrare temporala -- mereu primul
| where success == false
| project timestamp, name, resultCode, duration, operation_Id // doar coloanele utile
| sort by timestamp desc
| take 20
// summarize -- inima KQL: agregari pe grupuri
requests
| where timestamp > ago(24h)
| summarize
total = count(),
errors = countif(success == false),
p95 = percentile(duration, 95)
by name, bin(timestamp, 1h)
| extend errorRate = errors * 100.0 / total
Cu where, project, summarize, extend și bin acoperi 90% din investigații. render timechart la final transformă rezultatul în grafic direct în portal.
Incidentul: „chat-ul e lent pentru unii clienți"
Alerta din partea 4 s-a declanșat: p95 pe endpoint-ul de chat a depășit 3 secunde. Investigăm pas cu pas.
Pasul 1: confirmă și delimitează
requests
| where timestamp > ago(2h)
| where name == "POST /api/chat"
| summarize p50 = percentile(duration, 50),
p95 = percentile(duration, 95),
count()
by bin(timestamp, 5m)
| render timechart
Graficul arată: p95 a sărit de la 900ms la 4.2s acum 40 de minute; p50 e neschimbat. Deci nu toți sunt afectați — o coadă a distribuției suferă. Exact cazul în care media ar fi mințit (partea 1).
Pasul 2: cine e afectat?
TenantId-ul e în customDimensions pe log-uri (middleware-ul din partea 2). Îl aducem lângă durata request-urilor prin join pe operation_Id:
requests
| where timestamp > ago(1h) and name == "POST /api/chat"
| where duration > 3000
| join kind=inner (
traces
| where timestamp > ago(1h)
| extend TenantId = tostring(customDimensions.TenantId)
| where isnotempty(TenantId)
| distinct operation_Id, TenantId
) on operation_Id
| summarize slowRequests = count() by TenantId
| sort by slowRequests desc
Rezultatul: 94% din request-urile lente aparțin unui singur tenant. Delimitare completă — nu e o degradare globală.
Pasul 3: unde se duce timpul?
dependencies
| where timestamp > ago(1h)
| where operation_Id in ((
requests
| where timestamp > ago(1h) and name == "POST /api/chat" and duration > 3000
| project operation_Id))
| summarize p95 = percentile(duration, 95), calls = count() by type, target
| sort by p95 desc
Verdictul: dependența Cosmos DB pe containerul de cache semantic are p95 de 3.1s (normal: 40ms). OpenAI e neschimbat. Problema e la baza de date, pe un singur container.
Pasul 4: de ce?
traces
| where timestamp > ago(1h)
| extend TenantId = tostring(customDimensions.TenantId)
| where TenantId == "tenant-742"
| where severityLevel >= 2 // Warning+
| summarize count() by message = substring(message, 0, 120)
| sort by count_ desc
Top mesaj: rate limiting 429 de la Cosmos cu retry-uri. Iar metrica chat.cosmos.request_charge (partea 4) pe dimensiunea operației arată RU-uri crescute pe căutarea în cache pentru acest tenant — tenant nou, mare, cu cache-ul încă gol: fiecare miss declanșează căutarea vectorială scumpă, pe o partiție care și-a atins RU-urile. Hot partition, exact anatomia din seria Cosmos DB.
De la alertă la cauză: patru query-uri, zero deploy-uri, zero ghicit. Asta cumperi cu instrumentarea din părțile 1-4.
Query-urile de pus în sertar
Investigațiile au tipare. Salvează-le ca funcții partajate (query packs) înainte de incident:
// "Arata-mi tot despre operation_Id-ul asta" -- primul reflex la orice incident
let opId = "abc123...";
union requests, dependencies, traces, exceptions
| where operation_Id == opId
| project timestamp, itemType,
name = coalesce(name, message),
duration, resultCode = tostring(resultCode)
| sort by timestamp asc
// Top erori noi fata de saptamana trecuta
exceptions
| where timestamp > ago(1d)
| summarize today = count() by problemId
| join kind=leftouter (
exceptions
| where timestamp between (ago(8d) .. ago(1d))
| summarize lastWeek = count() by problemId
) on problemId
| where isnull(lastWeek) or today > lastWeek * 3
| sort by today desc
Încheierea seriei
Cinci părți, un sistem complet: pilonii și diviziunea muncii dintre ei, log-uri structurate cu context automat, trace-uri care traversează serviciile și cozile, metrici de business cu dashboards și alerte, și KQL-ul care le leagă pe toate la ora incidentului.
Cu aceasta, seria mare Cloud-native cu Azure și .NET își închide ultimul capitol major: securitate fără secrete, containere cu scalare elastică, messaging rezilient, pattern-uri Cosmos DB și acum observabilitate completă. Sistemul nu doar rulează — îți și spune ce face.
Dacă ai întrebări sau vrei să discuți cum construiești observabilitatea în proiectul tău, scrie-mi la contact@ludoprogramming.com.