// @title Attribution gaps // @summary How much observed AI consumption cannot be assigned to an accountable API and operation. The unattributed share stays in the denominator. // @posture attribute // @posture-note Connects consumption to an accountable API and operation, or reports that it cannot. // // How much observed AI consumption cannot be assigned to an accountable // workload. The unattributed share stays in the denominator — that is the // point of the query. // // Read-only. Runs in your Log Analytics workspace; sends nothing anywhere. // // WHY THIS QUERY JOINS TWO TABLES. // // ApiId and OperationId DO NOT EXIST on ApiManagementGatewayLlmLog. That // table carries model, deployment and token counts, and identifies the // request only by CorrelationId; the closest thing it holds is // OperationName, which is not the operation identity used for attribution. // Reading ApiId from it does not return empty — it fails to resolve, and // the query does not run at all. // // Request identity lives in ApiManagementGatewayLogs. Attribution is // therefore a property of the JOIN, and this query is the only place in the // check where the two tables have to meet. // // Both sides are collapsed to one row per CorrelationId before joining, so // the join cannot multiply one request into several. Identity fields are // taken only where the gateway records agree; disagreement reads as absent // rather than being resolved by picking. // // LEFT OUTER, anchored on the LLM side: a model call with no matching // gateway record is the most unattributed thing in the estate, and an inner // join would silently delete exactly that population from the result. let _startTime = ago(30d); let _endTime = now(); let llm = ApiManagementGatewayLlmLog | where TimeGenerated between (_startTime .. _endTime) | summarize PromptTokensSet = make_set(PromptTokens, 2), CompletionTokensSet = make_set(CompletionTokens, 2), TotalTokensSet = make_set(TotalTokens, 2) by CorrelationId | extend PromptTokens = iff(array_length(PromptTokensSet) == 1, tolong(PromptTokensSet[0]), long(null)), CompletionTokens = iff(array_length(CompletionTokensSet) == 1, tolong(CompletionTokensSet[0]), long(null)), TotalTokens = iff(array_length(TotalTokensSet) == 1, tolong(TotalTokensSet[0]), long(null)) | project CorrelationId, PromptTokens, CompletionTokens, TotalTokens; let gateway = ApiManagementGatewayLogs | where TimeGenerated between (_startTime .. _endTime) | summarize ApiIdSet = make_set(ApiId, 2), OperationIdSet = make_set(OperationId, 2), ProductIdSet = make_set(ProductId, 2) by CorrelationId | extend ApiId = iff(array_length(ApiIdSet) == 1, tostring(ApiIdSet[0]), ''), OperationId = iff(array_length(OperationIdSet) == 1, tostring(OperationIdSet[0]), ''), ProductId = iff(array_length(ProductIdSet) == 1, tostring(ProductIdSet[0]), ''), HasGatewayRecord = true | project CorrelationId, ApiId, OperationId, ProductId, HasGatewayRecord; let joined = llm | join kind=leftouter (gateway) on CorrelationId | extend HasGatewayRecord = coalesce(HasGatewayRecord, false), ApiId = coalesce(ApiId, ''), OperationId = coalesce(OperationId, ''), ProductId = coalesce(ProductId, '') | extend Attribution = case( not(HasGatewayRecord), 'UNATTRIBUTED — no gateway record for this model call', isempty(ApiId) and isempty(OperationId), 'UNATTRIBUTED — gateway record carries no API or operation identity', isempty(OperationId), 'PARTIAL — API known, operation not identified', isempty(ProductId), 'ATTRIBUTED — API and operation, no product', 'ATTRIBUTED — API, operation and product'); let totalRequests = toscalar(joined | count); joined | summarize Requests = count(), InputTokens = sum(PromptTokens), OutputTokens = sum(CompletionTokens), TotalTokens = sum(TotalTokens) by Attribution | extend ShareOfRequestsPct = round(100.0 * Requests / totalRequests, 1) | project Attribution, Requests, ShareOfRequestsPct, InputTokens, OutputTokens, TotalTokens | order by Requests desc