tiny-blocks/logger

Emits PSR-3 structured logs for PHP, with correlation tracking and configurable sensitive data redaction.

Maintainers

Package info

github.com/tiny-blocks/logger

pkg:composer/tiny-blocks/logger

Statistics

Installs: 251

Dependents: 1

Suggesters: 0

Stars: 1

Open Issues: 1

2.0.0 2026-06-27 12:24 UTC

README

License

Overview

Emits PSR-3 structured logs for PHP, with each entry carrying timestamp, component, correlation id, level, and a structured data payload. Supports pluggable redactions for sensitive fields such as passwords, emails, phone numbers, and identity documents. Built for consumption by log aggregators and SIEM pipelines in production environments.

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.

<?php

declare(strict_types=1);

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

<?php

declare(strict_types=1);

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

<?php

declare(strict_types=1);

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

<?php

declare(strict_types=1);

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:

<?php

declare(strict_types=1);

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.

<?php

declare(strict_types=1);

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:

<?php

declare(strict_types=1);

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

<?php

declare(strict_types=1);

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:

<?php

declare(strict_types=1);

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.

<?php

declare(strict_types=1);

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:

<?php

declare(strict_types=1);

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.

<?php

declare(strict_types=1);

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:

<?php

declare(strict_types=1);

use TinyBlocks\Logger\Redactions\NameRedaction;

NameRedaction::from(fields: ['name', 'full_name', 'firstName'], visiblePrefixLength: 3);
# "Gustavo"       → "Gus****"
# "Gustavo Freze" → "Gus**********"
# "Maria"         → "Mar**"

Composing multiple redactions

<?php

declare(strict_types=1);

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:

<?php

declare(strict_types=1);

use TinyBlocks\Logger\Redaction;

final readonly class TokenRedaction implements Redaction
{
    public function redact(array $payload): array
    {
        foreach ($payload as $key => $value) {
            if (is_array($value)) {
                $payload[$key] = $this->redact(payload: $value);
                continue;
            }

            if ($key === 'token' && is_string($value)) {
                $payload[$key] = '***REDACTED***';
            }
        }

        return $payload;
    }
}

Then add it to the logger:

<?php

declare(strict_types=1);

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

<?php

declare(strict_types=1);

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.