Fluxtail
Log Management Guides

PHP Monolog Guide: Structured Logging and Log Shipping

Configure PHP Monolog 3 for structured JSON logs, safe context, handler ordering, log shipping, and verification in Fluxtail Live Tail.

By Fluxtail Engineering Updated

PHP Monolog 3 turns PSR-3 log calls into structured records that handlers, formatters, and processors can route to files, stdout, or a log collector.

This guide uses Monolog 3.12.0. It shows how to emit newline-delimited JSON, add searchable context, redact sensitive values, preserve exception details, and forward PHP logs to Fluxtail.

Install Monolog 3

Monolog 3 requires PHP 8.1 or newer. Install it through Composer:

composer require monolog/monolog:^3.12
composer show monolog/monolog

The first command follows Composer's standard dependency installation process. The second confirms the installed version.

Monolog implements the PSR-3 logger interface, so application code can depend on Psr\Log\LoggerInterface instead of a specific handler. This makes it easier to change the log destination without rewriting every call site.

Sources: Monolog repository, Composer basic usage, and Monolog releases.

Write structured JSON logs to stdout

Start with a StreamHandler and JsonFormatter. Writing to php://stdout works well in containers because the container runtime or a log collector can forward the output without adding network requests to the PHP request path.

<?php

require __DIR__ . '/vendor/autoload.php';

use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
use Monolog\LogRecord;

$stdout = new StreamHandler('php://stdout', Level::Info);
$stdout->setFormatter(new JsonFormatter(
    appendNewline: true,
    ignoreEmptyContextAndExtra: true,
    includeStacktraces: true,
));

$logger = new Logger('checkout');

$logger->pushProcessor(
    static fn (LogRecord $record): LogRecord => $record->with(
        extra: array_merge($record->extra, [
            'service' => 'checkout-api',
            'environment' => getenv('APP_ENV') ?: 'production',
            'host' => gethostname() ?: 'unknown',
        ]),
    ),
);

$logger->pushHandler($stdout);

$logger->info('Order accepted', [
    'order_id' => 'ord_4821',
    'request_id' => 'req_b719',
    'payment_provider' => 'stripe',
]);

A record from this example looks like the following line; the timestamp and host vary at runtime:

{"message":"Order accepted","context":{"order_id":"ord_4821","request_id":"req_b719","payment_provider":"stripe"},"level":200,"level_name":"INFO","channel":"checkout","datetime":"2026-09-15T14:32:08.410000+00:00","extra":{"service":"checkout-api","environment":"production","host":"php-03"}}

Keep identifiers such as request_id, order_id, and deployment_id as separate fields. A message like Order ord_4821 accepted for req_b719 is harder to filter and aggregate.

The Monolog handler, formatter, and processor documentation describes the built-in components and their responsibilities.

Understand handlers, channels, and processors

A Monolog record passes through three main building blocks:

  • A handler decides where a matching record goes.
  • A formatter converts the record into JSON, text, or another wire format.
  • A processor adds or changes fields before handlers receive the record.

A channel identifies the part of the application that created the record. Levels such as INFO, WARNING, and ERROR describe severity.

Handler order and bubbling

pushHandler() adds a handler to the top of the stack. The last handler pushed runs first.

use Monolog\Handler\StreamHandler;
use Monolog\Level;

$logger->pushHandler(
    new StreamHandler('php://stdout', Level::Info),
);

$logger->pushHandler(
    new StreamHandler('/var/log/checkout-errors.log', Level::Error),
);

For an error, the file handler runs first. Its default bubble value is true, so the record continues to the stdout handler. Set bubbling to false only when a matching handler should stop later handlers from receiving the record.

Handler order matters when combining filters, buffers, files, and network destinations. Define it deliberately and test both ordinary and error-level records.

Channels identify log sources

Use short, stable channel names such as checkout, billing, or worker:

$billingLogger = new Logger('billing');
$workerLogger = new Logger('worker');

Do not create a new channel for each customer, request, or job. Put those values in structured context instead. Stable channels remain useful for filtering, while high-cardinality identifiers still stay searchable.

Processors add shared context

A processor prevents every call site from repeating fields such as service name, environment, version, or request ID.

Request-specific context can come from middleware:

$logger->pushProcessor(
    static fn (LogRecord $record): LogRecord => $record->with(
        extra: array_merge($record->extra, [
            'request_id' => $_SERVER['HTTP_X_REQUEST_ID'] ?? null,
            'service_version' => getenv('APP_VERSION') ?: 'unknown',
        ]),
    ),
);

Avoid adding entire request bodies, session objects, or user models. Extract only fields that are safe and useful during investigation.

