Early access is open. Free plan during early access. Start free
Logthread

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 desc

How a KQL query runs#

  • Tables: logs, spans, trace_ids, metrics_gauge, metrics_sum, metrics_histogram, metrics_exponential_histogram and metrics_summary. A leading obs. is accepted.
  • Time range: the range in the picker is applied for you, right after the first table, on that table's time column (Timestamp for logs and spans, Start for trace_ids, TimeUnix for 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 10001 and 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:

ConstructExample
projectproject Timestamp, SeverityText, ServiceName, Body
order by (with asc or desc)order by Timestamp desc
taketake 100
top N bytop 20 by p95_ms desc
summarize ... bysummarize errors = count() by ServiceName
percentile(column, n)percentile(Duration, 95) / 1e6
bin(column, step)by bin(Timestamp, 5m)
containswhere 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 100

Logs: events over time (chartable)

logs
| summarize events = count() by bin(Timestamp, 5m)
| order by Timestamp asc

Logs: search the message

logs
| where Body contains "timeout"
| project Timestamp, ServiceName, Body
| take 100

Spans: 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 desc

Spans: failing services

spans
| where StatusCode == "Error"
| summarize errors = count() by ServiceName
| order by errors desc

Metrics: the busiest counters

metrics_sum
| summarize total = sum(Value) by MetricName
| top 20 by total desc

Metrics: gauge averages

metrics_gauge
| summarize avg_value = avg(Value) by MetricName
| take 50

Reference#

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#

OperatorSyntaxWhat it does
whereT | where predicateKeep the rows for which the predicate is true.
projectT | project column [, name = expression ...]Keep only the named columns, or compute new ones.
extendT | extend name = expression [, ...]Add computed columns and keep the existing ones.
summarizeT | summarize aggregation [, ...] [by column [, ...]]Group the rows and aggregate each group.
countT | countThe number of rows, as one row with the column Count.
distinctT | distinct column [, ...]The distinct combinations of the named columns.
topT | top n by column [asc | desc]The first n rows in the order of a column.
sort byT | sort by column [asc | desc] [, ...]Order the rows. order by is the same.
order byT | order by column [asc | desc] [, ...]Order the rows. sort by is the same.
takeT | take nAny n rows. limit is the same.
limitT | limit nAny n rows. take is the same.
unionT | union UThe rows of both tables, for the columns they share.
printprint name = expression [, ...]One row of computed values, with no table.

Functions#

FunctionSyntaxWhat it does
agoago(timespan)The time that long before now.
datetimedatetime(2026-10-16 [10:00:00])A point in time, in UTC.
binbin(value, size)Round a value or time down to a multiple of the size.
betweenvalue between (low .. high)True when the value is in the range, ends included.
invalue in (a, b, ...)True when the value is one of the list.
hastext 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_cstext has_cs "word"Whole-word match, case sensitive.
containstext contains "part"Match anywhere in the text, ignoring case.
startswithtext startswith "prefix"Match at the start of the text.
matches regextext matches regex "pattern"Match a regular expression.
countcount()The number of rows in the group (see the aggregations).
strlenstrlen(text)The length of the text.
tostringtostring(value)The value as text.
attributeLogAttributes["key"] or LogAttributes.keyRead 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#

AggregationSyntaxWhat it does
countcount()The number of rows.
dcountdcount(column)The approximate number of distinct values.
percentilepercentile(column, p)The value below which p percent of the rows fall.

Tables and columns#

TableColumns
logsTimestampTime (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))
spansTimestamp (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_idsTraceId (String), Start (SimpleAggregateFunction(min, DateTime64(9))), End (SimpleAggregateFunction(max, DateTime64(9)))
metrics_gaugeResourceAttributes (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_sumResourceAttributes (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_histogramResourceAttributes (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_histogramResourceAttributes (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_summaryResourceAttributes (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#

CodeRetryMeaning
query_invalidNoThe statement is not valid, or uses something that is not available. The message says where.
query_too_expensiveNoThe query reads or returns too much. Narrow the time range or add filters.
query_timeoutNoThe query ran past its time limit. Narrow the time range or add filters.
busyYesToo many statements are running for the organization. Retry after the Retry-After seconds.
read_quota_exceededYesThe organization's hourly read allowance is used up. Retry after the Retry-After seconds.

Limits#

LimitValue
Statements at once8 per workspace
Live tails at once4 per workspace
Exports at once2 per workspace
Time per query30 s
Time per export10 min
Size of a result100,000 rows or 64 MiB
Data read per statement50 GB
Data read per hour1 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 verifiedNotes
has, startswith, matches regex, strlen, tostringText 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, printA 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.