You can install the package via Composer:
composer require adilazhari/laravel-traceYou may publish all of the package's resources at once:
php artisan vendor:publish --tag="laravel-trace"Or, you may publish each resource individually:
php artisan vendor:publish --tag="laravel-trace-config"php artisan vendor:publish --tag="laravel-trace-migrations"
php artisan migrateEvery setting in config/laravel-trace.php can be overridden from .env
without publishing the config file. Defaults shown below are what applies
when the variable is unset.
| Config key | Env variable | Type | Default | Purpose |
|---|---|---|---|---|
enabled |
LARAVEL_TRACE_ENABLED |
bool | true |
Master switch for the whole package. |
instrumentation.database.enabled |
LARAVEL_TRACE_DATABASE_QUERY_ENABLED |
bool | true |
Records a span per SQL query. Not related to storage.database.* below - see "Database query instrumentation vs. database storage". |
queue.enabled |
LARAVEL_TRACE_QUEUE_ENABLED |
bool | true |
Restores/records trace context around queued job processing. |
http.propagate_outbound |
LARAVEL_TRACE_HTTP_PROPAGATE_OUTBOUND |
bool | false |
Attaches the active trace context to outbound Http:: client requests. |
http.header |
(not env-driven) | string | X-Trace-Context |
Header name used for inbound/outbound context propagation. |
storage.driver |
LARAVEL_TRACE_STORAGE_DRIVER |
string | memory |
Where traces/spans are persisted: memory or database. |
storage.database.connection |
(not env-driven) | string|null | null |
Connection used by the database storage driver; null uses the app's default connection. |
storage.database.swallow_exceptions |
LARAVEL_TRACE_STORAGE_DATABASE_SWALLOW_EXCEPTIONS |
bool | true |
Logs and swallows a database storage write failure instead of letting it throw. |
storage.retention.enabled |
LARAVEL_TRACE_RETENTION_ENABLED |
bool | false |
Whether laravel-trace:prune deletes anything without an explicit --days/--before. |
storage.retention.days |
LARAVEL_TRACE_RETENTION_DAYS |
int | 7 |
Age (in days) at which a terminal trace becomes eligible for pruning. |
storage.retention.chunk_size |
LARAVEL_TRACE_RETENTION_CHUNK_SIZE |
int | 500 |
Traces deleted per batch by the pruner. |
# .env
LARAVEL_TRACE_ENABLED=true
LARAVEL_TRACE_DATABASE_QUERY_ENABLED=true
LARAVEL_TRACE_QUEUE_ENABLED=true
LARAVEL_TRACE_HTTP_PROPAGATE_OUTBOUND=false
LARAVEL_TRACE_STORAGE_DRIVER=database
LARAVEL_TRACE_STORAGE_DATABASE_SWALLOW_EXCEPTIONS=true
LARAVEL_TRACE_RETENTION_ENABLED=true
LARAVEL_TRACE_RETENTION_DAYS=7
LARAVEL_TRACE_RETENTION_CHUNK_SIZE=500Boolean env values accept true/false, 1/0, or Laravel's (true)/(false)
forms; an unset or empty value falls back to the default rather than being
read as false.
These are two independent concerns that happen to share the word "database":
instrumentation.database.enabled- whether the package records a span for every SQL query your application runs. This has nothing to do with where traces/spans themselves are stored.storage.database.*- configuration for the optionaldatabasestorage driver (connection, exception-swallowing) that traces/spans can be persisted to instead of the defaultmemorydriver. See Database storage below.
You can, for example, run with SQL query instrumentation off
(LARAVEL_TRACE_DATABASE_QUERY_ENABLED=false, so no database.query spans
are recorded) while still persisting whatever traces/spans are recorded to
the database storage driver, or vice versa - the two toggles are unrelated.
Pre-1.0, laravel-trace.database.enabled has been renamed to
laravel-trace.instrumentation.database.enabled to remove the ambiguity
with storage.database.* above. There is no compatibility alias for the old
key - keeping one silently would reintroduce the exact ambiguity this rename
fixes. If you have published config/laravel-trace.php or set
LARAVEL_TRACE_DATABASE_ENABLED anywhere, update it to
instrumentation.database.enabled / LARAVEL_TRACE_DATABASE_QUERY_ENABLED.
Laravel Trace never attaches itself to a route automatically. To trace HTTP
requests, register TraceRequest wherever you want tracing to start - a
route, a group, or every request.
// routes/web.php
use AdilAzhari\LaravelTrace\Http\Middleware\TraceRequest;
Route::middleware(TraceRequest::class)->group(function () {
Route::get('/orders/{order}', [OrderController::class, 'show']);
});To trace every request instead, append it in bootstrap/app.php:
use AdilAzhari\LaravelTrace\Http\Middleware\TraceRequest;
use Illuminate\Foundation\Configuration\Middleware;
->withMiddleware(function (Middleware $middleware) {
$middleware->append(TraceRequest::class);
})Without this middleware - or a manual Tracer::start() call, below - nothing
is traced: database queries, events, and queue jobs are only instrumented
while a trace is already active.
Once the middleware is applied to a route, every matching request is wrapped
in a trace and an http.request span with no further code:
Route::middleware(TraceRequest::class)->get('/orders/{order}', function (Order $order) {
return $order->fresh('items');
});- The trace is named
http.request, withhttp.methodandhttp.pathattributes captured up front. - The span records
http.status_codeon success; if the route throws, the span and trace are both failed with the exception recorded, and the exception is rethrown unchanged. - The trace context is always cleared once the response is produced - successfully or not - so it never leaks into whatever handles the next request in the same process.
For anything outside a traced HTTP request - a console command, a scheduled
task, or extra detail inside one - instrument code directly with the
Tracer contract (there's no facade for the write side; resolve it from the
container or inject AdilAzhari\LaravelTrace\Contracts\Tracer):
use AdilAzhari\LaravelTrace\Contracts\Tracer;
use AdilAzhari\LaravelTrace\Span\SpanType;
$tracer = app(Tracer::class);
$trace = $tracer->start('import.customers', ['source' => 'csv']);
$span = $tracer->span('parse.file', SpanType::Action);
try {
// ... do the work ...
$span->close();
} catch (Throwable $exception) {
$span->fail($exception);
throw $exception;
}
$tracer->completeTrace($trace);span()throwsLogicExceptionif no trace is active yet - callstart()first, or only instrument code that runs inside a request/job/ listener that already has one.$span->attributes([...])(alias:addAttributes()) adds to a span before it closes; attributes arestring|int|float|bool|nullvalues only.
Calling span() while another span is open nests it under whichever span is
currently active, and closing a span restores its parent as the active one -
parent/child relationships are never managed by hand:
$outer = $tracer->span('process.order', SpanType::Action);
$inner = $tracer->span('charge.payment', SpanType::Action);
$inner->close(); // process.order becomes the active span again
$outer->close();Once a trace is active - via the middleware or a manual start() call - the
following are recorded with no further code:
| What | Span name | Type | Toggle |
|---|---|---|---|
| A database query | database.query |
SpanType::Database |
laravel-trace.instrumentation.database.enabled |
| A non-queued event listener | listener.<class> |
SpanType::Listener |
on whenever a trace is active |
| A queued job being processed | queue.job |
SpanType::Job |
laravel-trace.queue.enabled |
Wildcard listeners and the package's own internal listeners are never wrapped, to avoid double-instrumenting and self-referential spans. A database storage write failure (as opposed to an application query) is caught, logged, and swallowed by default - see Database storage, below - instrumentation itself never throws just because a query ran.
AdilAzhari\LaravelTrace\Span\SpanType:
Http- an HTTP request, recorded by theTraceRequestmiddleware.Database- a SQL query, recorded by the automatic instrumentation.Listener- a non-queued event listener, recorded automatically.Job- a queued job being processed, recorded automatically.Action- your own business-logic spans (manual instrumentation).Event- your own domain-event-shaped spans (manual instrumentation) - distinct fromListener, which is reserved for the framework's own event dispatch.
A trace's identity crosses process boundaries as a TraceContext: a trace
ID and, if a span is active, the current span ID.
HTTP. An inbound request carrying the X-Trace-Context header (format
<trace-id> or <trace-id>-<span-id>, header name configurable via
laravel-trace.http.header) continues the propagated trace rather than
creating a new local root trace: Tracer::start() is never called for that
request, but spans are still recorded against the propagated trace ID as
normal. (The database storage driver additionally writes a placeholder
Running trace row so those spans have a parent to reference - see
Database storage, below - without ever creating a local Trace object.) A
malformed header is ignored and a fresh local trace starts instead.
Attaching the header to outbound requests made through Laravel's HTTP
client is opt-in:
// config/laravel-trace.php
'http' => [
'propagate_outbound' => true,
],Queues. Dispatching a job while a trace is active automatically embeds
the current context in the job's payload. When the job is processed, that
context is restored before the job runs (and cleared again afterwards)
whenever laravel-trace.queue.enabled is true - no extra code needed on
either side of the dispatch.
Tracer::span()throwsLogicExceptionwhen called with no active trace.- A write to the
databasestorage driver failing (bad connection, missing table) is logged and swallowed by default - seestorage.database.swallow_exceptionsbelow - so tracing failures never break the request or job being traced. - Reading (
TraceReader/SpanReader) and pruning (TracePruner) failures are not swallowed; they propagate, since those are calls your own code makes deliberately rather than ambient instrumentation. - If a process is killed or fatals mid-trace, whatever trace/spans were
already recorded stay
Runningpermanently - there is no automatic reconciliation for this yet, and pruning (below) never deletes aRunningrow regardless of age.
The default memory driver is suitable mainly for short-lived debugging or
tests, not for relying on in a real deployment. Two different things are
involved here, and they don't share the same lifetime:
- The active trace context - which trace/span is currently open - is
always cleared at the end of a request or job (by the
TraceRequestmiddleware'sfinallyblock, or byQueueJobListeneronce a job finishes), regardless of how the underlying PHP process is managed. - The underlying
InMemoryTraceStore/InMemorySpanStore, however, are container singletons that only clear when the application instance they belong to is torn down - not necessarily when the OS process is. Under a classic (non-Octane) PHP-FPM or CLI request, Laravel rebuilds its container from scratch on every request, so in practice this store never outlives the request that created it, even though the underlying worker process itself is commonly reused for the next one. A runtime that deliberately keeps one application instance alive across many units of work instead - aqueue:workworker processing job after job, or an Octane/Swoole worker serving request after request - keeps that same store alive too, and it accumulates every trace and span recorded for as long as the process runs, with nothing built in to bound it. - Nothing persists between separate
artisan/request processes in any case- see Querying and Pruning traces, below, for what that means for those features specifically.
Use the database driver (with retention pruning configured) rather than
memory for anything beyond short-lived local debugging in a long-lived
worker process.
If you must keep using the memory driver inside a long-lived worker,
InMemoryStorageCleaner gives you an explicit way to bound that growth
yourself, at whatever boundary you choose:
use AdilAzhari\LaravelTrace\Tracing\InMemoryStorageCleaner;
app(InMemoryStorageCleaner::class)->clear();This empties both InMemoryTraceStore and InMemorySpanStore together, so
you never clear one and leave the other stale. A few things to note:
- It is entirely caller-controlled. The package never calls this
automatically at request or job termination - doing so would silently
break reading back what the current process just recorded, which the
memorydriver otherwise supports for the life of the application instance. You decide when clearing is safe for your worker - e.g. once per iteration of aqueue:workloop, after you've read back anything you needed. - It has no effect on the
databasedriver. It only touches the two in-memory singletons directly; database rows are untouched either way. Retention pruning (below) remains the supported mechanism for bounding persistent database storage. - It is separate from clearing the active trace context. Clearing what trace/span is currently open (handled automatically, per above) and clearing what has already been recorded are two different concerns.
- This is not automatic eviction or a size-based policy - just an explicit operation you can call when you know it's safe to.
By default, traces and spans are held in memory for the lifetime of the
request. To persist them, publish and run the migrations (above), then set
the storage driver to database:
// config/laravel-trace.php
'storage' => [
'driver' => 'database',
'database' => [
'connection' => null, // defaults to your app's default connection
'swallow_exceptions' => true,
],
],A storage write failure is logged and swallowed by default, so a database
outage or a missing table never breaks the host application - set
swallow_exceptions to false while debugging your setup to let it throw
instead.
Once traces are being persisted you can read them back. The read API returns
plain Trace and Span objects (never Eloquent models) and works the same
against the memory and database drivers.
use AdilAzhari\LaravelTrace\Facades\LaravelTrace;
use AdilAzhari\LaravelTrace\Trace\TraceStatus;
use AdilAzhari\LaravelTrace\Span\SpanType;
// Look one up by id
$trace = LaravelTrace::trace('01J9Z8...'); // ?Trace
$span = LaravelTrace::span('01J9ZA...'); // ?Span
// Fluent query -> Illuminate collection of Trace objects
$failed = LaravelTrace::traces()
->whereName('http.request')
->whereStatus(TraceStatus::Failed)
->startedAfter(now()->subHour())
->minDurationMs(500)
->whereAttribute('http.method', 'POST') // equality only
->orderBy('duration_ms', 'desc')
->get();
// Offset pagination (LengthAwarePaginator)
$page = LaravelTrace::traces()->onlyErrors()->paginate(perPage: 25);
// A trace's spans, flat (oldest first) or as a parent/child tree
$spans = LaravelTrace::spansForTrace($trace->id);
$tree = LaravelTrace::spanTree($trace->id); // list<SpanNode>
// Spans on their own
$slowQueries = LaravelTrace::spans()
->whereTrace($trace->id)
->whereType(SpanType::Database)
->minDurationMs(100)
->get();You can also type-hint the contracts directly
(AdilAzhari\LaravelTrace\Contracts\TraceReader /
SpanReader) with an immutable TraceQuery / SpanQuery. SpanReader also
has a children(SpanId $parentId): Collection method - not exposed through
the facade or SpanQueryBuilder - that returns the direct children of the
given span (one level down only, not the full subtree), oldest first, on
both drivers:
use AdilAzhari\LaravelTrace\Contracts\SpanReader;
$children = app(SpanReader::class)->children($span->id); // Collection<int, Span>Notes and limits for this release:
- Traces and spans can be filtered by id, name, status, started-at range,
duration range, error presence, and attribute equality. Attribute
filtering compiles to a cross-database JSON path lookup;
LIKE, containment, and nested attribute paths are not supported. Attribute keys are restricted to letters, digits,_,-and.- but only thedatabasedriver enforces this, throwingInvalidArgumentExceptionfor a key outside that set; thememorydriver does not validate the key at all (it simply never matches, since nothing was ever recorded under an invalid key). Code that only ever ran againstmemoryin tests can hit this for the first time againstdatabasein production. - Sorting is limited to
started_at,finished_at,duration_ms,name(andtypefor spans), always with a deterministicidtiebreak. The default order is newest first. - Pagination is offset-based only.
- The
memorydriver only sees what the current process recorded; it does not persist between requests. - There is no dashboard, HTTP API, or exporter yet.
Traces and spans accumulate forever unless you prune them. Publish and run the migration (above), then set a retention window:
// config/laravel-trace.php
'storage' => [
'retention' => [
'enabled' => false, // a fresh install never deletes anything on its own
'days' => 7,
'chunk_size' => 500,
],
],and run:
php artisan laravel-trace:pruneOptions:
--days=N Prune traces started more than N days ago, overriding retention.days
--before=DATE Prune traces started before this date/time, overriding --days
--chunk=N Traces deleted per batch, overriding retention.chunk_size
--dry-run Report exact matching counts without deleting anything
--force Skip the confirmation prompt
Notes:
- Only completed or failed traces are pruned. A still-
Runningtrace is never deleted, no matter how old - handling traces abandoned by a crashed process is left to a future release. Age is measured fromstarted_at. - If
retention.enabledisfalseand neither--daysnor--beforeis given, the command explains that and exits successfully without deleting anything - safe to leave in a scheduled job while retention is off. Passing--days/--beforeexplicitly always runs the prune, regardless ofenabled. --dry-runruns the exact same eligibility query real pruning would and reports precisely what it matched, without deleting.- A database failure during pruning is never swallowed - unlike recording, a failed maintenance run needs to be visible.
- Under the
memorydriver, the command explains that it has nothing to prune: the in-memory store belongs to the process that recorded it and does not persist between separateartisaninvocations. - The package does not schedule this command for you. Add it to your
own application's scheduler, e.g. in
routes/console.php:Schedule::command('laravel-trace:prune --force')->daily();
Please see CHANGELOG for more information on what has changed recently.
Thank you for considering contributing to Laravel Trace! Please review our contributing guide to get started.
Please review our security policy on how to report security vulnerabilities.
Laravel Trace is open-sourced software licensed under the MIT license.