Skip to content
HALKON telemetry ingest & retention
query service nominal | queries in flight — | ingest→query lag p95 — s | refreshed —

Reference · HKQL v3

HKQL

A pipeline of stages separated by |. Each stage takes a table and returns a table. There are no joins across regions, no subqueries in the where clause, and no way to write a query whose cost you cannot see before you run it.

shapeevery query looks like this
from <source>
| where     <predicate>        # narrow first, always
| extend    <name> = <expr>    # derive columns
| summarize <agg> by <keys>    # collapse
| sort by   <column> desc
| limit     <n>

Sources are logs, metrics, traces and agent_health. A query without an explicit time predicate is rewritten to the last 15 minutes rather than scanning your retention window by accident.

Worked examples

1. Error rate per pod, one-minute buckets

HKQLscans ~ 40 M events
from logs
| where ts > ago(1h) and service == "checkout-api"
| where severity in ("ERROR", "FATAL")
| summarize errors = count() by pod, t = bin(ts, 1m)
| sort by t asc, errors desc
First rows of the result. Column types are inferred and shown in the header.
pod : stringt : timestamperrors : int64
checkout-api-7d9f42026-09-01T09:14:00Z318
checkout-api-7d9f42026-09-01T09:15:00Z294
checkout-api-2b81c2026-09-01T09:15:00Z11
checkout-api-7d9f42026-09-01T09:16:00Z7

2. Latency percentiles from a histogram metric

HKQLscans ~ 6 M samples
from metrics
| where ts > ago(6h) and name == "http_request_duration_seconds"
| where route startswith "/api/"
| summarize
    p50 = histogram_quantile(0.50, le, value),
    p95 = histogram_quantile(0.95, le, value),
    p99 = histogram_quantile(0.99, le, value)
  by route, t = bin(ts, 5m)
| extend p99_ms = round(p99 * 1000, 1)
| sort by p99_ms desc
| limit 20

3. Pull a field out of unstructured text

HKQLregex_extract is a cost class C function
from logs
| where ts between ("2026-08-19T13:00:00Z", "2026-08-19T15:00:00Z")
| where body contains "upstream timeout"          # cheap filter first
| extend upstream = regex_extract(body, 'upstream=([a-z0-9.\-]+)', 1)
| where isnotnull(upstream)
| summarize hits = count(), hosts = count_distinct(source) by upstream
| sort by hits desc
| limit 25
Order matters and the planner will not save you

Putting regex_extract before the contains filter in the query above raises the scan cost by roughly 40× on the same data. HKQL executes stages in the order you write them, deliberately, so that the cost estimate you are shown is the cost you get.

4. Correlate a trace with the logs around it

HKQLlookup is the only cross-source operator
from traces
| where ts > ago(30m) and duration_ms > 2000
| summarize slow = count() by trace_id, service
| lookup logs on trace_id within 30s
| project trace_id, service, severity, body
| limit 200

5. Agent fleet health

HKQLthe same query the alert rule on the next page uses
from agent_health
| where ts > ago(5m)
| summarize state = last(hkagent_state), buffer_mb = last(buffer_bytes) / 1048576 by source
| where state >= 2
| sort by buffer_mb desc

Run against an account with 400 hosts this returns in well under a second: agent_health is a narrow, low-cardinality source held in a separate block family.

Pipeline operators

Operator Form Effect Notes
fromfrom <source>Selects logs, metrics, traces or agent_healthMust be the first stage. Exactly one per query.
wherewhere <predicate>Row filterPushed into the block index when it touches indexed attributes.
extendextend <n> = <expr>Adds computed columnsEvaluated per surviving row. Place after your cheap filters.
projectproject <cols>Keeps and orders columnsproject-away removes instead.
summarizesummarize <aggs> by <keys>Collapses rows into groupsGroup count is capped at 100 000; exceeding it is an error, not a truncation.
sort bysort by <col> asc|descOrders the resultOnly after summarize or limit; sorting a raw scan is refused.
limitlimit <n>Caps the row countDefault 1 000, maximum 50 000 for interactive, 1 000 000 for export.
lookuplookup <src> on <key> within <d>Time-bounded correlationThe only cross-source operator. within is mandatory and capped at 5 min.
mv-expandmv-expand <col>One row per array elementRejected if it would multiply the row count beyond 20×.
renderrender <kind>Chart hint for the consoleline, area, bars, table, heatmap. Ignored by the API.

There is no join. Two sources meet only through lookup, which is bounded in time and therefore bounded in cost.

Function table

Cost class is what the planner charges before it runs anything: A is index-resident, B reads column data, C touches the raw body. Query concurrency and the per-query scan budget are plan limits, listed on the pricing page.

Aggregations

