Monolog

Inject LoggerInterface, learn what the eight PSR-3 levels mean operationally, and set the minimum level per environment.

Syntax$logger->error(string|Stringable $message, array $context = []): void

A log line records something that happened so a person can read it afterwards. Symfony does not implement logging itself. It ships MonologBundle, which wires Monolog, a standalone logging library, into the service container and exposes a logger as a service.

You never construct that logger. Type-hint a constructor argument as Psr\Log\LoggerInterface and autowiring passes the configured one in. That interface comes from PSR-3, a standard several libraries implement, so your class never names Monolog.

The eight levels

PSR-3 defines eight severities, one method each. debug: detail only useful while reproducing something. info: a normal event worth keeping. notice: normal but unusual, such as a fallback being used. warning: recoverable trouble somebody reads tomorrow. error: a request or job failed and a person should look. critical: a dependency is gone and part of the product is down. alert: page someone now. emergency: the system is unusable.

Minimum level

A handler is a destination for records, configured in config/packages/monolog.yaml. Each handler has a level, the lowest severity it accepts: debug in development, much higher in production.

What trips people up

Levels are a contract with whoever is on call. Log routine events as error and the real errors stop being visible; no configuration repairs that.

Example

Example · php
// src/Service/RefundPolicy.php
declare(strict_types=1);

namespace App\Service;

use Psr\Log\LoggerInterface;

final class RefundPolicy
{
    public function __construct(private readonly LoggerInterface $logger)
    {
    }

    public function isRefundable(string $orderId, int $daysSincePurchase): bool
    {
        if ($daysSincePurchase <= 30) {
            $this->logger->info('Refund allowed', ['order' => $orderId]);

            return true;
        }

        // A declined refund is a decision somebody will ask about later.
        $this->logger->warning('Refund declined: window expired', [
            'order' => $orderId,
            'days' => $daysSincePurchase,
        ]);

        return false;
    }
}

// $policy->isRefundable('A-1187', 44);
// Output: false   (plus one WARNING record on the 'app' channel)

When to use it

  • Record why a payment or refund was declined so support can answer the customer tomorrow.
  • Find out which requests failed overnight without reproducing them.
  • Keep full detail in development while production records failures only.

More examples

Shorthand and longhand level calls

The eight named methods are shorthand for log() with the level as its first argument. Reach for log() when the level is itself a variable, for example when replaying records that arrived from another system.

Example · php
use Monolog\Level;
use Psr\Log\LogLevel;

// Shorthand: one method per level.
$logger->warning('Temperature high', ['celsius' => 31.4]);

// Longhand: the same record, level passed as an argument.
$logger->log(LogLevel::WARNING, 'Temperature high', ['celsius' => 31.4]);

// Monolog 3 also accepts its own enum here.
$logger->log(Level::Warning, 'Temperature high', ['celsius' => 31.4]);

A different minimum level per environment

Development keeps everything in a file you can tail. Production accepts error and above only, which is the cheapest way to cut log volume before adding anything cleverer.

Example · yaml
# config/packages/monolog.yaml
when@dev:
    monolog:
        handlers:
            main:
                type: stream
                path: '%kernel.logs_dir%/%kernel.environment%.log'
                level: debug

when@prod:
    monolog:
        handlers:
            main:
                type: stream
                path: php://stderr
                level: error

Discussion

  • Be the first to comment on this lesson.