Logging in PHP with PSR-3 and Monolog

Logging in PHP with PSR-3 and Monolog

Your PHP app is already logging, but the logs probably aren't helping enough. A warning lands in a file, a fatal error shows up somewhere else, and the one request that broke a checkout has no request ID, no user context, and no easy path into the central system your team watches. That's the point where logging in PHP stops being a syntax question and becomes an operations problem.

Good logging has to survive production reality. It needs to hide from the browser, stay readable under pressure, carry correlation data across services, and plug into the stack your team uses for alerting and investigation. PHP already gives the primitives, and Monolog gives a practical path to structured output, but the greatest value comes from making logs machine-parsable and correlation-friendly from the start.

Table of Contents

Understanding Key Logging Concepts

A lot of PHP systems still behave like every error is a one-off. A developer drops an error_log() call into a controller, another team member adds var_dump(), and soon the only thing the logs prove is that the app was noisy. That setup is fine for a throwaway script, but it's a poor fit for production work where operators need to separate a framework warning from a business event.

PHP's built-in logging model

PHP's logging model starts in php.ini. Administrators can set error_reporting to decide which errors get captured, use display_errors = Off so messages stay out of the browser, and point error_log at a chosen file path. PHP also supports programmatic logging through error_log() and syslog(), so both infrastructure-level issues and application events can land in persistent storage for later debugging and operations (Datadog's PHP logging guide).

That separation matters. Infrastructure-level logging captures things like parse errors, warnings, and engine-level failures. Application-level logging records business events, checkpoints, and controlled error states that your own code decides to emit.

Practical rule: keep browser output clean, keep file output durable, and decide early which events belong to the runtime versus the application.

Why PSR-3 matters in real projects

PSR-3 gives libraries and services a common logging interface, so a package doesn't need to know whether the underlying sink is a file, syslog, or a central observability platform. That standardisation is the difference between a codebase that logs in isolated pockets and one that can route messages consistently across controllers, services, queues, and integration code.

Monolog fits neatly into that ecosystem because it adds structured severity levels and handlers without forcing one storage target. In practice, that means a single logger can write to files during local work, then move to a broader pipeline in production without rewriting application code (Monolog overview).

Setting Up PSR-3 Logging with Monolog

A clean Monolog setup starts with Composer and a single bootstrap point. That keeps the application from inventing its own logging style in every controller, and it makes it obvious where handlers and severity rules live. The goal isn't just to “log something”, it's to create one logger instance that every service can trust.

A laptop on a wooden desk displaying PHP code for a logger class with developer accessories nearby.

Building a central logger

Monolog's value comes from its handlers. A StreamHandler writes to a file, while severity levels let the app route messages with intent instead of dumping everything into one undifferentiated stream. That's why Monolog became one of the best-known PHP logging libraries, and why it remains a common anchor in modern deployments (Monolog reference).

A typical bootstrap looks like this in principle:

$logger = new \Monolog\Logger('app');
$logger->pushHandler(new \Monolog\Handler\StreamHandler('/var/log/app/app.log', \Monolog\Level::Info));

That gives the project one shared entry point. Controllers and services can then accept the logger through dependency injection instead of instantiating their own file writers.

A single logger service is easier to secure, easier to test, and much easier to swap later when the sink changes.

Using it from application code

Once the logger exists, usage should stay boring. A controller can emit an info event when a request starts, a warning when validation is borderline, and an error when an operation fails. That pattern gives operators a severity ladder instead of a wall of text.

A minimal usage pattern looks like this:

$logger->info('Checkout started');
$logger->warning('Customer address missing optional field');
$logger->error('Payment provider rejected request');

The important trade-off is that file logging is simple, but not enough by itself for distributed systems. A local file is useful for diagnosis, yet it doesn't automatically solve correlation across services. That's why the next step is to make each record carry context that downstream tooling can read reliably.

Designing Structured JSON Logs

Plain text logs are easy to write and annoying to search. They don't guarantee field order, they don't guarantee schema, and they don't give downstream tools a predictable shape to parse. Structured logs solve that by treating each event as data first and text second.

A diagram comparing plain text logs with structured JSON logs and the benefits of formatted logging.

What belongs in each record

The strongest gap in many PHP guides is correlation. A major underserved angle is structured, correlation-friendly logging in PHP, especially how to include request IDs, user or session context, and JSON output that tools can parse reliably (structured logging gap). That's the material that turns a generic error into a traceable incident.

A good record usually keeps these fields stable:

  • Timestamp for ordering and timeline reconstruction.
  • Level for severity and routing.
  • Message for the human summary.
  • Request ID for correlation across requests.
  • User or session context for investigation.
  • Extra context for service-specific details.

That structure doesn't need to be verbose. It needs to be consistent.

JSON formatting and context injection

Monolog's JSON formatting works because it creates output that machines can ingest without custom parsing rules. One guide explicitly recommends JSON over custom formats because bespoke layouts require bespoke parsers, and it also highlights UTC handling to avoid timestamp confusion across systems (JSON and UTC guidance).

A practical pattern is to register processors that add shared fields automatically, then let the logger inject per-event data from the application layer. That approach keeps schema decisions in one place and avoids copy-paste drift across controllers.

For a useful mental model, compare each log entry to a row in a dataset. The row should always have the same core columns, and the variable context should remain clearly labelled. That's what makes dashboards, searches, and alert rules dependable.

This comparison of structured logging approaches in another ecosystem is useful as a reference point because the underlying design problem is the same. The format has to be stable enough for machines, but still readable enough for humans during incidents.

