Logs, Dashboard, and Charts

Formula Language

One language selects entries, calculates values, and produces Charts. This is the authoritative language reference.

Overview

An entry formula is logs() or logs(filter), used only at the top level. It cannot be nested, used in math, or requested in series mode. A numeric formula is math over aggregate calls. An aggregate looks at the logs in the Dashboard timeframe and returns one number. You can combine aggregates and plain numbers with + - * / and parentheses.

The Logs tab keeps its message search and level filter. It builds an entry formula from those controls; search text is literal, not formula syntax. Use the query API for full entry formulas and numeric comparisons. Dashboard and Chart editors accept numeric formulas.

text
1logs()
2logs(level = ERROR and data.durationMs > 1000)
3logs(data.durationMs >= 1000 and data.durationMs < 5000)
4count()
5count(level = ERROR)
6count(level in (ERROR, WARNING) and message contains "failed")
7count(flowId = "checkout-123")
8count(data.user.plan = "pro" and data.durationMs != 250)
9distinct(data.user.id)
10distinct(data.user.id, data.event = "app_used")
11sum(data.amount, data.status = "paid")
12avg(data.durationMs)
13min(data.durationMs)
14max(data.durationMs)
15count(level = ERROR) / count() * 100
16count(data.plan = "pro") - count(data.plan = "free")

The same language is used in the Dashboard card editor, the chart editor, and the Admin API. Raw SQL, pipes, joins, arbitrary grouping, arrays, regex, explicit null/missing checks, and general or/not syntax are not accepted.

Aggregates

count()
aggregate
The number of logs in the timeframe.
count(filter)
aggregate
The number of logs that match the filter.
distinct(field)
aggregate
The number of unique values of a field. Missing and null values are ignored.
distinct(field, filter)
aggregate
The number of unique values among logs that match the filter.
sum(data.field[, filter])
aggregate
The total of numeric values. Missing and non-numeric values are ignored.
avg(data.field[, filter])
aggregate
The average of numeric values. Missing and non-numeric values are ignored.
min(data.field[, filter])
aggregate
The smallest numeric value. Missing and non-numeric values are ignored.
max(data.field[, filter])
aggregate
The largest numeric value. Missing and non-numeric values are ignored.

A numeric formula needs at least one aggregate. Aggregates cannot be nested inside other aggregates. Numeric aggregates only accept data fields. When no numeric values match, their result is null and the UI shows a dash.

For distinct on a data field, strings, numbers, and booleans are counted; objects and arrays are ignored. JSON numbers 1 and 1.0 are the same value.

Filters

A filter is one or more conditions joined with and. Each condition is field operator value. There is no or and no not; use in (…) to match several values.

Fields

level
= , != , in
Level names are bare words: DEBUG, INFO, WARNING, or ERROR. Quotes are also accepted.
flowId
= , != , in , contains
Quoted text from 1 to 128 characters.
message
= , != , in , contains
Quoted text from 1 to 200 characters.
data.{keyPath}
= , != , in , contains , > , >= , < , <=
A JSON key path such as data.user.plan. Values can be quoted text up to 200 characters, a number, or true / false.

Operators

=
equals
The field has exactly this value.
!=
not equals
The field has a different value. It also matches logs where the field is missing.
in (a, b, c)
one of
The field has one of the listed values. Duplicates are removed. A list with one value is stored as =.
contains "text"
substring
ASCII case-insensitive substring match on text fields. Not available for level.

Comparisons >, >=, <, and <= require a data.* field and a numeric literal. They match only JSON numbers. Strings, booleans, nulls, arrays, objects, and missing values do not match. Thresholds need not appear in samples. Conditions cannot compare fields or contain arithmetic.

Equality is case-sensitive and type-aware. != is the complement of equality, including other types, null, and missing fields. It is not a missing-field check. Membership may mix scalar types. contains treats % and _ literally and does not promise Unicode case folding.

Stored data.x= 42!= 42in (42, "42", true)> 40
Number 42truefalsetruetrue
Number 43falsetruefalsetrue
String "42"falsetruetruefalse
Boolean truefalsetruetruefalse
Boolean falsefalsetruefalsefalse
Array or objectfalsetruefalsefalse
Null or missingfalsetruefalsefalse

Values

Text values use double quotes: flowId = "checkout". Use a backslash to escape a quote or a backslash inside the text. Numbers and true / false are only allowed for data fields. Types stay separate: 1, "1", and true are three different values.

Single and double quotes are accepted. A backslash quotes the next character literally; it never creates a newline. Actual line breaks in strings are rejected. The printer escapes backslashes and double quotes. Encode the complete formula with JSON.stringify or your language’s JSON serializer.

text
1Formula: logs(message = "\n")
2JSON: {"formula":"logs(message = \"\\n\")"}
3Parsed text: the letter n
text
1Formula: logs(message = "\\n")
2JSON: {"formula":"logs(message = \"\\\\n\")"}
3Parsed text: one backslash followed by n

