Search Shortcut cmd + k | ctrl + k

Query OpenTelemetry traces, logs, and metrics with SQL — or stream live OTLP/HTTP into DuckDB, DuckLake, or Iceberg

Maintainer(s): smithclay

Installing and Loading

INSTALL otlp FROM community;
LOAD otlp;

Example

-- Load the extension
LOAD otlp;

-- Traces: find the slow spans
SELECT trace_id, span_name, service_name, duration
FROM read_otlp_traces('traces.jsonl')
WHERE duration > 1e9            -- longer than one second
ORDER BY duration DESC
LIMIT 10;

-- Or run a live OTLP/HTTP endpoint and query what arrives
SELECT listen_url, auth_token FROM otlp_serve('otlp:localhost:4318');
-- POST telemetry to http://localhost:4318/v1/logs.
-- Rows commit automatically in the background; otlp_stop commits remaining rows.
SELECT status FROM otlp_stop('otlp:localhost:4318');
SELECT * FROM otlp_logs;

-- Metrics: latest gauge readings (works native and in the browser)
SELECT timestamp, service_name, metric_name, value
FROM read_otlp_metrics_gauge('metrics.jsonl')
ORDER BY timestamp DESC;

-- Logs: just the errors, read straight from S3
SELECT timestamp, severity_text, body, service_name
FROM read_otlp_logs('s3://bucket/logs-*.jsonl')
WHERE severity_text = 'ERROR';

-- Protobuf works too, on native builds
SELECT * FROM read_otlp_traces('traces.pb') LIMIT 10;

About otlp

OpenTelemetry, in SQL

Point this extension at OTLP data and query it like any other table. Traces, logs, and metrics arrive as proper columns — service_name, trace_id, duration, value — not JSON you have to dig through. The schema follows the OpenTelemetry ClickHouse exporter, so columns are typed, quick to filter, and stable.

There are two ways in.

Read files

Six table functions read OTLP exports straight from disk or object storage:

  • read_otlp_traces() — spans, with attributes, events, links, and a computed duration
  • read_otlp_logs() — records, with severity, body, and trace correlation
  • read_otlp_metrics_gauge(), read_otlp_metrics_sum(), read_otlp_metrics_histogram(), read_otlp_metrics_exp_histogram()

JSON, JSONL, and protobuf are detected automatically. Paths can be local, a glob, S3, HTTP(S), Azure, or GCS. The browser build (DuckDB-WASM) reads all three formats — try the interactive demo.

Serve live ingest (native builds)

otlp_serve() starts an OTLP/HTTP endpoint inside DuckDB. Point a Collector or SDK exporter at http://localhost:4318 and the rows land in tables you can query:

SELECT listen_url, auth_token
FROM otlp_serve('otlp:localhost:4318');

Leave catalog empty to land rows in the default catalog for quick local work. Set it to an attached DuckLake lakehouse or writable Iceberg REST catalog for durable lakehouse ingest.

Ingest is buffered and group-committed. A POST returns 202 Accepted once rows are parsed and buffered; they turn durable at the next automatic background commit, currently when the oldest buffered row is about 5 seconds old or admitted request-body bytes reach about 64 MiB. otlp_server_list() reports the counters, and otlp_stop() commits what is left before it shuts down. otlp_flush() is optional: use it only when readers need the latest accepted rows durable immediately while the server keeps running. Stop before you close the database — a plain close does not commit buffered rows.

Use it for

  • Reading and analyzing Collector or SDK exports
  • A small, always-on OTLP sink that writes to DuckDB, DuckLake, or Iceberg
  • Converting telemetry to Parquet, CSV, or anything DuckDB writes
  • Inspecting traces, logs, and metrics during local development

Good to know

  • The live server is native-only and HTTP-only — no WASM, no gRPC.
  • File reads are capped at 100 MB each.
  • Summary metrics and the unified read_otlp_metrics() are not implemented yet.

References

Added Functions

