Span query language reference

This page lists every field, function, operator, and search operator the Uptrace span query language accepts. The language works on spans, logs, and events. To learn how to write queries, start with Querying spans; to select whole traces, see Querying traces.

A query is a list of clauses separated by |:

sql
group by service_name | p50(_dur_ms) | where _status_code = 'error'

Built-in fields

A built-in field starts with an underscore, so it cannot collide with an attribute. An attribute name replaces dots with underscores, so service.name becomes service_name.

FieldTypeDescription
_idstrThe record's own id.
_parent_idstrThe id of the record's parent span.
_trace_idstrThe id of the trace the record belongs to.
_group_idstrThe id of the group the record was fingerprinted into.
_grouping_rule_idstrThe id of the log grouping rule that assigned the hash.
_typestrThe coarse category ingest classified the record into.
_systemstrThe categorization facet derived from the type, for example db:postgresql.
_kindstrThe OpenTelemetry span kind, for example server or client.
_namestrThe raw span name, which grouping uses.
_event_namestrThe OpenTelemetry event name, for a span event.
_display_namestrThe human-facing label ingest computed.
_timetimeThe time the record started, with microsecond accuracy.
_dur_msfloatThe record's duration in milliseconds.
_status_codestrThe record's status code: ok, error, or unset.
_status_messagestrThe record's status message.
_error_countintThe number of errors the record counts.
_error_ratefloatThe share of records that are errors, as a ratio of two counts.

Data types

Uptrace discovers an attribute's type from the data. Each type accepts its own comparison operators:

TypeOperators
str=, !=, in, not in, like, not like, contains, not contains, ~ (regexp), !~, exists
int and float=, !=, <, <=, >, >=, exists
bool=, !=
[]strcontains, exists
time<, <=, >, >=

Annotate a type when discovery is not enough, which happens when one attribute holds different types across records:

sql
foo::string | foo::int

Uptrace uses the annotation as a hint for reading columnar data. It does not convert the value.

String literals

sql
"I'm a string\n"           -- double quotes, with escape sequences
'I\'m a string\n'          -- single quotes, with escape sequences
`^some-prefix-(\w+)$`      -- backticks, no escape sequences, for a regexp

Units

Uptrace reads a unit from a name suffix, and then formats the column for you: bytes, seconds, milliseconds, utilization. When you cannot rename the attribute, alias the expression:

sql
sum(heap_size) as heap_size_bytes

Clauses

ClauseExampleDescription
selectselect count(), p50(_dur_ms)Names the result columns. The keyword is optional, so a bare expression is a selector too.
wherewhere _status_code = 'error'Filters records before grouping.
group bygroup by service_name, host_nameGroups records, and adds the columns to the result.
havinghaving p50(_dur_ms) > 100msFilters the groups after aggregation.
searcherror -timeoutFree-text search — see Search syntax.

Rename a grouping column with as, and manipulate its value with a transform function:

sql
group by lower(service_name) as service, upper(host_name) as host
group by extract(host_name, `^uptrace-prod-(\w+)$`) as host

Filters

A leading not negates a filter, a function call, or a group in parentheses. It binds tighter than and and or:

sql
where not ipInRange(client_address, "10.0.0.0/8")
where not (http_route = "/api" or http_route = "/health")

A filter matches only a record that carries every attribute it reads, and a negation keeps that rule: !=, not in, and not (...) skip a record without the attribute. Add or attr not exists to keep those records too.

Comparing two attributes

A bare word after =, !=, <, <=, >, or >= is a second attribute, not a string value. Quote a string value:

sql
where http_response_body_size > http_request_body_size
where client_address != server_address
where deployment_environment = "prod"

Both operands must have comparable types: two numbers, two strings, two booleans, or two times. The right side is one attribute, so move arithmetic to the left, as in where a - 2 * b > 0. When no record carries the right-side attribute, = or != on a string fails with a hint to quote the value.

Aggregate functions

