Monolog
Inject LoggerInterface, learn what the eight PSR-3 levels mean operationally, and set the minimum level per environment.
$logger->error(string|Stringable $message, array $context = []): voidA 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
// 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.
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.
# 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