A production incident rarely starts with a missing log line. It starts with a flood of entries that share no stable fields, no useful channel, and no clear route to the system that can search them. PHP Monolog gives PHP services a consistent logging pipeline, but the default file output is only the beginning. A production-ready setup needs deliberate channels, handler ordering, structured JSON, exception context, safe processors, and a documented transport into centralized log management.
Table of Contents
- What PHP Monolog Does and Why Teams Standardize On It
- Installing Monolog and Logging Your First Message
- Configuring Handlers Channels and Log Levels That Scale
- Structuring Logs With Processors Formatters and Context
- Sending PHP Logs to Fluxtail Over HTTP GELF and OTLP
- Performance Security and Reliability Best Practices
- Putting It All Together and Next Steps
What PHP Monolog Does and Why Teams Standardize On It
Monolog is a widely adopted PHP logging library that implements the PSR-3 interface, the standard designed to improve logger interoperability across PHP frameworks and applications. Its public APIs began accepting PSR-3 log levels in version 1.11.0, while its older internal level scheme predates PSR-3, as documented in the Monolog README. That compatibility keeps application code focused on logging events rather than on a particular delivery backend.
The useful mental model is a pipeline:
- Logger: Creates a record with a message, severity, channel, timestamp, context, and extra fields.
- Channel: Names the application area producing the record, such as
payments,worker, orhttp. - Handler: Decides whether to accept the record and where to send it.
- Processor: Enriches the record before delivery.
- Formatter: Serializes the record for people or machines.
Each logger has a channel name and a stack of handlers. Records traverse that stack in order. If a handler fully handles a record, propagation stops, which makes handler ordering and bubbling important for preventing duplicate delivery or accidental drops. A payments channel can therefore route failures differently from a noisy cache channel without forcing every caller to know where logs are stored.
Practical rule: Treat channels as triage boundaries, not as labels added after the fact. A channel should answer which subsystem needs attention.
Monolog has evolved across PHP generations. The current 3.x line requires PHP 8.1 or above, the 2.x line works with PHP 7.2 or above, and the older 1.x line supported PHP 5.3 up to 8.1, according to the project README. Its Packagist history reaches back to 1.0.0-RC1, and the package page lists version 3.12.0 published on 2026-09-09. Independent package-scraper data reports more than 1.0 billion total downloads, monthly downloads in the tens of millions, and more than 21,000 GitHub stars, signals of sustained adoption across PHP production environments, as recorded on the Monolog Packagist page.
Installing Monolog and Logging Your First Message
Composer provides the cleanest starting point. A new project can install Monolog and generate its autoloader with:
mkdir monolog-php
cd monolog-php
composer require monolog/monolog
Create index.php in the project directory:
<?php declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
$logger = new Logger('app');
$handler = new StreamHandler('php://stdout', Level::Debug);
$logger->pushHandler($handler);
$logger->info('Application started.');
Run the script:
php index.php
With the default formatter, the output is a human-readable line similar to:
[2026-09-10T10:15:22.438291+00:00] app.INFO: Application started. [] []
The channel appears as app, the severity is INFO, and the empty arrays represent context and extra. The exact timestamp will depend on the local runtime.