FunctionDescription
count()Number of matched records. Takes no arguments.
countAll()Same as count(): both count every record a pre-aggregated row weighs.
countDistinct()Number of distinct records. Takes no arguments.
uniq(attr)Number of distinct values of the attribute.
sum(attr)Sum of the values.
avg(attr)Average of the values.
min(attr)Smallest value.
max(attr)Largest value.
median(attr)Median of the values.
p50(attr)50th percentile. Also p75, p90, p95, p99.
quantile(level, attr)Quantile at an arbitrary level, for example quantile(0.8, _dur_ms).
any(attr)An arbitrary value from the group.
anyLast(attr)The last value in the group.
top3(attr)The three most frequent values, as an array. Also top10.
stddev(attr)Population standard deviation of the values.
argMax(x, y)The value of x in the record where y is the largest, for example argMax(_trace_id, _dur_ms).
argMin(x, y)The value of x in the record where y is the smallest.
uniqValues(attr[, n])Up to n distinct values, as an array in no specified order. n defaults to 100, and is at most 1000.
apdex(t1, t2)Apdex score of span duration, with a satisfied threshold t1 and a tolerating threshold t2, for example apdex(500ms, 3s).
merge(agg($alias.attr))Re-aggregates the per-trace partials of a cross-join expression — see Querying traces.

count() counts records, and uniq(attr) counts distinct values of an attribute. To count affected users in each group of errors:

sql
group by _group_id | uniq(enduser_id) | where _status_code = 'error'

Sampling weight

A stored record can stand for many records: a sampler that keeps one span in ten stores it with the weight 10, which is _count. The value aggregates weight each record by _count, so they describe the records the sampled ones stand for:

  • sum is the weighted total, and avg is the weighted mean.
  • stddev is the weighted population standard deviation.
  • median, quantile, and p50…p99 are weighted quantiles.

count() already sums the weights. min, max, any, anyLast, uniq, argMax, argMin, and uniqValues do not weight, because a weight does not change their answer.

Conditional aggregates

Add the If suffix to any aggregate to pass a condition as its last argument. The aggregate then reads only the records the condition matches:

sql
countIf(_status_code = "error")
sumIf(http_request_content_length, _kind = "server")
p50If(_dur_ms, service_name = "api")
uniqIf(enduser_id, _status_code = "error")

The suffix works for every aggregate, and it requires at least one argument.

Virtual columns

A virtual column is a shortcut for a common expression:

ColumnThe same as
_error_ratecountIf(_status_code = "error") / count()

Transform functions

A transform function returns a new value for each record. Use one in a grouping expression, a filter, or an aggregate's argument.

Strings

FunctionDescription
lower(attr)Converts the value to lowercase.
upper(attr)Converts the value to uppercase.
trimPrefix(attr, "prefix")Removes the leading prefix.
trimSuffix(attr, "suffix")Removes the trailing suffix.
extract(attr, pattern)Extracts the part the regexp captures.
replace(attr, substring, replacement)Replaces every occurrence of the substring.
replaceRegexp(attr, pattern, replacement)Replaces every part that matches the regexp.
sql
group by replace(host_name, 'uptrace-prod-', '') as host
group by replaceRegexp(host, `^`, 'prefix ') as host

Rates

FunctionDescription
perMin(expr)Divides the value by the number of minutes in the time interval.
perSec(expr)Divides the value by the number of seconds in the time interval.
rate(expr)Same as perSec.

per_min and per_sec are accepted spellings of the first two.

sql
perMin(count()) | perSec(count())

Numbers, IP addresses, and strings

FunctionDescription
bucket(x, step)Rounds the number down to a multiple of step. A duration or size step converts to the unit of x.
ipv4Subnet(addr, prefix)The IPv4 network of the address in CIDR form, for example "10.1.2.0/24". null for a value that is not an IPv4 address.
ipInRange(addr, "cidr")Whether the address is in the CIDR prefix. Works for IPv4 and IPv6.
length(attr)String length in UTF-8 code points, not bytes.
sql
group by bucket(_dur_ms, 100ms) as latency | count()
group by ipv4Subnet(client_address, 24) as subnet | count()
where ipInRange(client_address, "10.0.0.0/8")
where length(http_route) > 100

