KQL · failures
Failed requests joined to their exceptions, by problem ID
Join failed requests to the exceptions recorded in the same operation and group them by problemId, to see which code paths cause the failures.
requestsexceptions failures
let lookback = 24h;
let failedRequests = requests
| where timestamp > ago(lookback)
| where tostring(success) =~ "false"
| summarize resultCode = take_any(resultCode) by operation_Id;
exceptions
| where timestamp > ago(lookback)
| join kind=inner failedRequests on operation_Id
| summarize
exceptions = sum(itemCount),
failed_operations = dcount(operation_Id),
example_type = take_any(type),
example_message = take_any(outerMessage)
by problemId, operation_Name, resultCode
| order by exceptions desc Returns: One row per problem ID, operation and result code among failed requests, with the sampling-adjusted exception count, how many failed operations it appeared in, and an example exception type and message.
Pitfalls
- Not every failed request has an exception. A handler that returns a 500 without throwing, or an exception your code catches and never tracks, leaves the request with no match here; compare the total with failed requests by result code.
- join defaults to innerunique, which deduplicates the left side on the key. kind=inner keeps every exception, which is what you want when counting them.
- The right side is one row per operation_Id on purpose. Joining to raw request rows would repeat each exception once per failed request in that operation.
- Sampling keeps or drops whole operations together, because it decides by operation ID, so a sampled-in failed request normally brings its exceptions with it.
How it works
A failed request tells you that something broke; the exception recorded in the same operation usually tells you what. This query puts the two together and groups by problemId, which the data model describes as the field that identifies where in code an exception was thrown, typically the exception type plus a function from the call stack. One problem ID is one bug, however many requests it broke.
Line by line
let failedRequests = ...;collects failed requests in the window and reduces them to one row peroperation_Id, keeping oneresultCode.operation_Idis shared by every telemetry item in the same operation, so it is the key that links a request to its exceptions.join kind=inner failedRequests on operation_Idkeeps only exceptions from operations that contain a failed request. Microsoft’s own Classic API examples join exceptions to requests onoperation_Idthe same way.exceptions = sum(itemCount)counts exceptions the sampling-aware way.failed_operations = dcount(operation_Id)counts distinct operations. At its default accuracy,dcount()is exact for small sets (up to about 1,000 values) and an estimate above that.example_typeandexample_messagegive one exception type and outer message per row, enough to recognise the bug without opening anything.order by exceptions descputs the noisiest problem first.
Reading the result
A problem ID that appears across many operations is shared code: a database helper, an auth check, a serializer. One that appears in a single operation is that route’s own bug. Rows with the same problem ID but different result codes are worth a look: the same exception surfacing as a 500 in one place and a 400 in another usually means one code path catches and translates it and another doesn’t.
Take any problem ID into exception details for a problem ID for messages, stack frames and affected users. The ASP.NET MVC and Web API guide explains which exceptions the classic SDK records on its own and which you track yourself, the usual reason a failed request has no exception.
Variations
- Failures without an exception: swap
kind=innerforkind=rightantiand group the result byresultCodeto count failed operations that have no exception at all. - Workspace tables: use
AppRequestsandAppExceptions,TimeGenerated,Success,OperationId,ProblemId,ExceptionType(the classictype),OuterMessageandItemCount.