The first common mistake is creating a logger without attaching a handler. Logging calls can look correct in application code, but no destination will receive records until pushHandler() adds one. The second is assuming the default output is structured JSON. Symfony's integration documentation states that Monolog handlers use Monolog\Formatter\LineFormatter by default, although the formatter can be replaced with monolog.formatter.json or another formatter, as shown in the Symfony formatter documentation.
For local verification, php://stdout keeps the example simple. Containerized deployments often use stdout or stderr so the runtime or collection layer can take responsibility for forwarding and retention. File output still has a place on a single host, but it shouldn't become the only production path when incident investigation depends on centralized search.
Configuring Handlers Channels and Log Levels That Scale
A single StreamHandler proves the pipeline works, but production routing needs separation. A logger can have multiple handlers, each with its own destination and minimum level. A file or stdout handler may receive routine events, while a remote handler receives warnings and errors. Handler order determines which destination sees the record first, and bubbling determines whether later handlers also receive it.
The eight RFC 5424 levels are the foundation for severity routing. Monolog's built-in filtering is intentionally limited to those levels, so routing by deployment, tenant, subsystem, or incident state requires processors or custom fields before handling. Teams should define a small number of meaningful channels rather than creating a new channel for every class.
| Handler | Best For | Key Behavior |
|---|---|---|
StreamHandler |
stdout, stderr, or a direct file | Flexible destination with a configurable minimum level |
RotatingFileHandler |
Host-local files requiring rotation | Writes to rotating files, but retention and disk pressure still need operational ownership |
SyslogHandler |
Existing syslog infrastructure | Sends records to a syslog destination using severity-aware routing |
| Socket, HTTP, GELF, or OTLP handlers | Centralized transport | Sends records to a remote receiver, where network errors, authentication, payload shape, and retry behavior require explicit validation |
A typical local and incident-oriented stack might look like this:
<?php declare(strict_types=1);
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
$logger = new Logger('payments');
$console = new StreamHandler('php://stdout', Level::Info);
$alerts = new StreamHandler('/var/log/payments-alerts.log', Level::Error);
$logger->pushHandler($alerts);
$logger->pushHandler($console);
$logger->info('Payment request accepted.', [
'order_id' => 'order-48291',
]);
$logger->error('Payment request failed.', [
'order_id' => 'order-48291',
]);
The order should be chosen intentionally. A handler that fully handles a record can stop propagation, while a bubbling handler lets the record continue through the stack. Teams that see duplicate entries should inspect handler ordering, bubbling, and whether multiple channels point at the same destination.
The log severity levels guide can help teams define a shared vocabulary for what belongs in routine search results versus incident alerts. Severity alone isn't enough for routing, but inconsistent severity makes every downstream decision harder.
Structuring Logs With Processors Formatters and Context
Readable lines help during local development. Structured JSON works better when a search system must filter by channel, severity, request identifier, or exception class. Symfony documents both line and JSON formatter options, and Monolog's processor model provides the enrichment needed before serialization.
<?php declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
$handler = new StreamHandler('php://stdout', Level::Info);
$handler->setFormatter(new JsonFormatter());
$logger = new Logger('orders');
$logger->info('Order placed successfully.', [
'order_id' => 'order-48291',
'customer_id' => 'customer-1024',
'correlation_id' => 'request-7f31',
]);
try {
throw new RuntimeException('Inventory service unavailable.');
} catch (Throwable $exception) {
$logger->error('Order processing failed.', [
'order_id' => 'order-48291',
'exception' => $exception,
]);
}
A record will contain fields such as message, context, level, level_name, channel, datetime, and extra. The context array belongs to the individual event. The extra array is the natural home for processor-added fields.
Processors add consistency
Monolog processors are callables applied to records before handlers process them. Built-in processors can add request URI, client IP, process ID, memory usage, hostname, or file and method origin. A custom processor can add deployment metadata and correlation identifiers:
use Monolog\LogRecord;
final class RequestContextProcessor
{
public function __construct(
private readonly string $requestId,
private readonly string $environment,
) {
}
public function __invoke(LogRecord $record): LogRecord
{
return $record->with(
extra: array_merge($record->extra, [
'request_id' => $this->requestId,
'environment' => $this->environment,
]),
);
}
}
Register it globally only when every handler needs the fields. If only the remote transport needs deployment metadata, attach the processor to that handler instead. Processors run on every event, so expensive closures, deep introspection, and high-cardinality values can increase work during the exact periods when traffic and error volume are already high.
Exceptions need structured context
Passing a Throwable under the exception key preserves useful diagnostic information for JSON formatting. A flattened message loses the relationship between the exception type, source location, and previous cause.
Security boundary: Redaction must happen before the first handler sends a record to a file, stdout collector, or remote receiver. A downstream platform can't reliably remove a secret that has already escaped the application boundary.
Stable field names matter as much as JSON syntax. Teams should normalize timestamps to UTC, use the same correlation field across services, and avoid putting arbitrary request payloads into every record. The log normalization guide provides a useful reference for treating schema consistency as an operational control rather than a formatting preference.

