Processors
The built-in Monolog processors — what they add to every log record, how they are ordered, and how to configure each one.
Processors
Monolog processors run on every log record before it reaches a handler. They add structured fields to the record's extra array — data like trace IDs, file locations, service metadata, request context, and (for the Audit channel) hash-chain links. Vortos ships six processors and applies them in a fixed order.
Processor Execution Order
Processors are added in reverse push order, so they execute as follows:
1. StructuredLogProcessor adds ECS/OTel service fields
2. RequestContextProcessor adds http, user, tenant context
3. CorrelationIdProcessor adds trace_id (when tracing is active)
4. IntrospectionProcessor adds file, line, class, function
5. RedactionProcessor redacts sensitive values
6. HashChainProcessor chains the final record ← Audit channel only, runs lastRedaction runs after every other processor has added its fields, so it can scrub anything they introduce. The hash chain (Audit channel only) runs after redaction, so record_hash covers the final, fully-enriched and redacted record.
IntrospectionProcessor
Adds the source location of the log call to every record.
Fields added to extra:
{
"file": "src/User/Application/RegisterUserHandler.php",
"line": 42,
"class": "App\\User\\Application\\RegisterUserHandler",
"function": "__invoke"
}Configuration:
$config->introspection(bool $enabled = true);Enabled by default in dev. Disabled by default in prod — file/line lookup traverses the call stack and adds overhead at high throughput.
CorrelationIdProcessor
Injects the active OpenTelemetry trace ID into every log record. When combined with a tracing backend (Jaeger, Tempo), you can jump from a log line to the exact trace that produced it.
Fields added to extra:
{
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}When vortos-tracing is not active or the current request has no active span, the processor silently skips — no trace_id field is added.
Configuration:
$config->correlationId(bool $enabled = true);Enabled by default when vortos-tracing is installed.
RequestContextProcessor
Adds HTTP request and user identity context to every record produced during a request. In a FrankenPHP worker where a single PHP process handles many requests sequentially, this processor reads fresh context per-record — it does not cache the request at startup.
Fields added to extra:
{
"http": {
"method": "POST",
"path": "/users",
"client_ip": "203.0.113.10",
"user_agent": "Mozilla/5.0 ..."
},
"user": {
"id": "usr_01hx...",
"tenant_id": "org_01hy..."
}
}user.id and user.tenant_id are populated only when a UserIdentityInterface is available in the current request context. Outside HTTP requests (console commands, worker processes), the http block is omitted.
Configuration:
$config->requestContext(bool $enabled = true);Enabled by default.
StructuredLogProcessor
Adds Elastic Common Schema (ECS) and OpenTelemetry-compatible service fields to every record. Log aggregators that understand ECS (Kibana, Datadog) use these fields for automatic service grouping and environment filtering.
Fields added to extra:
{
"service.name": "myapp",
"service.version": "1.2.0",
"deployment.environment": "production",
"event.dataset": "myapp.app",
"log.logger": "app"
}Configuration:
$config->structured(bool $enabled = true);
$config->service(string $name, string $version = '', string $environment = '');Set your service name, version, and environment before the container compiles:
return static function (VortosLoggingConfig $config): void {
$config->structured(true);
$config->service(
name: $_ENV['APP_NAME'] ?? 'myapp',
version: $_ENV['APP_VERSION'] ?? '',
environment: $_ENV['APP_ENV'] ?? 'prod',
);
};RedactionProcessor
Scrubs sensitive values from the context and extra arrays before any record reaches a handler. Runs last — after all other processors have added their fields — to catch values introduced by any processor.
Default redacted keys:
password, passwd, secret, token, access_token, refresh_token,
authorization, api_key, apikey, private_key, client_secret,
email, phone, ssn, cookie, set_cookieBehaviour:
- Exact key match —
O(1)lookup against a normalized set - Nested arrays — scanned recursively up to a fixed depth
- Message string —
\rand\nescaped to mitigate CRLF log injection - Wildcard custom keys — regex fallback, only applied when custom keys are configured
Example:
Input record context:
{
"email": "alice@example.com",
"password": "hunter2",
"ip": "203.0.113.10"
}After redaction:
{
"email": "[REDACTED]",
"password": "[REDACTED]",
"ip": "203.0.113.10"
}Configuration:
// Enable/disable redaction (enabled by default)
$config->redaction(bool $enabled = true);
// Add custom keys to the redaction list
$config->redaction(true, ['card_number', 'iban', 'national_id']);Redaction Is Not a Safety Net
Pattern-based redaction catches common shapes (JWTs, AWS keys, auth headers, card numbers, emails) but not arbitrary PII. If you concatenate sensitive data into the message string — $logger->info("User {$email} logged in") — only values matching a known pattern are redacted. Always use structured context and static message strings.
HashChainProcessor
Appends a tamper-evident hash chain to every record on a sink configured with hashChain(true) — the Audit sink, by default. Each record's hash covers the previous record's hash, so altering or deleting a historical record invalidates every subsequent hash.
Fields added to extra:
{
"prev_hash": "3f2a9c...",
"record_hash": "9c1b4e..."
}record_hash = sha256(prev_hash + canonical_json(record)). The first record in a chain uses a fixed genesis value for prev_hash.
Configuration:
$config->sink(LogChannel::Audit->value)->hashChain(true); // default for AuditState (the running prev_hash) is held by a HashChainStateInterface implementation, registered as vortos.logger.hash_chain_state. The default InMemoryHashChainState resets on process restart — implement the interface against persistent storage for a continuous chain across restarts.