skeylup / owlogs-agent
Owlogs agent — ship Laravel logs to an Owlogs server for aggregation and viewing.
Requires
- php: ^8.1
- illuminate/console: ^8.65|^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^8.65|^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/log: ^8.65|^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/queue: ^8.65|^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^8.65|^9.0|^10.0|^11.0|^12.0|^13.0
- monolog/monolog: ^2.0|^3.0
Requires (Dev)
- laravel/pint: ^1.14
- livewire/livewire: ^3.0|^4.0
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0
- pestphp/pest: ^2.0|^3.0|^4.0
- pestphp/pest-plugin-laravel: ^2.0|^3.0|^4.0
README
Ship Laravel logs — with rich context, tracing IDs, caller location, queue job metadata, and sanitized request body — asynchronously to Owlogs.
Drop-in, zero-config, Octane-safe. Works with Laravel 8.65 through 13.
Requirements
- PHP 8.1+ and Laravel 8.65+.
- Full feature parity from Laravel 11. On Laravel 8/9/10, cross-job context propagation (trace_id / breadcrumbs inheriting from the dispatching request) falls back to a per-process polyfill — same-request and single-CLI correlation still work, only the HTTP-request → queued-job hand-off is best-effort.
- A queue driver (redis, database, sqs, etc.) — required: the ship job runs on the queue, no HTTP call blocks a request.
- A persistent cache store (redis, file, database, memcached) — the debounce marker uses
Cache::add; thearraydriver prevents de-duplication across requests. - Redis or a writable filesystem — for the cross-process log buffer store (see How it works).
- An Owlogs account (free sign-up at owlogs.com) to get a workspace API key.
Tested Laravel × PHP matrix
| Laravel | PHP | Monolog |
|---|---|---|
| 8.65+ | 8.1 | 2.x |
| 9.x | 8.1 | 2.x |
| 10.x | 8.1+ | 3.x |
| 11.x | 8.2+ | 3.x |
| 12.x | 8.2+ | 3.x |
| 13.x | 8.3+ | 3.x |
EOL notice — Laravel 8 and 9 are upstream EOL (no more security patches). The agent runs on them today; we cannot promise to chase Laravel regressions there.
Features
- Automatic context enrichment on every log entry:
trace_id,span_id,origin,user_id,app_name,app_env,app_url,git_sha,uri,route_name,route_action,ip,user_agent,duration_ms, etc. - Distributed tracing: the same
trace_idflows from an HTTP request into every queue job it dispatches, so you can reconstruct the full lifetime of a request. - Caller location: each log entry carries the
file:lineandClass@methodwhereLog::*()was actually called — not the framework frame. - Queue job metadata: job class, attempt, queue, connection, plus the public (scalar) properties of the job payload.
- Artisan command metadata: command name and arguments for CLI origin logs.
- Sanitized request input: POST / PUT / PATCH bodies are captured with
password,secret,token,authorization,cookie,credit_cardvalues redacted. - Exception stacktraces including up to 3 levels of chained exceptions.
- Performance spans via the
Measurehelper and optional automatic DB query tracking with N+1 detection. - Opt-in lifecycle auto-logging for route matched, auth events, job lifecycle, mail, cache, slow queries, scheduled tasks, model changes, DB transactions, Livewire calls, GraphQL operations, and more. Replaces the old breadcrumb pattern: each captured event becomes a standalone log line tagged with the shared
trace_id— the timeline correlates the same way without the per-row payload overhead. - Async delivery via a debounced queue job: N flushes (HTTP request + queue jobs it dispatches) in the same window collapse to a single
ShipBufferedLogsJob— no cascade, no blocking. - Runtime-aware buffering: non-Octane accumulates in RAM and flushes once per request/job/command boundary; Octane batches across requests with a 2 s / 20-log window so workers ship fewer, bigger payloads.
- Octane-safe: no container / request injection into singletons, all state is reset between requests.
Quickstart
composer require skeylup/owlogs-agent php artisan owlogs:install --key=owl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
That's the recommended path. The installer validates the key against the ingest endpoint (a bad key aborts before anything is written), writes the OWLOGS_* block to .env (diff-aware — existing lines are updated in place, package defaults are never added), auto-detects the buffer store (redis when reachable, file otherwise) and finishes by running php artisan owlogs:doctor. Useful flags: --ingest-url=, --buffer-store=redis|file, --queue=, --env-file=, --no-validate, --emit-test-logs, --skip-doctor. Grab the key from your workspace's API keys page on owlogs.com.
Manual alternative — add the key to .env yourself:
OWLOGS_API_KEY=owl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
That's it. With LOG_CHANNEL=stack (Laravel's default), any Log::info(...), Log::error(...), etc. is now shipped to Owlogs asynchronously via a queue job — the agent registers the owlogs channel on boot and appends it to your stack channel for you. No edits to config/logging.php or LOG_STACK required.
Prefer remote-only shipping? Set LOG_CHANNEL=owlogs.
If OWLOGS_API_KEY is empty, the queue job is still dispatched but returns silently without hitting the network — handy for local development.
If the project uses Laravel Boost, run php artisan boost:update to pick up the bundled AI instrumentation skill — your AI assistant will then know how to add workflow-level logging across the app automatically.
Customising the config (optional)
Only publish the config file when you actually want to tweak something beyond env vars:
php artisan vendor:publish --tag=owlogs-agent-config
Opt out of auto-registration
If you'd rather wire things manually (custom stack logic, explicit LOG_STACK, or an alternate channel definition), set:
OWLOGS_AUTO_REGISTER_STACK=false
Then declare the channel yourself in config/logging.php:
'owlogs' => [ 'driver' => 'custom', 'via' => \Skeylup\OwlogsAgent\Handlers\RemoteLogChannel::class, 'level' => env('LOG_LEVEL', 'debug'), 'tap' => [\Skeylup\OwlogsAgent\LogContextTap::class], ],
…and add owlogs to your LOG_STACK (e.g. LOG_STACK=single,owlogs).
Upgrading from an earlier version
Earlier versions required you to declare the owlogs channel in config/logging.php and list it in LOG_STACK by hand. Both are now done at boot. After upgrading you can safely:
- Remove the manual
'owlogs' => [...]entry fromconfig/logging.php. - Drop
owlogsfromLOG_STACKin.env(e.g.LOG_STACK=single).
Either is also fine to keep — a pre-existing channel definition is never overwritten, and the stack injection skips owlogs if it's already present.
How it works
┌──────────────┐ ┌───────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌───────────────────────┐
│ Log::info() │ → │ LogContext │ → │ RemoteHandler │ → │ LogBufferStore │ → │ ShipBufferedLogsJob │
│ │ │ Tap │ │ (RAM, policy) │ │ (Redis / file) │ │ (queued, debounced) │
└──────────────┘ └───────────────┘ └─────────────────┘ └─────────────────┘ └──────────┬────────────┘
▲ ▲ │
│ │ ▼
flush on Cache::add POST /api/owlogs/ingest
boundary debounce X-Api-Key: owl_…
(or Octane (once per
window) 10 s window)
The pipeline has two independent stages — a per-process RAM buffer and a cross-process persistent buffer — both with their own bounded-size / bounded-time guarantees. Together they collapse what used to be one queue job per flush into one ship job per debounce window.
- Context enrichment. Global middleware
AddLogContextpopulates Laravel'sContextfacade on every HTTP request with tracing, routing, user and timing fields. The same context flows into queue jobs (Context::hydrated) and artisan commands (CommandStarting). - Caller trace + formatter.
LogContextTapattaches a Monolog processor that resolves the realfile:line/Class@methodof the caller (skipping framework frames) and sets a JSON formatter. - RAM buffer, runtime-aware.
RemoteHandleraccumulates records in memory. When to drain RAM → store is chosen by the activeFlushPolicy:EndOfRequestPolicy(non-Octane: PHP-FPM / Herd, artisan one-shot,queue:work): no mid-request flush. Drain onapp()->terminating(),Queue::after,CommandFinished, plus aregister_shutdown_functionsafety net.OctaneWindowPolicy(Swoole / RoadRunner / FrankenPHP): drain when the RAM buffer reachesoctane.batch_count(20) oroctane.window_ms(2 000) have elapsed since the first buffered record. Under Swoole,Octane::tickenforces the window even during idle periods; RR / FrankenPHP check on eachRequestTerminated+WorkerStopping.- Runtime is detected from
$_SERVER['LARAVEL_OCTANE'].OWLOGS_FLUSH_STRATEGY=octane|end_of_requestforces one or the other (tests / edge cases). - An unconditional hard ceiling at
2 × batch_size/2 × max_payload_bytesforces a flush no matter the policy — protects against a runaway caller looping onLog::*.
- Cross-process store. On flush, rows are appended to a
LogBufferStore:redis(default) — single Redis list, atomic Lua-scriptedLRANGE+LTRIMdrain. Multiple concurrent processes append safely; a drain always returns distinct rows. RequiresOWLOGS_BUFFER_REDIS_CONNECTION(defaultdefault).file— JSONL underOWLOGS_BUFFER_FILE_PATH(defaultstorage/app/owlogs/buffer.jsonl) with advisoryflock(LOCK_EX). Zero dependencies beyond a writable filesystem.memory— in-process, testing only.
- Debounced dispatch. After appending,
RemoteHandler::flush()tries to dispatch oneShipBufferedLogsJobguarded byCache::add('owlogs:ship:pending', ttl). Any flush fired within the debounce window (ship.debounce_ms, default 10 000) sees the marker and appends without dispatching — so an HTTP request that fans out to 15 queue jobs still queues a single ship job. - Ship job.
ShipBufferedLogsJob::handle()releases the marker, drains up toship.batch_count(256) rows from the store, splits the payload bymax_payload_bytes(each split becomes one HTTPS POST, gzipped, tohttps://www.owlogs.com/api/owlogs/ingest), and self-re-dispatches (no delay) whilestore.size() > 0.RemoteHandler::$suppressedis heldtruefor the duration so nothing the job does can feed back into the buffer. - Retries. Each POST retries 3 times with backoff
[5, 30, 120]seconds on 5xx / network errors. 4xx client errors (bad key, invalid payload),403(no active subscription) and429(quota exhausted) abandon without retry. The rows already drained from the store are serialized into the job payload, so a retry re-ships the same batch (at-least-once, never lost on transient 5xx).
Enriching logs from your models
Implement HasLogContext on any Eloquent model to expose safe, curated metadata in logs — instead of dumping the whole model.
use Skeylup\OwlogsAgent\Contracts\HasLogContext; class User extends Authenticatable implements HasLogContext { public function toLogContext(): array { return [ 'email' => $this->email, 'role' => $this->role, 'plan' => $this->subscription?->plan_name, ]; } public function getLogContextLabel(): string { return trim("{$this->first_name} {$this->last_name}"); } }
When the authenticated user implements this interface, the data is attached to every log entry under extra.user and extra.user_label. When passed explicitly as context (Log::info('...', ['user' => $user])), the same transformation happens — no more accidental serialization of sensitive fields.
Performance measurement
use Skeylup\OwlogsAgent\Measure; // Manual span Measure::start('generate_invoice', ['invoice_id' => 42]); // ... work ... Measure::stop('generate_invoice'); // Closure wrapper $result = Measure::track('stripe.charge', fn () => Stripe::charge($cents)); // Instant checkpoint Measure::checkpoint('cache.hit', ['key' => 'user:42']);
Spans are stored on Laravel's Context and shipped with the last log entry of the flushed batch.
Enable automatic DB query tracking with:
OWLOGS_MEASURE_DB=true OWLOGS_N_PLUS_ONE_THRESHOLD=5
Every query is recorded as a db span, and when the same normalized SQL runs more than N times an n+1 marker is added to the measures array.
Breadcrumbs (deprecated)
The Skeylup\OwlogsAgent\Breadcrumb helper still ships but every method is a no-op — existing call sites keep compiling without throwing. The feature is retired in favour of richer auto-logs (see next section): each framework event becomes a standalone log line tagged with the shared trace_id. Filter by trace_id in the Owlogs UI to get the chronological narrative the old breadcrumb chain used to produce, without the per-record payload duplication and the extra quota cost.
For custom business steps that no framework event captures, emit a regular log line:
Log::info('[checkout.kyc_step_3_passed]', ['user_id' => $user->id]);
Same trace correlation, less ceremony.
Auto-logging lifecycle events
Most lifecycle events are captured automatically out of the box. Flip any switch to false in .env to disable a category.
| Env var | Default | Event |
|---|---|---|
OWLOGS_AUTO_ROUTE_MATCHED |
true |
Router resolves the route — first line of every HTTP trace |
OWLOGS_AUTO_JOB_DISPATCHED |
true |
A job is queued |
OWLOGS_AUTO_JOB_STARTED |
true |
A worker picks up a job |
OWLOGS_AUTO_JOB_COMPLETED |
true |
A job completes successfully |
OWLOGS_AUTO_JOB_FAILED |
true |
A job fails (exception + attempt) |
OWLOGS_AUTO_JOB_RETRYING |
true |
A retry is requested |
OWLOGS_AUTO_AUTH_LOGIN |
true |
User logs in (email, IP, UA) |
OWLOGS_AUTO_AUTH_FAILED |
true |
Failed login attempt |
OWLOGS_AUTO_AUTH_LOGOUT |
true |
User logs out |
OWLOGS_AUTO_AUTH_PASSWORD_RESET |
true |
Password reset completed |
OWLOGS_AUTO_AUTH_VERIFIED |
true |
Email verified |
OWLOGS_AUTO_MAIL_SENT |
true |
Mail sending / sent |
OWLOGS_AUTO_NOTIFICATION_SENT |
true |
Notification dispatched |
OWLOGS_AUTO_NOTIFICATION_FAILED |
true |
Notification failed |
OWLOGS_AUTO_SLOW_QUERY |
true |
Queries slower than the threshold below |
OWLOGS_AUTO_SLOW_QUERY_MS |
500 |
Slow-query threshold in ms |
OWLOGS_AUTO_MIGRATION |
false |
Migration ran (opt-in — noisy on deploys) |
OWLOGS_AUTO_DB_TRANSACTION |
false |
DB transaction committed / rolled back (opt-in — useful to debug silent rollbacks) |
OWLOGS_AUTO_CACHE_HIT / OWLOGS_AUTO_CACHE_MISS |
false |
Cache events (opt-in — very high volume) |
OWLOGS_AUTO_HTTP_CLIENT |
true |
Outgoing HTTP client errors (>= 4xx) |
OWLOGS_AUTO_SCHEDULE |
false |
Scheduled task failed (opt-in) |
OWLOGS_AUTO_LIVEWIRE_CALL |
true |
Livewire Component::method calls as standalone timeline rows (no-op when livewire/livewire is not installed; context enrichment also runs regardless) |
OWLOGS_AUTO_GRAPHQL_OPERATION |
true |
Lighthouse GraphQL operations as standalone timeline rows (no-op when nuwave/lighthouse is not installed; context enrichment also runs regardless) |
OWLOGS_AUTO_MODEL_CHANGES |
true |
Eloquent created / updated / deleted (scoped via model_changes_models) |
OWLOGS_AUTO_EVENT_DISPATCH |
true |
App-level events (excluding framework internals) |
Full configuration reference
All of the following can be overridden in config/owlogs.php after publishing.
Core
| Env var | Default | Description |
|---|---|---|
OWLOGS_ENABLED |
true |
Master kill-switch |
OWLOGS_API_KEY |
— | Workspace API key (sent as X-Api-Key) |
OWLOGS_AUTO_REGISTER_STACK |
true |
Auto-define the owlogs channel and append it to stack on boot |
OWLOGS_JSON |
true |
Use JsonFormatter (vs. LineFormatter) |
Integrations (Livewire / GraphQL)
These rewrite the opaque single-endpoint URIs of Livewire and Lighthouse so the Owlogs UI groups traces by the actual operation. Each is a no-op when its package is absent.
| Env var | Default | Description |
|---|---|---|
OWLOGS_LIVEWIRE_HOOK |
true |
Rewrite /livewire/update as POST /livewire — Component::method (no-op without livewire/livewire) |
OWLOGS_GRAPHQL_HOOK |
true |
Rewrite /graphql as POST /graphql — mutation createReport and stash the operation breakdown under extra.graphql_operations (no-op without nuwave/lighthouse) |
OWLOGS_GRAPHQL_IGNORE_INTROSPECTION |
true |
Skip introspection queries (__schema / __type) — IDE schema fetches, pure noise |
Transport (HTTP POST to Owlogs)
| Env var | Default | Description |
|---|---|---|
OWLOGS_INGEST_URL |
https://www.owlogs.com/api/owlogs/ingest |
Ingest endpoint |
OWLOGS_COMPRESSION |
true |
Gzip the request body before POSTing |
OWLOGS_QUEUE |
default |
Queue name for ShipBufferedLogsJob |
OWLOGS_QUEUE_CONNECTION |
— | Queue connection (null = app default) |
OWLOGS_TIMEOUT |
30 |
HTTP timeout in seconds |
OWLOGS_MAX_PAYLOAD_BYTES |
524288 (512 KB) |
Hard cap used by the ship job's chunker: any batch larger than this is split into multiple POSTs |
Ship debounce (queue → Owlogs)
Multiple flushes within the debounce window collapse to a single ShipBufferedLogsJob. The job drains up to batch_count rows per run, self-re-dispatches while the store still has rows.
| Env var | Default | Description |
|---|---|---|
OWLOGS_SHIP_DEBOUNCE_MS |
10000 |
Delay applied to each ship dispatch; also the effective de-duplication window |
OWLOGS_SHIP_BATCH_COUNT |
256 |
Max rows drained (and shipped) per ship-job run |
Buffer store (cross-process queue of pending rows)
| Env var | Default | Description |
|---|---|---|
OWLOGS_BUFFER_STORE |
redis |
redis, file, or memory |
OWLOGS_BUFFER_REDIS_CONNECTION |
default |
Redis connection name (from config/database.php) |
OWLOGS_BUFFER_REDIS_KEY |
owlogs:buffer |
List key used in Redis |
OWLOGS_BUFFER_FILE_PATH |
storage/app/owlogs/buffer.jsonl |
JSONL file path when buffer_store=file |
RAM flush policy (per-process)
| Env var | Default | Description |
|---|---|---|
OWLOGS_OCTANE_WINDOW_MS |
2000 |
Octane only: flush RAM when this many ms elapsed since the first buffered record |
OWLOGS_OCTANE_BATCH_COUNT |
20 |
Octane only: flush RAM when this many records are buffered |
OWLOGS_FLUSH_STRATEGY |
— | octane, end_of_request, or unset (auto-detect) |
OWLOGS_BATCH_SIZE |
50 |
Hard ceiling: RAM buffer is force-drained when it hits batch_size * 2 rows (runaway-loop protection) |
OWLOGS_MIN_FLUSH_INTERVAL_MS |
500 |
Deprecated. No longer read; kept to avoid breaking older .env files |
Measurement
| Env var | Default | Description |
|---|---|---|
OWLOGS_MEASURE_DB |
false |
Auto-instrument DB queries |
OWLOGS_MEASURE_MEMORY |
true |
Attach peak memory to each batch |
OWLOGS_N_PLUS_ONE_THRESHOLD |
5 |
Identical-SQL count to flag as N+1 |
Individual context fields can be toggled under config('owlogs.fields') if you want to opt out of e.g. ip or user_agent.
Ignoring noisy URIs
config('owlogs.ignored_uris') is a list of path patterns (matched with Str::is, so * is supported) whose logs are dropped before hitting the transport.
Laravel's broadcasting/auth endpoint fires on every websocket handshake and usually has no signal — it is ignored by default. Set OWLOGS_IGNORE_BROADCASTING=false to forward it again.
Add your own after publishing the config:
'ignored_uris' => [ 'broadcasting/auth', 'horizon/*', 'health', ],
| Env var | Default | Description |
|---|---|---|
OWLOGS_IGNORE_BROADCASTING |
true |
Append broadcasting/auth to ignored_uris |
Ignoring noisy events
The event_dispatch auto-log forwards every app-level event to Owlogs as an event.dispatched: <ShortName> entry. Third-party packages that fire an event on every request (e.g. Spatie\LaravelSettings\Events\SettingsLoaded) can drown out the signal.
config('owlogs.ignored_events') is a list of event class names whose dispatched log is dropped before hitting the transport. It uses the same Str::is matcher as ignored_uris, so wildcards (*) are supported and a single entry can silence a whole package namespace at once.
'ignored_events' => [ // Exact FQN \Spatie\LaravelSettings\Events\SettingsLoaded::class, // Whole-namespace wildcard 'Spatie\\LaravelSettings\\Events\\*', 'App\\Events\\Internal\\*', ],
Notes:
- Only the
auto_log.event_dispatchlistener is filtered. ExplicitLog::*('event.dispatched: ...')calls go through unchanged. - Framework / Laravel-package events (
Illuminate\…,Laravel\…,Livewire\…,Filament\…,Stancl\…, etc.) are already filtered internally, no need to list them. - Per-environment exclusions are just plain PHP — e.g.
'ignored_events' => app()->isProduction() ? [...] : [].
Payload format
For transparency, here's what each flush POSTs to Owlogs:
{
"logs": [
{
"trace_id": "01JKXZ4…",
"span_id": "01JKXZ4…",
"origin": "http",
"level_name": "ERROR",
"level": 400,
"channel": "owlogs",
"message": "Payment declined",
"stacktrace": "Stripe\\Exception\\CardException: …",
"caller_file": "app/Http/Controllers/BillingController.php",
"caller_line": 87,
"caller_method": "BillingController@charge",
"uri": "POST https://app.example.com/billing/charge",
"route_name": "billing.charge",
"route_action": "App\\Http\\Controllers\\BillingController@charge",
"ip": "10.0.0.1",
"user_agent": "Mozilla/5.0 …",
"request_input": "{\"amount\":1000,\"currency\":\"EUR\"}",
"user_id": 42,
"app_name": "Example App",
"app_env": "production",
"app_url": "https://app.example.com",
"git_sha": "a1b2c3d4",
"job_class": null,
"job_attempt": null,
"queue_name": null,
"connection_name": null,
"duration_ms": 147,
"context": null,
"breadcrumbs": "[\"CreateOrderAction\",\"ChargeCard\"]",
"job_props": null,
"measures": "[{\"label\":\"stripe.charge\",\"duration_ms\":132.1}]",
"memory_peak_mb": 38,
"extra": "{\"user\":{\"email\":\"…\"}}",
"logged_at": "2026-04-16 10:11:12.345"
}
]
}
Expected responses:
2xx→ the agent moves on (server replies{"accepted": <count>})403(no active subscription) or429(quota exhausted) → the job fails immediately, no retry- Any other
4xx→ not retried (fix the key / payload) 5xxor network error → retried up to 3 times with backoff[5, 30, 120]seconds
Troubleshooting
Start with the doctor. It checks every link of the shipping chain in one pass — API key, ingest endpoint (authenticated ping), buffer store, cache debounce, queue connection, worker heuristic, ingest circuit and pending backlog — and exits non-zero only when a check fails (safe for CI):
php artisan owlogs:doctor
Flags: --reset-circuit closes a tripped ingest circuit before running the checks; --queue-timeout=5 sets how long the worker probe waits. Follow up with php artisan owlogs:emit-test-logs to push one log of every captured kind and confirm they reach your workspace.
If the doctor comes back clean but something still looks off, the manual checks below cover the usual suspects.
Jobs pile up in the failed_jobs table. Check the exception: if it's 401 / 403, your OWLOGS_API_KEY is wrong or the key was rotated — regenerate it from your workspace and update .env.
Logs never arrive. Run php artisan queue:work — without a worker, the dispatched ShipBufferedLogsJob will never execute and rows pile up in the store. Also verify OWLOGS_API_KEY is set (empty key = silent no-op), and that LOG_CHANNEL=stack (or LOG_CHANNEL=owlogs) — if LOG_CHANNEL points to a non-stack channel (e.g. single), the auto-registered owlogs entry in stack is bypassed.
Too many ShipBufferedLogsJob in the queue. The debounce marker (Cache::add) needs a shared persistent cache. Check CACHE_STORE (Laravel 11+) / CACHE_DRIVER — if it's array, each PHP request has its own marker and no de-duplication happens. Switch to redis, file, database, or memcached.
Rows stuck in the buffer. Inspect the store directly:
# Redis redis-cli LLEN owlogs:buffer redis-cli LRANGE owlogs:buffer 0 0 # File wc -l storage/app/owlogs/buffer.jsonl
A non-zero LLEN with no ship job in the queue usually means the cache marker is stale (e.g. the previous ship job crashed before releasing it). Run php artisan cache:forget owlogs:ship:pending to clear it — the next flush will re-arm dispatch.
Octane complains about bindings. The agent does not use container / request / config injection in singletons. If you see such warnings, they come from elsewhere in your app.
Caller location is wrong. If your logs go through a custom wrapper class, add its path to config('owlogs.caller.ignore_paths') so the frame-walker skips over it.
Security
- Redaction is centralised in
config('owlogs.redaction')and applied to everything the agent captures: request input, log context/extra, Livewire call params and model-change attributes. Never edit vendor files — publish the config and extend the lists:key_patterns— case-insensitive substrings matched against array keys. Defaults:password,secret,token,key,authorization,cookie,credit_card,two_factor. A matching key has its whole value (nested arrays included) replaced bymask(default********).value_regexes— PCRE patterns applied to every captured string value regardless of its key; each match is replaced bymask. Useful for secrets hiding inside free text, e.g.'/\b\d{16}\b/'for card numbers. Empty by default.
- Sampling (
config('owlogs.sampling')) trims volume before anything is buffered or shipped:levelssets a per-level keep probability (OWLOGS_SAMPLE_DEBUG=0.1keeps 10 % of debug rows), andtracesmaps URI patterns to trace-level sample rates — the decision is derived from thetrace_id, so a kept trace keeps all its rows and a dropped trace disappears entirely. - HTTPS: traffic is sent over TLS to
https://www.owlogs.comwith Laravel's default HTTP client verification. - Authentication: every request carries the
X-Api-Keyheader. Rotate the key from your workspace and updateOWLOGS_API_KEYto invalidate. - No global state: all tracing IDs live in Laravel's
Contextwhich is reset between requests / jobs.