Skip to content

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" }
}
  • timestamp names 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.
  • where scopes this table; it is ANDed with the view-level where.
  • 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 every noemata 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:

KindShorthand / objectNotes
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:

  1. 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.
  2. 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.