tiny-blocks / logger
Provides structured logging with support for correlation tracking and configurable sensitive data redaction.
Requires
- php: ^8.5
- psr/log: ^3.0
- tiny-blocks/collection: ^1.15
Requires (Dev)
- infection/infection: ^0.32
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
- squizlabs/php_codesniffer: ^4.0
README
Overview
Provides structured logging with support for correlation tracking and configurable sensitive data redaction.
Built on top of PSR-3, the library can be used anywhere a LoggerInterface is
expected.
Installation
composer require tiny-blocks/logger
How to use
Basic logging
Create a logger with StructuredLogger::create() and use the fluent builder to configure it. All PSR-3 log levels are
supported: debug, info, notice, warning, error, critical, alert, and emergency.
use TinyBlocks\Logger\StructuredLogger; $logger = StructuredLogger::create() ->withComponent(component: 'order-service') ->build(); $logger->info(message: 'order.placed', context: ['orderId' => 42]);
Output (default template, written to STDERR):
2026-02-21T16:00:00+00:00 component=order-service correlation_id= level=INFO key=order.placed data={"orderId":42}
Correlation tracking
A correlation ID can be attached at creation time or derived later using withContext. The original instance is never
mutated.
At creation time
use TinyBlocks\Logger\LogContext; use TinyBlocks\Logger\StructuredLogger; $logger = StructuredLogger::create() ->withContext(context: LogContext::from(correlationId: 'req-abc-123')) ->withComponent(component: 'payment-service') ->build(); $logger->info(message: 'payment.started', context: ['amount' => 100.50]);
Derived from an existing logger
use TinyBlocks\Logger\LogContext; use TinyBlocks\Logger\StructuredLogger; $logger = StructuredLogger::create() ->withComponent(component: 'payment-service') ->build(); $contextual = $logger->withContext(context: LogContext::from(correlationId: 'req-abc-123')); $contextual->info(message: 'payment.started', context: ['amount' => 100.50]);
Sensitive data redaction
Redaction is optional and configurable. Built-in redaction strategies are provided for common sensitive fields. Each strategy accepts multiple field name variations and a configurable masking length.
Document redaction
Masks all characters except the last N (default: 3).
use TinyBlocks\Logger\StructuredLogger; use TinyBlocks\Logger\Redactions\DocumentRedaction; $logger = StructuredLogger::create() ->withComponent(component: 'kyc-service') ->withRedactions(DocumentRedaction::default()) ->build(); $logger->info(message: 'kyc.verified', context: ['document' => '12345678900']); # document → "********900"
With custom fields and visible length:
use TinyBlocks\Logger\Redactions\DocumentRedaction; DocumentRedaction::from(fields: ['cpf', 'cnpj'], visibleSuffixLength: 5); # cpf "12345678900" → "******78900" # cnpj "12345678000199" → "*********00199"
Email redaction
Preserves the first N characters of the local part (default: 2) and the full domain.
use TinyBlocks\Logger\StructuredLogger; use TinyBlocks\Logger\Redactions\EmailRedaction; $logger = StructuredLogger::create() ->withComponent(component: 'user-service') ->withRedactions(EmailRedaction::default()) ->build(); $logger->info(message: 'user.registered', context: ['email' => 'john@example.com']); # email → "jo**@example.com"
With custom fields:
use TinyBlocks\Logger\Redactions\EmailRedaction; EmailRedaction::from(fields: ['email', 'contact_email', 'recoveryEmail'], visiblePrefixLength: 2);
Phone redaction
Masks all characters except the last N (default: 4).
use TinyBlocks\Logger\StructuredLogger; use TinyBlocks\Logger\Redactions\PhoneRedaction; $logger = StructuredLogger::create() ->withComponent(component: 'notification-service') ->withRedactions(PhoneRedaction::default()) ->build(); $logger->info(message: 'sms.sent', context: ['phone' => '+5511999887766']); # phone → "**********7766"
With custom fields:
use TinyBlocks\Logger\Redactions\PhoneRedaction; PhoneRedaction::from(fields: ['phone', 'mobile', 'whatsapp'], visibleSuffixLength: 4);
Password redaction
Masks the entire value with a fixed-length mask (default: 8 characters). The original value's length is never revealed in the output, preventing information leakage about password size.
use TinyBlocks\Logger\StructuredLogger; use TinyBlocks\Logger\Redactions\PasswordRedaction; $logger = StructuredLogger::create() ->withComponent(component: 'auth-service') ->withRedactions(PasswordRedaction::default()) ->build(); $logger->info(message: 'login.attempt', context: ['password' => 's3cr3t!']); # password → "********" $logger->info(message: 'login.attempt', context: ['password' => '123']); # password → "********" (same mask regardless of length)
With custom fields and fixed mask length:
use TinyBlocks\Logger\Redactions\PasswordRedaction; PasswordRedaction::from(fields: ['password', 'secret', 'token'], fixedMaskLength: 12); # "s3cr3t!" → "************" # "ab" → "************" # "long_password" → "************"
Name redaction
Preserves the first N characters (default: 2) and masks the rest.
use TinyBlocks\Logger\StructuredLogger; use TinyBlocks\Logger\Redactions\NameRedaction; $logger = StructuredLogger::create() ->withComponent(component: 'user-service') ->withRedactions(NameRedaction::default()) ->build(); $logger->info(message: 'user.created', context: ['name' => 'Gustavo']); # name → "Gu*****"
With custom fields and visible length:
use TinyBlocks\Logger\Redactions\NameRedaction; NameRedaction::from(fields: ['name', 'full_name', 'firstName'], visiblePrefixLength: 3); # "Gustavo" → "Gus****" # "Gustavo Freze" → "Gus**********" # "Maria" → "Mar**"
Composing multiple redactions
use TinyBlocks\Logger\StructuredLogger; use TinyBlocks\Logger\Redactions\DocumentRedaction; use TinyBlocks\Logger\Redactions\EmailRedaction; use TinyBlocks\Logger\Redactions\NameRedaction; use TinyBlocks\Logger\Redactions\PasswordRedaction; use TinyBlocks\Logger\Redactions\PhoneRedaction; $logger = StructuredLogger::create() ->withComponent(component: 'user-service') ->withRedactions( DocumentRedaction::default(), EmailRedaction::default(), PhoneRedaction::default(), PasswordRedaction::default(), NameRedaction::default() ) ->build(); $logger->info(message: 'user.registered', context: [ 'document' => '12345678900', 'email' => 'john@example.com', 'phone' => '+5511999887766', 'password' => 's3cr3t!', 'name' => 'John', 'status' => 'active' ]); # document → "********900" # email → "jo**@example.com" # phone → "**********7766" # password → "*******" # name → "Jo**" # status → "active" (unchanged)
Custom redaction
Implement the Redaction interface to create your own strategy:
use TinyBlocks\Logger\Redaction; final readonly class TokenRedaction implements Redaction { public function redact(array $data): array { foreach ($data as $key => $value) { if (is_array($value)) { $data[$key] = $this->redact(data: $value); continue; } if ($key === 'token' && is_string($value)) { $data[$key] = '***REDACTED***'; } } return $data; } }
Then add it to the logger:
use TinyBlocks\Logger\StructuredLogger; $logger = StructuredLogger::create() ->withComponent(component: 'auth-service') ->withRedactions(new TokenRedaction()) ->build(); $logger->info(message: 'user.logged_in', context: ['token' => 'abc123']); # token → "***REDACTED***"
Custom log template
The default output template is:
%s component=%s correlation_id=%s level=%s key=%s data=%s
You can replace it with any sprintf compatible template that accepts six string arguments (timestamp, component,
correlationId, level, key, data):
use TinyBlocks\Logger\StructuredLogger; $logger = StructuredLogger::create() ->withComponent(component: 'custom-service') ->withTemplate(template: "[%s] %s | %s | %s | %s | %s\n") ->build(); $logger->info(message: 'custom.event', context: ['value' => 42]); # [2026-02-21T16:00:00+00:00] custom-service | | INFO | custom.event | {"value":42}
License
Logger is licensed under MIT.
Contributing
Please follow the contributing guidelines to contribute to the project.