Tap into your app's HTTP traffic. Filter, redact, and store inbound and outbound requests with zero boilerplate.
A highly configurable HTTP tracing package, built for Laravel. Wiretap automatically captures every outbound request and response — and optionally every inbound request too — including headers, payloads, status codes, and timing. Stores them securely with full control over what gets kept, what gets scrubbed, and where it all ends up. Works with any PHP application via a lightweight TraceWriter interface.
- Storage Backends: Persist traces to your SQL
database(default) or stream them to a structured Laravellogchannel. - Automatic Laravel Integration: Zero-config capture of all Laravel HTTP Client requests via event listeners. Opt-in capture of all inbound requests via global middleware.
- Native Guzzle Support: Drop-in
WiretapClientwrapper andWiretapMiddlewarefor existing Guzzle stacks. - Advanced Redaction: Automatically scrubs sensitive headers and recursively redacts JSON payload keys before anything touches storage.
- Filtering & Truncation: Allowlist/denylist hosts, exclude URL patterns, and cap body sizes to keep your storage lean.
- Eloquent Polymorphism: Attach traces to any Eloquent model with
withTraceable()/->traceable()andHasTraces— then query them back in one line. - Manual Tracing: Use
Wiretap::trace()to capture requests from raw cURL, custom SDKs, or any HTTP client — with built-in timing, caller attribution (callerClass/callerMethod), and safe exception handling.
- PHP 8.3+
- Laravel 11.0 / 12.0 / 13.0
You can install the package via composer:
composer require nordkit/wiretapYou can publish and run the migrations with:
php artisan vendor:publish --tag="wiretap-migrations"
php artisan migrateYou can publish the config file with:
php artisan vendor:publish --tag="wiretap-config"Upgrading from an earlier version? Migrations are auto-loaded by the service provider, so running
php artisan migrateis all that is needed to pick up new columns. If you previously published the migration files into your owndatabase/migrationsdirectory, re-publish with--forceto get the latest files before migrating:php artisan vendor:publish --tag="wiretap-migrations" --force php artisan migrate
If you are using this package in a standalone PHP application (without the Laravel framework), you will need to manually handle the database schema or inject a custom TraceWriter into the Wiretap.
If you choose to use the built-in database writer (which depends on illuminate/database), you must manually run this equivalent raw SQL to create the wiretap_traces table:
CREATE TABLE `wiretap_traces` (
`id` CHAR(26) NOT NULL,
`direction` VARCHAR(10) NOT NULL,
`driver` VARCHAR(20) NOT NULL,
`url` TEXT NOT NULL,
`method` VARCHAR(10) NOT NULL,
`request_headers` JSON NULL,
`request_body` LONGTEXT NULL,
`response_status` INT NULL,
`response_headers` JSON NULL,
`response_body` LONGTEXT NULL,
`duration_ms` INT UNSIGNED NOT NULL,
`error_message` TEXT NULL,
`ip_address` VARCHAR(45) NULL,
`caller_class` VARCHAR(255) NULL,
`caller_method` VARCHAR(255) NULL,
`traceable_type` VARCHAR(255) NULL,
`traceable_id` CHAR(26) NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `wiretap_traces_traceable_type_traceable_id_created_at_index` (`traceable_type`, `traceable_id`, `created_at`)
);To entirely bypass illuminate/database, you can implement the TraceWriter interface and store your traces using raw PDO, Monolog, or any other solution:
use Nordkit\Wiretap\Contracts\TraceWriter;
use Nordkit\Wiretap\HttpExchange;
class MyPdoWriter implements TraceWriter
{
public function write(HttpExchange $exchange): void
{
// Insert $exchange data using raw PDO, e.g.:
// $exchange->ipAddress — caller IP (null unless explicitly passed)
// $exchange->callerClass — originating class name (null unless explicitly passed)
// $exchange->callerMethod — originating method name (null unless explicitly passed)
}
}There is no equivalent of WiretapInboundMiddleware outside Laravel — you wire inbound tracing yourself inside your framework's request pipeline. Use Wiretap::trace() and pass the caller's IP via the ipAddress parameter:
use Nordkit\Wiretap\HttpDirection;
$timer = $wiretap->start();
// ... dispatch the request through your application ...
$wiretap->trace(
direction: HttpDirection::Inbound,
driver: 'my-framework',
url: 'https://api.myapp.com' . $_SERVER['REQUEST_URI'],
method: $_SERVER['REQUEST_METHOD'],
requestHeaders: getallheaders(),
requestBody: file_get_contents('php://input') ?: null,
responseStatus: http_response_code(),
responseHeaders: [],
responseBody: null,
timer: $timer,
ipAddress: $_SERVER['REMOTE_ADDR'] ?? null,
);Or construct an HttpExchange directly and call capture() when you need full control:
use Nordkit\Wiretap\HttpExchange;
use Nordkit\Wiretap\HttpDirection;
$wiretap->capture(new HttpExchange(
direction: HttpDirection::Inbound,
driver: 'my-framework',
url: $request->getUri(),
method: $request->getMethod(),
requestHeaders: $request->getHeaders(),
requestBody: $request->getBody(),
responseStatus: $response->getStatusCode(),
responseHeaders: $response->getHeaders(),
responseBody: $response->getBody(),
durationMs: $durationMs,
ipAddress: $request->getServerParam('REMOTE_ADDR'),
));Privacy note:
ip_addressisnullby default everywhere. Only populate it when you have a legitimate need and appropriate data-retention policies in place — IP addresses are personal data under GDPR and similar regulations.
Publish the config file to get started:
php artisan vendor:publish --tag="wiretap-config"Every option in config/wiretap.php is documented with an inline comment. The main areas to know about:
enabled/debug— kill switch and error visibility. By default, all tracing exceptions are swallowed silently; setdebug = trueto forward them to Laravel'sreport()handler.driver—database(default, queued viaWriteTraceJob) orlog(streams to a Laravel log channel).outbound.*— controls the Laravel HTTP Client and Guzzle adapters, plus host/path filtering for outbound requests.inbound.*— opt-in capture of incoming requests. Disabled by default (WIRETAP_INBOUND=false). Includes the same host/path filtering as outbound.store_request_body/store_response_body— toggle body capture. Binary content types (image/*,video/*,audio/*,multipart/form-data,application/octet-stream,application/pdf,application/zip) are never stored as raw bytes — instead a[binary: filename.ext]placeholder is stored, with the filename extracted fromContent-Dispositionwhere available.max_body_bytes— caps stored body size to 64 KB by default. Set tonullfor unlimited.redact_request_headers/redact_response_headers/redact_body_keys— lists of headers and JSON keys to scrub before anything reaches storage.pruning.*— automatic deletion of old traces viaphp artisan wiretap:prune. Disabled by default, only applies to thedatabasedriver.
Caller attribution: The
caller_classandcaller_methodcolumns are alwaysnullfor automatic Laravel HTTP Client and Guzzle traces. They are only populated when you passcallerClass/callerMethodtoWiretap::trace()manually.
Traces accumulate quickly. Enable automatic pruning by setting:
WIRETAP_PRUNING_ENABLED=true
WIRETAP_PRUNING_KEEP_DAYS=90 # default: 90 daysWhen WIRETAP_PRUNING_ENABLED=true and wiretap.driver is database, Wiretap automatically registers a daily schedule for wiretap:prune — no entry in your scheduler is required.
You can also run it on demand or with a custom retention window:
php artisan wiretap:prune
php artisan wiretap:prune --days=30Note: Pruning only applies to the
databasedriver. Runningwiretap:prunewhenwiretap.driverislogwill print a warning and exit cleanly.
The package automatically integrates with the Laravel HTTP Client. You don't need any additional setup.
use Illuminate\Support\Facades\Http;
$response = Http::withHeaders(['X-First' => 'foo'])
->get('https://api.github.com/users/octocat');
// The request and response are now automatically stored in the database.Wiretap can also capture all incoming requests to your application. This is disabled by default — opt-in by setting the environment variable:
WIRETAP_INBOUND=trueOr enable it directly in the config:
'inbound' => [
'laravel_http' => true,
],When enabled, WiretapInboundMiddleware is automatically pushed onto the global HTTP kernel. Every request your app receives — and its response — will be traced through the same pipeline as outbound traffic, including redaction and filtering.
You can limit which inbound requests are traced using the include_paths and exclude_paths options. For example, to only trace webhook callbacks:
'inbound' => [
'laravel_http' => true,
'include_paths' => ['#^/webhooks#'],
'exclude_paths' => ['#^/health#'],
],Or restrict tracing to a specific subdomain in a multi-domain app:
'inbound' => [
'laravel_http' => true,
'include_hosts' => ['webhooks.myapp.com'],
],Note:
inbound.include_hosts/exclude_hostsare matched against theHostheader of the incoming request — i.e. your own app's domain. They are not matched against the remote caller's IP or hostname.
To capture the caller's IP address, enable the opt-in flag:
WIRETAP_INBOUND_STORE_IP=trueOr in the config:
'inbound' => [
'store_ip' => true,
],The value is stored in the ip_address column (varchar 45, covers IPv4 and IPv6) and is populated via $request->ip(), which respects your app's TrustProxies configuration.
Privacy note: IP addresses are personal data under GDPR and similar regulations. This option is disabled by default — only enable it when you have a legitimate need and appropriate data-retention policies in place.
You can associate HTTP traces with your Eloquent models using polymorphic relations. This is useful for tracking which requests belong to a specific record, such as a user, order, or shipment.
When using the Laravel HTTP Client, you can use the built-in macro to attach a model to the request:
use Illuminate\Support\Facades\Http;
$order = Order::find(1);
Http::withTraceable($order)
->post('https://api.example.com/orders/sync', $order->toArray());Note:
withTraceable()stores the model in a per-process singleton and is designed for sequential requests. When usingHttp::pool()with multiple concurrent requests, only the most recently set traceable will be attached. For concurrent use-cases, construct anHttpExchangedirectly with the desiredtraceableand callWiretap::capture()manually.
For inbound requests, attach a model to the trace using the ->traceable() route macro. It scans the route's already-resolved model bindings and pushes the first match onto the trace scope:
use App\Models\Order;
Route::post('/orders/{order}/sync', OrderSyncController::class)
->traceable(Order::class);Note:
->traceable()relies on route model binding being resolved before it runs. Routes registered in theweborapimiddleware group satisfy this automatically viaSubstituteBindings.
To easily retrieve the associated traces, add the provided trait (or manually define the morphMany relationship) on your Eloquent model:
use Illuminate\Database\Eloquent\Model;
use Nordkit\Wiretap\Laravel\Concerns\HasTraces;
class Order extends Model
{
use HasTraces;
}Now you can access the previous HTTP traces directly from your model instance:
$traces = $order->traces;The package provides a WiretapClient wrapper that handles all logging automatically. It is registered as a singleton in the Laravel service container and can be injected directly via the constructor:
use Nordkit\Wiretap\Guzzle\WiretapClient;
class GitHubService
{
public function __construct(private readonly WiretapClient $http) {}
public function getUser(string $username): array
{
$response = $this->http->get("https://api.github.com/users/{$username}");
// The request and response are automatically logged.
return json_decode((string) $response->getBody(), true);
}
}You can associate requests with an Eloquent model using withTraceable(), mirroring the Laravel HTTP Client behavior:
$response = $this->http
->withTraceable($order)
->post('https://api.example.com/orders/sync', ['json' => $order->toArray()]);Note:
withTraceable()is designed for sequential requests. The traceable is consumed (reset tonull) after the next request is dispatched.
To pass custom Guzzle config options (e.g. base_uri, timeout), resolve the client manually with WiretapClient::make():
use Nordkit\Wiretap\Guzzle\WiretapClient;
use Nordkit\Wiretap\Wiretap;
$client = WiretapClient::make(app(\Nordkit\Wiretap\Wiretap::class), [
'base_uri' => 'https://api.example.com',
'timeout' => 10,
]);If you need to attach logging to an existing Guzzle HandlerStack (e.g. wrapping a third-party SDK), push the middleware directly:
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use Nordkit\Wiretap\Guzzle\WiretapMiddleware;
use Nordkit\Wiretap\Wiretap;
$stack = HandlerStack::create();
$stack->push(WiretapMiddleware::make(app(\Nordkit\Wiretap\Wiretap::class)));
$client = new Client(['handler' => $stack]);
$client->request('GET', 'https://api.github.com/repos/guzzle/guzzle');If you need to trace requests made outside of Laravel's HTTP Client or Guzzle (for example, raw cURL requests or third-party SDKs), you can easily record entries using the built-in timer and the Wiretap::trace() helper method.
The trace() method safely swallows all exceptions, guaranteeing that tracing will never halt your application execution.
use Nordkit\Wiretap\HttpDirection;
use Nordkit\Wiretap\Laravel\Facades\Wiretap;
// 1. Start the internal timer — returns a Closure that yields elapsed ms when called
$timer = Wiretap::start();
// 2. Perform your manual request/interaction...
$response = $customSdk->syncData(['foo' => 'bar']);
// 3. Trace the execution using the timer Closure
Wiretap::trace(
direction: HttpDirection::Outbound,
driver: 'custom-sdk',
url: 'https://api.example.com/sync',
method: 'POST',
requestHeaders: ['Content-Type' => 'application/json'],
requestBody: json_encode(['data' => 'sync-me']),
responseStatus: 200,
responseHeaders: ['Content-Type' => 'application/json'],
responseBody: json_encode(['status' => 'success']),
timer: $timer, // Automagically resolves the request duration
errorMessage: null, // Populate if your manual implementation encountered an error
callerClass: self::class, // Optional: record which class initiated the call
callerMethod: __FUNCTION__, // Optional: record which method initiated the call
);composer testPlease see CHANGELOG for more information on what has changed recently.
Please see RELEASING for instructions on how to cut a new release.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.