Query Language Reference
Monoscope uses a subset of the Kusto Query Language (KQL) to search and aggregate logs, traces, and metrics. The same dialect runs in the Log Explorer editor, dashboard widgets, alert monitors, the CLI, and the MCP server.
A query is an optional data source, a filter expression, and zero or more pipe operators. Each pipe (|) passes its output to the next step.
[source] [filter_expression] [| pipe_operator]...
spans
| where attributes.http.request.method == "GET" and attributes.http.response.status_code >= 400
| summarize count() by attributes.http.response.status_code
| sort by count_ desc
| take 10
Data Sources
| Source | Aliases | Description |
|---|---|---|
| Spans and logs | spans, otlp_logs_and_spans |
OpenTelemetry logs and spans. This is the default when no source is given. |
| Metrics | metrics, telemetry.metrics |
Time-series metric points (metric_name, value, attributes.*, resource.*). |
Filter Operators
| Operator | Description | Example |
|---|---|---|
==, != |
Equal, not equal | status_code != 200 |
>, <, >=, <= |
Numeric comparison | duration > 1000 |
has, !has |
Contains a whole word (case-insensitive) | body has "timeout" |
contains, !contains |
Contains a substring (case-insensitive) | attributes.url.path contains "/api" |
startswith, !startswith |
Starts with a string | resource.service.name startswith "payment-" |
endswith, !endswith |
Ends with a string | attributes.url.path endswith ".json" |
matches regex |
Quoted regex, case-sensitive | name matches regex "^GET /Cart$" |
matches, =~ |
Slash regex, case-insensitive | body =~ /^ERROR:.*/ |
in, !in |
Value is (not) in a list | attributes.http.request.method in ("GET", "POST") |
has_any, has_all |
Contains any / all of the words | body has_any ["error", "critical"] |
and, or, () |
Logical operators and grouping | (status_code >= 500) or (duration > 5000) |
Precedence is (), then and, then or. Use == for equality, not Lucene’s :.
Pipe Operators
| Operator | Syntax | Description |
|---|---|---|
where |
where condition |
Filter rows. |
summarize |
summarize agg [, agg...] [by field, ...] |
Aggregate, optionally grouped. |
extend |
extend name = expr [, ...] |
Add computed columns, keep existing ones. |
project |
project name = expr [, ...] |
Keep only the listed columns, optionally renamed. |
sort by / order by |
sort by field desc |
Order results (asc or desc). |
take / limit |
take N |
Limit the number of rows. |
| summarize avg(duration), max(duration) by resource.service.name, name
| extend duration_ms = round(duration / 1e6, 2)
| project service = resource.service.name, attributes.http.response.status_code
Aggregation Functions
Basic Aggregations
| Function | Description |
|---|---|
count(), count(field) |
Number of rows, or of non-null values |
sum(field), avg(field), min(field), max(field) |
Sum, mean, minimum, maximum |
median(field), stdev(field) |
Median, standard deviation |
range(field) |
max - min |
countif(condition) |
Rows where the condition is true |
dcount(field [, accuracy]) |
Approximate distinct count (accuracy hint 0-4) |
Percentiles
| Function | Description |
|---|---|
p50(field), p75, p90, p95, p99, p100 |
Fixed percentiles |
percentile(field, N) |
Nth percentile |
percentiles(field, N1, N2, ...) |
Several percentiles at once |
Arithmetic works inside the argument, for example percentiles(duration / 1e6, 50, 95, 99) for milliseconds. Percentiles are approximate.
Counter and Gauge Functions
Metric points need different aggregations depending on the metric type. These three functions are for the metrics source.
| Function | Use it for | Result per time bin |
|---|---|---|
rate(value) |
Counters | Per-second rate |
increase(value) |
Counters | How many in the bin |
last(value) |
Gauges | The last value in the bin |
rate(value) is a counter-aware per-second rate.
-
It is calculated per series first. A series is one metric name with one full set of attributes. The per-series rates are then summed into the
bygroups. - For cumulative counters, it takes the difference between consecutive points and divides by the elapsed seconds. If the value drops, the counter was reset (for example, a process restart), and the new value counts as the difference. This matches Prometheus.
- For delta-temporality counters, it is the sum of the deltas divided by the bin width in seconds.
- It is correct at any time range, including when a bin holds only one point.
- It has no meaning for gauges.
increase(value) uses the same per-series, reset-aware differences, summed per bin. It answers “how many in this interval”. A stat tile over increase(value) shows the total for the selected time window.
last(value) returns the last value in each bin. Use it for gauges such as memory in bytes, queue depth, or pressure percentages. A stat tile over last(value) shows the latest value, not a sum of bins.
Choose the aggregation from the OpenTelemetry metric type:
| Metric type | Use | Do not use |
|---|---|---|
| Counter (OTel Sum, monotonic) | rate(value), increase(value) |
sum(value) or range(value) over the raw cumulative values |
| Gauge | last(value), avg(value), max(value) |
rate, increase |
| Histogram | percentile, percentiles, p50 … p99 |
// Requests per second
metrics | where metric_name == "http.server.request.count" | summarize rate(value) by bin_auto(timestamp)
// Requests per second, one series per route and status code
metrics | where metric_name == "http.server.request.count" | summarize rate(value) by bin_auto(timestamp), attributes.http.route, attributes.http.response.status_code
// Requests per interval
metrics | where metric_name == "http.server.request.count" | summarize increase(value) by bin_auto(timestamp)
// Current memory use
metrics | where metric_name == "process.memory.usage" | summarize last(value) by bin_auto(timestamp)
Scalar Functions
| Function | Description | Example |
|---|---|---|
strcat(s1, s2, ...) |
Concatenate strings | strcat(attributes.http.request.method, " ", attributes.url.path) |
coalesce(v1, v2, ...) |
First non-null value | coalesce(user_id, "anonymous") |
iff(cond, then, else) |
If-then-else | iff(status_code >= 400, "error", "ok") |
case(p1, v1, ..., else) |
Multi-branch conditional | case(status_code >= 500, "5xx", status_code >= 400, "4xx", "ok") |
tofloat, todouble |
Convert to float | tofloat(errors) / tofloat(total) |
toint, tolong |
Convert to integer | toint(duration / 1e6) |
tostring |
Convert to text | tostring(status_code) |
round(value, decimals) |
Round to N decimal places | round(error_rate, 2) |
Time Functions
| Function | Description | Example |
|---|---|---|
now() |
Current time | timestamp <= now() |
ago(span) |
Time relative to now | timestamp >= ago(1h) |
bin(timestamp, span) |
Fixed-width time buckets | summarize count() by bin(timestamp, 5m) |
bin_auto(timestamp) |
Bucket width picked from the query time range | summarize count() by bin_auto(timestamp) |
Time spans use ns, us, ms, s, m, h, d, w, for example ago(7d).
bin_auto picks these widths:
| Time range | Bin width |
|---|---|
| Up to 2 minutes | 1 second |
| 2 to 5 minutes | 5 seconds |
| 5 to 15 minutes | 10 seconds |
| 15 minutes to 1 hour | 30 seconds |
| 1 to 6 hours | 1 minute |
| 6 to 14 hours | 5 minutes |
| 14 to 48 hours | 10 minutes |
| 2 to 7 days | 1 hour |
| 7 to 30 days | 6 hours |
| More than 30 days | 1 day |
In a time-series query, every column after the time bin in by is a grouping key: summarize ... by bin_auto(timestamp), a, b draws one series for each combination of a and b.
Field References
| Form | Example |
|---|---|
| Top-level field | status_code >= 400 |
| Nested JSON | request_body.user.id == "12345" |
| Array index | errors[0].message has "timeout" |
| Any array element | errors[*].type == "ValidationError" |
| OpenTelemetry attribute | attributes.http.request.method == "POST" |
| Resource attribute | resource.service.name == "payment-service" |
| Trace context | context.trace_id == "abc123def456" |
Values can be strings ("GET" or 'GET'), numbers, true/false, null, lists, durations (5m), and regexes (/pattern/).
Named Aggregations
Name a result with alias=function(...). Without an alias, the column gets a default name: count() is count_, countif(...) is countif_, and other functions use function_field, for example avg_duration or p95_duration.
| summarize total=count(), errors=countif(status_code >= 400), p99_latency=p99(duration) by resource.service.name
Examples
// Error rate by service over the last day
timestamp >= ago(24h)
| summarize total=count(), errors=countif(status_code >= 400), error_rate=round(countif(status_code >= 400) * 100.0 / count(), 2) by resource.service.name
| sort by error_rate desc
// Top error messages
status_code >= 500
| summarize count() by body
| sort by count_ desc
| take 10
// Latency percentiles in milliseconds over time
timestamp >= ago(6h)
| summarize percentiles(duration / 1e6, 50, 90, 95, 99) by bin(timestamp, 5m)
// Unique users per day
timestamp >= ago(7d)
| summarize unique_users=dcount(user_id) by bin(timestamp, 1d)
// Request rate per route
metrics
| where metric_name == "http.server.request.count"
| summarize rate(value) by bin_auto(timestamp), attributes.http.route
Limitations
-
No
join,union, subqueries,let,mv-expand, orrender. Visualization is chosen in the widget, not in the query. -
Comparisons with a missing field return false. Use
coalesce(field, default)for defaults. Aggregations skip null values. -
Strings are not converted to numbers. Write
status_code == 200, notstatus_code == "200". -
Filter on a time range first, prefer
hastocontains, and usetakewhile you explore.