Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Saloon Logger Plug In Laravel Package

bit-mx/saloon-logger-plug-in

A Laravel plug-in for the Saloon HTTP client that adds observability to every HTTP interaction. It logs requests, responses and exceptions to a database table, linking them with a single ULID trace_id, and censors sensitive fields in payloads and headers. Enable it by applying traits to your connect...

View on GitHub
Deep Wiki
Context7

Getting Started

This is a small Laravel package that hooks into Saloon v3 and writes every request/response/exception to a database table, tied together by a single trace_id (ULID). Before writing any custom logging, get one connector instrumented end-to-end so you can see what the stored records actually look like.

Minimal steps:

composer require bit-mx/saloon-logger-plug-in
php artisan vendor:publish --tag=saloon-logger-migrations
php artisan migrate
# optional, do this now rather than later:
php artisan vendor:publish --tag=saloon-logger-config

Then apply the traits:

// Connector
use BitMx\SaloonLoggerPlugIn\Traits\HasLogging;

class StripeConnector extends Connector
{
    use HasLogging;
    public function resolveBaseUrl(): string { return 'https://api.stripe.com'; }
}

// Request — note the contract + trait pair, plus defaultBody()
use BitMx\SaloonLoggerPlugIn\Contracts\HasDefaultBody;
use BitMx\SaloonLoggerPlugIn\Traits\ProvidesDefaultBody;

class ChargeCustomerRequest extends Request implements HasDefaultBody
{
    use ProvidesDefaultBody;

    protected Method $method = Method::POST;

    public function __construct(public string $customerId, public int $amount) {}

    public function resolveEndpoint(): string { return '/v1/charges'; }

    protected function defaultBody(): array
    {
        return ['customer' => $this->customerId, 'amount' => $this->amount];
    }
}