The Monolog usage guide covers handler stacks, processors, and channels in more detail.

Redact secrets before logs leave the application

The safest rule is to never pass credentials or private payloads to the logger. Redaction is a second line of defense, not permission to log everything. Monolog 3.12 includes a formatter for masking sensitive values:

use Monolog\Formatter\JsonFormatter;
use Monolog\Formatter\RedactingFormatter;

$json = new JsonFormatter(
    appendNewline: true,
    ignoreEmptyContextAndExtra: true,
    includeStacktraces: true,
);

$stdout->setFormatter(new RedactingFormatter(
    formatter: $json,
    sensitiveKeys: [
        'authorization',
        'password',
        'passwd',
        'pwd',
        'secret',
        'token',
        'access_token',
        'api_key',
        'apikey',
        'auth',
        'cookie',
    ],
));

RedactingFormatter masks matching keys recursively and also checks the formatted output for copies of those secret values. Avoid sending sensitive data to the logger in the first place; redaction is a second line of defense. Monolog introduced this formatter in the 3.11 release line and improved nested-array redaction in 3.12, as documented in the current Monolog releases.

Record exceptions without losing context

Pass the exception object in context. JsonFormatter can normalize it and include the stack trace when includeStacktraces is enabled.

try {
    $paymentGateway->authorize($order);
} catch (Throwable $exception) {
    $logger->error('Payment authorization failed', [
        'exception' => $exception,
        'order_id' => $order->id,
        'request_id' => $requestId,
        'provider' => 'stripe',
    ]);

    throw $exception;
}

Use a clear event message plus stable fields. Do not place the full exception text inside the message, because that makes matching equivalent failures harder.

Ship Monolog logs to Fluxtail

For containerized PHP services, keep Monolog on stdout and let a collector handle remote delivery:

  1. Monolog writes one JSON object per line.
  2. The container runtime captures stdout.
  3. A collector parses each JSON line.
  4. The collector forwards records to the exact Fluxtail receiver URL.
  5. Fluxtail stores the records in the receiver's configured stream.

Use the receiver-bound token created for that active receiver and send it as an Authorization: Bearer header. Do not append a path to the receiver URL shown by Fluxtail. Keep the URL and token in secret-backed configuration.

Configure buffering, retries, and short timeouts in the collector so a temporary network failure does not delay PHP requests. Confirm that the collector parses each line as JSON instead of nesting the entire line inside a plain message field.

See log aggregation for the broader collection path, log management for retention and investigation practices, and the Fluxtail Live Tail page for the resulting investigation flow.

Verify PHP logs in Fluxtail

After forwarding begins, emit a recognizable test event:

$logger->notice('Deployment verification', [
    'deployment_id' => 'deploy_2026_09_15_01',
]);

Open the Fluxtail Live Tail and verify:

  1. Select the receiver's configured stream.
  2. Search for Deployment verification.
  3. Confirm the event has NOTICE severity.
  4. Confirm the collector preserved service, environment, and host.
  5. Inspect deployment_id and the exception context to confirm structured data survived ingestion.

Consistent fields make the same logs easier to investigate through Live Tail, hosted OAuth MCP connections, and Fluxtail's built-in AI log analysis. Agents and AI chat are most useful after service names, request IDs, environments, and error context are structured instead of buried in message strings.

Avoid common Monolog mistakes

  • Do not concatenate searchable identifiers into the message.
  • Do not log authorization headers, cookies, passwords, or access tokens.
  • Do not create channels from request IDs or customer IDs.
  • Do not assume the first pushed handler runs first.
  • Do not disable bubbling without checking which destinations stop receiving errors.
  • Do not put an unbuffered remote request in every application log call.
  • Do not emit multiline JSON when the collector expects one event per line.
  • Do not rename fields differently in each service.
  • Do not treat successful HTTP delivery as proof that fields are searchable.

Production checklist

Before deploying a Monolog PHP configuration:

  • Require PHP 8.1 or newer and a supported Monolog 3 release.
  • Emit newline-delimited JSON.
  • Use stable service, environment, host, channel, and version fields.
  • Add request, trace, order, or job identifiers as structured context.
  • Redact sensitive keys recursively.
  • Record exceptions as exception objects.
  • Confirm handler order and bubbling behavior.
  • Keep receiver URLs and tokens outside source control.
  • Verify a unique deployment event in the destination.
  • Test failure behavior when the collector or receiver is unavailable.

Structured records turn PHP logs into evidence that can be filtered and correlated. Create a Fluxtail account to connect a receiver and inspect those records in Live Tail. Fluxtail offers Starter and Pro plans; current details are available on the pricing page.