Repository navigation
Logs, traces and errors on your own server, without running a stack #1240
thegdsks
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
A customer tells you checkout was slow last night. You open a terminal and find that the only record is
docker logson a container that was replaced by the next deploy.The usual fix is a shopping list. Grafana for dashboards, Loki for logs plus a shipper to feed it, Prometheus for metrics, Tempo or Jaeger for traces, Sentry for errors. Each has its own storage, retention and upgrade path. Self-hosting Sentry alone is a known commitment: one maintainer's write-up describes a hardware floor of 16GB of RAM and 4 cores, and "a ton of docker containers". On a small VPS the monitoring can outweigh the app.
Levelrail is a self-hosted deployment platform that collects logs, metrics, request stats, traces and error reports itself and stores them on the node where the app runs. Nothing is shipped to a central store and nothing is indexed until you search. If you want Grafana, it connects directly. If you want Datadog or Axiom, you add an export.
Below is each piece, with the commands to turn it on.
The data flow
Everything is collected where the container runs, and the control plane answers queries by asking the node. No ingestion pipeline sits burning CPU while nobody is looking.
graph LR C["App containers<br/>stdout, stderr, Docker stats"] --> N["Node-local stores<br/>logs, metrics, traces, errors"] E["Edge (embedded Caddy)<br/>request stats"] --> N O["Your OTel SDK"] -->|"OTLP/HTTP"| R["OTLP receiver"] S["Your Sentry SDK"] -->|"DSN"| I["Sentry-compatible ingest"] R --> N I --> N N -->|"federated query"| CP["Control plane"] CP --> D["Dashboard<br/>search, tail, charts, waterfalls"] CP -->|"Loki-compatible API"| G["Your Grafana"] CP -.->|"opt-in export"| X["Datadog, New Relic,<br/>Axiom, Better Stack,<br/>Loki, syslog, S3"] CP -.->|"scrape /metrics or remote_write"| P["Prometheus, Mimir,<br/>Grafana Cloud"]Solid lines are always on. Dotted lines only exist after you add a destination.
On by default, and what you switch on
Container logs (search, live tail, download), container metrics every 15 seconds, edge request stats with p50/p95/p99 and a per-route breakdown, and deploy markers on every chart need no setup. Traces, errors, Grafana access and exports are per-app or per-destination switches, so nothing stores more data or opens a door until you say so.
1. Logs
Levelrail reads each container's Docker log stream, compresses it and indexes it for full-text search. The dashboard has search with level chips, structured field filters and a live tail. The CLI reads the same data:
levelrail-cli apps logs web --since 24h --q timeout levelrail-cli apps logs web --follow levelrail-cli logs search web --level error --field 'status>=500' --since 1hAfter a failed deploy or a crashloop, the last 200 lines of the failing container are one command away:
Saved queries
Name the search you run every morning. Saved queries can be private or shared with everyone who can see the app, and pinned ones sit at the top of the app's menu.
Lines that differ only in numbers, ids or IPs are grouped into error signatures, and
levelrail-cli logs signatures web --since 24h --newshows only the ones that did not exist before the latest deploy.Pipelines: clean logs before they are stored
A pipeline is up to 20 rules, applied in order to every line before storage, live tail or export. The kinds are
multiline(join a stack trace into one record),redact,drop, andparse(named regex groups become queryable fields). Patterns are Go RE2, so there is no lookahead or backreference.Save this as
rules.json:rules.json, all four rule kinds
{ "rules": [ { "kind": "multiline", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}" }, { "kind": "drop", "pattern": "GET /healthz HTTP" }, { "kind": "redact", "pattern": "password=[\\S]+", "replace": "[REDACTED]" }, { "kind": "parse", "pattern": "^(?P<level>[A-Z]+) (?P<timestamp>[^ ]+) (?P<msg>.+)" } ] }Dry-run it against real lines first, then apply it:
levelrail-cli apps log-pipeline preview web --file rules.json --input lines.txt levelrail-cli apps log-pipeline set web --file rules.jsonChanges take effect within about 15 seconds and apply to new lines. Redaction covers the stored, searched, downloaded and exported copy.
Volume budgets
Set a cap per app and a chatty service trims only its own oldest lines when it goes over. Other apps keep their history.
levelrail-cli apps log-budget set web --max-mb 512 levelrail-cli logs usagelogs usagelists apps noisiest first with stored size, ingest rate, percent of budget used and a forecast of days until the cap is reached.APP_LOG_BUDGET_MBsets a fleet-wide default.Retention is 15 days by default (
APP_LOGS_RETENTION). To keep logs longer, log archive ships them as compressed NDJSON to a storage destination on a schedule.2. Metrics, request stats and deploy markers
Every app gets seven container metrics at 15 second resolution: CPU, memory use and limit, network in and out, disk read and write. Raw samples roll up into 1 minute and 1 hour buckets, so a month-long chart stays cheap to draw.
Request metrics usually need an extra tool. Here the embedded Caddy ingress counts requests per route, so you get rate, error share and latency percentiles per app without touching the app:
Request paths are reduced to route templates to keep cardinality bounded, and query strings and client addresses are never recorded.
Deploy markers sit on every metrics chart, which turns "why did latency double at 3am" into reading a chart: the line steps up right where a marker is. Click a marker for the status, image and commit. Click any point on a chart and the investigation panel lists what changed around that moment, including deploys, config changes, container restarts and resource saturation.
The same view from the CLI, centered on 03:10 with a 10 minute window:
3. Grafana as a Loki data source
Keep Grafana if you like it. The control plane serves a read-only subset of the Loki HTTP API over the node-local log store, so Grafana Explore searches your app logs with no shipper and no second copy of the data.
readability (Settings, API tokens)./api/v1/loki, for examplehttps://levelrail.example.com/api/v1/loki.Authorizationwith the valueBearer <token>.Apps the token cannot read are left out of results and out of label values, so a token scoped to one project only shows that project.
What LogQL is supported
=,!=,=~,!~app,streamandlevelonlyappmatcher=or=~|=,!=,|~,!~APP_LOKI_MAX_RANGE)limitMore examples that work:
Anything outside that subset (metric queries like
rateandcount_over_time, parser stages like| json, other labels) gets an HTTP 400 with a Loki-style error body instead of a guessed answer. For counts and trends, use the log explorer in the dashboard, or point Grafana at the Prometheus scrape endpoint covered in the exports section.4. Traces
Tracing is off until you enable it for an app, and nothing is stored before that. Your app sends spans over OTLP/HTTP to the control plane, and the dashboard shows each trace as a waterfall beside the app's logs.
levelrail-cli apps tracing enable webYou get an ingest key (shown once) and three environment variables:
Set them on the app and redeploy. The endpoint takes OTLP over HTTP as
http/protobuforhttp/json. If your SDK defaults to gRPC, setOTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf.Because the standard OpenTelemetry environment variables carry the endpoint and key, the code stays small. Node:
Python:
Both exporters read the endpoint and header from those variables. If your containers reach the control plane under a different hostname than the one printed, replace the host in
OTEL_EXPORTER_OTLP_ENDPOINT.Read the results where you prefer:
Spans live on the control plane node and age out with the logs retention window. Sampling happens in your SDK, so set a sampler if volume is high. Each key belongs to one app and can only write spans.
Every request through the edge also gets an
X-Request-Idand a W3Ctraceparentheader, andlevelrail-cli apps access web --slow-ms 500lists slow requests with their trace ids.5. Errors with the Sentry SDK
If you have used Sentry, the change is one DSN. Levelrail speaks the Sentry ingest protocol, groups events into issues and ties each issue to the deploy that was current when it arrived.
levelrail-cli apps error-tracking enable webThe DSN is shown once, and running
enableagain rotates it (the old one gets 401s):Set it on the app, redeploy, and keep your SDK code. JavaScript:
Python:
Events group by exception type plus the innermost in-app stack frame (line numbers ignored), and a resolved issue that gets a new event reopens as a regression.
Browser SDKs work too. Values under keys such as
authorization,cookie,passwordandtokenare scrubbed before storage, and each app can send 600 requests a minute before the receiver answers 429 withRetry-After.Set
releaseto your commit SHA, and "which deploy broke this" becomes a column instead of a hunt.6. Exports and alerts
Data export copies logs and metrics to the tools you already pay for, from Settings, Data export or the CLI. Nothing leaves the node until you add a destination, and a destination that is down never slows a container, a deploy or the API: each one has a bounded queue, retries with backoff, and a circuit breaker.
logs_datadog,logs_newrelic,logs_axiom,logs_betterstacklogs_lokilogs_otlp,metrics_otlplogs_http,logs_syslogmetrics_remote_writeGET /metricswith a read tokenLogs to Axiom:
And metrics to Grafana Cloud over remote write:
GRAFANA_TOKEN=glc_xxx levelrail-cli export destinations create --name grafana --kind metrics_remote_write \ --endpoint https://prometheus-prod-01.grafana.net/api/prom/push \ --username 123456 --api-key-env GRAFANA_TOKENUnsent log lines are spooled to disk, so a control plane restart does not lose what a down receiver has not accepted. Delivery is at least once.
Alerts
Rules evaluate every 30 seconds and notify only when state changes (firing or resolved), so a long incident does not turn into a stream of repeats. Channels include email, Slack, Discord, Telegram and webhooks. Connect a channel, then create a rule:
Log-based rules come from a search, so an alert can be "ten checkout timeouts in five minutes" instead of a metric you had to invent:
Attach a channel when you create the rule. One without a channel is saved but notifies nobody, and the CLI warns you when that happens.
Putting it together
A first hour for an app called
web, with Levelrail running and the app deployed:levelrail-cli apps log-budget set web --max-mb 512.levelrail-cli apps error-tracking enable web, setSENTRY_DSN, redeploy.levelrail-cli apps tracing enable web, set the three OTEL variables, redeploy together with the DSN.{app="web"}in Explore.After that a slow request leaves a trail. Edge stats show which route got slow, the deploy marker shows when, the trace shows which span, log search shows what the app said at the time, and the error page shows the exception and the release that introduced it. All of it sits on the same box as the app.
Troubleshooting
readability and a URL ending in/api/v1/loki.appmatcher.levelrail-cli apps tracing enable <app>and set the new header.OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf.levelrail-cli apps error-tracking enable <app>.APP_EXPORT_ALLOW_PRIVATE_NETWORKS=trueon the control plane.Links
Try it and tell us what breaks
If you run a few apps on a box you own and the observability stack has looked like a second job, install Levelrail, deploy one app and see how much you get before touching a setting.
Then tell us what is missing. Which Grafana queries do you need beyond the LogQL subset? Which export destination should come next? Did the trace or error setup trip you up anywhere? Reply here with what you tried. A star on the repo helps other self-hosters find it, and bug reports help more.
All reactions