Sending PHP Logs to Fluxtail Over HTTP GELF and OTLP
A supported integration should use a documented receiver and protocol, not an invented native Monolog handler. Fluxtail's protocol-first model distinguishes shared HTTP JSON and OTLP receivers on TLS port 443, which use receiver-bound Bearer credentials, from dedicated Syslog, GELF, StatsD, Fluent Forward, and Beats destinations. The Monolog application can therefore format records for the selected receiver and use a compatible transport layer.
For HTTP JSON, the application should map stable Monolog fields into the payload expected by the configured receiver. The following example shows the shape of the application-side serialization and the use of a named stream, without guessing a product endpoint:
$payload = [
'stream' => 'php-orders',
'host' => gethostname() ?: 'unknown',
'severity' => $record->level->getName(),
'message' => $record->message,
'context' => $record->context,
'extra' => $record->extra,
'timestamp' => $record->datetime->format(DATE_ATOM),
];
The configured HTTP client must send that JSON payload to the receiver URL supplied during Fluxtail setup, over TLS port 443, with the Bearer credential bound to that receiver. The application shouldn't hard-code undocumented paths, token formats, retry guarantees, or alternate ports. Receiver authentication and payload validation are deployment configuration concerns, not Monolog assumptions.
GELF uses a dedicated destination rather than the shared HTTP JSON receiver. OTLP follows the shared OTLP receiver path on TLS port 443 and uses its receiver-bound Bearer credential. Teams choosing between them should start with the protocol already supported by the collection path. The data ingestion example shows the kind of receiver-specific mapping that should be verified before deployment.
Named streams separate application areas into triage-ready boundaries. A php-orders stream can stay distinct from worker or infrastructure streams, while channel, severity, host, and message remain available for filtering. After shipping a test record, verification should happen in live tail first, then in analytics and alert rules. Fluxtail also provides built-in AI chat and hosted MCP for chat-based queries, but those features are useful only after the record schema and stream routing are correct.
Troubleshooting should proceed from the boundary inward:
- Authentication: Confirm the Bearer credential belongs to the selected receiver.
- Transport: Confirm TLS port 443 and the receiver protocol match.
- Payload: Validate JSON fields, timestamp serialization, severity mapping, and stream naming.
- Visibility: Check live tail before investigating analytics or alerts.
Performance Security and Reliability Best Practices
The main performance cost isn't the logger method alone. It comes from the combined handler stack, processors, formatters, and destination I/O. Monolog has been included in independent benchmark comparisons with native PHP logging approaches and other libraries, so teams should benchmark their own configuration rather than copy a generic result, as reflected in the Monolog usage documentation.
A practical control list:
- Keep processors cheap: Avoid high-cardinality context, repeated introspection, and expensive closures on every event.
- Filter deliberately: Built-in filtering covers the eight RFC 5424 levels. Use processors or custom fields when routing needs more than severity.
- Redact before delivery: Remove credentials, tokens, and sensitive payload values before any handler writes or transmits the record.
- Stabilize schemas: Use consistent field names and UTC timestamps across services.
- Align telemetry IDs: If OpenTelemetry is present, preserve trace and span identifiers in the same structured context used by Monolog.
- Prevent duplication: Choose either automatic instrumentation or an explicit telemetry handler for a given path unless duplicate events are intentional.
A small local benchmark that measures the configured processors, JSON serialization, and actual destination is more useful than measuring only $logger->info(). During incident periods, the safest design is the one that keeps enrichment bounded and makes transport failures visible without blocking application work indefinitely.
Putting It All Together and Next Steps
A reliable PHP Monolog pipeline follows a clear progression: start with a handler that proves local output, split application areas into channels, replace line output with JSON, add bounded contextual fields, and ship through a receiver-specific protocol into named streams. Verify the first event in live tail, then build analytics and alerts around stable fields rather than message text.
Teams can expand one source at a time:
- Validate one PHP service and one named stream.
- Add worker and subsystem channels only when their investigation paths differ.
- Align correlation fields with trace and span identifiers.
- Add alerts and use built-in AI chat or hosted MCP for targeted queries.
Fluxtail access and pricing are available by request, and it doesn't offer a permanent Free plan.
Fluxtail provides centralized log ingestion over HTTP, Syslog, OTLP, GELF, and other documented protocols, with named streams, live tail, analytics, alerts, built-in AI chat, and hosted MCP for investigation workflows. Teams should validate the receiver, credential, payload, and stream with one PHP Monolog service, then visit Fluxtail to request access and confirm the appropriate setup.