Function Signature Sources Cost Notes
countcount()allAAnswered from block headers when no row filter applies.
count_distinctcount_distinct(col)allBExact below 216 values, then HyperLogLog with 1.6% error.
sumsum(numeric)allBOverflow raises an error rather than wrapping.
avgavg(numeric)allBNull values are skipped, not treated as zero.
min / maxmin(col), max(col)allABlock-header resident for timestamps and numerics.
percentilepercentile(col, q)allBt-digest, relative error under 1% at the tails.
p50 / p90 / p95 / p99p99(col)allBShorthand for percentile at fixed q.
histogram_quantilehistogram_quantile(q, le, value)metricsBLinear interpolation inside the bucket, as the convention expects.
stddevstddev(numeric)allBPopulation, not sample. Use stddev_samp for the other one.
raterate(counter, window)metricsBCounter resets are detected and bridged.
deltadelta(gauge, window)metricsBFirst minus last inside the window. No reset handling.
first / lastfirst(col), last(col)allBBy timestamp within the group, not by arrival order.
topktopk(col, k)allBSpace-saving sketch; exact for k below 1 000.
anyany(col)allAAn arbitrary value from the group. Cheapest way to carry a column through.

Scalar functions

Function Signature Returns Cost Notes
nownow()timestampAFixed once at plan time, so a long query stays self-consistent.
agoago(duration)timestampAago(90m), ago(2d). Units: s, m, h, d, w.
binbin(ts, duration)timestampAFloors to the bucket. Buckets are aligned to the epoch, not to the query start.
bin_autobin_auto(ts)timestampAPicks a bucket that yields 60–200 points over the query range.
roundround(n, digits)float64AHalf away from zero.
coalescecoalesce(a, b, …)anyAFirst non-null argument.
iffiff(pred, a, b)anyABoth branches must share a type.
isnull / isnotnullisnull(col)boolAMissing attribute and explicit null are the same thing here.
strlenstrlen(s)int64BUTF-8 code points, not bytes. bytelen gives bytes.
tolower / touppertolower(s)stringBASCII-only fast path; falls back to full Unicode when needed.
substringsubstring(s, start, len)stringBZero-indexed. Out-of-range clamps instead of erroring.
splitsplit(s, sep)arrayBFeed to mv-expand to get one row per element.
parse_jsonparse_json(s)dynamicCAccess members with .. Malformed input yields null, not an error.
regex_extractregex_extract(s, re, group)stringCRE2 syntax. No backreferences, no lookaround, guaranteed linear time.
regex_matchesregex_matches(s, re)boolCPrefer contains or startswith when they will do.
parse_kvparse_kv(s, pairsep, kvsep)dynamicCFor key=value key2=value2 log lines.
ipv4_in_subnetipv4_in_subnet(col, cidr)boolBOperates on values you sent; we do not enrich or geolocate.
format_bytesformat_bytes(n)stringAPresentation only. Never use it before an aggregation.

Operators == != < <= > >= in between contains startswith endswith matches and or not. String comparison is case-sensitive; append _cs-free variants such as contains_ci for case-insensitive matching.

What a query costs to run

Queries are not billed. They are, however, scheduled, and a query that would scan a quarter of your retention window is going to wait behind one that will not. The console shows the estimate before you press run.

The estimator is a block count, not a guess: block headers carry min and max timestamps plus a per-attribute value sketch, so the planner knows how many blocks a predicate can touch before reading any of them.

interactive_timeout60 s
export_timeout900 s
max_groups100 000
max_result_rows50 000 interactive
max_scan_per_query2 TB logical
concurrency4 / 16 / 32 by plan
0 15 30 45 60 interactive_timeout 60 s 0.4 s 0.9 s 3.1 s 9.6 s 27 s 51 s 1 GB 10 GB 100 GB 400 GB 1 TB 2 TB median wall-clock seconds by logical bytes scanned, cost class B predicate Cost class C over the same volumes is roughly 3.5× slower.
Fig. 8 — The 2 TB bar is the scan ceiling, drawn in red because a query that reaches it is a query that should have had a tighter time predicate.

Running a query from a script

Query keys carry the prefix hk_qk_ and are read-only. The endpoint is the query host of your region, which is a different name from the ingest host and can be firewalled separately.

shellNDJSON result rows on stdout
curl -sS --http2 \
  -H 'Authorization: Bearer hk_qk_REPLACE_ME' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/x-ndjson' \
  --data '{
    "query": "from logs | where ts > ago(15m) and severity == \"ERROR\" | summarize n = count() by service | sort by n desc | limit 10",
    "format": "ndjson"
  }' \
  https://query.halkon.sbs/v1/query
response headersthe planner tells you what it did
HTTP/2 200
lp-query-id:      q-4c19ae72
lp-blocks-read:   1148
lp-bytes-scanned: 31742885888
lp-rows-emitted:  10
lp-elapsed-ms:    842
lp-cost-class:    B
lp-region:        eu-north-1

Request issued from this page at — would carry lp-request-time equal to that value; the example above is a captured response and its ids are fixed.

Result formats

formatAcceptStreamingUse for
ndjsonapplication/x-ndjsonyesPipelines, anything that should not buffer the whole result.
jsonapplication/jsonnoSmall results, dashboards, ad-hoc scripts.
csvtext/csvyesSpreadsheets. Header row carries the inferred types as a comment line.
arrowapplication/vnd.apache.arrow.streamyesExports above 100 000 rows and anything feeding a dataframe.
promtext/plain; version=0.0.4noExposing a saved query to a scraper or a Grafana data source.

Saved queries get a stable id and can be read by any tool that speaks one of these formats. That is the whole integration story — there is no plugin to install and nothing proprietary on the wire.