Easy logging in Symfony

The last couple of years I’ve written a lot of code that synchronizes data from and to a Symfony store or application. One important thing I learned is that storing extensive logs can avoid a lot of discussions and help debugging big time.

Logging in Symfony is as easy as this:

use Psr\Log\LoggerInterface;
class MyClass
{
    public function __construct(
        private LoggerInterface $logger
    ) {}

    public function method()
    {
        $this->logger->info('Something has happened');
    }
}

By default, whenever you add an argument type hinted with Psr\Log\LoggerInterface the default logger is injected. This one stores everything in a [env].log.

It goes without saying this file would become pretty useless if you saved all your custom logs there. So what I tend to do is create a new logging channel for every piece of code responsible for a certain set of actions:

monolog:
  channels:
    - product_import
  handlers:
    product_import_handler:
      type: rotating_file
      path: '%kernel.logs_dir%/import/products/log.log'
      max_files: 30
      channels: product_import

From now on a new Logger service is available under the monolog.logger.product_import alias. Everything I log using this service is stored in var/logs/import/products/. What’s even cooler is that I don’t have to clean up my logs since Monolog automatically creates a new file every day, with a max of 30 days.

Now, there are a couple of ways to inject the Logger into your services, you could do it manually:

services:
    App\Command\ImportProductsCommand:
        arguments:
            $logger: '@monolog.logger.product_import'

But then you’d have to do this for every service that needs that specific logger, binding it to a variable will make things a lot easier:

services:
    _defaults:
        bind:
            $myCustomProductImportLogger: '@monolog.logger.product_import'

This way you can inject it anywhere by adding a $productImportLogger argument:

class MyClass
{
    public function __construct(LoggerInterface $myCustomProductImportLogger)
    {
        // ....
    }
}

But there’s even an easier way that I discovered only recently. Since MonologBundle 3.5 you don’t even have to configure anything! Each channel can be injected by simply adding a [channelName]Logger argument:

class MyClass
{
    public function __construct(LoggerInterface $productImportLogger)
    {
        // ....
    }
}

So there you go, no more packed, messy production.log file you need to dig in.