Implementing Log Rotation and Performance Tuning

A log file that never rotates becomes an operational problem fast. It's not just disk usage, it's also the drag from larger files, repeated writes, and the time wasted digging through noisy historical output. The fix is to treat log retention as part of the logging design, not an afterthought.

A diagram illustrating the four-step process for optimizing log management to improve application performance.

Rotation strategy that stays usable

A practical rotation setup keeps files bounded and names predictable. Monolog's rotating file handlers are commonly used for this pattern, and the logic is straightforward, roll logs by day or by size, keep only the backups you need, and let older files move out of the hot path. The exact retention policy depends on the environment, but the operational principle stays the same, current logs should be easy to reach and old logs should not dominate disk use.

That lines up with production PHP guidance to enable log_errors = On, set error_reporting = E_ALL, and route logs to a site- or environment-specific file so parse errors, fatal errors, warnings, and deprecations are captured without exposing them to users (production PHP logging pattern). A single shared file for every site is usually a bad trade-off. Separate files make triage faster.

Logs need a lifecycle. If a file never expires, the app eventually spends more time managing history than helping the next incident.

Performance trade-offs worth keeping in mind

Buffered writes reduce I/O churn, but they also introduce a trade-off. Immediate flushes are safer when the process might die soon after the event, while batching is friendlier to performance during steady traffic. The right choice depends on whether the log is serving as a forensic trail, a near-real-time signal, or both.

The PHP side should still start from sane defaults in configuration, then move to application-level tuning only when the workload justifies it. For a rotation policy reference, the logrotate guide for PHP environments is a practical companion because it shows how file lifecycle and application logging fit together.

A sensible baseline is simple. Keep the output structured, keep the file writable by the service account, and make sure rotation doesn't depend on manual cleanup. That combination solves most production headaches without adding much complexity.

Forwarding Logs to External Systems

Local files are fine for the first layer of diagnosis, but central systems are where logs become operationally useful. Once logs are forwarded, teams can search across hosts, correlate with alerts, and keep a long enough history to spot recurring failures. The key decision is which transport fits the workload without creating a new failure mode.

A comparison chart showing common Python logging handlers including SyslogHandler, SocketHandler, and Custom HTTP Handlers for log forwarding.

Choosing the right handler path

Monolog supports multiple delivery styles, and each one has a different failure profile. SyslogHandler fits environments already built around Unix logging. SocketHandler is useful when direct network streaming is acceptable. Custom HTTP handlers are the cleanest fit when logs need to move into API-driven systems and observability services.

A useful rule is to pick the simplest transport that matches reliability requirements. Syslog is familiar and operationally cheap. HTTP is flexible and easier to integrate with modern platforms, but it needs stronger failure handling. Socket-based delivery sits in the middle, depending on how the network and destination are managed.

A centralized pipeline becomes even more effective when runtime errors are unified before they leave the process. Zend's guidance recommends converting runtime errors with set_error_handler() and catching shutdown-time fatals with register_shutdown_function(), then emitting structured output such as JSON through a logging library like Monolog (Zend PHP error logging). That pattern keeps error handling consistent before forwarding even starts.

Practical rule: don't forward raw noise. Convert errors into a stable structure first, then decide where they should land.

Reliability and fallback behavior

Forwarding is only useful if it doesn't erase the local trail. A resilient design keeps a local file as the fallback when network delivery fails, then sends the same structured record to the external system when the path is healthy. That protects investigation workflows during outages.

For teams comparing central monitoring options, a guide to SIEM use for AWS environments is a helpful adjacent read because the same concerns show up again, transport choice, retention, and correlation all matter more than raw volume. The service-side destination is less important than whether the message arrives with the fields needed for search and alerting.

This is also where request IDs pay off. Once each forwarded event carries the same correlation key as the local log, the incident review can move from guesswork to a clean timeline.

Troubleshooting and Best Practices

Most broken PHP logging setups fail in ordinary ways. JSON arrives with inconsistent keys, timestamps drift because one service writes local time and another writes UTC, or a buffer holds the only useful event until the process exits badly. Those failures are tedious, but they're fixable if the configuration stays disciplined.

A few checks catch most problems quickly:

  • Validate schema early. If the same field changes names across services, searching becomes unreliable.
  • Normalize timestamps. Mixed time zones turn an incident timeline into a guessing game, which is why UTC handling is a safer default.
  • Filter sensitive data. Expert guidance warns that malformed or overly verbose logs can leak sensitive information, so only approved fields should reach the sink (Zend PHP error logging).
  • Watch for missing context. If request IDs or session markers aren't injected automatically, correlation breaks the first time traffic crosses a service boundary.

For a broader operations perspective, the observability notes from Ryware are a useful complement because they reinforce the same practical idea, logs are most valuable when they can be joined with metrics and traces without manual cleanup. The Python logging to file reference is also useful as a contrast point for the same operational trade-offs in another stack.

The best next step is a short audit. Check php.ini, verify that display_errors stays off in production, confirm that structured output includes request IDs, and test what happens when the external sink is unavailable. Clean logs aren't just easier to read, they're easier to trust.


If PHP logs are still living in separate files, inconsistent formats, or browser output, fix that before the next incident does it for you. Start by standardizing PSR-3, move your app to Monolog, add request IDs and structured JSON, then connect the result to a system that gives your team searchable, correlated visibility. For teams that want that operational discipline without stitching together more tools, Fivenines is a practical place to look next.