Observability for Laravel that runs on one command — the same one on your laptop and on the server.
telemetryd serveThat is the whole setup. No configuration file, no collector to run alongside it, no
decisions about storage backends or index tiers. Logs, traces and metrics go into one
directory and come back out through the Loki, Tempo and Prometheus APIs that
laravel-telemetry-ui already speaks,
so laravel-telemetry works against it
without a line of change.
Three things follow from that, and they are the reasons to pick it:
Local and production are the same program. Not a lighter build for development and a real one for the server — one binary, one storage format, one query subset. What you reproduce on your laptop behaves the way it will behave in production, because it is production. Debugging against a stand-in that is almost the same is how you fix the wrong thing.
Your telemetry never leaves your infrastructure. One binary, one port, one data directory, and nothing else to run. Nothing phones anywhere, including telemetryd's own landing page.
And it can leave whenever you want. Every signal moves in and out as OTLP, and the read APIs are three standards rather than ours. If this stops being the right tool, point the UI at Loki and Prometheus and run one export command. The exit is designed in, not discovered later.
telemetryd is scoped deliberately: one team, a handful of apps, one VPS or a dev laptop. It targets that case completely rather than scaling to a large fleet.
Single-node is a design choice, not a limitation we plan to remove. It is what lets us delete sharding, consensus, object-store tiering and a query scheduler — and ship one 2.6 MB binary instead of a stack of services to operate. If you outgrow one node, you have outgrown telemetryd, and we would rather say so than pretend otherwise.
Status: feature complete. All three signals go in as OTLP/HTTP JSON (plus Prometheus
remote_write), land in Parquet segments, and come back out through the Loki, Tempo and Prometheus query APIs — with live tail, and retention enforcing both time and a disk budget. Nothing in the contract answers501.BUILD-STATUS.md is the honest, current list of what works, what the known gaps are, and what is deliberately absent.
curl -fsSL https://raw.githubusercontent.com/cboxdk/telemetryd/main/install.sh | shDetects the platform, verifies the release signature when cosign is present and the
checksum always, and refuses to install on a mismatch. On macOS:
brew install cboxdk/tap/telemetrydA .deb and the raw binaries are on the releases page,
and there is a container image at ghcr.io/cboxdk/telemetryd — see Docker.
On a server, write a configuration with generated tokens and install the service unit — a hardened systemd unit on Linux, a launchd plist on macOS:
sudo telemetryd init # config + one token per surface, printed once
telemetryd service print # read the unit before installing it
sudo telemetryd service installPutting it on a server behind TLS is a page of its own.
telemetryd serveIt listens on 127.0.0.1:4319, stores data in ./telemetryd-data (or your platform data
directory), keeps 7 days of logs and traces and 30 days of metrics, and stays under a
10 GiB disk budget. Those defaults are the only difference between this and the server —
the same binary, told different numbers.
Or as a container, with the same amount of configuration:
docker run -d -p 4319:4319 -v telemetryd-data:/var/lib/telemetryd \
ghcr.io/cboxdk/telemetryd:latestA container binds 0.0.0.0, which telemetryd refuses to do unauthenticated, so the
image generates tokens on first start and prints them once — docker logs has them.
Every setting is an environment variable, so nothing needs a file. See
Docker.
telemetryd status
telemetryd query # everything from the last hour — start hereOr open /debug in a browser — locally at http://127.0.0.1:4319/debug, and on a
server at whatever hostname you put in front of it. The last records for logs, traces and
metrics, with no query to compose. An instance with tokens asks for the admin token on the
page and keeps it in a cookie no script can read; one without is simply open.
Point cboxdk/laravel-telemetry at http://127.0.0.1:4319 and query it back:
# logs
curl -G http://127.0.0.1:4319/loki/api/v1/query_range \
--data-urlencode 'query={app="checkout", level="error"} |= "declined"'
# metrics
curl -G http://127.0.0.1:4319/api/v1/query \
--data-urlencode 'query=rate(http_requests_total[5m])'
# a trace
curl http://127.0.0.1:4319/api/traces/4bf92f3577b34da6a3ce929d0e0e4736Point all three of laravel-telemetry-ui's connectors — Loki, Tempo and
Prometheus — at that one base URL.
Full documentation is in docs/ — start with the quickstart.
| Getting started | Installing, sending data, testing |
| Core concepts | Architecture, signals, storage, performance |
| Cookbook | Running as a service, exposing it safely, sizing the budget |
| Configuration | Every option, with defaults |
| Extension points | What is adjustable, and what is not |
| Security | Threat model and honest scope |
The reasoning lives next to the code it explains: why a decision was made, and where the first attempt was wrong, are in the doc comments of the module that carries it. What is built, what is not, and what is deliberately absent is in BUILD-STATUS.md.
Pointing an AI agent at this? llms.txt is the short version, and the installation, sending-data and service pages each carry a copy-paste brief. The part worth reading first is which query constructs are refused — an agent that knows Loki and Prometheus will otherwise write valid expressions this does not run.
API contract: COMPATIBILITY.md — derived from laravel-telemetry-ui's
actual connector source, not from the upstream API references.
A self-hosted tool that can only be read through its own interface has quietly become a place data goes in. Every signal moves in every direction, as OTLP — the format every other backend already accepts.
telemetryd export --since 24h > dump.ndjson # to a file
telemetryd export --to http://new-host:4319 # straight to another instance
telemetryd import --from https://logs.internal # in from somewhere elseBetween two telemetryds, records are read straight from the store rather than
re-derived from a query language, so all three signals come across as stored. From a
foreign backend it goes through the read APIs each speaks — logs, traces, and metrics
through Prometheus remote read, which is the only one of the three that returns stored
samples rather than points on a step grid.
--progress writes to stderr while the data goes to stdout, so export | gzip keeps
its meter. See the transfer guide.
telemetryd refuses to start on a non-loopback address with no token configured —
including 0.0.0.0, which is the bind that actually exposes people. Telemetry
routinely contains emails, tokens and stack traces, so this fails closed. The error
tells you the three ways to fix it and generates a token to paste.
[auth]
ingest_token = "file:/run/secrets/ingest" # guards /v1/*, /api/v1/write
query_token = ["old-token", "new-token"] # guards the read APIs; a list rotates
admin_token = "env:TELEMETRYD_ADMIN" # guards /status and /metricsThree independent tokens, because app servers push, humans read, and dashboards
scrape — different credentials, different rotation. They are not a hierarchy: an admin
token does not grant reads. Comparison is constant-time over SHA-256, so token length
does not leak. Token values never reach a log line, /status, or telemetryd validate
output; that is enforced by the type, not by discipline.
Point it at Cbox ID and it will accept access tokens too, validated against the issuer's published keys rather than by calling the provider — so an identity provider that is down never stops you reading the logs that would explain why:
[auth.oidc]
issuer = "https://acme.cboxid.com"Scopes map to the same three roles. See the single sign-on guide.
Deliberately out of scope in v1, and stated rather than silently absent:
- Automatic certificate issuance. telemetryd can terminate TLS — set
server.tls.cert_fileandkey_file— but it does not obtain or renew certificates. Bring one from your CA, from certbot, or from whatever issues them where you run. A proxy in front remains a fine answer at a public edge, where one usually exists already; the built-in path is for internal deployments that have nowhere to put one. - Per-app tokens. The
applabel is a namespace, not a security boundary. - mTLS, user accounts. Out of frame for a single-team tool.
The write-ahead log defaults to wal_sync = "interval" at 100 ms. On hard power
loss you can lose up to 100 ms of telemetry. That is a deliberate default — per-write
fsync would cap ingest at the device's sync rate — but it is your call:
[storage]
wal_sync = "always" # fsync every batchA crash that tears the log tail is detected at startup, repaired, and reported three
ways: a WARN log, wal_truncations in /status, and
telemetryd_wal_truncations_total. Lost records are never silent.
The same principle covers limits generally. When a cap is hit — cardinality, body size, queue depth — telemetryd rejects loudly with a labelled counter and a structured error, rather than dropping data quietly.
| Endpoint | Auth | Purpose |
|---|---|---|
/ |
always open | What this server is and which routes it answers — HTML for a browser, an identity document for Accept: application/json, plain text otherwise. Names the product, never the deployment. |
/healthz |
always open | Liveness. Touches nothing, so it cannot fail for the wrong reason. |
/status |
admin token | Disk usage vs budget, WAL stats, recovery events, limits, retention. Falls back to the query token when no admin token is set. Without a token it answers the identity document instead of refusing, so a client that only knows this path can still tell what it reached. Both are 200; the absence of storage is how you tell them apart. |
/metrics |
admin token | Prometheus exposition of telemetryd's own metrics. |
cargo fmt --all --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps # CI runs this; a doc link
# to a private item fails the build
cargo deny check # advisories, licenses, bans, sources
python3 scripts/generate-sbom.py --check # SBOM is not stale
python3 scripts/check-docs.py # docs structure, frontmatter, links
cargo bench -p telemetryd-store # measures the constants
cargo test -p telemetryd-store --test scale --release # asserts the asymptoticsPerformance is held in place from two directions: benchmarks measure the constants,
and tests/scale.rs asserts the asymptotics as segment-open counts rather than
wall-clock times — so they mean something on a shared CI runner. See
CI additionally cross-compiles all four release targets
({x86_64,aarch64}-unknown-linux-musl, {x86_64,aarch64}-apple-darwin) and asserts
the musl builds are statically linked — "no glibc surprises" is a constraint, not an
aspiration.
MIT