Package · logger

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:

php
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:

php
LoggerFactory::getLogger(self::class)->info('login attempt', [
  'username' => 'alice',
  'password' => 'hunter2',      // ← masked
  'metadata' => [
      'token' => 'secret-jwt',  // ← nested, also masked
  ],
]);
text
[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, and password all match.
  • Recursive — nested arrays are traversed to any depth.
  • Applies to the per-call context. Fields coming from the context storage land in extra later — 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:

text
0. SensitiveMaskingProcessor   ← runs first: extra is still empty
1. ContextInjectingProcessor   ← merges the context fields in afterwards

For 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.

php
$monolog = $manager->channel('http')->monolog();

$contextProcessor = $monolog->popProcessor();          // ContextInjectingProcessor
$monolog->pushProcessor(new SensitiveMaskingProcessor());
$monolog->pushProcessor($contextProcessor);            // back on top — it now runs first

Simpler 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 ***):

text
password  passwd  secret  token  access_token  refresh_token
api_key  apikey  authorization  auth  cookie  set-cookie
credit_card  card_number  cvv  ssn  pin

The authoritative list lives in Log format reference.

Add your own keys

Pass extra keys to the constructor — they’re merged with the defaults:

php
$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:

php
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.