Connecting server logs
Upliftr is AI debugging agents that work across your flow and your servers, and this is their deepest feature and hardest-to-copy seam: correlate a UI failure to the backend error that caused it. They read your logs with read-only access, they never mutate your code, data, or infrastructure. When an agent reproduces a bug in a real browser and the failing request carries a trace_id, and you've connected a log backend, Upliftr fetches the matching server log lines and Claude root-causes the failure to the exact backend line: the backend error, the suspected cause, and a suggested fix, attached to the run report and the filed issue.
The mechanics of the
trace_id→ logs → diagnosis chain are in Self-heal & root-cause.
How correlation works
- During a case, the Playwright driver records every failed network response, including the
traceparentheader, and extracts atrace_id. - When an assertion fails, Upliftr calls the configured log backend with that
trace_id, querying a recent time window (default ~3 minutes back, up to 200 lines). - Claude reads the matched log lines plus the UI failure and the failing request, and returns a structured diagnosis:
category,server_error,suspected_cause,suggested_fix,confidence, and up to five evidence lines. - That diagnosis is merged into the issue and report.
Your app needs to do one thing for this to work: propagate a trace id (the standard traceparent header is ideal) and log it server-side, so a UI request and its backend log line share an id. The whole chain is best-effort, if there's no trace_id or no backend configured, you still get the in-browser root cause; the server enrichment is purely additive.
Zero-instrumentation: the drop-in trace snippet
If your app doesn't already emit traceparent (most SPAs don't), you don't need a full OpenTelemetry setup. Paste a tiny, dependency-free script into your app — staging is enough — and every same-origin fetch/XHR gains a W3C traceparent:
<script src="https://YOUR-UPLIFTR/api/v1/tracing/snippet.js"></script>Or copy the script inline (served at that URL). It's same-origin only (never leaks a trace header cross-origin), adds no dependencies, and sends no data anywhere. Then make sure your backend logs the incoming traceparent (most frameworks can log request headers), so the UI request and its server log line share the id. That's the whole setup — it turns the backend root-cause from "works if you already run tracing" into "works out of the box".
Trace ids may be logged in different forms (W3C hex, a dashed UUID, or Datadog's decimal APM id). Upliftr queries all common representations automatically, so a format mismatch won't silently miss the line. If your log shipper batches, set
LOG_REQUERY_DELAYS="5,15"to re-query a couple of times before giving up.
Connect one source, or many
For a single system, set LOG_BACKEND to one of loki, datadog, elasticsearch, gcp, http, sentry, command, or file, plus its config. Real environments rarely log to one place, though, so you can also connect many sources at once (see Multiple log sources) and the agents query them all, correlating by trace_id.
Like the issue trackers, use a dedicated, read-only Upliftr token for each log source (e.g. a scoped Datadog, Elasticsearch, or Sentry token), set once by an admin, not an engineer's personal key.
Loki
LOG_BACKEND=loki
LOKI_URL=https://loki.example.com
LOKI_SELECTOR={job=~".+"} # optional; default {job=~".+"}Queries GET {LOKI_URL}/loki/api/v1/query_range with <selector> |= "<trace_id>" over the window.
Datadog
LOG_BACKEND=datadog
DATADOG_API_KEY=...
DATADOG_APP_KEY=...
DATADOG_SITE=datadoghq.com # optional; e.g. datadoghq.euPosts to https://api.<site>/api/v2/logs/events/search filtered by the trace_id.
Elasticsearch
LOG_BACKEND=elasticsearch
ELASTIC_URL=https://es.example.com
ELASTIC_API_KEY=...
ELASTIC_INDEX=logs-* # optional; default logs-*Posts a query_string search for "<trace_id>" against {ELASTIC_URL}/{index}/_search, reading message / log from each hit.
Sentry
LOG_BACKEND=sentry
# in LOG_SOURCES (below): {"type":"sentry","org":"your-org","token":"...","url":"https://sentry.io"}Queries Sentry's events API (GET {url}/api/0/organizations/{org}/events/?query=trace:<id>) with a Bearer token, reading the title / message of each event. Works with sentry.io and self-hosted Sentry (set url). For multiple Sentry projects, add one source per project in LOG_SOURCES, each with its own token.
Custom HTTP endpoint
Expose your own endpoint that accepts ?trace_id= and returns log lines, the simplest way to connect a homegrown logging system.
LOG_BACKEND=http
LOG_HTTP_URL=https://logs.example.com/search
LOG_HTTP_TOKEN=... # optional; sent as Bearer authThe response is normalized flexibly: a JSON array of strings, an object with a logs / lines / results / data array, or plain text (split by line) all work.
File
Point at a log file or JSONL dump, perfect for local testing and the bundled demo.
LOG_BACKEND=file
LOG_FILE_PATH=/var/log/app/server.logUpliftr scans the file for lines containing the trace_id. The sample app uses exactly this:
LOG_BACKEND=file
LOG_FILE_PATH=examples/sample-app/server.logGCP Cloud Logging
Where a containerized service's stdout lands on GKE / Cloud Run. Upliftr queries the Cloud Logging API for entries matching the request's trace (across the trace field, jsonPayload.trace_id, and free text, ORing the trace-id formats).
LOG_BACKEND=gcp
GCP_PROJECT=my-project
GCP_TOKEN=ya29... # a short-lived OAuth access token (e.g. `gcloud auth print-access-token`)Cloud Logging uses OAuth, not a static API key — supply a fresh access token (or mint one from a service-account JSON upstream and pass it as GCP_TOKEN).
Command / tail (stdout, docker logs, journald)
For a service that logs to stdout with no queryable aggregator — a bare process, a container, a systemd unit. Upliftr runs a command that emits recent logs and greps it for the trace (matching the trace-id formats, like the file backend).
LOG_BACKEND=command
LOG_COMMAND=docker logs --tail 5000 my-api # or: journalctl -u my-api --no-pager -n 5000
# or: tail -n 5000 /var/log/app.logSelf-hosted only
This shells out on the host, so it's refused in cloud / multi-tenant mode (UPLIFTR_RUN_EGRESS=restricted). Use it only on a self-hosted instance you control.
Multiple log sources
Most real systems spread logs across several places, one service in Loki, another in Sentry, a gateway behind a custom endpoint. The root cause often lives in a different service's logs than where the symptom shows up, so connect them all. Set LOG_SOURCES to a JSON array; each entry is { "type": ..., "label": ..., ...config } with the same keys as the single-backend forms (url, token, selector, api_key, org, path, …).
[
{ "type": "sentry", "label": "api", "org": "acme", "token": "..." },
{ "type": "loki", "label": "gateway", "url": "https://loki.internal", "selector": "{app=\"gateway\"}" },
{ "type": "http", "label": "storefront", "url": "https://shop.internal/_logs", "token": "..." },
{ "type": "datadog", "label": "billing", "api_key": "...", "app_key": "..." }
]The agents query every source for the trace_id, and each returned line is tagged with its [label], so a single failure is correlated across services that log to different systems, with different URLs and tokens. A source that's unreachable is skipped, it can't sink the others. When LOG_SOURCES is set it takes precedence over the single LOG_BACKEND above. (In the dashboard: Settings → Server logs → Multiple log sources.)
See it end-to-end
cd examples/sample-app && uvicorn app:app --port 8500 # the buggy demo app
backend/.venv/bin/python demo/break_demo_app.py # find the bug AND the backend reasonThe German checkout silently shows USD instead of EUR (HTTP 200, nothing throws in the browser), and POST /api/checkout?cc=DE returns a 500 logging [ERROR] trace=<id> GeoLookupError …. The agent's absent_text: ["USD"] check fails the run, resolves the failing request's trace_id to that exact server.log line, and Claude writes up the backend error + suspected cause + suggested fix, filed as a deduped ticket.
What you get in the issue
When correlation succeeds, the filed issue's Root cause section gains a Server diagnosis block:
Server diagnosis (backend_exception, confidence 0.82)
- Error: GeoLookupError: no region for country code 'DE'
- Suspected cause: the pricing service maps country → currency via a table missing 'DE'
- Suggested fix: add 'DE' → 'EUR' to the currency map, or fall back to EUR for EU codes
- Log evidence:
[ERROR] trace=… GeoLookupError: no region for country code 'DE'See Filing issues for the full issue anatomy, and Settings & env for every variable in one place.