Query functions

Embed
Preview of the card
<a href="https://forumulate.com/p/watchtower/docs/functions"><img src="https://forumulate.com/api/public/orgs/watchtower/spaces/docs/pages/4700f65d-a218-41e9-98b8-1a03a9b36f03/social-card.svg?v=3e7ae33eec8b" alt="Query functions" width="600" height="315"></a>

These are the functions of the pipe language (stats, eval) and the KQL match() helper. The table is checked against the registry in crates/wt-search/src/functions.rs by the test function_catalog_matches_doc: one row per function, written exactly as the registry renders it.

Aggregates and timeslice go in stats; scalar functions go in eval and in aggregate arguments. Arguments that name a log attribute are converted to numbers where a number is needed (sum, avg, min, max, percentile, arithmetic, comparisons with a number); a value that is not a number counts as missing.

Function

Kind

Description

count([x])

aggregate

Rows, or rows where x is not null.

count_distinct(x)

aggregate

Exact number of distinct values of x.

sum(x)

aggregate

Sum of x as a number.

avg(x)

aggregate

Mean of x.

min(x)

aggregate

Smallest x.

max(x)

aggregate

Largest x.

percentile(x, p)

aggregate

The p-th percentile of x; p is a number above 0 up to 100.

rate([x])

aggregate

Rows (or the sum of x) per second over the time slice, or over the range without one.

timeslice(d)

bucket

Group by time buckets of d (30s, 5m, 1h, 1d; 1 second to 1 day); the column is bucket.

coalesce(a, b, ...)

scalar

The first argument that is not null or empty.

if(cond, a, b)

scalar

a when cond is true, else b.

lower(s)

scalar

Lower-case s.

upper(s)

scalar

Upper-case s.

json_extract(s, "a.b")

scalar

The string at a dotted path in the JSON text s (at most 8 steps).

substring(s, from[, len])

scalar

Part of s starting at from (1-based).

regex_extract(s, "pattern", idx)

scalar

Capture group idx (1 to 9) of the first RE2 match of pattern in s, or empty.

split(s, "sep", idx)

scalar

Part idx (1-based, at most 1000) of s split on sep, or empty.

len(s)

scalar

Length of s in bytes.

uniq(x)

aggregate

Approximate number of distinct values of x (cheaper than count_distinct).

histogram_quantile(x, q)

aggregate

The q-th quantile of x from a t-digest sketch; q is above 0 up to 1.

top_k(x, k)

aggregate

The k (1 to 50) most frequent values of x as one comma-separated text.

now()

scalar

The current time in nanoseconds since the epoch.

ago("5m")

scalar

Nanoseconds since the epoch for the time that long ago (1s to 30d: s, m, h, d, w).

date_trunc("hour", t)

scalar

Round the time column t down to a second, minute, hour, day, week or month.

round(x[, n])

scalar

x rounded to n (0 to 9, default 0) decimal places.

floor(x)

scalar

x rounded down.

ceil(x)

scalar

x rounded up.

abs(x)

scalar

Absolute value of x.

cidr(ip, "10.0.0.0/8")

scalar

1 when the IPv4 address is inside the range, else 0.

url_parts(url, "host")

scalar

One part of a URL: scheme, host, port, path or query.

hash(s)

scalar

A 64-bit hash of s as text, for grouping without showing the value.

base64(s)

scalar

s encoded as base64.

tostring(x)

scalar

x as text.

tonumber(x)

scalar

x as a number, or missing when it is not one.

array_len(s[, "sep"])

scalar

Items of a JSON array in s, or of s split on sep.

array_contains(s, "sep", v)

scalar

1 when s split on sep has the item v, else 0.

array_uniq(s, "sep")

scalar

Number of distinct items of s split on sep.

pattern(s)

scalar

s with numbers, ids and uuids replaced by <*>, to group lines by message shape.

Each function compiles to an allow-listed ClickHouse function; anything else is rejected by the SQL surface (crates/wt-sql).

Calculated fields

A calculated field is a saved scalar expression that you reference as #name in a pipe query, for example logs | eval slow = #is_slow | stats count() by slow. They belong to one user in one project, at most 5 each, and a query may use at most 5. #name is replaced by (expression) before the query is parsed, so it works wherever a scalar does (eval, aggregate arguments, by after eval) and nowhere else. A calculated field cannot use another one. Manage them with GET/POST /v1/projects/{p}/calculated-fields and DELETE /v1/projects/{p}/calculated-fields/{id}.

Dataset field mappings

PUT /v1/projects/{p}/datasets/{id}/field-mappings stores up to 64 aliases from a short name to an OpenTelemetry attribute key (resource. prefix for resource attributes). A pipe query that names that dataset (dataset in the request) reads the alias as the attribute. A real column of the same name wins.

Stages

The stage list, with an example for each, is served by GET /v1/query/reference?lang=pipe and comes from crates/wt-search/src/stage_ref.rs; the test pipe_stage_reference_matches_parser parses every example. Notes on the less obvious ones:

  • parse and parse-where captures can be typed: %{ms:int}, float, ip, word, uuid, hex. Typed numbers become numeric columns. parse-where drops rows where any capture is empty.

  • dedup keeps the oldest row per key and orders the result by time.

  • join and union read a second query in [ ... ] (no nesting, at most 2 per query). The joined query must end in stats or fields; names may not clash.

  • timewrap needs stats ... by timeslice(..) first and adds period and offset_s.

  • streamstats supports count, sum, avg, min and max over window rows (default 10, at most 1000) ordered by time.

Log patterns

The fingerprint pipeline stage adds pattern_id (a stable hash of the masked message) and pattern to each log at ingest, so logs | stats count() by pattern_id and ingest-time drop or metric rules work without clustering. pattern(body) gives the same masked shape at query time. Pattern clusters (POST /v1/projects/{p}/logs/patterns) also return typed_patterns, where a wildcard keeps its type (<int>, <ip>, <uuid>, <hex>, <float>, <str>) when every line agreed. POST /v1/projects/{p}/logs/patterns/action returns the pipeline stages that drop a pattern or count it as a metric; it changes nothing.

Updated