kolaybi / request-tracer
Standalone request tracing package for Laravel — captures outgoing HTTP/SOAP requests and optionally incoming requests.
Requires
- php: ^8.4
- laravel/framework: ^12.0 || ^13.0
Requires (Dev)
- laravel/pint: ^1.27
- orchestra/testbench: ^10.0
- pestphp/pest: ^4.0
Suggests
- ext-simplexml: Required for extracting SOAP operation names in trace logs
- ext-soap: Required for SOAP request tracing via TracingSoapClient
This package is auto-updated.
Last update: 2026-08-11 14:30:04 UTC
README
Standalone request tracing package for Laravel. Captures outgoing HTTP and SOAP requests, and optionally incoming requests. All traces are stored asynchronously via queued jobs.
Requirements
- PHP 8.4+
- Laravel 12+
ext-soapandext-simplexml(only if using SOAP tracing)
Installation
composer require kolaybi/request-tracer
Publish the config:
php artisan vendor:publish --tag=request-tracer-config php artisan migrate
Configuration
// config/kolaybi/request-tracer.php return [ 'connection' => env('REQUEST_TRACER_DB_CONNECTION'), 'schema' => env('REQUEST_TRACER_DB_SCHEMA'), 'queue_connection' => env('REQUEST_TRACER_QUEUE_CONNECTION', 'redis'), 'queue' => env('REQUEST_TRACER_QUEUE', 'request_logging'), 'tenant_column' => 'tenant_id', 'tenant_cast' => 'integer', // 'integer', 'string', or any Eloquent cast type 'user_cast' => 'integer', // 'integer', 'string', or any Eloquent cast type 'max_body_size' => (int) env('REQUEST_TRACER_MAX_BODY_SIZE', 0), 'retention_days' => (int) env('REQUEST_TRACER_RETENTION_DAYS', 0), 'mask_sensitive' => (bool) env('REQUEST_TRACER_MASK_SENSITIVE', false), 'mask_value' => env('REQUEST_TRACER_MASK_VALUE', '[REDACTED]'), 'sensitive_keys' => env( 'REQUEST_TRACER_SENSITIVE_KEYS', 'authorization,proxy-authorization,cookie,set-cookie,x-api-key,api-key,apikey,token,access_token,refresh_token,id_token,password,passcode,secret,client_secret,private_key', ), 'context_provider' => null, 'outgoing' => [ 'enabled' => env('REQUEST_TRACER_OUTGOING_ENABLED', true), 'table' => 'outgoing_request_traces', 'model' => OutgoingRequestTrace::class, 'sample_rate' => (float) env('REQUEST_TRACER_OUTGOING_SAMPLE_RATE', 1.0), 'only' => env('REQUEST_TRACER_OUTGOING_ONLY', ''), // Comma-separated host/path patterns (supports wildcards: 'api.example.com*') 'except' => env('REQUEST_TRACER_OUTGOING_EXCEPT', ''), // Comma-separated host/path patterns (supports wildcards: '*.internal.com*') ], 'incoming' => [ 'enabled' => env('REQUEST_TRACER_INCOMING_ENABLED', false), 'table' => 'incoming_request_traces', 'model' => IncomingRequestTrace::class, 'sample_rate' => (float) env('REQUEST_TRACER_INCOMING_SAMPLE_RATE', 1.0), 'only' => env('REQUEST_TRACER_INCOMING_ONLY', ''), // Comma-separated paths (supports wildcards: 'api/orders*') 'except' => env('REQUEST_TRACER_INCOMING_EXCEPT', ''), // Comma-separated paths (supports wildcards: 'health*,telescope*') 'capture_response_body' => (bool) env('REQUEST_TRACER_INCOMING_CAPTURE_RESPONSE', false), 'channel_header' => env('REQUEST_TRACER_INCOMING_CHANNEL_HEADER'), // Header name to read channel from (e.g. 'Channel') ], ];
If REQUEST_TRACER_MASK_SENSITIVE=true, matching keys in headers and JSON/form payloads are masked before storage.
Middleware
Add RequestTracerMiddleware to your middleware stack. It generates a trace_id for every request and records incoming traces when enabled.
// bootstrap/app.php use KolayBi\RequestTracer\Middleware\RequestTracerMiddleware; ->withMiddleware(function (Middleware $middleware) { $middleware->prepend(RequestTracerMiddleware::class); })
The middleware accepts an optional channel parameter to tag incoming traces by route group:
// Per route group — channel set via middleware parameter Route::middleware([RequestTracerMiddleware::class.':web'])->group(...) Route::middleware([RequestTracerMiddleware::class.':mobile'])->group(...) // Channel from request header — set incoming.channel_header in config // Header value takes priority over middleware parameter
Context Provider
Implement TraceContextProvider to supply tenant, user, and server information:
use KolayBi\RequestTracer\Contracts\TraceContextProvider; class AppTraceContextProvider implements TraceContextProvider { public function tenantId(): int|string|null { return auth()->user()?->company_id; } public function userId(): int|string|null { return auth()->id(); } public function clientIp(): ?string { return request()?->ip(); } public function serverIdentifier(): ?string { return gethostname(); } }
Register it in your config:
'context_provider' => AppTraceContextProvider::class,
Usage
HTTP Tracing
Outgoing HTTP requests are traced automatically when outgoing.enabled is true. Use channel() or traceOf() to tag requests:
Http::channel('payment-gateway')->post('https://api.example.com/charge', $data); Http::traceOf('bank-api') ->withTraceExtra(['order_id' => 123]) ->get('https://bank.example.com/status');
channel() and traceOf() also accept an enum. Backed enums resolve to their value, pure enums to the case name:
enum Channel: string { case PaymentGateway = 'payment-gateway'; } Http::channel(Channel::PaymentGateway)->post('https://api.example.com/charge', $data);
SOAP Tracing
Extend TracingSoapClient for traced SOAP calls:
use KolayBi\RequestTracer\Soap\TracingSoapClient; $client = TracingSoapClient::with('https://service.example.com?wsdl'); $client->channel('e-invoice')->SomeOperation($params);
TracingSoapClient supports lazy initialization — set the WSDL later if needed:
$client = new TracingSoapClient(); $client->setWsdl('https://service.example.com?wsdl'); $client->setOptions(['soap_version' => SOAP_1_2]);
Incoming Tracing
Enable in config:
REQUEST_TRACER_INCOMING_ENABLED=true
The middleware records every incoming request with method, path, route, status, timing, headers, and optionally the response body.
Note:
response_sizeis always recorded regardless of thecapture_response_bodysetting. It is read from the SymfonyResponsecontent when available, and falls back to theContent-Lengthheader when content is unavailable.
Debugging
Display a chronological waterfall of all traces linked to a trace_id:
php artisan request-tracer:waterfall 01JEXAMPLE123
The command first prints a summary header:
Trace ID …………………………………………………… 01JEXAMPLE123
Tenant ID ………………………………………………… 42
User ID ……………………………………………………… 7
Client IP ………………………………………………… 192.168.1.10
First Start …………………………………………… 2026-02-28 12:00:00.000
Last End …………………………………………………… 2026-02-28 12:00:01.250
Total Duration …………………………………… 1250ms
Followed by a waterfall table of all traces sorted by start time:
+---+-----+----------+------+-----------------------------------------------+--------+----------+-----------------+--------+-------------------------+
| # | ID | Type | Method | Endpoint | Status | Duration | Channel | Server | Start |
+---+-----+----------+------+-----------------------------------------------+--------+----------+-----------------+--------+-------------------------+
| 1 | 501 | INCOMING | POST | api.example.com/webhooks (api/webhooks) | 200 | 1250ms | — | web-01 | 2026-02-28 12:00:00.000 |
| 2 | 830 | OUTGOING | GET | https://bank.example.com/status?ref=abc | 200 | 320ms | bank-api | web-01 | 2026-02-28 12:00:00.100 |
| 3 | 831 | OUTGOING | POST | https://payment.example.com/charge | 201 | 890ms | payment-gateway | web-01 | 2026-02-28 12:00:00.350 |
+---+-----+----------+------+-----------------------------------------------+--------+----------+-----------------+--------+-------------------------+
This is useful for inspecting the full request flow — incoming request plus all outgoing calls it triggered — in chronological order.
Inspect
Drill into a single trace by its ULID with progressive verbosity:
# Metadata only (type, method, endpoint, status, duration, timing, sizes, etc.) php artisan request-tracer:inspect 01JEXAMPLE123 # + request/response headers php artisan request-tracer:inspect 01JEXAMPLE123 -v # + request/response body (truncated to 20 lines) php artisan request-tracer:inspect 01JEXAMPLE123 -vv # + body at 40 lines + exception, message, stats, extra (outgoing) php artisan request-tracer:inspect 01JEXAMPLE123 -vvv # Everything, no truncation php artisan request-tracer:inspect 01JEXAMPLE123 --full
The command searches both incoming and outgoing tables automatically — no need to specify which.
Tail
Live-tail new traces as they arrive:
php artisan request-tracer:tail # Filters (all case-insensitive) php artisan request-tracer:tail --type=incoming --status=5xx php artisan request-tracer:tail --host="*.bank.example.com" --channel=payments php artisan request-tracer:tail --method=GET,POST # Wider lines with progressive verbosity php artisan request-tracer:tail -v # full endpoint (no truncation) + record id php artisan request-tracer:tail -vv # + trace id, tenant, user, client ip php artisan request-tracer:tail -vvv # + request/response sizes, server, error message
--interval=N controls the poll interval in seconds (default 2), and the record id shown at -v can be fed straight into request-tracer:inspect.
Search
Query stored traces with filters:
php artisan request-tracer:search --status=5xx --type=outgoing php artisan request-tracer:search --host="*.bank.example.com" --path="/api/orders*" php artisan request-tracer:search --method=POST --channel=payments php artisan request-tracer:search --from="2026-08-01" --to="2026-08-11 12:00:00" php artisan request-tracer:search --min-duration=1000 --limit=100
Results are rendered newest-first in a table (ID, type, method, endpoint, status, duration, channel, created). --status accepts an exact code or a range like 5xx, --host and --path support * wildcards, and --limit caps the row count (default 50). Both incoming and outgoing tables are searched unless --type narrows it. The ID column feeds straight into request-tracer:inspect and request-tracer:diff.
Stats
Aggregate statistics for a time window (default: last 24 hours):
php artisan request-tracer:stats php artisan request-tracer:stats --hours=6 --type=outgoing
Shows total requests, 2xx/4xx/5xx counts, 5xx error rate, and avg/min/max durations, plus the top 5 hosts and top 5 channels — reported separately for incoming and outgoing traces.
Diff
Compare two traces side-by-side, with progressive verbosity like inspect:
# Metadata diff (method, endpoint, status, duration, sizes) php artisan request-tracer:diff 01JEXAMPLE111 01JEXAMPLE222 # + request/response header diff php artisan request-tracer:diff 01JEXAMPLE111 01JEXAMPLE222 -v # + request/response body diff with similarity percentage php artisan request-tracer:diff 01JEXAMPLE111 01JEXAMPLE222 -vv
Identical fields print once; differing fields are highlighted as old → new. Added, removed, and changed headers are marked + / - / ~. Both IDs are resolved across incoming, outgoing, and dated archive tables, so a current trace can be compared against a rotated one — useful for "why does this request behave differently than last week's?"
Data Retention
Table Rotation (recommended)
Rotate trace tables daily — the current table is atomically swapped with a fresh empty one, and the old data moves to a dated archive table (e.g. outgoing_request_traces_20260309). Archives older than retention_days are dropped automatically.
php artisan request-tracer:rotate php artisan request-tracer:rotate --days=30
Schedule it to run daily:
$schedule->command('request-tracer:rotate')->daily();
Row-level Purge
Alternatively, delete old rows from the current table in chunks:
php artisan request-tracer:purge --days=30 php artisan request-tracer:purge --days=90 --chunk=10000
Or set retention_days in config and schedule it:
$schedule->command('request-tracer:purge')->daily();
Preserving Selected Traces
Some traces are operationally critical (e.g. integrations with external systems for audit purposes) and must survive rotation and retention. The request-tracer:preserve command sweeps rows matching configured glob patterns from the just-rotated archive table into a permanent *_persistent table.
Configure persist patterns per direction:
REQUEST_TRACER_INCOMING_PERSIST=api/*,kolaybi/* REQUEST_TRACER_OUTGOING_PERSIST=*api*
- Incoming patterns match
request->path()(same asINCOMING_ONLY). - Outgoing patterns match the trimmed
host + pathstring (same asOUTGOING_ONLY).
Wire the command to run immediately after rotation:
$schedule->command('request-tracer:rotate') ->daily() ->then(function () { $this->call('request-tracer:preserve'); });
Backfill or re-run a specific archive:
php artisan request-tracer:preserve --date=20260511 php artisan request-tracer:preserve --all php artisan request-tracer:preserve --direction=incoming
The command is idempotent: re-running over the same archive does not duplicate rows (uses INSERT IGNORE / ON CONFLICT DO NOTHING keyed on the ULID PK).
How It Works
Outgoing Traces
RequestSendinglistener stores per-request trace metadata (started_at) in request attributes andRequestTimingStoreHttp::channel()/Http::traceOf()andHttp::withTraceExtra()set per-request metadata inrequest_tracerattributes- Event listeners (
ResponseReceived,ConnectionFailed) build trace attributes and dispatchStoreTraceJob - Any
X-Trace-*headers present on requests/responses are stripped before persisting
Incoming Traces
RequestTracerMiddlewaregenerates atrace_idand records start time- After the response is returned,
IncomingTraceRecorderbuilds attributes and dispatchesStoreTraceJob - The same
trace_idlinks incoming and outgoing traces within a request
SOAP Traces
TracingSoapClientstores channel/extra as instance properties__doRequest()dispatches events with timing, channel, and extra- Properties are reset after each call (no leakage between calls)
Workers / Queue Jobs
- The middleware does not run on workers, but outgoing tracing works identically
trace_idis lazily generated viaContexton the first outgoing call- Laravel resets
Contextbetween queue jobs, so each job gets a freshtrace_id
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
License
Please see License File for more information.