Skip to content

Query with Quack

Run the duckdb-otlp Docker image in local-ducklake mode, enable the image’s Quack server, and connect to it from a DuckDB process outside Docker.

Use this setup when you want the container to handle ingestion and DuckLake writes while a local DuckDB shell, notebook, or script queries telemetry through Quack.

Quack is stable as of DuckDB 2.0. These examples use its CONNECT statement. The container binds Quack to 0.0.0.0, and on any non-loopback bind Quack serves TLS, with a self-signed certificate unless you supply one. Clients pin that certificate by its fingerprint.

Create .env:

DUCKDB_MODE=local-ducklake
DUCKDB_OTLP_TOKEN=dev-token-123456
DUCKDB_CATALOG=lake
DUCKDB_QUACK_ENABLED=1
DUCKDB_QUACK_ADDR=0.0.0.0:9494
DUCKDB_QUACK_TOKEN=dev-quack-token-123456

DUCKDB_OTLP_TOKEN protects OTLP/HTTP ingest on port 4318. DUCKDB_QUACK_TOKEN protects DuckDB query access on port 9494.

Quack generates its self-signed certificate on first start and keeps it on the data volume, under /data/.duckdb/extension_data/quack. A redeployed container that mounts the same /data volume keeps the same fingerprint, so pinned clients keep working. A fresh volume gets a new certificate. To use a certificate from your own CA instead, set:

DUCKDB_QUACK_SSL_CERT_FILE=/certs/quack.pem
DUCKDB_QUACK_SSL_KEY_FILE=/certs/quack.key

Local DuckLake mode uses DUCKLAKE_CATALOG_PATH=/data/ducklake/catalog.duckdb and DUCKLAKE_DATA_PATH=/data/ducklake/storage by default. Set those variables when you want DuckLake metadata or Parquet files somewhere else inside the mounted /data volume.

Prefer to run the same services manually in the DuckDB shell? See Run manually.

Terminal window
mkdir -p data
docker run --rm --name duckdb-otlp \
--env-file .env \
-p 4318:4318 \
-p 9494:9494 \
-v "$(pwd)/data:/data" \
ghcr.io/smithclay/duckdb-otlp:latest

The container initializes DuckLake, starts the OTLP/HTTP ingest server, and starts Quack in the same DuckDB process. The server names the catalog lake and makes it the default catalog Quack exposes, so attached clients reach the signal tables (otlp_logs, otlp_traces, …) directly instead of through a fully qualified lake.main.* name.

Once Quack is up, the container prints how to reach it, including the certificate fingerprint:

Query over Quack at https://0.0.0.0:9494
TLS certificate SHA-256 fingerprint: 3D:2C:B0:...:4F:8F
ATTACH 'quack:127.0.0.1:9494' AS otel (TYPE quack, SSL_FINGERPRINT '3D:2C:B0:...:4F:8F');

Run docker logs duckdb-otlp to see it again. Leave the container running while clients send OTLP/HTTP requests and local DuckDB clients query through Quack.

POST a log record

In another terminal:

Terminal window
curl -sS http://localhost:4318/v1/logs \
-H 'Authorization: Bearer dev-token-123456' \
-H 'Content-Type: application/json' \
-d '{"resourceLogs":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"quack-local-ducklake-demo"}},{"key":"deployment.environment","value":{"stringValue":"docs"}}]},"scopeLogs":[{"scope":{"name":"duckdb-otlp-guide"},"logRecords":[{"timeUnixNano":"1704067200000000000","observedTimeUnixNano":"1704067200123456789","severityNumber":9,"severityText":"INFO","body":{"stringValue":"hello from local DuckDB over Quack"},"attributes":[{"key":"guide","value":{"stringValue":"query-with-quack"}}]}]}]}]}'

Response:

{"status":"buffered","rows":1,"batches":1}

Rows are accepted before they are durable. They commit automatically in the background, on graceful shutdown, or immediately after an explicit flush.

In another terminal, start the DuckDB CLI on your host machine:

Terminal window
duckdb

Install and load Quack in the local DuckDB process:

INSTALL quack;
LOAD quack;

Create a Quack secret scoped to the container endpoint, attach the remote DuckDB process with the fingerprint the container printed, then connect the session to it:

CREATE SECRET duckdb_otlp_quack (
TYPE quack,
SCOPE 'quack:localhost:9494',
TOKEN 'dev-quack-token-123456'
);
ATTACH 'quack:localhost:9494' AS otel (TYPE quack, SSL_FINGERPRINT '<fingerprint>');
CONNECT otel;