Numeric literals use decimal digits with an optional fraction and minus sign. No exponent syntax is accepted. Boolean literals are case-insensitive and print as true or false. A valid data path is accepted even when it has not appeared in the dataset.

A data key path is 1 to 200 characters after data.. Each dot-separated part must start with a letter or underscore and may then contain letters, numbers, or underscores. Keys containing dots, spaces, or hyphens are not supported.

Time and result rules

Time, page size, and series mode are request options. Saved cards and Charts store no active range. Use start: now-5m, now-15m, or now-30m with end: now. now and now-<non-negative integer><unit> accept ms, s, m, h, d, and w. Days are fixed 24-hour durations; weeks are seven days. Absolute timestamps require Z or an explicit offset. The server captures one clock for all bounds and calculations in a request and returns UTC bounds.

Entries default to All time. Either bound may be omitted or null; entry ranges have no duration or future limit. Numeric execution requires both bounds, 0 < duration ≤ 2592000000 ms (30 days), and end ≤ server time + 300000 ms. There is no timezone-change detection. A configuration-only saved read omits both bounds.

All ranges include start and exclude end. Series buckets use mathematical floor in Unix milliseconds, including before 1970. at is the aligned bucket start; point start and end are clipped to the requested range. Only non-empty intervals are returned. Counts and distinct counts in empty buckets are zero; other empty aggregates are null.

Entry pages default to 100 and allow 1–1,000; Logs UI pages use 100. Follow next unchanged for more pages. Aggregates use every matching row. Results return formula, kind, and timeframe. Numeric parts contain each unique aggregate’s formula, logsFormula, field, and raw value, in first-occurrence order. Matching logs for a field aggregate can include rows whose field did not contribute.

Z8Log never deletes log rows. Time bounds only select reads. Existing whole-dataset deletion is separate.

Math

Use + - * / and parentheses. Normal precedence applies: * and / bind tighter than + and -. Numbers can be integers or decimals. A leading - negates a value.

Division by zero gives an undefined value. The card or chart shows - and the API returns { "raw": null, "formatted": "-" }.

Empty count and distinct return 0; empty sum, avg, min, and max return null. Null operands make arithmetic null. Non-finite database aggregate results and unsafe integer results become null. Counts and distinct counts also require non-negative integers. Sums use floating-point numbers to avoid integer overflow; unsafe results become null without failing other calculations. Arithmetic uses JavaScript floating-point numbers; division by zero and non-finite results become null. A finite arithmetic result is not rejected only because it exceeds the safe integer range. Floating-point math can have rounding error.

Percentages

The card format percent only adds one decimal place and a percent sign. The formula itself must multiply by 100.

json
1{
2  "title": "Error rate",
3  "tone": "red",
4  "formula": "count(level = ERROR) / count() * 100",
5  "format": "percent"
6}

Case and normalization

Keywords (count, distinct, sum, avg, min, max, and, in, contains), the fixed field names, and level names are not case-sensitive. Data key paths and text values are case-sensitive, except that contains ignores ASCII case when matching.

The server stores a normalized version of every formula. Spacing is fixed, keywords are lower-cased, level names are upper-cased, and in with one value becomes =. The normalized text is what you get back from the Admin API.

text
1COUNT( Level IN (error) AND message CONTAINS "Timeout" )/count()*100
2
3→ count(level = ERROR and message contains "Timeout") / count() * 100

Limits

Formula length
400 characters
The whole formula text, both as written and after normalization.
Aggregates
8 per formula
All aggregate calls together.
Conditions
5 per filter
Conditions joined with and inside logs() or one aggregate.
in values
20 per condition
The input list limit is checked before duplicates are removed.
Nesting
16 levels
Nested parentheses.
Numbers
no exponent form
Very small or very large numbers that would print as 1e-7 or 1e21 are rejected.

The page controls the timeframe. Charts also choose the time buckets. There is no way to put a timeframe, a time bucket, or a group by inside a formula.

Errors

An invalid formula is rejected with a message that names the column of the problem, for example Unknown field "user" at column 7. Use level, flowId, message, or data.key. The card editor shows the message while you type. The Admin API returns status 400 and an error that starts with Formula:.

Grammar

The full grammar in EBNF-like form.

text
1formula    = "logs" "(" [filter] ")" | expression
2expression = term (("+" | "-") term)*
3term       = factor (("*" | "/") factor)*
4factor     = number | "-" factor | "(" expression ")" | aggregate
5aggregate  = "count" "(" [filter] ")"
6           | "distinct" "(" field ["," filter] ")"
7           | numericAggregate "(" dataField ["," filter] ")"
8numericAggregate = "sum" | "avg" | "min" | "max"
9filter     = condition ("and" condition)*
10condition  = field "=" value
11           | field "!=" value
12           | field "in" "(" value ("," value)* ")"
13           | field "contains" string
14           | dataField (">" | ">=" | "<" | "<=") number
15field      = "level" | "flowId" | "message" | "data" "." key ("." key)*
16dataField  = "data" "." key ("." key)*
17value      = string | number | "true" | "false" | levelName