otelq
Give your AI coding agent eyes on your app's traces, logs, and metrics.
otelq is a tiny command-line tool that turns the OpenTelemetry signals your application already emits into answers — straight from the terminal, in the same loop your AI agent codes in. Run your code, then have your agent ask "did the request error?", "what was slow?", "show me trace X" and get a structured answer back. No Jaeger, no Grafana, no SigNoz, no server, no UI.
Why otelq
- Built for AI coding agents. Feed close-the-loop verification with real traces, logs, and metrics from any OpenTelemetry-compliant app: make a change, run it, and let the agent confirm from telemetry that it actually worked.
- Lightweight, fast, token-efficient. A single-file CLI invoked on demand — structured
json/csv/tableoutput an agent can parse, not dashboards to scrape, no MCPs crunching your tokens. No always-on services burning resources or context. - Zero heavy infrastructure. A stock OpenTelemetry Collector writes signals to plain JSONL files; otelq reads them in-process with DuckDB. Nothing to deploy, nothing to run between queries. A one-shot bundled demo gets you querying real signals in seconds.
- Fully local, fully isolated. Telemetry never leaves your machine — it lives in a directory you own and read directly. Nothing is shipped to a backend, a vendor, or the cloud.
Install & run
Install
-
Install Astral
uv. -
Clone otelq locally:
git clone https://github.com/robertgartman/otelq
-
From inside the clone, open your favourite agentic coding tool and invoke the skill — it wires otelq into the target project's Collector and installs the otelq query skill there:
/target-project-setup <path-to-the-repo-you-want-to-use-with-otelq>
Run
(1) Through your AI coding agent (the main path) — this is what otelq is built for. Once the otelq query skill is installed in your project (step 3 above), just ask your agent in plain language — "did the last run error?", "what was slow?", "show me trace X", "otel RCA on the last error" — and it drives the otelq CLI for you, reading the skill for the commands and query loop. You rarely type otelq yourself; the agent runs it in the same loop it codes in. If you want to nudge the flow, run /otelq summary.
The two paths below are the same CLI invoked by hand — useful for a quick check or for the agent under the hood.
(2a) Zero-install via PyPI (recommended) — run otelq straight from PyPI with uvx; no clone, no install. This is what the skill-based AI workflow uses:
uvx otelq summary # pin a version with: uvx otelq@0.1.0 summary
(2b) From the repo or a local clone — otelq.py is a PEP 723 single-file script, so uv can run it directly:
uv run otelq.py summary
Take it for a test run
See it work in under a minute — no app to instrument. Clone the otelq repo and run the demo: it starts the Collector (in Docker) and pushes a few seconds of synthetic traces, metrics, and logs through it with telemetrygen, the official OpenTelemetry load generator.
When done, you have telemetry data that otelq can parse and query. The demo instructions below runs an initial summary query.
git clone https://github.com/robertgartman/otelq
cd otelq
With just — a small command runner (brew install just, cargo install just, or see its repo):
just otel-demo # Collector + generators, then waits for the flush
just otel-down # stop and clean up
printf '%s\n' "=== Demo queries ===" \
"just otelq summary" \
"just otelq errors" \
"just otelq slow --top 10" \
"just otelq trace <trace_id>" \
"just otelq logs --level ERROR --grep 'timeout'" \
"just otelq metric <name>" \
"just otelq sql 'select * from traces limit 5'" \
"== Running Summary =="
just otelq summary # summary based metrics stored under telemetry folder
Or with plain Docker Compose — no command runner needed:
# start the Collector (no published host ports) and run the generators
docker compose -f compose.yaml -f compose.demo.yaml --profile otel up -d
docker compose -f compose.yaml -f compose.demo.yaml --profile demo up
sleep 7 # let the Collector flush its 5s batch
docker compose -f compose.yaml -f compose.demo.yaml --profile otel --profile demo down
printf '%s\n' "=== Demo queries ===" \
"uv run otelq.py summary" \
"uv run otelq.py errors" \
"uv run otelq.py slow --top 10" \
"uv run otelq.py trace <trace_id>" \
"uv run otelq.py --format json logs --level ERROR --grep 'timeout'" \
"uv run otelq.py metric <name>" \
"uv run otelq.py sql 'select * from traces limit 5'" \
"== Running Summary =="
uv run otelq.py summary # uv runs the single-file CLI — no install
Both paths need Docker and uv; the just path additionally needs just. The demo generators live only in this repo as a testing aid — they are never part of integrating otelq into your own project.
Architecture
At runtime, every component lives and runs on your machine:
---
title: "otelq runtime architecture"
config:
themeVariables:
clusterBkg: transparent
---
flowchart TB
subgraph github["otelq GitHub repo"]
otelq-code["otelq Python code"]
end
subgraph host["Local host — nothing leaves your machine"]
apps["Your applications and tools generating OpenTelemetry<br/>(tools, services, tests, scripts)"]
agent["AI coding agent"]
otelq-uvx["otelq · running with uvx"]
subgraph docker["Docker container"]
collector["OpenTelemetry Collector"]
end
subgraph project["Your project"]
signals["folder: ./.telemetry<br/><br/>traces.jsonl · logs.jsonl · metrics.jsonl"]
cache["folder: ./.telemetry/.otelq-cache<br/><br/>parquet query cache"]
skill["otelq skill"]
end
end
apps -->|"OTLP · gRPC :4317 / HTTP :4318"| collector
collector -->|"writes JSONL · bind mount"| signals
agent -->|invokes| otelq-uvx
agent -->|reads| skill
otelq-uvx -->|reads| signals
otelq-uvx -->|"reads / writes"| cache
otelq-uvx -->|fetch| otelq-code
classDef default fill:#d5fcb0
classDef whiteBg fill:#f0f0f0,stroke:#999999
class docker,project whiteBg
Your application(s) send OpenTelemetry over OTLP to a Collector running in Docker. The Collector writes each signal as plain JSONL into a .telemetry/ directory bind-mounted from your project. otelq runs on the host — invoked directly or by the otelq skill — and reads those .jsonl files in-process with DuckDB, keeping an incremental parquet cache under .telemetry/.otelq-cache/ for fast repeat queries.
The bind-mounted directory is the entire contract: the Collector writes traces.jsonl, logs.jsonl, and metrics.jsonl; otelq reads those same files. There is no network coupling between the Collector and the CLI — the shared directory is the API.
Using otelq in your project, with your OTEL Collector
otelq is a pure consumer of the telemetry directory — it never owns or runs a Collector. In any real setup the Collector belongs to your project: it is the one your application already sends OTLP to. You connect otelq by teeing that Collector's output to a directory otelq can read — add otelq's file exporters to the Collector so it also writes traces.jsonl / logs.jsonl / metrics.jsonl, then point otelq at that directory. otelq never starts, stops, or cleans that Collector; it only reads the files and owns its .otelq-cache/ subtree.
The direction of integration matters: you work from the otelq repo and integrate otelq into your target project (identified by its absolute path, e.g. /Users/me/dev/my-service) — not the other way around. You invoke your coding agent onto a target-project-setup skill in this repo.
# otelq runs straight from PyPI via uvx — no clone, no install:
alias otelq="uvx otelq"
otelq collector-config # prints the exporters + pipeline wiring to add
# ...paste the fragment into your project's Collector config, bind-mount its ./.telemetry, restart...
otelq --dir /Users/me/dev/my-service/.telemetry doctor # verify your wiring satisfies the contract
collector-config is generated from otelq's pinned constants, so it never drifts from the contract; doctor checks a telemetry directory against it. The file exporter requires the *-contrib Collector image. The target-project-setup skill automates all of this and asks for the target project's path; see below. When exercising your own app is inconvenient, the skill can also confirm the wiring end-to-end with a throwaway telemetrygen probe — committed, run against your Collector over its own network, then reverted — flagging first if the teed pipeline also feeds a real backend.
No Collector yet? otelq bundles one purely so you can try the tool without instrumenting anything — see Take it for a test run. That bundled stack (and the Compose files and optional
justrecipes that manage it) is a demo and local-dev aid, not a deployment model: in real use the Collector lives in your project, and otelq just reads what it writes.
Your project's production environment
otelq is a local development tool — nothing about it ships to production. The OpenTelemetry Collector, however, remains a perfectly valid (though not strictly necessary) component of your production stack: the same Collector your application sends OTLP to locally can run in production too, fronting your real observability backend.
The thing that must not carry over is otelq's wiring. When otelq is integrated into your project it adds a file-exporter pipeline that writes traces.jsonl / logs.jsonl / metrics.jsonl to a local .telemetry/ directory — that is exactly what otelq reads, and exactly what you do not want in production, where you ship telemetry to a remote service rather than storing it on a box.
So if you keep the Collector in production, make the configuration this project introduced into your Docker Compose environment-conditional:
- Local / dev — the
fileexporters and the bind-mounted.telemetry/directory are active, so otelq can query the signals on your machine. - Production — that local-storage path is switched off and the same pipelines instead point at production-grade, OTLP-compliant collectors or backends (your APM/observability vendor, a managed OTLP endpoint, etc.), shipping telemetry to the remote service instead of writing JSONL to disk.
Concretely, that means parameterizing the pieces otelq added — gating the file exporters and the .telemetry/ bind mount behind a profile or environment variable, and selecting the production exporter set when deploying — so a single Compose definition flips cleanly between "store telemetry locally for otelq" and "ship telemetry to a remote, production-compliant collector."
Worktree isolation
Agentic development increasingly means several git worktrees of the same repository checked out side by side, each exercised at the same time by a different agent or human. That collides with otelq's capture model in a way worth understanding up front.
- The Collector is shared. Only one dev Collector can run per host — it binds the fixed OTLP ports
:4317/:4318(see Architecture). Every worktree's instrumented app targets that same endpoint, so all of their telemetry funnels through one Collector. - All signals land in one directory. That Collector writes JSONL into the single
.telemetry/directory it bind-mounts — the one under the checkout where you started it (typically your default/primary checkout). Worktrees do not get their own.telemetry/; their signals are appended right alongside everyone else's. - So concurrent worktrees produce confusing telemetry. Traces, logs, and metrics from
feature-x,hotfix, andmainall interleave in the same files with nothing to tell them apart — a slow span you're chasing might belong to a run in an entirely different worktree.
The fix: a git-derived resource attribute
OpenTelemetry already has a spec-blessed, zero-code way to stamp identity onto every signal: the OTEL_RESOURCE_ATTRIBUTES environment variable. Every official SDK merges it onto the Resource carried by every span / log / metric at startup — no application code change required.
otelq leans on exactly that, tagging each worktree with two attributes:
otelq.worktree.id— the worktree's absolute path (git rev-parse --show-toplevel), guaranteed unique per worktree. This is the discriminator.otelq.worktree.branch— the checked-out branch, a human-friendly secondary label.
Once telemetry carries these tags, otelq's built-in commands (errors, slow, logs, metric, and the summary census) automatically scope to the current worktree — its own rows plus untagged infrastructure — so you see only what your work emitted. Nothing changes until the tags are present: the feature is fully opt-in. (sql is never auto-scoped; see the predicate in Commands.)
Wiring it up
The identity has to reach the app's environment before the SDK starts. Two ways:
1. Let otelq write it. From inside a worktree:
otelq set_resource_attributes # merges OTEL_RESOURCE_ATTRIBUTES into ./.env.local
It derives the id/branch from the current checkout and merges them into that worktree's .env.local, preserving any other keys. Then source that file before launching your process so the SDK picks it up:
set -a; . ./.env.local; set +a
# ...run your instrumented app / tests...
Automate it by calling set_resource_attributes from your worktree's launch script or task runner.
2. Manage .env.local yourself. It's just a dotenv file — set OTEL_RESOURCE_ATTRIBUTES by hand if you prefer:
OTEL_RESOURCE_ATTRIBUTES="otelq.worktree.id=/Users/me/dev/my-service-feature-x,otelq.worktree.branch=feature-x"
Either way, point otelq at the shared store when querying from a worktree (it has no .telemetry/ of its own):
otelq --dir ../my-service/.telemetry errors # auto-scoped to THIS worktree
Where the files live
~/dev/
├── my-service/ ← default checkout; the Collector runs here
│ ├── .telemetry/ ← Docker bind mount — the ONE shared store
│ │ ├── traces.jsonl · EVERY worktree's signals land here
│ │ ├── logs.jsonl
│ │ └── metrics.jsonl
│ └── .env.local ← OTEL_RESOURCE_ATTRIBUTES for the default checkout
│ (otelq.worktree.id = …/my-service)
│
└── my-service-feature-x/ ← a git worktree based off the same repo (no .telemetry/ needed)
└── .env.local ← OTEL_RESOURCE_ATTRIBUTES for THIS worktree
(otelq.worktree.id = …/my-service-feature-x)
Both checkouts' apps send to the one Collector, which writes into my-service/.telemetry/; each app's signals are stamped from its own .env.local, and otelq uses that stamp to show you just your worktree's slice.
Commands
This is a dump from running uv run otelq.py --help within the project root:
usage: otelq [-h] [--version] [--dir DIR]
[--format {table,json,jsonl,csv,compact}] [--all] [--no-cache]
[--verbose] [--since SINCE] [--regex REGEX]
[--session-id SESSION_ID] [--resource-attr KEY[=VALUE]]
[--attr KEY[=VALUE]] [--all-worktrees]
{summary,sql,errors,slow,trace,logs,metric,await,history,triage,collector-config,doctor,troubleshoot,set_resource_attributes,help}
...
Query OTLP telemetry captured by the dev OTel Collector.
positional arguments:
{summary,sql,errors,slow,trace,logs,metric,await,history,triage,collector-config,doctor,troubleshoot,set_resource_attributes,help}
summary counts and time span per signal
sql run an ad-hoc SQL query
errors error spans and ERROR/FATAL logs
slow slowest spans
trace all spans of one trace as a tree
logs filtered log records
metric time series for one metric
await block until a wrapped query is satisfied or --timeout
expires (exit 0 satisfied / 1 timed out / 2 error).
Polls in-process, so the budget is real; a caller-side
loop cannot measure its own spawn cost
history ranked past-query history — the templates most likely
to crack an investigation (triage assistant; also as
sql views history_queries/history_invocations)
triage start or continue an investigation from history: auto-
runs the most likely next query when the evidence is
strong (Markov step over past sessions), suggests the
follow-up invocation, or admits it doesn't know and
lists the top templates
collector-config print the file-export fragment to add to an existing
Collector
doctor check that --dir satisfies the telemetry contract
troubleshoot print the capture → query loop and common fixes
set_resource_attributes
write git-derived otelq.worktree.id/branch into
./.env.local (opt-in worktree tagging; source it
before launching the app)
help show help for otelq or a command
options:
-h, --help show this help message and exit
--version print otelq's version and exit
--dir DIR telemetry folder; when omitted otelq resolves it
itself — $OTELQ_DIR, else the nearest .telemetry/ at
or above the working directory (from a linked git
worktree, the main checkout's, since one Collector
serves them all). The resolved dir and the rule that
chose it are printed in the response header
--format {table,json,jsonl,csv,compact}
output format (default: compact, the fewest-token
format for agents; pass --format table for a human-
readable view)
--all widen the query to the full raw history (cold scan)
--no-cache bypass the parquet cache entirely (pure cold scan)
--verbose print the resolved time window and route to stderr
--since SINCE restrict to a trailing time window: Ns/Nm/Nh/Nd (e.g.
30s, 10m, 2h, 1d)
--regex REGEX keep only rows matching this pattern in some cell
(summary/errors/slow/trace/logs/metric only); reported
in the response header
--session-id SESSION_ID
tag this and consecutive related invocations with a
shared id (default: a generated UUIDv7); echoed in the
header and session footer
--resource-attr KEY[=VALUE]
keep only rows whose OTel Resource attribute KEY
equals VALUE (exact match); omit =VALUE to match any
non-empty value. Repeatable (AND).
summary/errors/slow/trace/logs/metric only
--attr KEY[=VALUE] keep only records whose OWN attribute KEY equals VALUE
(span/log/metric attributes, chosen by signal) — as
opposed to --resource-attr, which matches the
producer. Same forms and rules; repeatable (AND)
--all-worktrees include every worktree's telemetry (disable default
worktree scoping)
timestamps: ALL timestamps — printed by otelq and written into a
`sql` query — are UTC. Write `sql` timestamp literals bare
('YYYY-MM-DD HH:MM:SS') or 'Z'-suffixed ('...T10:00:00Z'); never
a non-Z offset (e.g. +02:00) — DuckDB silently drops it instead
of converting, so the comparison would be silently wrong.
argument order:
--dir / --format / --all / --no-cache / --since / --regex /
--verbose are GLOBAL flags and must come BEFORE the subcommand:
otelq --since 10m --format compact errors
(not: otelq errors --since 10m). Per-command flags (--top, --service,
--level, --grep) go AFTER the subcommand.
output format (pick the fewest tokens the consumer can parse):
--format compact DEFAULT. BEST for agents/LLMs: a single
{"columns":[...],"rows":[[...]]} object — column
names once, each row a positional array. Lossless
and the smallest machine format (no repeated keys).
Reconstruct rows with zip(columns, row).
--format json a JSON array of per-row objects; use only when a
--format jsonl consumer needs self-describing rows / streaming.
--format csv spreadsheet/interchange.
--format table for a human reading the terminal, not for parsing.
time window (wall-clock, over each record's event-time):
(default) the trailing 30 minutes
--since Ns|Nm|Nh|Nd only the trailing window, e.g. 30s, 10m, 2h, 1d
--all the full captured history (no window)
`trace` ignores the window — a trace id is looked up across all
history, and a unique id prefix is accepted.
row limits:
errors / slow / logs / metric cap output with --top N and print a
one-line notice to stderr when the result was truncated.
worktree scoping (opt-in; engages only when telemetry carries
otelq.worktree.id tags — otherwise every command is unchanged):
errors / slow / logs / metric default to the CURRENT worktree
(plus untagged rows) and print a `Worktree scope:` header line.
--all-worktrees include every worktree (disable scoping)
(GLOBAL; `summary`, `trace`, and `sql` are never scoped). To
inspect one specific other worktree, filter in `sql` on
otelq.worktree.id. Run `set_resource_attributes` to write the
git-derived tags into ./.env.local for the launcher to source.
regex filtering (summary/errors/slow/trace/logs/metric only):
--regex PATTERN keep only rows matching PATTERN in some cell.
Applied BEFORE rendering, so JSON escaping/CSV
quoting/table padding never affect precision —
precise field-level matching, not `| grep` on
already-rendered text. The response header
reports the verbatim pattern and how many rows
it removed, so you're never blind to what was
filtered (unlike piping through grep). Standard
Python re syntax, case-sensitive by default —
use inline (?i) for case-insensitive. Applies to
the same already --top-capped result; raise
--top to search further. Not supported for sql
(use WHERE col ~ 'pattern' — DuckDB has native
regex) or collector-config/doctor/troubleshoot.
sql views (for `otelq sql "<query>"`):
data model: the duckdb-otlp v0.6.0 reader schema, adopted
verbatim (see the duckdb-otlp project docs for full semantics).
Below is a curated subset. Explore the full live
schema with standard DuckDB introspection, e.g.
sql "DESCRIBE traces" or sql "PRAGMA table_info('logs')" — it
reveals extra columns (span_attributes/log_attributes/
metric_attributes, resource_attributes, scope_attributes, ...)
carrying whatever custom OTel tags an app actually emits.
resource_attr(attrs, 'key') — an otelq-DEFINED macro (NOT an
OpenTelemetry concept and NOT a DuckDB builtin) returning the
string value of an OTel Resource attribute, or NULL when it is
absent, empty, or the column is NULL/unparseable. Use it instead
of hand-writing json_extract_string over resource_attributes: it
is exact-match, needs no escaping for dotted keys, and never
raises on a malformed row, e.g.
sql "SELECT * FROM logs
WHERE resource_attr(resource_attributes, 'smoke.run_id')
= 'abc123'"
The built-in commands take --resource-attr KEY=VALUE for the same
job (repeatable, AND); `sql` is never rewritten, so here you write
the macro yourself.
span_edges / span_tree — otelq-DEFINED derived relations (NOT part
of the reader schema) that make span structure traversable.
span_edges span_id, parent_span_id, trace_id — only edges whose
parent actually exists IN THE SAME trace.
span_tree span_id, trace_id, root_id, depth, is_orphan —
root_id is the CONNECTED COMPONENT the span belongs
to; two spans are connected iff their root_id match.
is_orphan marks a span whose parent was named but
never exported (sampled away / still in flight / a
non-exporting service) — a broken link, as opposed
to a genuine root, which has an empty parent.
Sharing a trace_id is NOT the same as being connected: a dangling
parent leaves spans in one trace split across components. Use
root_id, not trace_id, to ask "did these actually connect?", e.g.
telling a step that never ran apart from a hop that lost trace
context:
sql "WITH want(service_name, name) AS (
VALUES ('svcA','span.one'),('svcB','span.two')),
hit AS (
SELECT w.service_name, w.name, st.root_id FROM want w
LEFT JOIN traces t ON t.service_name = w.service_name
AND t.name = w.name
LEFT JOIN span_tree st ON st.span_id = t.span_id)
SELECT CASE
WHEN count(*) FILTER (WHERE root_id IS NULL) > 0
THEN 'missing'
WHEN (SELECT count(DISTINCT service_name || name)
FROM hit GROUP BY root_id ORDER BY 1 DESC LIMIT 1)
= (SELECT count(*) FROM want) THEN 'connected'
ELSE 'disconnected' END FROM hit"
otelq reports structure, never a verdict on it — you write the
predicate, then gate it with the exit code (add `await` to wait for
it: exit 0 satisfied, 1 not, 2 error).
IMPORTANT — isolate your `sql` to this worktree: unlike the built-in
commands, `sql` is NEVER auto-scoped, so on a shared Collector it
sees EVERY concurrent worktree's rows. Scope a query to THIS
worktree (its own rows OR untagged infra) by AND-ing the
mine-or-untagged predicate on otelq.worktree.id into your WHERE.
Reference the reserved $WORKTREE_ID parameter — otelq binds it to
THIS worktree's id from .env.local at query time (never rewriting
your text), so the snippet is identical across worktrees, e.g.
sql "SELECT * FROM logs WHERE
(resource_attr(resource_attributes, 'otelq.worktree.id')
IS NULL
OR resource_attr(resource_attributes, 'otelq.worktree.id')
= $WORKTREE_ID)"
Run `set_resource_attributes` to print THIS worktree's exact id and
a ready-to-paste copy of that predicate — no need to hand-build it.
traces start_time_unix_nano (event-time),
duration_time_unix_nano (ns), trace_id, span_id,
parent_span_id, service_name, name (span name), kind,
status_code (0=unset,1=ok,2=error), status_status_message
logs time_unix_nano (event-time), trace_id, service_name,
severity_text, severity_number, body
metrics time_unix_nano (event-time), service_name, name,
metric_type, value, unit
(metric_type: gauge|sum|histogram|exp_histogram;
value = gauge/sum's double_value else int_value,
the sum of histogram/exp)
per-type metric relations (metrics unions whichever are present):
metrics_gauge, metrics_sum int_value, double_value
metrics_histogram, metrics_exp_histogram count, sum, min, max
(+ bucket_counts/explicit_bounds, or scale/zero_count/…)
(the OTel Summary metric type is unsupported by the reader extension)
*_unix_nano event-time columns are naive UTC TIMESTAMP_NS — see
"timestamps" above for the
literal convention when filtering on them.
the built-in commands read only the telemetry under --dir. `sql`
is an escape hatch that runs with YOUR user's file access (it can
read/write local files via read_csv/COPY), so treat untrusted
queries with the same care as a shell command.
Run `otelq troubleshoot` for the capture → query loop and common fixes.
Run otelq help <command> (or otelq <command> -h) for the full, authoritative
behavior of any command.
DuckDB pin note
The DuckDB runtime dependency is pinned exactly. This is deliberate. otelq reads OTLP JSONL via the community duckdb-otlp extension, which is built per DuckDB version — a floating DuckDB would silently fail to load the extension. CI runs an extension-probe step that loads the extension against the pinned version so the pin and the published extension stay in lockstep. See context/adr/ADR-003 for the decision and trade-offs.
Agentic engineering
This repo is built to be developed with AI engineering:
AGENTS.md— start here. The entry point for agents working in this repo.context/CONTEXT.md— the documentation system (PRD / SPEC / ADR / CONTRACT routing rules)..agents/skills/otelq— the otelq skill: capture OTEL signals from the dev Collector and query them with otelq. A.claudeshim (.claude/skills/otelq) mirrors it for Claude Code..agents/skills/target-project-setup— the target-project-setup skill: run from this repo to wire otelq's file-export pipeline into another project's existing Collector (the integrated setup above). It asks for the target project's absolute path and verifies the result withotelq doctor.
Contributing
just lint # ruff
just otelq-test # pytest suite
See CONTRIBUTING.md for the full setup, the
maintainer branch/PR/merge workflow for this public repo, and the PR checklist.
Participation is governed by the
CODE_OF_CONDUCT.md; report vulnerabilities per
SECURITY.md. Issues and pull requests welcome at
github.com/robertgartman/otelq.
Acknowledgements
otelq stands on the shoulders of two outstanding open-source projects:
- DuckDB — the in-process analytical database that makes otelq's fast, dependency-light querying possible. Heartfelt thanks to the DuckDB team and its contributors for building such a remarkable engine.
duckdb-otlp— the community extension that teaches DuckDB to read OTLP telemetry. Thanks to Clay Smith and the duckdb-otlp contributors for the work that otelq builds directly upon.
This project would not exist without their craftsmanship. 🦆
License
MIT © 2026 Robert Gartman. See LICENSE.
Release files for otelq 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| otelq-2.0.0.tar.gz | 157.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| otelq-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 245.8 kB
Release files / otelq-2.0.0.tar.gz
| Download URL | otelq-2.0.0.tar.gz |
|---|---|
| Size | 157.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e4fe02b35a6f67c6a8553e94f473b1571a20d1dba125c0c267a843295e1aee4d
|
|
BLAKE2b-256 checksum How to use checksums |
af28bf758b4a0e085701079a7bbd820f229c68a60aa9bd6f37c7c59c51060c44
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 13, 2026.
Transparency logRelease files / otelq-2.0.0-py3-none-any.whl
| Download URL | otelq-2.0.0-py3-none-any.whl |
|---|---|
| Size | 88.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
067e038f8ade3400f85db823fa1041271b050e8cc6fb73835e039aedb55b63d5
|
|
BLAKE2b-256 checksum How to use checksums |
5e385514d3261b12096c3585a8abe4d0730f89d90c531e8c1258e4bee9a5dba0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 13, 2026.
Transparency log