RO EN

Observability (5) — KQL pentru investigarea incidentelor

Observability (5) — KQL pentru investigarea incidentelor ✨ Imagine generată cu AI
Doru Bulubașa
12 august 2026
41 vizualizări

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.