Skip to content
Get Started for Free

Kusto Query Language (KQL)

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 query and the Azure Monitor Query SDK call.
  • On an Application Insights component, through the classic query API that az monitor app-insights query calls, with the classic table names such as requests and customEvents.
  • 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 transformKql of 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.

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:

Terminal window
lstk az start-interception

This 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:

Terminal window
lstk az stop-interception

This reconfigures the az CLI to send commands to the official Azure management REST API.

Every query runs on a Log Analytics workspace. Create a resource group and a workspace:

Terminal window
az group create --name rg-kql-demo --location westeurope
Output
{
"id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-kql-demo",
"location": "westeurope",
...
"name": "rg-kql-demo",
"properties": {
"provisioningState": "Succeeded"
},
...
}
Terminal window
az monitor log-analytics workspace create \
--name ws-kql-demo \
--resource-group rg-kql-demo \
--location westeurope
Output
{
...
"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:

Terminal window
WORKSPACE_ID=$(az monitor log-analytics workspace show \
--workspace-name ws-kql-demo \
--resource-group rg-kql-demo \
--query customerId \
--output tsv)

print returns one row with the values you name, which makes it a quick way to try scalar functions:

Terminal window
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"
Output
[
{
"TableName": "PrimaryResult",
"day": "2026-11-06T00:00:00Z",
"greeting": "Hello, KQL",
"parsed": "42"
}
]

datatable declares a table inline. Summarize its rows by level and by minute:

Terminal window
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"
Output
[
{
"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 two inline tables, expand a dynamic array into rows, and parse a value out of a string:

Terminal window
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"
Output
[
{
"City": "Rome",
"Item": "apple",
"OrderId": "1",
"TableName": "PrimaryResult"
},
{
"City": "Rome",
"Item": "pear",
"OrderId": "1",
"TableName": "PrimaryResult"
},
{
"City": "Oslo",
"Item": "plum",
"OrderId": "2",
"TableName": "PrimaryResult"
}
]

range generates a series of values, a convenient source for time series:

Terminal window
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"
Output
[
{
"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.

A query the engine can’t run returns the error Azure returns, with the operator and the expression that failed:

Terminal window
az monitor log-analytics query \
--workspace "$WORKSPACE_ID" \
--analytics-query "datatable(Name: string) ['a'] | where Size > 1"
Output
ERROR: (BadArgumentError) The request had some invalid properties
Code: BadArgumentError
Message: The request had some invalid properties
Inner 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 and its resource group:

Terminal window
az group delete --name rg-kql-demo --yes

The engine grows with the queries LocalStack’s users and Microsoft’s own example queries need. This section lists what it runs today.

  • Statements: let, for scalar values and tabular expressions.
  • Tabular sources: a table of the workspace or the component, datatable, print, and range.

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.

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.

  • 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, and trim_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, and week_of_year.
  • Numbers: abs, ceiling, exp, exp10, exp2, floor, isfinite, isinf, isnan, log, log10, log2, max_of, min_of, pow, round, sign, and sqrt.
  • Dynamic values: array_concat, array_index_of, array_length, array_slice, array_sort_asc, array_sort_desc, bag_keys, bag_pack, pack, pack_array, and parse_json.
  • Conversions: tobool, todatetime, todecimal, todouble, todynamic, toguid, toint, tolong, toreal, tostring, and totimespan, with their aliases.
  • Conditionals and nulls: case, coalesce, iff (and iif), isnotnull, isnull, and not.
  • Encodings and URLs: base64_decode_tostring, base64_encode_tostring, parse_url, url_decode, url_encode, and url_encode_component.
  • Columns, types, and identifiers: column_ifexists, gettype, and new_guid.

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.

  • 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 a let that 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: dcount and percentile return 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(), and resource() 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_TABLE rows, and the rows are gone when the emulator stops. See the Log Analytics configuration.
Was this page helpful?