What gets recorded
Everything through the Http facade, anything on a Guzzle stack you instrument, SDKs that find their client through discovery once you allow it, and nothing on a host you ignore.
Requizon records at the transport. It adds one middleware to Laravel's HTTP client, and the same middleware can be pushed onto any Guzzle handler stack. Whatever passes through it is recorded unless its host is on the ignore list. Nothing at the call site decides. SDKs that find their client through discovery can be recorded too, once you allow it.
Laravel's HTTP client#
With instrument.laravel_client on (the default), Requizon registers its middleware as global middleware
on Illuminate\Http\Client\Factory. Every request the factory builds carries it, which covers:
Http::get(),Http::post()and every other call through the facadeHttp::pool(), where each request in the pool is recorded on its own- A
Factoryinjected through the container rather than the facade - Packages and SDKs that make their calls through Laravel's HTTP client
The factory is only hooked when something resolves it, so a request that makes no outbound calls never constructs the HTTP client. Under Octane the middleware is installed once per factory, not once per request. How global middleware on the HTTP client sits beside request and job middleware is in Laravel middleware for requests, queued jobs and the HTTP client.
Transfers, not calls#
The middleware sits inside Guzzle's redirect handling, and Laravel's retry() sends the whole request
again, as does Guzzle's Middleware::retry() pushed with withMiddleware(). A call that
follows two redirects is recorded as three rows, and a call retried twice is recorded as three rows. Each hop had
its own latency and could fail in its own way, so each gets a row. Expect the dashboard's totals to be higher than
the number of calls your code thinks it made.
The one exception is Guzzle's cURL handler resending a request it could not complete, such as a POST
that met a 307 or a connection that dropped before any response. That is one row, timed across every attempt,
because your code never saw the attempts that were tried again.
Guzzle clients and SDKs#
An SDK that builds its own Guzzle client never touches Laravel's HTTP client, so it is not recorded until you push the middleware onto its handler stack:
use BoringO11y\Requizon\Guzzle\RecordHttpRequests;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
$stack = HandlerStack::create();
$stack->push(app(RecordHttpRequests::class));
$client = new Client(['handler' => $stack]);
Most SDKs that accept a Guzzle client or handler take it as a constructor option. Resolve the middleware from the
container with app(), as above, so it uses the same recorder and configuration as everything else.
Pushing it onto a stack whose requests also pass through the Laravel client's copy is safe. The first copy marks the request's options, the second sees the mark and steps aside, and the transfer is recorded once.
Guzzle 7 and 8 are both supported. To record only the stacks you instrument by hand, turn the global hook off:
REQUIZON_INSTRUMENT_LARAVEL=false
SDKs that use client discovery#
Some SDKs never build a client you could reach. They ask php-http/discovery for whatever PSR-18 client the application has installed. Requizon can answer that question with a client that records, so their calls are recorded without a handler stack to push onto:
composer require php-http/discovery
REQUIZON_DISCOVERY=true
It is off by default, because it changes which client third-party code receives, and that is not a thing to do to
an application uninvited. Calls caught this way go through the same naming, ignore_hosts and redaction
as every other call. Two things are worth doing before you turn it on:
-
Map the host to the name its other calls use. If the same API is also called through
Http::, give its host anapisentry, or the two can land under different names. -
Put your error reporter in
ignore_hosts. Requizon reports its own failures through your exception handler, so recording the reporter's calls is a loop.
| SDKs | |
|---|---|
| Recorded | Those that resolve a synchronous PSR-18 client through discovery |
| Not recorded | Those handed a client, or that construct their own (AWS, Sentry v4). Use the Guzzle middleware where they accept a handler |
| Not recorded | Those not built on Guzzle or PSR-18 at all. Stripe ships its own cURL client |
| Not recorded | Async clients resolved through HttpAsyncClientDiscovery |
The client those libraries get is a plain Guzzle client wrapped in the recorder. If your application relies on discovery resolving something else, hand Requizon the original, without Requizon's middleware on it or each call is recorded twice:
use BoringO11y\Requizon\Requizon;
Requizon::discoverUsing(fn () => new Symfony\Component\HttpClient\Psr18Client);
The recording client is registered as early as Requizon's service provider runs. A library that already discovered
a client in an earlier provider's register() is holding that one, and keeps it.
Ignoring hosts#
ignore_hosts is a list of Str::is() patterns matched against the request's hostname, without
the scheme or port. A matching call is not recorded at all: no row, no failure detection, no body read.
// config/requizon.php
'ignore_hosts' => [
'*.datadoghq.com',
'*.ingest.sentry.io',
'logs.example.internal',
'localhost',
],
An exporter that ships logs, spans or metrics over HTTP through Laravel's client is outbound traffic like any other. Left in, it records a row for every batch it ships, and on a busy application the telemetry becomes the largest API on the dashboard.
Hosts are the only thing the ignore list matches. To drop some paths of an API while keeping others, collapse them
with paths.patterns instead.
How a call is recorded#
Recording hooks Guzzle's on_stats option, which reports every transfer, including one that got no
response at all, which is how a DNS failure or a timeout gets a row. The row is written once the call has settled,
before your code receives the result, so it records what your code actually got. The duration is cURL's own
transfer time, so a request that waited in a pool() behind the concurrency limit is not charged for the
wait. We built a smaller version by hand in
logging and timing every outgoing request with a Guzzle middleware and on_stats.
If your code already passed an on_stats callback, it still runs, and it runs first.
What a recorded call holds#
A row carries the API name, host, normalised path, the path
as it was called, method, status code, transfer duration and failure classification, and then what the two
messages said: the query string in query_params, the request body in request_params, the
response body of a failed call (or of a success, once
you ask), and whichever headers header capture is set to keep.
The query string and the body are stored separately, whatever the method. A POST to
/v1/charges?expand[]=customer keeps its expand, a HEAD or
OPTIONS keeps its query, and a GET that sends a JSON body, as search APIs often do, keeps
the body. The Details panel shows the two as Query and Body. Both are stored by parameter name and both go through
redaction, per-API rules and the callback included.
Headers are stored under recording.headers, which takes a mode per direction: out of the box request
headers are not kept at all and response headers are kept for failures — the same calls whose response body is
kept. The Headers page covers the modes, the
only and except filters, and the names that are masked.
Recording never breaks the call#
An exception escaping the recorder could otherwise turn into a failure of the request it was observing. So every step of recording is wrapped: an exception is passed to your application's exception handler and then swallowed, and if the exception handler itself throws (a logging channel that cannot be built during a database outage, say), that is swallowed too. The price is that a problem inside Requizon shows up in your error tracker, not as a failed call.
The promise fallback#
A few handlers never invoke on_stats: some SDK shims, and some of the stub shapes
Http::fake() accepts. With instrument.promise_fallback on, Requizon notices when a transfer
finished without stats and records it anyway, timed by the wall clock. That time includes any wait in a request pool,
so treat it as a guarantee the call is not lost rather than as a measurement. The same goes for an attempt whose stats
carry no transfer time, as Http::fake()'s do: with the fallback off, it counts as 0 ms.
In your test suite#
Http::fake() responses go through the same middleware, so a test suite with Requizon enabled writes rows
to requizon_http_requests, timed by the clock as no transfer took place. Turn it off for tests:
<!-- phpunit.xml -->
<env name="REQUIZON_ENABLED" value="false"/>
What is never recorded#
Anything that goes through neither Guzzle nor client discovery:
file_get_contents() on a URL, raw cURL, Symfony's HttpClient used directly, and SDKs built on another
transport. Requizon has no hook in those, and cannot see them.