function_name function_type description comment examples
otap_serve table Start a live OTAP/Arrow ingest server fed by several listeners – one buffer set and one sealer behind all of them, so any one of the URIs names the whole server to otlp_stop() and otlp_flush(). The transport is always gRPC: the canonical Arrow{Logs,Traces,Metrics}Service bidirectional streams, which are disjoint from the services otlp_serve registers. Only the otap: scheme is accepted. Rows land in the catalog named by the catalog parameter – an attached database, a DuckLake or Iceberg catalog among them – or in the default catalog when it is unset. They are buffered in memory and group-committed (sealed) in batches, so an accepted request is not durable yet: otlp_flush() commits on demand and otlp_stop() commits before returning. Returns one row per listener. NULL [SELECT * FROM otap_serve(['otap:localhost:4317', 'otap:localhost:14317'], token := 'replace-with-a-real-token');]
otap_serve table Start a live OTAP/Arrow ingest server on one listen URI. The transport is always gRPC: the canonical Arrow{Logs,Traces,Metrics}Service bidirectional streams, which are disjoint from the services otlp_serve registers. Only the otap: scheme is accepted. Rows land in the catalog named by the catalog parameter – an attached database, a DuckLake or Iceberg catalog among them – or in the default catalog when it is unset. They are buffered in memory and group-committed (sealed) in batches, so an accepted request is not durable yet: otlp_flush() commits on demand and otlp_stop() commits before returning. Returns one row per listener. NULL [SELECT * FROM otap_serve('otap:localhost:4317', token := 'replace-with-a-real-token');]
otap_serve table Start a live OTAP/Arrow ingest server on the default listen URI otap:localhost:4317. The transport is always gRPC: the canonical Arrow{Logs,Traces,Metrics}Service bidirectional streams, which are disjoint from the services otlp_serve registers. Only the otap: scheme is accepted. Rows land in the catalog named by the catalog parameter – an attached database, a DuckLake or Iceberg catalog among them – or in the default catalog when it is unset. They are buffered in memory and group-committed (sealed) in batches, so an accepted request is not durable yet: otlp_flush() commits on demand and otlp_stop() commits before returning. Returns one row per listener. NULL [SELECT * FROM otap_serve(token := 'replace-with-a-real-token');]
otlp_flush table Force a synchronous seal (group-commit) on the live ingest server that owns listener listen_uri, so rows accepted so far become readable in the target catalog. The server keeps running. NULL [SELECT * FROM otlp_flush('otlp:localhost:4318');]
otlp_seal_list table List the recent seal (group-commit) attempts of every live ingest server, with append and commit timings, rows and admitted bytes committed, and the error of any that failed. NULL [SELECT listen_uri, seal_sequence, rows_committed, duration_ms FROM otlp_seal_list();]
otlp_serve table Start a live OTLP ingest server fed by several listeners – one buffer set and one sealer behind all of them, so any one of the URIs names the whole server to otlp_stop() and otlp_flush(). The transport is OTLP/HTTP by default (POST /v1/logs, /v1/traces, /v1/metrics); transport := 'grpc' serves OTLP/gRPC unary Export instead. Only the otlp: scheme is accepted. Rows land in the catalog named by the catalog parameter – an attached database, a DuckLake or Iceberg catalog among them – or in the default catalog when it is unset. They are buffered in memory and group-committed (sealed) in batches, so an accepted request is not durable yet: otlp_flush() commits on demand and otlp_stop() commits before returning. Returns one row per listener. NULL [SELECT * FROM otlp_serve(['otlp:localhost:4318', 'otlp:localhost:4317'], transport := ['http', 'grpc'], token := 'replace-with-a-real-token');]
otlp_serve table Start a live OTLP ingest server on one listen URI. The transport is OTLP/HTTP by default (POST /v1/logs, /v1/traces, /v1/metrics); transport := 'grpc' serves OTLP/gRPC unary Export instead. Only the otlp: scheme is accepted. Rows land in the catalog named by the catalog parameter – an attached database, a DuckLake or Iceberg catalog among them – or in the default catalog when it is unset. They are buffered in memory and group-committed (sealed) in batches, so an accepted request is not durable yet: otlp_flush() commits on demand and otlp_stop() commits before returning. Returns one row per listener. NULL [SELECT * FROM otlp_serve('otlp:localhost:4318', token := 'replace-with-a-real-token');]
otlp_serve table Start a live OTLP ingest server on the default listen URI otlp:localhost:4318. The transport is OTLP/HTTP by default (POST /v1/logs, /v1/traces, /v1/metrics); transport := 'grpc' serves OTLP/gRPC unary Export instead. Only the otlp: scheme is accepted. Rows land in the catalog named by the catalog parameter – an attached database, a DuckLake or Iceberg catalog among them – or in the default catalog when it is unset. They are buffered in memory and group-committed (sealed) in batches, so an accepted request is not durable yet: otlp_flush() commits on demand and otlp_stop() commits before returning. Returns one row per listener. NULL [SELECT * FROM otlp_serve(token := 'replace-with-a-real-token');]
otlp_server_list table List every live ingest listener on this database with its server's target catalog, transport and live counters: rows received, buffered rows and bytes, seal totals and failures, last seal age and last error. NULL [SELECT listen_uri, transport, buffered_rows, seals_total FROM otlp_server_list();]
otlp_stop table Stop every live ingest server on this database, committing each one's buffered rows before returning. One row per stopped server. Use this before closing a database you did not start every server on: a plain close drops buffered rows. NULL [SELECT * FROM otlp_stop();]
otlp_stop table Stop the live ingest server that owns listener listen_uri, including its other listeners, committing its buffered rows before returning. NULL [SELECT * FROM otlp_stop('otlp:localhost:4318');]
otlp_uri_parser scalar Parse an OTLP/OTAP listen URI into a struct of host, port, ipv6 and the http:// URL a listener would bind. The otlp: or otap: scheme is required; the host defaults to localhost and the port to 4318 for otlp: or 4317 for otap:, the same defaults otlp_serve and otap_serve apply. The argument must be constant. NULL [otlp_uri_parser('otlp:localhost:4318')]
read_otap_logs table Read OpenTelemetry log records from an OTAP (OpenTelemetry Arrow Protocol) BatchArrowRecords file, returning the same 18 columns as read_otlp_logs. path is a single file or a glob, resolved through DuckDB's file systems (local, S3, HTTP(S), Azure, GCS). NULL [SELECT * FROM read_otap_logs('logs.bar');]
read_otap_metrics_exp_histogram table Read OpenTelemetry exponential histogram data points from an OTAP (OpenTelemetry Arrow Protocol) BatchArrowRecords file, returning the same 27 columns as read_otlp_metrics_exp_histogram. One file can hold several metric shapes; this reader takes its own and skips the rest, so the four metric readers all read the same file. path is a single file or a glob, resolved through DuckDB's file systems (local, S3, HTTP(S), Azure, GCS). NULL [SELECT * FROM read_otap_metrics_exp_histogram('metrics.bar');]
read_otap_metrics_gauge table Read OpenTelemetry gauge data points from an OTAP (OpenTelemetry Arrow Protocol) BatchArrowRecords file, returning the same 17 columns as read_otlp_metrics_gauge. One file can hold several metric shapes; this reader takes its own and skips the rest, so the four metric readers all read the same file. path is a single file or a glob, resolved through DuckDB's file systems (local, S3, HTTP(S), Azure, GCS). NULL [SELECT * FROM read_otap_metrics_gauge('metrics.bar');]
read_otap_metrics_histogram table Read OpenTelemetry explicit-bucket histogram data points from an OTAP (OpenTelemetry Arrow Protocol) BatchArrowRecords file, returning the same 22 columns as read_otlp_metrics_histogram. One file can hold several metric shapes; this reader takes its own and skips the rest, so the four metric readers all read the same file. path is a single file or a glob, resolved through DuckDB's file systems (local, S3, HTTP(S), Azure, GCS). NULL [SELECT * FROM read_otap_metrics_histogram('metrics.bar');]
read_otap_metrics_sum table Read OpenTelemetry sum (counter) data points from an OTAP (OpenTelemetry Arrow Protocol) BatchArrowRecords file, returning the same 19 columns as read_otlp_metrics_sum. One file can hold several metric shapes; this reader takes its own and skips the rest, so the four metric readers all read the same file. path is a single file or a glob, resolved through DuckDB's file systems (local, S3, HTTP(S), Azure, GCS). NULL [SELECT * FROM read_otap_metrics_sum('metrics.bar');]
read_otap_traces table Read OpenTelemetry trace spans from an OTAP (OpenTelemetry Arrow Protocol) BatchArrowRecords file, returning the same 24 columns as read_otlp_traces. path is a single file or a glob, resolved through DuckDB's file systems (local, S3, HTTP(S), Azure, GCS). NULL [SELECT * FROM read_otap_traces('traces.bar');]
read_otlp_logs table Read OpenTelemetry log records from OTLP JSON, NDJSON or protobuf files into a flat 18-column table. The encoding is detected from the file. path is a single file or a glob, resolved through DuckDB's file systems (local, S3, HTTP(S), Azure, GCS). NULL [SELECT * FROM read_otlp_logs('logs.pb');]
read_otlp_metrics table Not implemented: every call raises. OTLP metrics have shape-specific schemas, so read one of read_otlp_metrics_gauge, read_otlp_metrics_sum, read_otlp_metrics_histogram or read_otlp_metrics_exp_histogram instead. NULL  
read_otlp_metrics_exp_histogram table Read OpenTelemetry exponential histogram data points from OTLP JSON, NDJSON or protobuf files into a flat 27-column table with scale, zero bucket and positive/negative buckets. One file can hold several metric shapes; this reader takes its own and skips the rest, so the four metric readers all read the same file. path is a single file or a glob, resolved through DuckDB's file systems (local, S3, HTTP(S), Azure, GCS). NULL [SELECT * FROM read_otlp_metrics_exp_histogram('metrics.pb');]
read_otlp_metrics_gauge table Read OpenTelemetry gauge data points from OTLP JSON, NDJSON or protobuf files into a flat 17-column table. One file can hold several metric shapes; this reader takes its own and skips the rest, so the four metric readers all read the same file. path is a single file or a glob, resolved through DuckDB's file systems (local, S3, HTTP(S), Azure, GCS). NULL [SELECT * FROM read_otlp_metrics_gauge('metrics.pb');]
read_otlp_metrics_histogram table Read OpenTelemetry explicit-bucket histogram data points from OTLP JSON, NDJSON or protobuf files into a flat 22-column table with bucket bounds and counts. One file can hold several metric shapes; this reader takes its own and skips the rest, so the four metric readers all read the same file. path is a single file or a glob, resolved through DuckDB's file systems (local, S3, HTTP(S), Azure, GCS). NULL [SELECT * FROM read_otlp_metrics_histogram('metrics.pb');]
read_otlp_metrics_sum table Read OpenTelemetry sum (counter) data points from OTLP JSON, NDJSON or protobuf files into a flat 19-column table with aggregation_temporality and is_monotonic. One file can hold several metric shapes; this reader takes its own and skips the rest, so the four metric readers all read the same file. path is a single file or a glob, resolved through DuckDB's file systems (local, S3, HTTP(S), Azure, GCS). NULL [SELECT * FROM read_otlp_metrics_sum('metrics.pb');]
read_otlp_metrics_summary table Not implemented: every call raises. OTLP summary data points are not decoded; the other read_otlp_metrics_* readers count them as skipped. NULL  
read_otlp_traces table Read OpenTelemetry trace spans from OTLP JSON, NDJSON or protobuf files into a flat 24-column table, including events, links and a computed duration. path is a single file or a glob, resolved through DuckDB's file systems (local, S3, HTTP(S), Azure, GCS). NULL [SELECT * FROM read_otlp_traces('traces.pb');]

Overloaded Functions

This extension does not add any function overloads.

Added Types

This extension does not add any types.

Added Settings

This extension does not add any settings.