SSL_FINGERPRINT makes the client use TLS and accept only that certificate. Without it, a localhost client tries plain HTTP and fails with Unsupported protocol error. The Docker port mapping forwards local port 9494 to quack:0.0.0.0:9494 inside the container.

The same ATTACH works from a remote host. With a certificate from a CA your clients trust, drop SSL_FINGERPRINT. A tunneling extension such as Query-farm/quackscale keeps the Quack listener off the public network.

After CONNECT otel, DuckDB routes ordinary SQL to the container’s DuckDB process and streams results back. The daemon makes its telemetry catalog (named lake here) the default, so the signal tables — otlp_logs, otlp_traces, otlp_metrics_gauge, otlp_metrics_sum, otlp_metrics_histogram, otlp_metrics_exp_histogram — are reachable by their plain names.

The server accepts rows before it makes them durable. For a short local test, flush the ingest buffer first:

SELECT * FROM otlp_flush('otlp:0.0.0.0:4318');

Then query the signal tables directly:

SELECT time_unix_nano, service_name, severity_text, body
FROM otlp_logs
WHERE service_name = 'quack-local-ducklake-demo'
ORDER BY time_unix_nano DESC
LIMIT 5;

The same works for traces:

SELECT trace_id, name, service_name, duration_time_unix_nano
FROM otlp_traces
ORDER BY start_time_unix_nano DESC
LIMIT 20;

While connected, aggregations, DDL, and table functions run in the server session without SQL-in-a-string wrapping:

SELECT service_name, count(*) AS rows
FROM otlp_logs
GROUP BY ALL
ORDER BY rows DESC;

Use quack_query for a one-shot call without a connected session, for example in small automation scripts:

FROM quack_query(
'quack:localhost:9494',
'SELECT service_name, count(*) AS rows FROM otlp_logs GROUP BY ALL ORDER BY rows DESC',
token := 'dev-quack-token-123456',
ssl_fingerprint := '<fingerprint>'
);

When you are done with the interactive remote session, disconnect it:

DISCONNECT;

Stop cleanly

Terminal window
docker stop duckdb-otlp

The image sends otlp_stop('otlp:0.0.0.0:4318') during shutdown, so remaining buffered rows are committed before the process exits.

To run this configuration in a DuckDB 2.0+ shell instead of the daemon, execute the SQL below. Replace bracketed values with the corresponding values from this guide. Keep the shell open while clients send telemetry.

Terminal window
mkdir -p data/ducklake/storage
duckdb data/duckdb-otlp-control.duckdb
-- The daemon embeds otlp statically; the shell loads the extension explicitly.
INSTALL otlp FROM community;
LOAD otlp;
-- Attach the local DuckLake catalog used by the Docker guide.
INSTALL ducklake;
LOAD ducklake;
ATTACH 'ducklake:data/ducklake/catalog.duckdb' AS lake (
DATA_PATH 'data/ducklake/storage'
);
-- Create the target schema before starting ingestion.
CREATE SCHEMA IF NOT EXISTS lake.main;
-- Match the daemon's default-catalog setting for unqualified queries.
USE lake;
-- Start OTLP/HTTP. Seal cadence, file sizes, and buffer limits use defaults;
-- override only as needed — see the Live Ingest Reference:
-- https://smithclay.github.io/duckdb-otlp/reference/serve/
SELECT listen_url, catalog_name, schema_name
FROM otlp_serve(
'otlp:0.0.0.0:4318',
catalog := 'lake',
schema := 'main',
token := 'dev-token-123456',
allow_other_hostname := true
);
-- The guide's daemon configuration also enables Quack.
INSTALL quack;
LOAD quack;
SELECT listen_uri
FROM quack_serve(
'quack:0.0.0.0:9494',
token := 'dev-quack-token-123456',
allow_other_hostname := true
);
-- The fingerprint clients pin: Quack serves TLS on this non-loopback bind.
SELECT info['ssl_fingerprint'] FROM quack_server_list();

Before closing DuckDB, stop both listeners cleanly:

-- Stop Quack, then commit buffered telemetry and stop OTLP.
CALL quack_stop('quack:0.0.0.0:9494');
SELECT status, dropped_rows
FROM otlp_stop('otlp:0.0.0.0:4318');

After you stop the container, remove the local DuckLake files if you no longer need them:

Terminal window
rm -rf data