Views
The view is a frame’s semantic layer: it picks the tables, applies a
where filter, and defines named scalar expressions (with) — derived
columns and metrics — that the page refers to by name. See
Concepts for how the view and page fit together.
"view": { "tables": [ /* ViewTableDefinition[] — required, may be empty */ ], "where": "ServiceName != ''", // optional: filters every table "with": { /* shared scalars */ } // optional: scalars across all tables}Tables
Each entry in tables has a required from, plus optional where, with,
timestamp, and disable_auto_scope as siblings of from:
{ "from": { "table": "otel_traces" }, "timestamp": "Timestamp", "where": "ServiceName != ''", "with": { "DurationMs": "Duration / 1e6" }}timestampnames the column that is the table’s time axis. Set it so time-bucketed queries (see timeseries) and the global time range know which column to bucket and filter on.wherescopes this table; it is ANDed with the view-levelwhere.- A where expression is a SQL string, or
{ "sql": "...", "parameters": { … } }when you need to pass bound parameters explicitly.
From a database table
{ "from": { "table": "otel_traces" } }from.table is the table name as a string. Prefer extending a base view over
querying raw tables directly — the semantic layer already defines the columns and
metrics you want.
Extending another frame
{ "from": { "frame": "@opentelemetry_core/traces" } }from.frame reuses another frame’s view as your table. The child inherits the
parent’s derived columns and metrics (its whole with vocabulary) and its
filters; your own with and where layer on top, overriding by name. This is how
every OTel dashboard is built: on top of @opentelemetry_core/traces, which
defines DurationP95, ErrorRate, IsEntrySpan, and the rest once.
Pass parameters to a parameterized parent with the object form:
{ "from": { "frame": { "name": "@opentelemetry_core/traces", "params": { "ServiceName": "checkout" } } }}Installed packs under
@frames/@<integration>/are overwritten on everynoemata up. Extend them by reference as above — never edit the installed file.
Scalars and metrics
with maps a name to a scalar expression, in one of two forms:
"with": { "DurationMs": "Duration / 1e6", // shorthand: SQL string "DurationP95": { "expression": "quantile(0.95)(DurationMs)", "title": "Latency (p95)", "format": { "duration": "milliseconds" } }}The object form adds a display title and a format (see below). Otherwise a
bare SQL string is enough.
A scalar is a metric when its expression aggregates (count(), sum, avg,
quantile, countIf, …); otherwise it is a row-level derived column. Nothing
marks the difference in syntax — it follows from the function you call.
Compose scalars by name. A scalar may reference any other scalar in scope, so you build derived columns once and assemble metrics from them:
"with": { "IsError": "lower(StatusCode) = 'error' OR HttpStatusCode >= 500", "ErrorCount":"countIf(IsError)", "ErrorRate": "ErrorCount / nullif(count(), 0)"}Use nullif(x, 0) in denominators and the *OrNull aggregates
(avgOrNull, quantileOrNull) so empty windows render as gaps, not errors.
Formatting
A scalar’s format controls how its values display (axes, table cells, stat
cards). It is a shorthand string or an object. The kinds:
| Kind | Shorthand / object | Notes |
|---|---|---|
number | { "number": { "decimals": 1, "compact": true } } | plain numbers |
percent | "percent" / { "percent": { "input": "0-1" } } | 0-1 or 0-100 input |
duration | "duration" / { "duration": "milliseconds" } | input unit → auto output |
rate | { "rate": "seconds" } | per-unit rates, e.g. req/s |
bytes | "bytes" | binary/decimal byte sizes |
currency | { "currency": "USD" } | currency code required |
date | "date" / { "date": "…" } | absolute/relative date output |
Time units are milliseconds, seconds, minutes, hours, days, weeks,
months, years. See .data/schemas/blocks.json (Format) for every field.
Parameters and scoping
Declare params at the frame level (a sibling of view):
"params": [{ "type": "string", "column": "ServiceName", "required": true }]Each param is a typed slot bound to a column, which is also its URL query key.
Params do two things:
- Auto-scope. Every view table gets an implicit
column = <value>filter for each param, so the frame self-scopes to the entity the URL addresses. A table that doesn’t carry the column (or that you scope manually) opts out with"disable_auto_scope": true. - Reach the page. Resolved param values are available to blocks, and to the
title via Handlebars:
"title": "{{ServiceName}}".
Inside a where you can also reference a param explicitly as a ClickHouse bound
placeholder, {ServiceName:String}:
{ "from": { "frame": "@opentelemetry_core/traces" }, "where": "ServiceName = {ServiceName:String}" }This is the mechanism behind the entity dashboards: one services/{ServiceName}
frame definition renders for every service. Continue with Blocks to
build the page, or jump to a full recipe.