Firetiger Query
Firetiger stores telemetry in Apache Iceberg tables with dynamic schema inference. Run DuckDB SQL against
it with the Firetiger MCP server's query tool (parameter: sql).
Quick Start
-- 1. Discover which tables exist
SHOW TABLES;
-- 2. Inspect a table's columns before querying it
DESCRIBE "opentelemetry/traces/checkout-service";
-- 3. Query it — time-filter first; add a LIMIT to bound the result
SELECT trace_id, name, end_time - start_time AS duration, start_time
FROM "opentelemetry/traces/checkout-service"
WHERE start_time >= NOW() - INTERVAL '1 hour'
ORDER BY start_time DESC
LIMIT 100;
Key gotcha: the query tool auto-injects USE "connections/iceberg-gateway", so you reference tables by
their quoted name ("opentelemetry/traces/checkout-service") — you don't prefix a catalog path. There is no
enforced row cap, but the tool returns every row as JSON, so always add a LIMIT (and explicit columns, not
SELECT *) to avoid dumping huge nested rows.
Table naming
Tables are namespaced by signal type and service name. Names with slashes must be double-quoted. The
{service_name} layout is the default shard key; a deployment can configure a different one, so always
SHOW TABLES; to see the real names.
| Signal |
Table |
| Traces |
"opentelemetry/traces/{service_name}" (one row per span) |
| Logs |
"opentelemetry/logs/{service_name}" |
| Metrics catalog |
"opentelemetry/metrics" (one row per metric — discover names here) |
| Metric data |
"opentelemetry/metrics/{metric_name}" (one table per metric; values inline) |
Metrics use a per-metric table with values inline — there is no series join and no
opentelemetry_metrics_gauges/_series tables. There is no separate "spans" or "events" table — a span's
events and links are nested LIST<STRUCT> columns inside the traces table.
Essential facts
| Fact |
Detail |
| Time columns |
start_time (traces), time (logs/metrics), all UTC. Filter on these first — tables are day-partitioned on them. Use a Z/+00:00 suffix on literal timestamps. |
| Duration |
Not stored — compute end_time - start_time. For a number, EXTRACT(EPOCH FROM (end_time - start_time)) gives seconds. |
| Status codes |
Integers: 0=UNSET, 1=OK, 2=ERROR. Filter errors with status.code = 2. |
| Severity |
severity_number integer: TRACE=1-4, DEBUG=5-8, INFO=9-12, WARN=13-16, ERROR=17-20, FATAL=21-24. |
| Attributes |
STRUCTs, dot-accessed (attributes.http.route, resource.attributes.service.name) — not maps, so no attributes['key']. Keys are snake_case-normalized. Inferred to depth 2; level 3+ is a JSON-typed column — extract with DuckDB JSON funcs (e.g. json_extract_string(attributes.request.context, '$.tenant_id')). |
| IDs |
trace_id is 16 bytes, span_id/parent_span_id 8 bytes (BLOB in DuckDB). Match with x'...' hex literals: WHERE trace_id = x'0123...'. |
| Sampling |
Traces may carry a nullable sample_rate (double). When present, multiply counts by 1 / sample_rate to estimate true volume. |
Querying connected sources
The same query tool can also reach sources you've connected to Firetiger — and join across them in one
query. Firetiger's own telemetry is the default source; other sources are addressed two ways:
- SQL sources (Postgres, MySQL, ClickHouse) — fully-qualify the table with the connection name.
"connections/{name}" is a quoted identifier (it contains a /), followed by schema/database and table:SELECT id, email FROM "connections/prod-postgres".public.users WHERE created_at >= NOW() - INTERVAL '1 day' LIMIT 100;
Prefer this to USE — it's self-contained and lets one query join across connections. (USE "connections/{name}"; is an optional shorthand that sets the default for unqualified names.)
- Observability backends (Datadog, Prometheus, GCP Monitoring) — call a function with the connection
name as a string argument and the vendor's own query language:
SELECT * FROM datadog_search_logs('connections/prod-datadog', 'status:error service:api', NOW() - INTERVAL '1 hour', NOW(), max_rows => 500);
Run list with resource: "connections" to see configured sources, and SELECT * FROM confit_functions(); to
see which functions your connections unlock. Full patterns, PromQL/GCP examples, cross-source joins, and gotchas
are in references/querying-connections.md. (Trino and Elasticsearch aren't
reachable through the query tool.)
What do you need?
| Task |
Reference |
| Full column reference for traces, logs, and deeply-nested attributes |
references/schema.md |
| Ready-to-run queries — recent/slow/error spans, latency percentiles, error logs, cross-service traces |
references/query-examples.md |
| Metrics — the metadata catalog, per-metric data tables, gauge/sum/histogram value columns |
references/metrics.md |
Querying connected sources — Postgres/MySQL/ClickHouse via fully-qualified "connections/{name}".table, Datadog/Prometheus/GCP via functions, cross-source joins |
references/querying-connections.md |
Common Mistakes
| # |
Mistake |
Fix |
| 1 |
SELECT * / no LIMIT |
Telemetry rows are wide (nested structs/lists) and every row is returned as JSON. Select explicit columns and add a LIMIT. |
| 2 |
Unquoted table name with slashes |
"opentelemetry/traces/my-service" — bare opentelemetry/traces/... is a parse error. |
| 3 |
Selecting a duration column |
There is none. Use end_time - start_time. |
| 4 |
Filtering status.code = 'ERROR' |
Status codes are integers — use status.code = 2. |
| 5 |
No time filter |
Always constrain start_time/time first — tables are day-partitioned, so this is what prunes the scan. |
| 6 |
Averaging an INTERVAL |
Wrap in EXTRACT(EPOCH FROM (end_time - start_time)) to aggregate durations as numbers. |
| 7 |
Guessing service/table names |
Run SHOW TABLES; and DESCRIBE "table" first — schemas are inferred and vary per service. |
| 8 |
Map syntax on attributes |
Attributes are structs — attributes.http.route, not attributes['http.route']. |
| 9 |
Reading a level-3+ attribute directly |
Beyond nesting depth 2 it's a JSON-typed column — json_extract_string(attributes.request.context, '$.key'). |
| 10 |
Wrong connection-name form when federating |
In FROM, "connections/pg".schema.table — a quoted identifier; in a vendor function, datadog_search_logs('connections/pg', …) — a string literal. Don't swap them. |
Related
- No data to query? Verify instrumentation with
firetiger-instrument.
- Diagnosing an incident?
firetiger-investigate wraps these queries in a tracked workflow.
1---2name: firetiger-query3description: Use when querying Firetiger telemetry with SQL — finding traces, searching logs, inspecting metrics, locating errors or slow requests, computing latency percentiles, aggregating observability data, or querying a connected source (Postgres, MySQL, ClickHouse, Datadog, Prometheus, GCP Monitoring) through the Firetiger MCP `query` tool. Always use this skill before writing a Firetiger query — it carries the critical gotchas (slashed table names must be quoted, duration is computed not stored, status codes are integers, attributes are structs not maps, fully-qualify "connections/{name}".table to reach a connected source) that make queries succeed.4license: Apache-2.05---67# Firetiger Query89Firetiger stores telemetry in Apache Iceberg tables with dynamic schema inference. Run **DuckDB SQL** against10it with the Firetiger MCP server's **`query`** tool (parameter: `sql`).1112## Quick Start1314```sql15-- 1. Discover which tables exist16SHOW TABLES;1718-- 2. Inspect a table's columns before querying it19DESCRIBE "opentelemetry/traces/checkout-service";2021-- 3. Query it — time-filter first; add a LIMIT to bound the result22SELECT trace_id, name, end_time - start_time AS duration, start_time23FROM "opentelemetry/traces/checkout-service"24WHERE start_time >= NOW() - INTERVAL '1 hour'25ORDER BY start_time DESC26LIMIT 100;27```2829**Key gotcha:** the `query` tool auto-injects `USE "connections/iceberg-gateway"`, so you reference tables by30their quoted name (`"opentelemetry/traces/checkout-service"`) — you don't prefix a catalog path. There is **no31enforced row cap**, but the tool returns every row as JSON, so always add a `LIMIT` (and explicit columns, not32`SELECT *`) to avoid dumping huge nested rows.3334## Table naming3536Tables are namespaced by signal type and service name. Names with slashes **must be double-quoted**. The37`{service_name}` layout is the default shard key; a deployment can configure a different one, so always38`SHOW TABLES;` to see the real names.3940| Signal | Table |41|--------|-------|42| Traces | `"opentelemetry/traces/{service_name}"` (one row per span) |43| Logs | `"opentelemetry/logs/{service_name}"` |44| Metrics catalog | `"opentelemetry/metrics"` (one row per metric — discover names here) |45| Metric data | `"opentelemetry/metrics/{metric_name}"` (one table per metric; values inline) |4647Metrics use a per-metric table with values inline — there is **no** `series` join and no48`opentelemetry_metrics_gauges`/`_series` tables. There is no separate "spans" or "events" table — a span's49`events` and `links` are nested `LIST<STRUCT>` columns inside the traces table.5051## Essential facts5253| Fact | Detail |54|------|--------|55| **Time columns** | `start_time` (traces), `time` (logs/metrics), all UTC. Filter on these first — tables are day-partitioned on them. Use a `Z`/`+00:00` suffix on literal timestamps. |56| **Duration** | Not stored — compute `end_time - start_time`. For a number, `EXTRACT(EPOCH FROM (end_time - start_time))` gives seconds. |57| **Status codes** | Integers: `0`=UNSET, `1`=OK, `2`=ERROR. Filter errors with `status.code = 2`. |58| **Severity** | `severity_number` integer: TRACE=1-4, DEBUG=5-8, INFO=9-12, WARN=13-16, ERROR=17-20, FATAL=21-24. |59| **Attributes** | STRUCTs, dot-accessed (`attributes.http.route`, `resource.attributes.service.name`) — **not** maps, so no `attributes['key']`. Keys are snake_case-normalized. Inferred to depth 2; level 3+ is a JSON-typed column — extract with DuckDB JSON funcs (e.g. `json_extract_string(attributes.request.context, '$.tenant_id')`). |60| **IDs** | `trace_id` is 16 bytes, `span_id`/`parent_span_id` 8 bytes (BLOB in DuckDB). Match with `x'...'` hex literals: `WHERE trace_id = x'0123...'`. |61| **Sampling** | Traces may carry a nullable `sample_rate` (double). When present, multiply counts by `1 / sample_rate` to estimate true volume. |6263## Querying connected sources6465The same `query` tool can also reach sources you've **connected** to Firetiger — and join across them in one66query. Firetiger's own telemetry is the default source; other sources are addressed two ways:6768- **SQL sources** (Postgres, MySQL, ClickHouse) — **fully-qualify the table** with the connection name.69 `"connections/{name}"` is a quoted identifier (it contains a `/`), followed by schema/database and table:70 ```sql71 SELECT id, email FROM "connections/prod-postgres".public.users WHERE created_at >= NOW() - INTERVAL '1 day' LIMIT 100;72 ```73 Prefer this to `USE` — it's self-contained and lets one query join across connections. (`USE74 "connections/{name}";` is an optional shorthand that sets the default for *unqualified* names.)75- **Observability backends** (Datadog, Prometheus, GCP Monitoring) — call a **function** with the connection76 name as a string argument and the vendor's own query language:77 ```sql78 SELECT * FROM datadog_search_logs('connections/prod-datadog', 'status:error service:api', NOW() - INTERVAL '1 hour', NOW(), max_rows => 500);79 ```8081Run `list with resource: "connections"` to see configured sources, and `SELECT * FROM confit_functions();` to82see which functions your connections unlock. Full patterns, PromQL/GCP examples, cross-source joins, and gotchas83are in [references/querying-connections.md](references/querying-connections.md). (Trino and Elasticsearch aren't84reachable through the `query` tool.)8586## What do you need?8788| Task | Reference |89|------|-----------|90| **Full column reference** for traces, logs, and deeply-nested attributes | [references/schema.md](references/schema.md) |91| **Ready-to-run queries** — recent/slow/error spans, latency percentiles, error logs, cross-service traces | [references/query-examples.md](references/query-examples.md) |92| **Metrics** — the metadata catalog, per-metric data tables, gauge/sum/histogram value columns | [references/metrics.md](references/metrics.md) |93| **Querying connected sources** — Postgres/MySQL/ClickHouse via fully-qualified `"connections/{name}".table`, Datadog/Prometheus/GCP via functions, cross-source joins | [references/querying-connections.md](references/querying-connections.md) |9495## Common Mistakes9697| # | Mistake | Fix |98|---|---------|-----|99| 1 | **`SELECT *` / no `LIMIT`** | Telemetry rows are wide (nested structs/lists) and every row is returned as JSON. Select explicit columns and add a `LIMIT`. |100| 2 | **Unquoted table name with slashes** | `"opentelemetry/traces/my-service"` — bare `opentelemetry/traces/...` is a parse error. |101| 3 | **Selecting a `duration` column** | There is none. Use `end_time - start_time`. |102| 4 | **Filtering `status.code = 'ERROR'`** | Status codes are integers — use `status.code = 2`. |103| 5 | **No time filter** | Always constrain `start_time`/`time` first — tables are day-partitioned, so this is what prunes the scan. |104| 6 | **Averaging an INTERVAL** | Wrap in `EXTRACT(EPOCH FROM (end_time - start_time))` to aggregate durations as numbers. |105| 7 | **Guessing service/table names** | Run `SHOW TABLES;` and `DESCRIBE "table"` first — schemas are inferred and vary per service. |106| 8 | **Map syntax on attributes** | Attributes are structs — `attributes.http.route`, not `attributes['http.route']`. |107| 9 | **Reading a level-3+ attribute directly** | Beyond nesting depth 2 it's a JSON-typed column — `json_extract_string(attributes.request.context, '$.key')`. |108| 10 | **Wrong connection-name form when federating** | In `FROM`, `"connections/pg".schema.table` — a **quoted identifier**; in a vendor function, `datadog_search_logs('connections/pg', …)` — a **string literal**. Don't swap them. |109110## Related111112- No data to query? Verify instrumentation with `firetiger-instrument`.113- Diagnosing an incident? `firetiger-investigate` wraps these queries in a tracked workflow.