Guide
KQL (Kusto Query Language)
KQL is the pipeline language of the SQL console and of the KQL tab in Explore. You start from a table and add steps with |. This page tells you which parts are verified, which are not, and how errors behave, so you do not have to guess. For plain SQL see the SQL console.
Open KQL#
Open SQL in the app and click KQL above the editor, or open Explore and switch a tab to KQL. You need the member role or higher. From an AI agent, use the MCP tool run_sql with "dialect": "kql" and an API key that has the sql scope.
logs
| where SeverityNumber >= 17
| summarize errors = count() by ServiceName
| order by errors descHow a KQL query runs#
- Tables:
logs,spans,trace_ids,metrics_gauge,metrics_sum,metrics_histogram,metrics_exponential_histogramandmetrics_summary. A leadingobs.is accepted. - Time range: the range in the picker is applied for you, right after the first table, on that table's time column (
Timestampfor logs and spans,Startfortrace_ids,TimeUnixfor metrics). The result reports whether the range was applied. A query that does not start from a table runs without a range. - Narrow further with a filter on the time column inside the query.
- Rows: the console adds
| take 10001and shows up to 10,000 rows, and says so when it cut the result. - Charts: keep the binned column's name, as in
summarize count() by bin(Timestamp, 5m), and Chart draws it over time. A binned time column comes back as a time. - Saving and export: Save and Recent remember the language. Export of KQL results is NDJSON only (Parquet is for SQL). Dashboard panels use SQL.
- Read-only, one query, the same limits and roles as the SQL console. Internal columns and keys are never returned: a query that names one is rejected with "is an internal column and cannot be queried" or "The query names an internal field".
What is verified#
The built-in KQL samples on the SQL page use these constructs, and Logthread's tests cover how the console adds the time range, caps the rows and hides internal columns around them:
| Construct | Example |
|---|---|
project | project Timestamp, SeverityText, ServiceName, Body |
order by (with asc or desc) | order by Timestamp desc |
take | take 100 |
top N by | top 20 by p95_ms desc |
summarize ... by | summarize errors = count() by ServiceName |
percentile(column, n) | percentile(Duration, 95) / 1e6 |
bin(column, step) | by bin(Timestamp, 5m) |
contains | where Body contains "timeout" |
They are used with where (== and >= comparisons), count(), sum() and avg().
Examples#
Every example here is one of the built-in samples on the SQL page: open KQL, then Start from an example.
Logs: the newest events
logs
| project Timestamp, SeverityText, ServiceName, Body
| order by Timestamp desc
| take 100Logs: events over time (chartable)
logs
| summarize events = count() by bin(Timestamp, 5m)
| order by Timestamp ascLogs: search the message
logs
| where Body contains "timeout"
| project Timestamp, ServiceName, Body
| take 100Spans: slowest operations. Duration is nanoseconds, so / 1e6 gives milliseconds.
spans
| summarize p95_ms = percentile(Duration, 95) / 1e6, calls = count() by SpanName
| top 20 by p95_ms descSpans: failing services
spans
| where StatusCode == "Error"
| summarize errors = count() by ServiceName
| order by errors descMetrics: the busiest counters
metrics_sum
| summarize total = sum(Value) by MetricName
| top 20 by total descMetrics: gauge averages
metrics_gauge
| summarize avg_value = avg(Value) by MetricName
| take 50Reference#
The tables below refresh automatically from the query engine: when you open this page they are replaced with the engine's current reference, and a line above them says when it was verified. Times are UTC. Large integers such as Duration and Count are 64-bit. metrics_exponential_histogram and metrics_summary are stored and queryable here; the Metrics page and alerts do not chart them. The schema browser on the SQL page lists the live columns.
Operators#
| Operator | Syntax | What it does |
|---|---|---|
where | T | where predicate | Keep the rows for which the predicate is true. |
project | T | project column [, name = expression ...] | Keep only the named columns, or compute new ones. |
extend | T | extend name = expression [, ...] | Add computed columns and keep the existing ones. |
summarize | T | summarize aggregation [, ...] [by column [, ...]] | Group the rows and aggregate each group. |
count | T | count | The number of rows, as one row with the column Count. |
distinct | T | distinct column [, ...] | The distinct combinations of the named columns. |
top | T | top n by column [asc | desc] | The first n rows in the order of a column. |
sort by | T | sort by column [asc | desc] [, ...] | Order the rows. order by is the same. |
order by | T | order by column [asc | desc] [, ...] | Order the rows. sort by is the same. |
take | T | take n | Any n rows. limit is the same. |
limit | T | limit n | Any n rows. take is the same. |
union | T | union U | The rows of both tables, for the columns they share. |
print | print name = expression [, ...] | One row of computed values, with no table. |
Functions#
| Function | Syntax | What it does |
|---|---|---|
ago | ago(timespan) | The time that long before now. |
datetime | datetime(2026-10-16 [10:00:00]) | A point in time, in UTC. |
bin | bin(value, size) | Round a value or time down to a multiple of the size. |
between | value between (low .. high) | True when the value is in the range, ends included. |
in | value in (a, b, ...) | True when the value is one of the list. |
has | text has "word" | Whole-word match, ignoring case. A phrase matches its words side by side, including words with a separator such as "gateway_request" (the engine's own translation of this is patched). |
has_cs | text has_cs "word" | Whole-word match, case sensitive. |
contains | text contains "part" | Match anywhere in the text, ignoring case. |
startswith | text startswith "prefix" | Match at the start of the text. |
matches regex | text matches regex "pattern" | Match a regular expression. |
count | count() | The number of rows in the group (see the aggregations). |
strlen | strlen(text) | The length of the text. |
tostring | tostring(value) | The value as text. |
attribute | LogAttributes["key"] or LogAttributes.key | Read one attribute of a map column (also ResourceAttributes, ScopeAttributes, SpanAttributes, Attributes); a missing attribute reads as an empty string (the engine's own translation of this is patched). |
Aggregations#
| Aggregation | Syntax | What it does |
|---|---|---|
count | count() | The number of rows. |
dcount | dcount(column) | The approximate number of distinct values. |
percentile | percentile(column, p) | The value below which p percent of the rows fall. |
Tables and columns#
| Table | Columns |
|---|---|
logs | TimestampTime (DateTime), Timestamp (DateTime64(9)), TraceId (String), SpanId (String), TraceFlags (UInt8), SeverityText (LowCardinality(String)), SeverityNumber (UInt8), ServiceName (LowCardinality(String)), Body (String), ResourceSchemaUrl (LowCardinality(String)), ResourceAttributes (Map(LowCardinality(String), String)), ScopeSchemaUrl (LowCardinality(String)), ScopeName (String), ScopeVersion (LowCardinality(String)), ScopeAttributes (Map(LowCardinality(String), String)), LogAttributes (Map(LowCardinality(String), String)), EventName (String), _ingested (DateTime64(3)) |
spans | Timestamp (DateTime64(9)), TraceId (String), SpanId (String), ParentSpanId (String), TraceState (String), SpanName (LowCardinality(String)), SpanKind (LowCardinality(String)), ServiceName (LowCardinality(String)), ResourceAttributes (Map(LowCardinality(String), String)), ScopeName (String), ScopeVersion (String), SpanAttributes (Map(LowCardinality(String), String)), Duration (UInt64), StatusCode (LowCardinality(String)), StatusMessage (String), Events.Timestamp (Array(DateTime64(9))), Events.Name (Array(LowCardinality(String))), Events.Attributes (Array(Map(LowCardinality(String), String))), Links.TraceId (Array(String)), Links.SpanId (Array(String)), Links.TraceState (Array(String)), Links.Attributes (Array(Map(LowCardinality(String), String))), _ingested (DateTime64(3)) |
trace_ids | TraceId (String), Start (SimpleAggregateFunction(min, DateTime64(9))), End (SimpleAggregateFunction(max, DateTime64(9))) |
metrics_gauge | ResourceAttributes (Map(LowCardinality(String), String)), ResourceSchemaUrl (String), ScopeName (String), ScopeVersion (String), ScopeAttributes (Map(LowCardinality(String), String)), ScopeDroppedAttrCount (UInt32), ScopeSchemaUrl (String), ServiceName (LowCardinality(String)), MetricName (LowCardinality(String)), MetricDescription (String), MetricUnit (String), Attributes (Map(LowCardinality(String), String)), StartTimeUnix (DateTime), TimeUnix (DateTime), Value (Float64), Flags (UInt32), Exemplars.FilteredAttributes (Array(Map(LowCardinality(String), String))), Exemplars.TimeUnix (Array(DateTime)), Exemplars.Value (Array(Float64)), Exemplars.SpanId (Array(String)), Exemplars.TraceId (Array(String)), _ingested (DateTime64(3)) |
metrics_sum | ResourceAttributes (Map(LowCardinality(String), String)), ResourceSchemaUrl (String), ScopeName (String), ScopeVersion (String), ScopeAttributes (Map(LowCardinality(String), String)), ScopeDroppedAttrCount (UInt32), ScopeSchemaUrl (String), ServiceName (LowCardinality(String)), MetricName (LowCardinality(String)), MetricDescription (String), MetricUnit (String), Attributes (Map(LowCardinality(String), String)), StartTimeUnix (DateTime), TimeUnix (DateTime), Value (Float64), Flags (UInt32), Exemplars.FilteredAttributes (Array(Map(LowCardinality(String), String))), Exemplars.TimeUnix (Array(DateTime)), Exemplars.Value (Array(Float64)), Exemplars.SpanId (Array(String)), Exemplars.TraceId (Array(String)), AggregationTemporality (Int32), IsMonotonic (Bool), _ingested (DateTime64(3)) |
metrics_histogram | ResourceAttributes (Map(LowCardinality(String), String)), ResourceSchemaUrl (String), ScopeName (String), ScopeVersion (String), ScopeAttributes (Map(LowCardinality(String), String)), ScopeDroppedAttrCount (UInt32), ScopeSchemaUrl (String), ServiceName (LowCardinality(String)), MetricName (LowCardinality(String)), MetricDescription (String), MetricUnit (String), Attributes (Map(LowCardinality(String), String)), StartTimeUnix (DateTime), TimeUnix (DateTime), Count (UInt64), Sum (Float64), BucketCounts (Array(UInt64)), ExplicitBounds (Array(Float64)), Exemplars.FilteredAttributes (Array(Map(LowCardinality(String), String))), Exemplars.TimeUnix (Array(DateTime)), Exemplars.Value (Array(Float64)), Exemplars.SpanId (Array(String)), Exemplars.TraceId (Array(String)), Flags (UInt32), Min (Float64), Max (Float64), AggregationTemporality (Int32), _ingested (DateTime64(3)) |
metrics_exponential_histogram | ResourceAttributes (Map(LowCardinality(String), String)), ResourceSchemaUrl (String), ScopeName (String), ScopeVersion (String), ScopeAttributes (Map(LowCardinality(String), String)), ScopeDroppedAttrCount (UInt32), ScopeSchemaUrl (String), ServiceName (LowCardinality(String)), MetricName (LowCardinality(String)), MetricDescription (String), MetricUnit (String), Attributes (Map(LowCardinality(String), String)), StartTimeUnix (DateTime), TimeUnix (DateTime), Count (UInt64), Sum (Float64), Scale (Int32), ZeroCount (UInt64), PositiveOffset (Int32), PositiveBucketCounts (Array(UInt64)), NegativeOffset (Int32), NegativeBucketCounts (Array(UInt64)), Exemplars.FilteredAttributes (Array(Map(LowCardinality(String), String))), Exemplars.TimeUnix (Array(DateTime)), Exemplars.Value (Array(Float64)), Exemplars.SpanId (Array(String)), Exemplars.TraceId (Array(String)), Flags (UInt32), Min (Float64), Max (Float64), AggregationTemporality (Int32), _ingested (DateTime64(3)) |
metrics_summary | ResourceAttributes (Map(LowCardinality(String), String)), ResourceSchemaUrl (String), ScopeName (String), ScopeVersion (String), ScopeAttributes (Map(LowCardinality(String), String)), ScopeDroppedAttrCount (UInt32), ScopeSchemaUrl (String), ServiceName (LowCardinality(String)), MetricName (LowCardinality(String)), MetricDescription (String), MetricUnit (String), Attributes (Map(LowCardinality(String), String)), StartTimeUnix (DateTime), TimeUnix (DateTime), Count (UInt64), Sum (Float64), ValueAtQuantiles.Quantile (Array(Float64)), ValueAtQuantiles.Value (Array(Float64)), Flags (UInt32), _ingested (DateTime64(3)) |
Error codes#
| Code | Retry | Meaning |
|---|---|---|
query_invalid | No | The statement is not valid, or uses something that is not available. The message says where. |
query_too_expensive | No | The query reads or returns too much. Narrow the time range or add filters. |
query_timeout | No | The query ran past its time limit. Narrow the time range or add filters. |
busy | Yes | Too many statements are running for the organization. Retry after the Retry-After seconds. |
read_quota_exceeded | Yes | The organization's hourly read allowance is used up. Retry after the Retry-After seconds. |
Limits#
| Limit | Value |
|---|---|
| Statements at once | 8 per workspace |
| Live tails at once | 4 per workspace |
| Exports at once | 2 per workspace |
| Time per query | 30 s |
| Time per export | 10 min |
| Size of a result | 100,000 rows or 64 MiB |
| Data read per statement | 50 GB |
| Data read per hour | 1 TB |
Not verified yet#
These worked in earlier descriptions of KQL, but Logthread has no test for them: the translator runs in storage, so the behaviour is the storage engine's, not ours. Try one on your data before you rely on it, and fall back to SQL if it is rejected.
| Not verified | Notes |
|---|---|
has, startswith, matches regex, strlen, tostring | Text functions. contains is the verified one. |
between, in, ago(), datetime() | The time range picker already bounds the query, so you rarely need them. |
extend, distinct, count as an operator, dcount() | Computed columns and distinct counts |
union, print | A query that starts with print or union runs without the picker's time range |
Column['key'] map access, such as LogAttributes['http.route'] | Check the first rows before filtering on it |
If the engine rejects a query, the console shows its message under the editor (code query_invalid). A query that reads too much data, runs too long or runs while the workspace is busy gets the same advice as SQL: narrow the range, filter earlier, retry shortly.
Next: the SQL console cookbook, or Analyze for pivots without a query.