Mask sensitive data
Secrets slip into logs easily — a request body with a password, a header with an
authorization token. SensitiveMaskingProcessor redacts them before they reach any
handler, so they never hit disk or your aggregator.
Add the processor to a channel
The processor is optional — add it after building the manager. Push it onto the channel’s underlying Monolog instance:
use Flytachi\Winter\Logger\Processor\SensitiveMaskingProcessor;
$manager->channel('http')->monolog()->pushProcessor(new SensitiveMaskingProcessor());Now any matching key is replaced with *** in both context and extra:
LoggerFactory::getLogger(self::class)->info('login attempt', [
'username' => 'alice',
'password' => 'hunter2', // ← masked
'metadata' => [
'token' => 'secret-jwt', // ← nested, also masked
],
]);[2024-01-01 12:00:00] [INFO ] -http- [4821] (AuthService): login attempt {"username":"alice","password":"***","metadata":{"token":"***"}}What it matches
- Case-insensitive on keys —
Password,PASSWORD, andpasswordall match. - Recursive — nested arrays are traversed to any depth.
- Applies to the per-call
context. Fields coming from the context storage land inextralater — see the next section.
Processor order: why extra is not masked
pushProcessor() puts a processor at the front of the queue, and Monolog runs the queue
top to bottom. The channel already carries a ContextInjectingProcessor (the manager adds it
at build time), so masking added afterwards ends up ahead of it and runs before the
context fields are part of the record at all:
0. SensitiveMaskingProcessor ← runs first: extra is still empty
1. ContextInjectingProcessor ← merges the context fields in afterwardsFor the per-call context this makes no difference — it is there from the start. But a
secret placed in the context storage ($storage->set('authorization', ...)) reaches the log
unmasked.
If those fields must be masked too, swap the order: take the context processor off, put masking underneath it, and push it back on top.
$monolog = $manager->channel('http')->monolog();
$contextProcessor = $monolog->popProcessor(); // ContextInjectingProcessor
$monolog->pushProcessor(new SensitiveMaskingProcessor());
$monolog->pushProcessor($contextProcessor); // back on top — it now runs firstSimpler still: keep secrets out of the context storage. It is for request and user identifiers, not for authorization headers.
The default masked keys (all replaced with ***):
password passwd secret token access_token refresh_token
api_key apikey authorization auth cookie set-cookie
credit_card card_number cvv ssn pinThe authoritative list lives in Log format reference.
Add your own keys
Pass extra keys to the constructor — they’re merged with the defaults:
$processor = new SensitiveMaskingProcessor(['patient_id', 'insurance_number']);
$manager->channel('http')->monolog()->pushProcessor($processor);Apply it to every channel
There’s no global switch — add the processor to each channel you want masked. A small loop at bootstrap covers them all:
foreach (['http', 'cli', 'sys'] as $name) {
if ($manager->hasChannel($name)) {
$manager->channel($name)->monolog()->pushProcessor(new SensitiveMaskingProcessor());
}
}Values are matched by key, not by content
The processor redacts based on the field name, not the value. A secret logged under an
unlisted key (e.g. data) or interpolated into the message string won’t be caught — keep
secrets in well-named context keys, and never put them in the message text.
Related
- Log format → Masked keys — the full default list
- API reference → Processors — constructor signature
- Request context — where
extrafields come from