tiime/monolog-masker

A lightweight, zero-dependency Monolog processor to keep sensitive data and secrets out of your logs.

Maintainers

Package info

github.com/Tiime-Software/MonologMasker

pkg:composer/tiime/monolog-masker

Statistics

Installs: 7

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0-beta 2026-05-29 15:48 UTC

This package is auto-updated.

Last update: 2026-05-29 15:54:03 UTC


README

logo Tiime
Monolog Masker

PHP Version Monolog Version License

A lightweight, zero-dependency Monolog processor to keep sensitive data and secrets out of your logs.

🛑 The Problem

When logging requests, exceptions, or context arrays, it's easy to accidentally leak sensitive information like passwords, API keys, or Personally Identifiable Information (PII) into your log files or monitoring systems (Datadog, Sentry, ELK). This can lead to security breaches and GDPR compliance issues.

✅ The Solution

monolog-masker provides a simple Processor for Monolog that recursively intercepts and masks sensitive keys before they are ever written to your logs.

📦 Installation

You can install the package via composer:

composer require tiime/monolog-masker

🚀 Quick start

Push the processor onto your logger. With the defaults you are protected in one line:

use Monolog\Logger;
use Tiime\MonologMasker\MaskerBuilder;

$logger = new Logger('app');
// Push FIRST so Monolog runs it LAST — see "Processor ordering" below.
$logger->pushProcessor(MaskerBuilder::create()->buildProcessor());

$logger->info('payment', [
    'db_password' => 'super-secret',
    'card_number' => '4242 4242 4242 4242',
    'amount'      => 1000,
]);
// context becomes:
// ['db_password' => '████████', 'card_number' => '████████', 'amount' => 1000]

The processor walks message, context and extra and masks, secure by default:

  • Sensitive keys — values whose key contains a known sensitive segment (password, token, api_key, authorization, …). Matching is segment-aware, so compound names like db_password, userToken or x-api-key are caught too (but not tokenizer). The whole sub-tree under a sensitive key is collapsed.
  • Sensitive values — tokens that look like a secret or PII (email, IBAN, JWT, Bearer …, AWS/Google keys, PEM blocks, and Luhn-validated card numbers), wherever they appear — including the log message. Only the matched sub-string is masked, so surrounding text is preserved.
  • Objects in the context are traversed too (JsonSerializable, Stringable, or public properties), so secrets carried by DTOs don't slip through unmasked.

⚙️ Configuration

Everything is wired through the fluent, immutable MaskerBuilder:

use Tiime\MonologMasker\MaskerBuilder;
use Tiime\MonologMasker\Strategy\PartialMaskStrategy;

$processor = MaskerBuilder::create()
    ->withSensitiveKeys(['x-internal-token'])   // add to the default key list
    ->withValuePatterns(['fr_phone' => '/\b0[1-9](?:\d{2}){4}\b/'])
    ->withStrategy(new PartialMaskStrategy(visible: 4)) // keep last 4 chars: "███1234"
    ->maxDepth(20)
    ->buildProcessor();

$logger->pushProcessor($processor);

Other knobs:

  • withKeyMatcher() / withValueMatcher() — replace detection entirely with your own KeyMatcherInterface / ValueMatcherInterface.
  • withoutValueMatching() — key-based masking only (skip value detection).
  • matchKeysExactly() — whole-string key matching instead of segment-aware (no compound-key detection, fewer false positives).
  • maskMessage(false) — stop masking the log message.
  • traverseObjects(false) — leave objects untouched.

Processor ordering

Monolog runs processors in reverse of their push order. To also mask the extra data added by other processors (e.g. WebProcessor), push the masker first so it runs last.

Depth limit

Recursion is bounded by maxDepth (default 16); anything deeper — and any object cycle — is replaced with [TRUNCATED] rather than traversed.

Masking strategies

Strategy Result When
FullMaskStrategy (default) ████████ Zero leakage — recommended
PartialMaskStrategy(visible: N) ███1234 Keep a tail for debugging/correlation

Both are configurable (placeholder, mask character, number of visible chars), and you can implement MaskStrategyInterface for anything else.

🎼 Symfony integration

Register the processor with the MonologBundle via the monolog.processor tag.

Heads-up: MaskerBuilder is immutable (with*() returns a new instance), so Symfony's calls: cannot configure it. Use the builder as a factory object (defaults) or a small factory class (custom config).

Defaults — one service:

# config/services.yaml
services:
    monolog_masker.builder:
        class: Tiime\MonologMasker\MaskerBuilder
        factory: ['Tiime\MonologMasker\MaskerBuilder', 'create']

    Tiime\MonologMasker\Processor\MaskingProcessor:
        factory: ['@monolog_masker.builder', 'buildProcessor']
        tags:
            # low priority so it runs LAST and also masks `extra` from other processors
            - { name: monolog.processor, priority: -100 }

Custom config — via a factory class:

// src/Logging/MaskingProcessorFactory.php
namespace App\Logging;

use Tiime\MonologMasker\MaskerBuilder;
use Tiime\MonologMasker\Processor\MaskingProcessor;
use Tiime\MonologMasker\Strategy\PartialMaskStrategy;

final class MaskingProcessorFactory
{
    public static function create(): MaskingProcessor
    {
        return MaskerBuilder::create()
            ->withSensitiveKeys(['x-internal-token'])
            ->withStrategy(new PartialMaskStrategy(visible: 4))
            ->buildProcessor();
    }
}
# config/services.yaml
services:
    Tiime\MonologMasker\Processor\MaskingProcessor:
        factory: ['App\Logging\MaskingProcessorFactory', 'create']
        tags:
            - { name: monolog.processor, priority: -100 }

Scope it to a handler or channel if needed: - { name: monolog.processor, handler: main } (or channel: app).

🧱 Architecture

The masking engine is decoupled from Monolog so it can be tested and reused on its own:

  • Masker — recursive, immutable engine (never mutates its input; traverses arrays and objects; bounded by a max depth that also guards against cycles).
  • MaskingProcessor — thin Monolog adapter (ProcessorInterface).
  • Matcher\* — pluggable detection: keys (KeyListMatcher, SegmentKeyMatcher) and values (RegexValueMatcher, CreditCardMatcher, ChainValueMatcher).
  • Strategy\* — pluggable masking (FullMaskStrategy, PartialMaskStrategy).
  • MaskerBuilder — fluent factory tying it all together with secure defaults.

✅ Development

This package follows the Tiime conventions (Docker + make):

make install    # install dependencies
make validate   # cs-check + phpstan (max) + coverage + infection + proofs — run before any commit
make test       # unit tests only
make coverage   # unit tests + 100% line/method coverage gate (pcov)
make infection  # mutation testing (MSI 100%)
make proofs     # property-based tests (black-box)

The suite is held to 100% line & method coverage and 100% MSI (mutation score, via Infection). Both are enforced as hard gates in CI, so a weak test that executes code without actually asserting its behaviour fails the build.

On top of the example-based unit tests, the library's invariants are checked with property-based tests (innmind/black-box) — idempotence, no-leak, structure preservation, depth bounds, etc. — over hundreds of randomly generated, auto-shrinking inputs. These live in tests/Property (suite property, run via make proofs); coverage and mutation gates stay scoped to the deterministic unit suite.

📄 License

MIT — see LICENSE.