From the terminal

up0 tail's filters and output modes, and up0 metrics ls / query, with real output.

From the terminal

Two command groups cover the same telemetry Explore shows, from a shell: up0 tail streams logs or traces, and up0 metrics lists and charts metrics. Both require an organization to be selected: set org_id on the active profile, or pass --org-id / set UP0_ORG_ID on each call.

A different, smaller query language

--query on both commands is sent to the server as q and compiled with the backend's Lucene-subset grammar. It is not the select ... from ... where ... language the console's search bar accepts. See Query language for how the two relate.

up0 tail

up0 tail short-polls the Query API's tail endpoint and streams new logs or traces to the terminal. It follows and stays open when stdout is a terminal, and makes a single bounded pass when piped, redirected, or run with --json.

Stream logs

up0 tail --type logs --service payments-api --status error --query 'timeout' --since 15m

Stream traces

up0 tail --type traces --service payments-api --status error --query 'checkout' --since 15m

--status targets a different column per --type: log severity (severity_text, case-insensitive) under --type logs, trace status (status_code) under --type traces.

Real output, up0 tail --type traces --since 1h --no-follow against a handful of seeded spans:

1647-gateway GET /checkout 40000000ns Ok 7b6a35f3749d47ffbc716ee6607e62ae
1647-orders POST /orders 30000000ns Ok 7b6a35f3749d47ffbc716ee6607e62ae
1647-gateway GET /checkout 40000000ns Ok 1c654aedae1645af9439509d40032ee7
1647-orders POST /orders 30000000ns Ok 1c654aedae1645af9439509d40032ee7
1647-lonely-worker cron.tick 5000000ns Ok 2a60ca6f82f3432abe92683ac8b6691b
... 3 more matching records this interval, showing latest 20 — narrow with --status/--service or increase your terminal's scrollback

Each trace line is service span_name duration status trace_id; each log line is timestamp severity service body.

--query / -q is sent to the server as q. The server compiles it with a Lucene-subset grammar (bare words, *substr*, "exact phrase", key:value, key:>N comparisons, AND/OR/NOT), the same grammar the Query API applies to its list, histogram, and tail endpoints alike. The CLI does no parsing of its own; it passes the string through as-is. Parenthesised grouping and a raw-SQL escape hatch are not implemented yet.

--view

--view <name> resolves a named DataView, a saved query with a display payload set, created from the console (see Saved queries and history), and applies its query text and time range as defaults. Any of --query/--since you also pass on the command line wins over the view's own value; the view only fills in what you left unset. The view's display half (column set, widths, sort) is looked at only to confirm the row is a DataView and is never applied. The CLI streams records, it does not render columns:

up0 tail --view checkout-errors

A name that matches an ordinary saved search, not a DataView, is not resolved. Only a saved query with a presentation payload counts.

Options

  • --type: logs or traces (default: logs)
  • --query / -q: free-text filter, sent to the server as q
  • --view: resolve a named DataView and apply its query/range as defaults
  • --since: initial lookback, e.g. 5m, 1h, 24h, 7d (default: 5m)
  • --service: filter by service_name
  • --status: filter by severity_text (--type logs) or status_code (--type traces)
  • --json: newline-delimited JSON, one record per line
  • --follow / -f / --no-follow: keep polling after the first batch; on by default when writing to a terminal, off when piped, redirected, or --json
  • --limit / -n: max records rendered per poll (default: 20)
  • --org-id: organization ID (or set UP0_ORG_ID)

tail warns on stderr rather than silently dropping records. If a poll hits the result limit, narrowing with --service/--status lets it catch up. If the follow loop fell behind the server's lookback window, records in that range were skipped and cannot be recovered through tail. Narrowing helps only when the cause is sustained volume, not when the poll itself was paused by a sleeping machine or a network outage.

All warnings and empty-result notes go to stderr, so stdout carries only record data and pipelines like up0 tail ... | grep ERROR see a clean stream.

up0 logs and up0 traces are retired. Use up0 tail --type logs and up0 tail --type traces instead.

up0 metrics