Where to look first:

  1. vendor/bit-mx/saloon-logger-plug-in/database/migrations/* — read the schema before you decide how you'll query it. Column names are what you'll be writing SQL against in an incident.
  2. config/saloon-logger.php after publishing — in particular the sanitizers.request / sanitizers.response arrays and the prune/backup options.
  3. The two traits in src/Traits/ — they're thin; reading them tells you exactly which Saloon lifecycle hooks fire.

First use case: do not roll this out everywhere on day one. Pick your single most painful third-party integration (payment gateway, legacy SOAP-ish API, a partner API that returns unhelpful 500s), instrument only that connector, and run a single request. Then select * from <log table> order by id desc limit 5 and eyeball the request/response/exception rows for the same trace_id. If the body is empty or the headers look wrong, fix that here before touching fifteen more connectors.

Implementation Patterns

Put the trait on a shared base connector. Almost every app has multiple connectors. Rather than remembering to add HasLogging to each, create App\Support\Http\LoggedConnector extends Connector { use HasLogging; } and extend it. This is the difference between "we have logging" and "we have logging on the two connectors someone remembered."

Same for requests: one abstract base. Implement HasDefaultBody on an App\Http\BaseRequest that requires subclasses to implement defaultBody(). Every concrete request then inherits both the contract and the trait. This is the pattern the README implies but doesn't spell out, and it's the only way this stays consistent at scale.

Treat trace_id as your correlation primitive. In an incident, the workflow is: user reports a broken checkout → find the Saloon log row → grab the trace_id → find every other row (and ideally every app log line) with that id. You can make this much stronger by pushing the trace into your normal log context at the point where you dispatch the request:

// pseudo — confirm the actual accessor name in the trait before relying on it
Log::withContext(['saloon_trace_id' => $request->getTraceId()]);

Even without that, the DB is queryable and grep-able, which is the whole point.

Own the sanitizer chain. The default config registers one request sanitizer and one response sanitizer (JSON ones). Treat that array as an ordered pipeline — each sanitizer receives the output of the previous one and must return a value. Anything you add later goes at the end of the array, and a bug in a late sanitizer can silently blank out everything an earlier one produced. Write a unit test per custom sanitizer that asserts both what is redacted and what is preserved.

Custom sanitizers are the extension point you'll actually use. The contracts are simple:

class MaskCardNumber implements SanitizerRequestContract
{
    public static function sanitize(mixed $data): mixed
    {
        if (is_array($data) && isset($data['card']['number'])) {
            $data['card']['number'] = str_repeat('*', 12) . substr($data['card']['number'], -4);
        }
        return $data;
    }
}

For responses, remember the signature is different — sanitize(Response $data) receives a Saloon Response, not an array, so you'll reach for $data->body() / $data->json() / $data->headers() yourself.

Schedule pruning from day one. The config exposes prune and backup options separately. A table that grows by one row per HTTP call gets expensive fast in any app that talks to a third party on every request. Add the prune command to routes/console.php (or your scheduler) in the same PR that adds the package — otherwise six months later nobody knows why the DB is 40 GB.

Environment gating. This package writes rows to your database on every HTTP call. In local/staging that's a gift. In production, on a hot path, it's a write amplification problem and a PII liability. Consider whether a high-traffic connector should skip HasLogging entirely, or whether you accept the write and lean on the prune schedule.

Testing. Because the output is database rows, tests are straightforward assertions rather than mock gymnastics: send a fake Saloon response (MockResponse) through the connector, then assertDatabaseHas on the log table for the expected method, endpoint, and status. This is a genuinely nice property of the package — you get HTTP-observability assertions almost for free.

Gotchas and Tips

Version floors are strict and unusual. The README pins laravel/framework: ^12.35.0 and saloonphp/saloon: ^3.0, PHP ^8.2. That Laravel floor is aggressive — check composer.json in the vendor directory before you assume it installs on your Laravel 11 or 12.x-early project. Note also that much of the third-party commentary floating around about this package references Saloon v4 and LoggerPlugin / $client->withPlugin(...) — that is not how this package works. The traits-and-contract approach in the README is authoritative.

The HasDefaultBody contract is not optional. If you apply ProvidesDefaultBody but forget implements HasDefaultBody — or forget to override defaultBody() — you get either a runtime error or empty payloads in the log. This is the #1 thing to check when "the request body is empty in the DB." It's also worth knowing why it exists: Saloon doesn't expose a request body uniformly for every method, so the package asks you to declare it. That means the logged body and the real wire body can drift if you build the payload somewhere other than defaultBody() (e.g. in a body() override). Keep payload construction in defaultBody() only.

Sanitizers run in config order and are non-idempotent. Each must return the data. Returning null from a request sanitizer doesn't error — it just means the next sanitizer gets null and you silently lose the payload. If your logs suddenly show empty bodies after a config change, audit the sanitizer array in reverse order.

Exceptions are only logged for HTTP/network failures. The README is explicit: a 500 logs the request and the exception. What it doesn't promise is that your own exceptions thrown before/after send() end up in the table. Wrap business logic exceptions in your own logging — don't assume the plugin catches them.

Config caching will bite you. If you tweak sanitizers and nothing changes, you forgot php artisan config:clear. This is a Laravel-wide footgun, but it's especially confusing here because the symptom is silent log-content changes, not an error.

Not an observability platform — do not pitch it as one. There's no metrics export, no OpenTelemetry, no log shipping. It's a well-scoped debugging/audit table. If you need distributed tracing across services, this complements nothing on its own; you'd export the trace_id to your real tracing stack. If your compliance story needs immutable, tamper-evident logs, a mutable database table is the wrong store — ship these rows to object storage with retention locks if that's the requirement (the backup option helps but isn't a substitute).

Maturity signal matters here. Zero stars, zero dependents, a very recent release history, and a changelog entry that is literally "Change to bit namespace." Vendor it mentally as a small, single-maintainer package: read the source (it's small), pin the version, and don't build your only audit trail on it without a fallback. The upside is that the surface area is tiny enough to fork in an afternoon if the maintainer walks away.

Naming inconsistency is a smell you'll hit in IDE autocomplete. The package name uses PlugIn (BitMx\SaloonLoggerPlugIn\...) while prose calls it "plugin/plug-in." When searching the vendor directory or writing use statements, match the namespace casing exactly — PHP namespaces are case-insensitive for resolution but case-sensitive for autoloader path mapping, and class-map collisions here are a classic source of "class not found" that wastes twenty minutes.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource