Capture every Claude Code session locally as queryable telemetry, without running Docker or sending data to a vendor. The result is a small SQL database of your own AI-coding activity that you can mine for automation patterns.
This setup takes about 10 minutes to install and runs as a single background binary plus a few helper scripts.
- Events for every tool execution, prompt, API request, skill activation, permission decision, etc.
- Metrics for token usage, cost, session counts, and active time.
- Distributed traces that show each prompt expanding into LLM calls and tool executions (beta).
- A DuckDB query layer with views over the raw OTLP data so you can answer questions like "which bash commands do I run most often" in one line of SQL.
You can paste this entire document into a fresh claude session with the
prompt "set this up for me" and Claude will execute the steps. Or follow them
by hand.
If you are Claude (or another automated installer) following this guide: there is one step where you must pause and ask the user a question rather than picking a default. It is called out as "Choose a privacy level" in step 6. The user's data sensitivity tolerance is not something you can infer from the guide. Do not enable any
OTEL_LOG_*variables until the user has chosen a level explicitly.
The Claude Code monitoring documentation is built on OpenTelemetry, which means anywhere you can ship OTLP, you can ship Claude Code telemetry. Anthropic's official monitoring guide repo uses Docker Compose with Prometheus and Grafana for team-scale dashboards. That is the right choice for organizations.
This document is for the individual developer who wants:
- The same data, without running Docker
- Local-only storage (nothing leaves the machine)
- A simple SQL surface for ad-hoc analysis, not a dashboard server
For comparison, two adjacent local-first tools were considered:
- otel-tui is a terminal UI that receives OTLP and renders traces, metrics, and logs live. It keeps a rolling 1000-record buffer in memory and has no on-disk persistence, so it is great for live observation but not for pattern analysis over time.
- otel-desktop-viewer is a browser UI backed by DuckDB. It has no documented CLI query interface, which makes it harder to script.
Neither persists data in a way that is easy to query from the shell, so the setup below uses the OpenTelemetry Collector binary writing JSONL files that DuckDB queries directly. See awesome-opentelemetry for further alternatives.
+---------------------+
| Claude Code (CLI) |
+----------+----------+
| OTLP/gRPC :4317
v
+---------------------+
| OTel Collector | single binary, no Docker
| otlp receiver --> |
| file exporter |
+----------+----------+
|
v
+---------------------+
| ~/.claude/otel/ |
| data/ |
| logs.jsonl | events: tool_result, user_prompt, ...
| metrics.jsonl | token usage, cost, sessions
| traces.jsonl | span hierarchy per prompt
+----------+----------+
| read_json()
v
+---------------------+
| DuckDB | SQL views over JSONL
+---------------------+
The collector writes append-only JSONL files with size-based rotation. DuckDB
reads those files in place using read_json(), so there is no separate import
step.
- macOS (tested on Apple Silicon) or Linux. Paths in this guide assume macOS;
on Linux substitute
~/.bashrcfor~/.zshrcand adjust the binary download URL for your architecture. - Homebrew (for installing DuckDB).
- Claude Code CLI v2.x or later.
brew install duckdbFor Linux or Windows install instructions see duckdb.org.
We need the contrib distribution because it includes the file exporter. There is no official Homebrew formula, so download from the GitHub release page:
mkdir -p ~/.claude/otel/{bin,data,queries,logs}
ARCH=$(uname -m | sed 's/x86_64/amd64/')
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
VERSION=v0.151.0 # check release page for latest
curl -L -o /tmp/otelcol-contrib.tar.gz \
"https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/${VERSION}/otelcol-contrib_${VERSION#v}_${OS}_${ARCH}.tar.gz"
tar xzf /tmp/otelcol-contrib.tar.gz -C ~/.claude/otel/bin/ otelcol-contrib
chmod +x ~/.claude/otel/bin/otelcol-contrib
rm /tmp/otelcol-contrib.tar.gz
~/.claude/otel/bin/otelcol-contrib --versionThe config receives OTLP on localhost:4317 (gRPC) and :4318 (HTTP), then
writes each signal type to its own JSONL file with rotation.
Save this as ~/.claude/otel/config.yaml:
receivers:
otlp:
protocols:
grpc:
endpoint: localhost:4317
http:
endpoint: localhost:4318
processors:
batch:
timeout: 5s
send_batch_size: 100
exporters:
file/traces:
path: ${env:HOME}/.claude/otel/data/traces.jsonl
rotation:
max_megabytes: 250
max_backups: 30
max_days: 90
flush_interval: 5s
file/metrics:
path: ${env:HOME}/.claude/otel/data/metrics.jsonl
rotation:
max_megabytes: 250
max_backups: 30
max_days: 90
flush_interval: 5s
file/logs:
path: ${env:HOME}/.claude/otel/data/logs.jsonl
rotation:
max_megabytes: 250
max_backups: 30
max_days: 90
flush_interval: 5s
service:
telemetry:
logs:
level: warn
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [file/traces]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [file/metrics]
logs:
receivers: [otlp]
processors: [batch]
exporters: [file/logs]Rotation caps each signal at roughly 7.5 GB (250 MB times 30 backups) and drops anything older than 90 days. Tune to taste.
Four small scripts go in ~/.claude/otel/bin/. Make them all executable
(chmod +x ~/.claude/otel/bin/*.sh).
start.sh starts the collector in the background and writes its PID:
#!/usr/bin/env bash
set -euo pipefail
OTEL_DIR="$HOME/.claude/otel"
PID_FILE="$OTEL_DIR/collector.pid"
LOG_FILE="$OTEL_DIR/logs/collector.log"
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
echo "Collector already running (pid $(cat "$PID_FILE"))"
exit 0
fi
nohup "$OTEL_DIR/bin/otelcol-contrib" \
--config "$OTEL_DIR/config.yaml" \
>> "$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
sleep 1
if kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
echo "Collector started (pid $(cat "$PID_FILE"))"
echo "Listening: gRPC :4317, HTTP :4318"
echo "Data: $OTEL_DIR/data/"
echo "Logs: $LOG_FILE"
else
echo "Collector failed to start. Last log lines:"
tail -20 "$LOG_FILE"
rm -f "$PID_FILE"
exit 1
fistop.sh:
#!/usr/bin/env bash
set -euo pipefail
PID_FILE="$HOME/.claude/otel/collector.pid"
if [[ ! -f "$PID_FILE" ]]; then
echo "Collector not running (no pid file)"
exit 0
fi
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
kill "$PID"
echo "Stopped collector (pid $PID)"
else
echo "Collector not running (stale pid file)"
fi
rm -f "$PID_FILE"status.sh:
#!/usr/bin/env bash
set -euo pipefail
OTEL_DIR="$HOME/.claude/otel"
PID_FILE="$OTEL_DIR/collector.pid"
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
echo "Collector running (pid $(cat "$PID_FILE"))"
else
echo "Collector not running"
fi
echo
echo "Data files:"
if [[ -d "$OTEL_DIR/data" ]] && compgen -G "$OTEL_DIR/data/*.jsonl*" > /dev/null; then
ls -lh "$OTEL_DIR/data/"
else
echo " (none yet)"
fiquery.sh opens DuckDB with the views pre-loaded:
#!/usr/bin/env bash
exec duckdb -init "$HOME/.claude/otel/queries/init.sql" "$@"This file is loaded automatically by query.sh. It defines views that
flatten the OTLP JSON envelope into a tabular shape and promote the most
common Claude Code event attributes to columns.
The views use a columns hint plus JSON path extraction so they work even
when a signal file is empty (no stub records required).
Save as ~/.claude/otel/queries/init.sql:
-- Helper: extract a string-coerced attribute by key from an OTLP attribute
-- list. Iterates the array via json_each so the macro doesn't depend on
-- inferred struct shapes.
CREATE OR REPLACE MACRO get_attr(attrs_json, name) AS (
(SELECT
COALESCE(
json_extract_string(je.value, '$.value.stringValue'),
json_extract_string(je.value, '$.value.intValue'),
json_extract_string(je.value, '$.value.doubleValue'),
json_extract_string(je.value, '$.value.boolValue')
)
FROM json_each(attrs_json) je
WHERE json_extract_string(je.value, '$.key') = name
LIMIT 1)
);
-- Raw log records flattened from the OTLP JSON envelope.
-- The `columns` hint keeps the top-level shape stable even with empty files.
CREATE OR REPLACE VIEW log_records AS
WITH files AS (
SELECT unnest(resourceLogs) AS rl
FROM read_json(
getenv('HOME') || '/.claude/otel/data/logs*.jsonl',
columns = {'resourceLogs': 'JSON[]'},
format = 'newline_delimited',
ignore_errors = true
)
), scopes AS (
SELECT
json_extract(rl, '$.resource.attributes') AS resource_attrs,
sl_je.value AS sl
FROM files, json_each(json_extract(rl, '$.scopeLogs')) sl_je
), records AS (
SELECT
resource_attrs,
lr_je.value AS lr
FROM scopes, json_each(json_extract(sl, '$.logRecords')) lr_je
)
SELECT
to_timestamp(CAST(json_extract_string(lr, '$.timeUnixNano') AS BIGINT) / 1e9) AS event_time,
resource_attrs,
json_extract(lr, '$.attributes') AS attrs,
json_extract(lr, '$.body') AS body
FROM records;
-- Convenience view: common Claude Code event attributes promoted to columns.
CREATE OR REPLACE VIEW events AS
SELECT
event_time,
get_attr(attrs, 'event.name') AS event_name,
get_attr(attrs, 'session.id') AS session_id,
get_attr(attrs, 'prompt.id') AS prompt_id,
get_attr(attrs, 'tool_name') AS tool_name,
get_attr(attrs, 'success') AS success,
TRY_CAST(get_attr(attrs, 'duration_ms') AS BIGINT) AS duration_ms,
TRY_CAST(get_attr(attrs, 'input_tokens') AS BIGINT) AS input_tokens,
TRY_CAST(get_attr(attrs, 'output_tokens') AS BIGINT) AS output_tokens,
TRY_CAST(get_attr(attrs, 'cache_read_tokens') AS BIGINT) AS cache_read_tokens,
TRY_CAST(get_attr(attrs, 'cache_creation_tokens') AS BIGINT) AS cache_creation_tokens,
TRY_CAST(get_attr(attrs, 'cost_usd') AS DOUBLE) AS cost_usd,
get_attr(attrs, 'model') AS model,
get_attr(attrs, 'query_source') AS query_source,
get_attr(attrs, 'skill.name') AS skill_name,
get_attr(attrs, 'tool_parameters') AS tool_parameters,
get_attr(attrs, 'command_name') AS command_name,
get_attr(resource_attrs, 'host.name') AS host_name,
attrs,
resource_attrs
FROM log_records;
-- Quick subset views per event type for common analyses.
CREATE OR REPLACE VIEW tool_results AS SELECT * FROM events WHERE event_name = 'tool_result';
CREATE OR REPLACE VIEW user_prompts AS SELECT * FROM events WHERE event_name = 'user_prompt';
CREATE OR REPLACE VIEW api_requests AS SELECT * FROM events WHERE event_name = 'api_request';
CREATE OR REPLACE VIEW api_errors AS SELECT * FROM events WHERE event_name = 'api_error';
CREATE OR REPLACE VIEW skill_activated AS SELECT * FROM events WHERE event_name = 'skill_activated';
CREATE OR REPLACE VIEW tool_decisions AS SELECT * FROM events WHERE event_name = 'tool_decision';
CREATE OR REPLACE VIEW compactions AS SELECT * FROM events WHERE event_name = 'compaction';
.print
.print Views: events, tool_results, user_prompts, api_requests, api_errors,
.print skill_activated, tool_decisions, compactions, log_records
.print Macros: get_attr(attrs, 'key.name')
.printClaude Code telemetry exposes increasing detail through a series of opt-in environment variables. Each one captures more data and creates more risk if the JSONL files leak. Pick the level that matches your environment.
Claude / automated installers: stop and ask the user which level they want before continuing. Do not pick a default. Show them the table below, let them choose, then proceed.
| Level | Adds these env vars | Captures | Reasonable when |
|---|---|---|---|
| Minimal | (none extra) | Counts, durations, model names, token counts, cost. No content. | Privacy-sensitive workplace; you only care about cost and usage trends. |
| Standard | OTEL_LOG_TOOL_DETAILS=1 |
Above + bash command bodies, skill names, file paths, MCP server names, command names | Pattern mining for automation. This is the most useful level for most people. |
| Verbose | OTEL_LOG_TOOL_DETAILS=1 + OTEL_LOG_USER_PROMPTS=1 |
Above + verbatim text of every prompt you type | You want to correlate prompts with the tool sequences they triggered. |
| Full | All of the above + OTEL_LOG_TOOL_CONTENT=1 |
Above + tool input/output bodies in trace spans (Read results, full Bash output, etc., truncated at 60 KB) | Forensic-grade. Treat ~/.claude/otel/data/ as a secrets file. |
There is one further variable, OTEL_LOG_RAW_API_BODIES, that captures the
entire Anthropic Messages API request and response bodies. It includes the
full conversation history on every turn. Most users should not enable it; if
you do, ship the directory through a log collector rather than letting it sit
on disk.
Once the level is chosen, append to ~/.zshrc (or ~/.bashrc on Linux),
including only the OTEL_LOG_* lines for the chosen level:
# Claude Code telemetry
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 # spans/traces (beta)
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_TRACES_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_METRIC_EXPORT_INTERVAL=10000
# Privacy level (uncomment based on your choice above)
# export OTEL_LOG_TOOL_DETAILS=1 # Standard, Verbose, Full
# export OTEL_LOG_USER_PROMPTS=1 # Verbose, Full
# export OTEL_LOG_TOOL_CONTENT=1 # Full only
alias otel='~/.claude/otel/bin/start.sh'
alias otel-stop='~/.claude/otel/bin/stop.sh'
alias otel-status='~/.claude/otel/bin/status.sh'
alias otel-query='~/.claude/otel/bin/query.sh'Reload:
source ~/.zshrcotel # start the collector
otel-status # confirm it's running
# Send a synthetic OTLP log to confirm end-to-end flow:
curl -sS -X POST http://localhost:4318/v1/logs \
-H 'Content-Type: application/json' \
-d '{
"resourceLogs": [{
"resource": {"attributes": [{"key": "service.name", "value": {"stringValue": "claude-code"}}]},
"scopeLogs": [{"logRecords": [{
"timeUnixNano": "'"$(date +%s)000000000"'",
"body": {"stringValue": "smoke test"},
"attributes": [
{"key": "event.name", "value": {"stringValue": "tool_result"}},
{"key": "tool_name", "value": {"stringValue": "TestTool"}},
{"key": "session.id", "value": {"stringValue": "smoketest"}}
]
}]}]
}]
}'
sleep 7
otel-query -c "SELECT event_time, event_name, tool_name, session_id FROM events;"If you see your test event in the output, the pipeline works. The collector keeps running in the background and will receive data from any new shell that sources the env vars above.
otel # start (idempotent)
otel-status # is it running, what files exist
otel-query # open interactive DuckDB shell with views loaded
otel-query -c "SELECT ..." # one-shot query
otel-stop # pause collectionThe env vars in .zshrc mean any new shell launching claude will export
telemetry to localhost:4317 automatically. Already-running shells need
source ~/.zshrc (or a new terminal window) to pick up the change.
To temporarily disable in a specific shell, set
CLAUDE_CODE_ENABLE_TELEMETRY=0 in that shell.
These run against the views defined in init.sql. Each maps to a real
question worth answering.
Top tools by use over the last 24 hours:
SELECT tool_name, COUNT(*) AS uses, ROUND(AVG(duration_ms)) AS avg_ms
FROM tool_results
WHERE event_time > now() - INTERVAL 1 DAY
GROUP BY 1
ORDER BY 2 DESC;Cost and cache hit rate per session:
SELECT
substr(session_id, 1, 8) AS session,
COUNT(*) AS llm_calls,
SUM(input_tokens) AS input_tokens,
SUM(output_tokens) AS output_tokens,
SUM(cache_read_tokens) AS cache_read,
ROUND(100.0 * SUM(cache_read_tokens)
/ NULLIF(SUM(input_tokens + cache_read_tokens), 0), 1) AS cache_hit_pct,
ROUND(SUM(cost_usd), 4) AS cost_usd
FROM api_requests
GROUP BY 1
ORDER BY cost_usd DESC NULLS LAST;Most-used skills:
SELECT skill_name, COUNT(*) AS activations
FROM skill_activated
WHERE skill_name IS NOT NULL
GROUP BY 1
ORDER BY 2 DESC;Bash commands you run repeatedly (automation candidates):
SELECT
json_extract_string(tool_parameters, '$.bash_command') AS cmd,
COUNT(*) AS uses,
SUM(duration_ms) AS total_ms
FROM tool_results
WHERE tool_name = 'Bash'
GROUP BY 1
ORDER BY 2 DESC
LIMIT 30;Permission decisions you keep accepting (allowlist candidates):
SELECT tool_name,
get_attr(attrs, 'decision_source') AS source,
COUNT(*) AS n
FROM tool_decisions
WHERE success IS NOT NULL
GROUP BY 1, 2
ORDER BY 3 DESC;Span hierarchy for traces (requires CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1):
WITH t AS (
SELECT unnest(resourceSpans) AS rs
FROM read_json(getenv('HOME') || '/.claude/otel/data/traces*.jsonl',
format='newline_delimited', union_by_name=true, ignore_errors=true)
), spans AS (
SELECT unnest(ss.spans) AS sp
FROM (SELECT unnest(rs.scopeSpans) AS ss FROM t)
)
SELECT
list_filter(sp.attributes, x -> x.key = 'session.id')[1].value.stringValue AS session_id,
sp.name AS span_name,
COUNT(*) AS n,
ROUND(AVG(CAST(list_filter(sp.attributes, x -> x.key = 'duration_ms')[1].value.intValue AS BIGINT))) AS avg_ms
FROM spans
GROUP BY 1, 2
ORDER BY 1, 2;This setup writes only to your local disk. Nothing is sent to a vendor. The Claude Code monitoring docs document the full data model; what follows is the practical view.
See the privacy-level table in step 6 above. The default scripts in this guide write nothing to your shell init until you choose a level.
- JSONL files are plaintext and unencrypted. Anyone with read access to
~/.claude/otel/data/can read everything captured. macOS file permissions protect against other users on the same machine, but not against malware running as your user, backup tools, or cloud-sync services that index your home directory. - Prompts often contain sensitive context even when you do not think of
them as secrets: pasted code with environment variable names, error
messages with stack traces, internal hostnames, presigned URLs, occasional
credentials. Enabling
OTEL_LOG_USER_PROMPTS=1captures all of them verbatim. Audit your habits before enabling Verbose or Full. - Bash command bodies leak credentials by accident. With
OTEL_LOG_TOOL_DETAILS=1, every command Claude Code runs is recorded. If Claude ever runsaws s3 cpwith a presigned URL,psql -h ... -U ...with a connection string, orcurl -H "Authorization: Bearer ...", those arguments are persisted. - Tool content can include large payloads. With
OTEL_LOG_TOOL_CONTENT=1, span events include tool input and output bodies up to 60 KB each. Reading a file containing secrets means those secrets land in the trace data. - Default retention is 90 days. Adjust
max_daysinconfig.yamlif you want a shorter window. Files rotate by size at 250 MB; rotated backups age out independently. - Do not place
~/.claude/otel/data/inside a tracked repository. It is outside the home dir's typical.gitignorepatterns. The default location in this guide (~/.claude/otel/data/) is safe as long as your home is not itself a git repo. - The collector log itself can leak metadata.
~/.claude/otel/logs/collector.logrecords OTLP errors and connection events. It is much less sensitive than the data files but worth knowing about if you share your machine.
Telemetry only flows while the collector is running. otel-stop is a clean
hard stop with no side effects: existing JSONL data stays on disk, no new
data is captured, and any active Claude Code session continues working
without telemetry until the collector comes back.
To wipe captured data: rm ~/.claude/otel/data/*.jsonl*.
To disable at the source even with the collector running, set
CLAUDE_CODE_ENABLE_TELEMETRY=0 in the shell that launches claude.
The collector handles concurrent sessions transparently. Every Claude Code
instance pushes to localhost:4317; events from different sessions interleave
in the JSONL files but each carries its own session.id attribute, so they
are trivially separable in queries.
If you regularly run 3+ concurrent sessions, raise the max_megabytes in the
collector's rotation config proportionally. The architecture itself does not
change.
The same env-var configuration that drives the local collector can ship telemetry to any OTLP-compatible cloud backend. For small teams (~10 developers) the free tiers of mainstream providers comfortably handle the data volume that AI-assisted coding produces, which is small compared to typical application telemetry (single-digit MB/day per developer).
| Provider | Free tier | Strengths | Caveats |
|---|---|---|---|
| Grafana Cloud | 50 GB logs, 10K metrics series, 50 GB traces, 14-day retention | OTLP-native, generous free tier, LogQL + PromQL + SQL Expressions, familiar Grafana UI, clean upgrade path | Three query languages to learn |
| Honeycomb | 20M events/month | Best-in-class trace exploration; BubbleUp surfaces outliers automatically; the prompt → tools span hierarchy from Claude Code renders beautifully | Trace-first; logs and metrics are second-class |
| Axiom | 500 GB/month | Single unified platform for logs/traces/metrics, OTel-native, APL (Kusto-like) queries | APL is yet another query language |
For most teams starting from this guide, Grafana Cloud's free tier is the path of least resistance: a 10-person team's Claude Code telemetry fits well inside the limits, the OTLP endpoint is a one-line change, and SQL Expressions are close enough to the DuckDB queries you've written locally that your query library translates with minimal rewrite.
- Datadog ($15-31/host/month minimum, built for 100+ engineering orgs)
- New Relic (free tier is capped at one user; gets pricey past that)
- Self-hosted SigNoz/ClickHouse (real engineering ownership cost; revisit only if you outgrow a SaaS free tier)
Whichever provider you pick, the change is the OTLP endpoint plus an authentication header. Example for Grafana Cloud:
# Replace the local-collector endpoint with the cloud ingest URL
export OTEL_EXPORTER_OTLP_ENDPOINT="https://otlp-gateway-prod-us-east-0.grafana.net/otlp"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <base64-encoded user:token>"
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
# Tag every signal with team identification so dashboards can segment by team/user
export OTEL_RESOURCE_ATTRIBUTES="team.id=engineering,cost_center=eng-123,user.name=$USER"Honeycomb uses a header-based API key (x-honeycomb-team); Axiom uses a
bearer token plus an x-axiom-dataset header. Each provider's docs have a
copy-pasteable snippet.
You can keep the personal local setup and feed a team backend at the same
time by adding a second OTLP exporter to config.yaml:
exporters:
file/logs: { ... } # existing
otlp/cloud:
endpoint: otlp-gateway-prod-us-east-0.grafana.net:443
headers:
Authorization: "Basic <base64-encoded user:token>"
service:
pipelines:
logs:
exporters: [file/logs, otlp/cloud] # fan out to bothThis pattern lets each developer keep their personal DuckDB-queryable history while the team gets aggregate visibility.
For deployments where developers should not have to configure their own shells, set these env vars centrally through the Claude Code managed settings file and distribute via MDM. Managed settings have higher precedence than user shell config, so the team backend is enforced.
- Claude Code monitoring documentation
- Anthropic's official monitoring guide repo (Docker / Prometheus / Grafana)
- OpenTelemetry Collector releases
- DuckDB documentation
- otel-tui (terminal live viewer)
- otel-desktop-viewer (browser viewer with DuckDB)
- awesome-opentelemetry directory
- Grafana Cloud
- Honeycomb
- Axiom