Kusto Query Language (KQL)
Introduction
Section titled “Introduction”The Kusto Query Language (KQL) is the language Azure Monitor uses to query logs: you write a pipeline of operators over a table, and each operator filters, shapes, or aggregates the rows the previous one returned.
LocalStack for Azure answers KQL queries with its own engine, built on Microsoft’s KQL grammar. The engine runs your queries wherever Azure would:
- On a Log Analytics workspace, through the Log Analytics query API that
az monitor log-analytics queryand the Azure Monitor Query SDK call. - On an Application Insights component, through the classic query API that
az monitor app-insights querycalls, with the classic table names such asrequestsandcustomEvents. - On a single resource’s records, through the resource-centric query API that the Azure Portal Emulator uses on each resource’s Logs page.
- In the
transformKqlof a data collection rule, which transforms the records that AKS Container Insights collects.
Results, column names, value encodings, and errors follow the shapes Azure returns, so a query written against Azure runs unchanged against the emulator. A construct the engine doesn’t support yet fails the whole query with an error that names it, rather than returning empty or partial results.
Getting started
Section titled “Getting started”This guide runs queries over inline data, with print, range, and datatable, so you can try the language before any of your resources sends logs.
It assumes basic knowledge of the Azure CLI and our lstk az proxy.
Launch LocalStack using your preferred method. For more information, see Introduction to LocalStack for Azure. Once the container is running, enable Azure CLI interception by running:
lstk az start-interceptionThis command points the az CLI away from the public Azure management REST API and toward the LocalStack for Azure emulator API.
To revert this configuration, run:
lstk az stop-interceptionThis reconfigures the az CLI to send commands to the official Azure management REST API.
Create a workspace
Section titled “Create a workspace”Every query runs on a Log Analytics workspace. Create a resource group and a workspace:
az group create --name rg-kql-demo --location westeurope{ "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-kql-demo", "location": "westeurope", ... "name": "rg-kql-demo", "properties": { "provisioningState": "Succeeded" }, ...}az monitor log-analytics workspace create \ --name ws-kql-demo \ --resource-group rg-kql-demo \ --location westeurope{ ... "customerId": "64b907a8-4773-460c-807e-1e65fde10cd2", ... "location": "westeurope", ... "name": "ws-kql-demo", "provisioningState": "Succeeded", ... "retentionInDays": 30, "sku": { ... "name": "PerGB2018" }, ...}The query API identifies a workspace by its workspace ID, the customerId of the workspace.
Read it into a variable:
WORKSPACE_ID=$(az monitor log-analytics workspace show \ --workspace-name ws-kql-demo \ --resource-group rg-kql-demo \ --query customerId \ --output tsv)Compute values with print
Section titled “Compute values with print”print returns one row with the values you name, which makes it a quick way to try scalar functions:
az monitor log-analytics query \ --workspace "$WORKSPACE_ID" \ --analytics-query "print greeting = strcat('Hello', ', ', 'KQL'), day = startofday(datetime(2026-11-06T09:30:00Z)), parsed = parse_json('{\"size\": 42}').size"[ { "TableName": "PrimaryResult", "day": "2026-11-06T00:00:00Z", "greeting": "Hello, KQL", "parsed": "42" }]Aggregate inline rows
Section titled “Aggregate inline rows”datatable declares a table inline. Summarize its rows by level and by minute:
az monitor log-analytics query \ --workspace "$WORKSPACE_ID" \ --analytics-query "datatable(Timestamp: datetime, Level: string, DurationMs: long) [ datetime(2026-11-06T09:00:10Z), 'Info', 120, datetime(2026-11-06T09:00:40Z), 'Info', 80, datetime(2026-11-06T09:01:05Z), 'Error', 950, datetime(2026-11-06T09:01:30Z), 'Info', 100 ] | summarize Requests = count(), AvgDurationMs = avg(DurationMs) by Level, Minute = bin(Timestamp, 1m) | order by Minute asc, Level asc"[ { "AvgDurationMs": "100", "Level": "Info", "Minute": "2026-11-06T09:00:00Z", "Requests": "2", "TableName": "PrimaryResult" }, { "AvgDurationMs": "950", "Level": "Error", "Minute": "2026-11-06T09:01:00Z", "Requests": "1", "TableName": "PrimaryResult" }, { "AvgDurationMs": "100", "Level": "Info", "Minute": "2026-11-06T09:01:00Z", "Requests": "1", "TableName": "PrimaryResult" }]Join and expand
Section titled “Join and expand”Join two inline tables, expand a dynamic array into rows, and parse a value out of a string:
az monitor log-analytics query \ --workspace "$WORKSPACE_ID" \ --analytics-query "let Orders = datatable(OrderId: int, Items: dynamic) [1, dynamic(['apple', 'pear']), 2, dynamic(['plum'])]; let Notes = datatable(OrderId: int, Note: string) [1, 'shipped to=Rome', 2, 'shipped to=Oslo']; Orders | join kind=inner Notes on OrderId | mv-expand Item = Items to typeof(string) | parse Note with 'shipped to=' City | project OrderId, Item, City | order by OrderId asc, Item asc"[ { "City": "Rome", "Item": "apple", "OrderId": "1", "TableName": "PrimaryResult" }, { "City": "Rome", "Item": "pear", "OrderId": "1", "TableName": "PrimaryResult" }, { "City": "Oslo", "Item": "plum", "OrderId": "2", "TableName": "PrimaryResult" }]Generate rows with range
Section titled “Generate rows with range”range generates a series of values, a convenient source for time series:
az monitor log-analytics query \ --workspace "$WORKSPACE_ID" \ --analytics-query "range Hour from datetime(2026-11-06T00:00:00Z) to datetime(2026-11-06T03:00:00Z) step 1h | extend Load = hourofday(Hour) * 10"[ { "Hour": "2026-11-06T00:00:00Z", "Load": "0", "TableName": "PrimaryResult" }, { "Hour": "2026-11-06T01:00:00Z", "Load": "10", "TableName": "PrimaryResult" }, { "Hour": "2026-11-06T02:00:00Z", "Load": "20", "TableName": "PrimaryResult" }, { "Hour": "2026-11-06T03:00:00Z", "Load": "30", "TableName": "PrimaryResult" }]A query that ends in render returns, besides its rows, the chart it asks for, when the client requests it.
The Azure Monitor Query SDK requests it with include_visualization=True, and the Logs pages of the Azure Portal Emulator draw the chart.
Read an error
Section titled “Read an error”A query the engine can’t run returns the error Azure returns, with the operator and the expression that failed:
az monitor log-analytics query \ --workspace "$WORKSPACE_ID" \ --analytics-query "datatable(Name: string) ['a'] | where Size > 1"ERROR: (BadArgumentError) The request had some invalid propertiesCode: BadArgumentErrorMessage: The request had some invalid propertiesInner error: { "code": "SemanticError", "message": "A semantic error occurred.", "innererror": { "code": "SEM0100", "message": "'where' operator: Failed to resolve column or scalar expression named 'Size'" }}Delete the workspace
Section titled “Delete the workspace”Delete the workspace and its resource group:
az group delete --name rg-kql-demo --yesSupported language
Section titled “Supported language”The engine grows with the queries LocalStack’s users and Microsoft’s own example queries need. This section lists what it runs today.
Statements and sources
Section titled “Statements and sources”- Statements:
let, for scalar values and tabular expressions. - Tabular sources: a table of the workspace or the component,
datatable,print, andrange.
Tabular operators
Section titled “Tabular operators”where, extend, project, project-away, project-keep, project-rename, project-reorder, take (and limit), count, distinct, getschema, sort (and order), top, summarize, join (every kind), union, mv-expand, parse, and render.
Aggregation functions
Section titled “Aggregation functions”any, arg_max, arg_min, avg, avgif, count, countif, dcount, dcountif, make_bag, make_list, make_list_if, make_set, make_set_if, max, maxif, min, minif, percentile, percentiles, stdev, sum, sumif, take_any, and variance.
Scalar functions
Section titled “Scalar functions”- Strings:
countof,extract,extract_all,indexof,isempty,isnotempty,notempty,replace,replace_regex,replace_string,reverse,split,strcat,strcat_array,strcat_delim,strlen,strrep,substring,tolower,toupper,trim,trim_end, andtrim_start. - Dates and times:
ago,bin,datepart,datetime_add,datetime_diff,datetime_part,dayofmonth,dayofweek,dayofyear,endofday,endofmonth,endofweek,endofyear,format_datetime,format_timespan,getmonth,getyear,hourofday,monthofyear,now,startofday,startofmonth,startofweek,startofyear,unixtime_microseconds_todatetime,unixtime_milliseconds_todatetime,unixtime_seconds_todatetime, andweek_of_year. - Numbers:
abs,ceiling,exp,exp10,exp2,floor,isfinite,isinf,isnan,log,log10,log2,max_of,min_of,pow,round,sign, andsqrt. - Dynamic values:
array_concat,array_index_of,array_length,array_slice,array_sort_asc,array_sort_desc,bag_keys,bag_pack,pack,pack_array, andparse_json. - Conversions:
tobool,todatetime,todecimal,todouble,todynamic,toguid,toint,tolong,toreal,tostring, andtotimespan, with their aliases. - Conditionals and nulls:
case,coalesce,iff(andiif),isnotnull,isnull, andnot. - Encodings and URLs:
base64_decode_tostring,base64_encode_tostring,parse_url,url_decode,url_encode, andurl_encode_component. - Columns, types, and identifiers:
column_ifexists,gettype, andnew_guid.
Charts
Section titled “Charts”render returns the visualization Azure Monitor draws: areachart, barchart, columnchart, piechart, scatterchart, table, and timechart, with the kind, series, and title properties, the only ones Azure Monitor supports.
For more information, see render operator.
Limitations
Section titled “Limitations”- Unsupported constructs fail the query: An operator, a function, or a statement the engine doesn’t support, such as
evaluate,make-series,mv-apply,lookup,serialize,ingestion_time(), or aletthat declares a function, fails the whole query with an error that names it, for example'evaluate' is not supported by the LocalStack KQL engine yet. - Exact approximations:
dcountandpercentilereturn exact values, where Azure returns estimates, so a result can differ from Azure’s in its last digits on large inputs. - No cross-resource queries:
workspace(),app(), andresource()fail the query, and a query covers the one workspace or component it’s sent to. - Records in memory: Each table keeps its rows in memory, up to
LS_AZURE_LOG_ANALYTICS_MAX_ROWS_PER_TABLErows, and the rows are gone when the emulator stops. See the Log Analytics configuration.