up0 metrics has two subcommands: ls answers what metrics do I have, and query returns one metric as a bucketed time series.

List what you have

Start here. Metric names come from your instrumentation, not from upzero, so nothing can guess them for you.

up0 metrics ls --since 1h
┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ metric_name              ┃ metric_type ┃ series_count ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ up0_ingest_test_6043b69e │ gauge       │            1 │
│ up0_ingest_test_62fd128a │ gauge       │            1 │
└──────────────────────────┴─────────────┴──────────────┘

Only metrics with data in the range are listed, so a metric that stopped reporting yesterday will not appear under --since 1h. Widen the range before concluding it is gone.

series_count is the number of distinct label combinations, the number to watch for cardinality: one metric carrying a request id or user id in a label can reach thousands, and the query API will refuse to compute over it rather than exhaust memory (see Cardinality limits).

Options:

  • --since: relative lookback, e.g. 15m, 1h, 24h, 7d (default: last 1h)
  • --start / --end: ISO 8601 range, mutually exclusive with --since
  • --json: emit the API response as JSON
  • --org-id: organization ID (or set UP0_ORG_ID)

Query one metric

up0 metrics query --metric-name example.queue.depth --since 1h --aggregation avg
example.queue.depth  aggregation=avg  type=gauge  bucket=72s
  2026-08-14T06:00:00  2026-08-14T06:01:12  76.5
  2026-08-14T06:01:12  2026-08-14T06:02:24  77.5
  ...
scanned 1993 rows in 0.0122s

Bucket width is derived from the range, roughly 50 buckets across whatever you ask for, so you control the range and never the width.

Split into one series per label value with --group-by, repeatable up to 10 keys:

up0 metrics query --metric-name example.queue.depth --group-by route --since 1h

The aggregation has to match the encoding.

metric_type from ls decides which aggregations can return anything. The full table is in Metrics. The short version: sum/avg/min/max/count need a gauge or counter; a histogram-shaped metric (histogram, exponential_histogram, summary) returns null for all five and needs quantile instead:

up0 metrics query --metric-name example.http.duration \
  --aggregation quantile --quantile 0.95 --since 1h

--quantile is a fraction in [0, 1], not a percentile: 0.95, not 95. It is required with --aggregation quantile and rejected otherwise.

Counters are not rate-corrected. A counter's raw value is its cumulative total, so charting one shows growth, not throughput. And an empty bucket is null, not 0, except for sum/count, where zero is a real answer.

Options:

  • --metric-name: exact metric name, required, one metric per request
  • --since: relative lookback (default: last 1h)
  • --start / --end: ISO 8601 range, mutually exclusive with --since
  • --aggregation: sum, avg, min, max, count, quantile (default: avg)
  • --quantile: fraction in [0, 1], required iff --aggregation quantile
  • --group-by: label key to split into its own series, repeatable, up to 10
  • --json: emit the API response as JSON
  • --org-id: organization ID (or set UP0_ORG_ID)

Cardinality limits

A metric whose labels carry an unbounded value (a request id, a user id, a pod name during a rolling deploy) can produce a practically unlimited number of series. Rather than let one such metric exhaust memory for everyone, the query API aborts and returns a 400 naming the problem:

INVALID_QUERY_PARAMS: this query has too many distinct label combinations to
compute exactly in the requested range. Narrow the range, or check whether a
label on this metric carries a request id, user id, or other
effectively-unbounded value.

Narrowing the range is the quick fix. The real fix is at the emitting instrumentation. up0 metrics ls sorts busiest-first, so the offender is usually the top row.

The same data in Explore

The console's Explore surface answers the same questions from the same endpoints, so the terminal and the browser never disagree:

TerminalExplore equivalent
up0 tail --type logsLogs, with Live tail on
up0 tail --type tracesTraces, with Live tail on
up0 metrics lsselect * from metrics (the catalog question)
up0 metrics query --aggregation avgselect avg('<metric>') from metrics

See Query language for the console's full grammar, and Saved queries and history for --view's counterpart on the console side.

On this page