aubes / shadow-logger-bundle
Monolog processor for anonymization
Package info
github.com/aubes/shadow-logger-bundle
Type:symfony-bundle
pkg:composer/aubes/shadow-logger-bundle
Requires
- php: >=8.1
- ext-openssl: *
- monolog/monolog: ^2.0 | ^3.0
- symfony/http-foundation: ^6.0 | ^7.0 | ^8.0
- symfony/http-kernel: ^6.0 | ^7.0 | ^8.0
- symfony/polyfill-php80: ^1.0
- symfony/property-access: ^6.0 | ^7.0 | ^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.1
- phpmd/phpmd: ^2.10
- phpspec/prophecy-phpunit: >=v2.0.1
- phpunit/phpunit: >=9.6
- vimeo/psalm: ^5.9 | ^6.0
This package is auto-updated.
Last update: 2026-03-16 21:20:20 UTC
README
This Symfony bundle provides a Monolog processor to transform log data in order to respect GDPR or anonymize sensitive data.
It allows IP anonymization, hashing, encryption, or removal of sensitive fields in logs.
Requirements
- PHP >= 8.1
- Symfony 6, 7 or 8
- Monolog 2 or 3
Installation
composer require aubes/shadow-logger-bundle
Configuration
# config/packages/shadow-logger.yaml shadow_logger: # Add "shadow-debug" in "extra" when a transformer throws an exception. # Recommended to use '%kernel.debug%' so it is only active in development. debug: '%kernel.debug%' # When a transformer throws an exception: # true → the field value is set to null # false → the original (untransformed) value is kept strict: true # Register ShadowProcessor on handlers OR channels (not both). # Scoping to specific handlers/channels is recommended for performance. handlers: ['app'] #channels: ['app'] # Salt used by the "hash" transformer (see Hash transformer section). encoder: salt: '%env(SHADOW_LOGGER_ENCODER_SALT)%' mapping: # Fields to transform in the log "context" context: user_ip: ['ip'] user_name: ['hash'] user_birthdate: ['remove'] # Fields to transform in the log "extra" extra: custom_field: ['remove']
Choosing between hashing and encryption
Both the hash and encrypt transformers protect sensitive data in logs, but they serve different purposes. Choosing the right one depends on what you need to do with the data after it is logged.
Hashing (hash)
Hashing is a one-way, irreversible operation. The original value cannot be recovered from the hash.
Use it when:
- You do not need to read back the original value
- You want to correlate log entries belonging to the same user (e.g. trace a user across multiple requests) without storing their identity — the same input always produces the same hash
- You want to pseudonymize data in compliance with GDPR
Limitations:
- If the space of possible values is small (e.g. an IP address), an attacker with access to the logs could reconstruct the original values through brute force
- The salt must be kept secret; rotating it means previously hashed values can no longer be correlated with new ones
- You cannot fulfill a GDPR "right of access" request using only the hashed value
Configuration:
shadow_logger: encoder: algo: 'sha256' # any algorithm from hash_algos() salt: '%env(SHADOW_LOGGER_ENCODER_SALT)%' binary: false
Always configure a
saltto prevent rainbow table attacks. Store it as a secret environment variable.
Encryption (encrypt)
Encryption is a reversible operation. The original value can be recovered using the key and the IV stored alongside it.
Use it when:
- You may need to read back the original value later (e.g. to respond to a GDPR right of access or right of erasure request, or to debug a production issue with proper authorization)
- You want to store sensitive data in logs in a protected form without losing it permanently
Limitations:
- Requires secure key management: if the key is compromised, all encrypted log entries are exposed
- Key rotation is complex: old entries encrypted with a previous key can no longer be decrypted with the new one
- The presence of the IV in the log reveals that the original field was non-empty, which may itself be sensitive
- More computationally expensive than hashing
Quick comparison
hash |
encrypt |
remove |
|
|---|---|---|---|
| Reversible | No | Yes (with key) | No |
| Correlate entries | Yes (same input → same output) | No (IV is random) | No |
| Protect against brute force | With a strong salt | Yes | Yes |
| GDPR right of access | No | Yes | No |
| Key management required | Salt only | Key + IV | None |
Transformers
The following transformers are available out of the box:
| Alias | Description |
|---|---|
ip |
Anonymizes an IPv4 or IPv6 address |
hash |
Hashes the value using the configured algorithm |
string |
Casts a scalar or Stringable object to string |
remove |
Replaces the value with --obfuscated-- |
encrypt |
Encrypts the value (requires encryptor configuration) |
truncate |
Masks the middle of a value, keeping start and/or end visible (requires truncator configuration) |
Chaining transformers
Transformers can be chained. They are applied in order, the output of one becoming the input of the next. For example, to hash a Stringable object:
shadow_logger: mapping: context: custom_field: ['string', 'hash']
Hash transformer
The hash transformer uses the configured encoder. See the Hashing section above for configuration options.
Encrypt transformer
The encrypt transformer supports two modes: a built-in encryptor based on OpenSSL, or a custom implementation.
Built-in encryptor
shadow_logger: encryptor: key: '%env(SHADOW_LOGGER_ENCRYPTOR_KEY)%' cipher: 'aes-256-cbc' # optional, default: aes-256-cbc
The key must be kept secret and should be provided via an environment variable. The cipher can be any algorithm supported by OpenSSL (openssl_get_cipher_methods()).
Custom encryptor
If you need a different encryption strategy, implement EncryptorInterface:
// src/Encryptor/EncryptorAdapter.php namespace App\Encryptor; use Aubes\ShadowLoggerBundle\Encryptor\EncryptorInterface; class EncryptorAdapter implements EncryptorInterface { public function encrypt(string $data, string $iv): string { // your encryption logic return $encryptedValue; } public function generateIv(): string { // generate a random IV return $iv; } }
Register it as a service (if not using autoconfiguration):
# config/services.yaml services: App\Encryptor\EncryptorAdapter: ~
Then reference it by its service ID:
shadow_logger: encryptor: 'App\Encryptor\EncryptorAdapter'
Output format
In both cases, the transformer replaces the field value with:
[
'iv' => 'abc123', // base64-encoded IV used during encryption
'value' => '...', // encrypted value
]
Truncate transformer
The truncate transformer masks the middle of a value while keeping a configurable number of characters visible at the start and/or end. It is useful for partially revealing values like card numbers, email addresses, or tokens.
Named variants are declared under truncators. Each variant becomes available as a transformer alias: default → truncate, others → truncate_{name}.
shadow_logger: truncators: default: # alias: "truncate" keep_start: 2 keep_end: 2 mask: '***' card: # alias: "truncate_card" keep_start: 4 keep_end: 4 mask: '****' email: # alias: "truncate_email" keep_start: 1 keep_end: 0 mask: '***' mapping: context: card_number: ['truncate_card'] # 4242424242424242 → 4242****4242 email: ['truncate_email'] # john@example.com → j*** token: ['truncate'] # abcdef1234 → ab***34
| Option | Description | Default |
|---|---|---|
keep_start |
Number of characters to keep at the beginning | 2 |
keep_end |
Number of characters to keep at the end | 2 |
mask |
String used to replace the hidden part | *** |
If the value is shorter than or equal to
keep_start + keep_end, it is replaced entirely by the mask.
Mapping
Nested fields
Field names can use dot notation to target nested array keys.
Given the following extra structure:
'user' => [ 'id' => 42, 'name' => [ 'first' => 'John', 'last' => 'Doe', ], 'ip' => '1.2.3.4', ]
You can map nested fields like this:
shadow_logger: mapping: extra: user.ip: ['ip'] user.name.first: ['hash'] user.name.last: ['remove']
Note: Dot notation uses the Symfony PropertyAccessor internally, which is slower than direct key access. Prefer flat field names when possible.
Custom transformer
Implement TransformerInterface:
// src/Transformer/CustomTransformer.php namespace App\Transformer; use Aubes\ShadowLoggerBundle\Transformer\TransformerInterface; class CustomTransformer implements TransformerInterface { public function transform(mixed $data): mixed { // transform and return the value return $data; } }
Register it as a service with the shadow_logger.transformer tag and an alias:
# config/services.yaml services: App\Transformer\CustomTransformer: tags: - { name: 'shadow_logger.transformer', alias: 'custom' }
The alias is the name used in the mapping configuration:
shadow_logger: mapping: context: some_field: ['custom']