Skip to main content

HTTP ingest

Push error events over HTTP instead of writing a log file — for apps, background workers, or anything else that would rather report an error the instant it happens than wait for a log file to be parsed. Off by default; enable per application in Settings → Plugins → HTTP ingest.

This is the transport underneath the capture SDKs (Laravel, generic PHP, browser) — those are convenience wrappers; this page covers what actually receives and processes the event.

It's the same pipeline as file-based logs, just a different entry point

A pushed event runs through the exact same normalization, grouping, occurrence-recording, timeline, and alerting pipeline as a parsed log line. It arrives tagged log_type=http, origin=ingested — from the issue list's point of view it's indistinguishable from something Log Lens found in a file, and enabled alert rules fire on it identically.

Authentication is independent of the dashboard

?api=ingest doesn't use the dashboard's API token — it has its own per-application ingest key, a write-only push token (get/rotate it from Settings → Plugins → HTTP ingest). Present it as:

  • header X-Log-Lens-Ingest-Key: <key>, or
  • Authorization: Bearer <key>, or
  • &key=<key> in the query string.

Because this key authenticates itself, ?api=ingest is one of only two routes in the whole API exempt from the dashboard's token/same-origin guard (the other is the Linear webhook) — a browser SDK or a headless worker with no dashboard credentials can still push events. A missing or wrong key returns 401. Rotating the key immediately invalidates the old one — any client still using it starts getting 401s.

What one request looks like

curl -X POST "$LOG_LENS_URL/?api=ingest&app=my-app" \
-H "X-Log-Lens-Ingest-Key: llk_…" -H "Content-Type: application/json" \
-d '{"events":[{"message":"Payment capture failed","severity":"ERROR","exception_class":"RuntimeException","module":"billing","context":{"order":123}}]}'

Send one event object, an array of events, or {"events":[…]} — up to 500 per request. Only message is required; everything else (severity, exception_class, stack, context, environment, channel, module, occurred_at, release, fingerprint, tags, request, user, breadcrumbs, server_name, runtime) enriches the resulting issue. Identical events group into one issue with multiple occurrences, exactly like repeated log lines — a custom fingerprint overrides that if you want to control grouping explicitly. The whole batch runs in one write transaction, so occurrence byte-offsets never race under concurrent requests.

Response is 202 with {accepted, received, dropped}.

Rate limiting

Each application accepts a capped number of events per rolling window — default 1000 events / 60 seconds (ingest.rate_max_events / ingest.rate_window_seconds). This is a sliding-window counter (the previous window's count is weighted by how much of it still overlaps "now"), not a naive fixed window, so you don't get a ~2× burst right at a window boundary.

Two distinct outcomes when you're over budget:

  • Partial: if a batch exceeds the remaining budget for this window, the excess events are silently dropped — you still get 202, with dropped telling you how many didn't make it. This protects the database from one runaway client without hard-failing a batch that's mostly fine.
  • Fully exhausted: if the window has no budget left at all, the whole request is rejected with 429 and a retry_after (seconds until the window rolls).

There's an optional secondary per-IP limit (off by default, ingest.rate_max_events_per_ip) for the case where you're exposing the browser SDK's key publicly and want to bound any single visitor's impact independent of the application-wide budget.

The current effective limit is always shown in Settings → Plugins → HTTP ingest, since it depends on both settings together.

  • Capture SDKs — the Laravel reporter, generic PHP client, and browser SDK all send through this endpoint.
  • Release tracking — the release field on a pushed event is how occurrences.release ever gets populated.
  • Alerting — runs over pushed events exactly as it does over parsed ones.

Inside a Laravel app

The Laravel adapter mounts the receiver on its own route, POST {prefix}/ingest (e.g. https://app.test/log-lens/ingest?app=my-app), outside the host's access gate and outside the web middleware group. Both would reject every legitimate caller: an SDK in a browser page or a headless worker has no session and no CSRF token. The ingest key is still required, so the route is not open — a missing or wrong key is a 401.

Settings → Plugins → HTTP ingest shows this URL automatically; use whatever it displays rather than assembling one by hand. To put your own throttling or IP allowlist in front of it, set log-lens.receiver-middleware.