A missing attribute matches neither ipInRange nor not ipInRange, and a stored value that is not an address never fails the query.

Type conversion

FunctionDescription
parseInt64(attr)Parses the string as a 64-bit integer.
parseFloat64(attr)Parses the string as a 64-bit float.
parseDateTime(attr)Parses the string as a date with time.

A value the function cannot parse becomes zero rather than an error.

Arrays

FunctionDescription
arrayJoin(attr)Expands an array attribute into one row per element, so you can group by it.
sql
group by arrayJoin(db_sql_tables) as table | count()

Time bucketing

Each function truncates a time value to the start of its bucket, which makes it useful for grouping over time:

FunctionRounds down to
toStartOfSecond(_time)The second.
toStartOfMinute(_time)The minute.
toStartOfFiveMinutes(_time)The 5-minute interval.
toStartOfTenMinutes(_time)The 10-minute interval.
toStartOfFifteenMinutes(_time)The 15-minute interval.
toStartOfHour(_time)The hour.
toStartOfDay(_time)The day.
sql
group by toStartOfDay(_time) as day | uniq(enduser_id) as visitors

bucket also truncates a time, with any step of whole seconds, an optional offset, and an optional IANA timezone:

FunctionDescription
bucket(_time, step[, offset][, tz])The start of the bucket. A day bucket starts at local midnight in tz, and a week bucket on Monday. Without tz, boundaries are in UTC.
timeOfDay(_time[, tz])The wall-clock time since midnight, in milliseconds, so it compares with a duration such as 8h.
dayOfWeek(_time[, tz])The ISO day of the week, from 1 (Monday) to 7 (Sunday).
sql
group by bucket(_time, 1d, "Europe/Berlin") as day | count()
where timeOfDay(_time, "Europe/Berlin") >= 8h and timeOfDay(_time, "Europe/Berlin") < 18h
where dayOfWeek(_time) in (6, 7)

JSON functions

When an attribute holds a JSON document, these functions read inside it. Two families are available, and both take the JSON string as the first argument.

The SQL/JSON path functions take one path:

FunctionReturnsDescription
JSON_EXISTS(attr, path)boolWhether the path exists.
JSON_QUERY(attr, path)strThe value at the path, as JSON.
JSON_VALUE(attr, path)strThe scalar value at the path.

The ClickHouse functions take one or more path segments:

FunctionReturnsDescription
JSONHas(attr, path…)boolWhether the member exists.
JSONLength(attr, path…)intNumber of elements or members.
JSONType(attr, path…)strThe JSON type of the member.
JSONExtractString(attr, path…)strThe member as a string.
JSONExtractInt(attr, path…)intThe member as a signed integer.
JSONExtractUInt(attr, path…)intThe member as an unsigned integer.
JSONExtractFloat(attr, path…)floatThe member as a float.
JSONExtractBool(attr, path…)boolThe member as a boolean.
JSONExtractKeys(attr, path…)strThe member's keys.
JSONExtractRaw(attr, path…)strThe member as raw JSON.
JSONExtractArrayRaw(attr, path…)strThe array's elements, each as raw JSON.
sql
group by JSONExtractString(http_request_body, 'user', 'plan') as plan | count()
where JSONHas(payload, 'error')

Search is the free-text clause of a query. Searching spans and logs documents it in full, with examples. The rules a reference needs:

  • Uptrace splits the search string on whitespace into matchers, and every matcher must match.
  • One matcher holds pipe-separated alternatives, so select|update matches either term.
  • Matching is case-insensitive substring matching, so err matches error.
  • -term excludes a term, attr:value scopes the search to one attribute, and *:value searches the full-text index.
  • Quote a value with " or ' to keep spaces and punctuation. A backtick value matches literally, with no escape processing.
  • A scope must be a valid OpenTelemetry attribute key, or *. A key: prefix that is neither is read as part of the word.
  • An unscoped search looks at the attributes the query groups by. Search scope maps each grouping to the attributes it searches.
text
error "database connection" -timeout
*:hello *:error|warning -*:debug

See also