Buy Access to Course
09.

Using Non-Standard Services: Logger Channels

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Let's add some logging to MarkdownHelper. As always, we just need to find which type-hint to use. Run:

./bin/console debug:autowiring

And look for log. We've seen this before: LoggerInterface. To get this in MarkdownHelper, just add a third argument: LoggerInterface $logger:

37 lines | src/Service/MarkdownHelper.php
// ... lines 1 - 5
use Psr\Log\LoggerInterface;
// ... lines 7 - 8
class MarkdownHelper
{
// ... lines 11 - 14
public function __construct(AdapterInterface $cache, MarkdownInterface $markdown, LoggerInterface $logger)
{
// ... lines 17 - 19
}
// ... lines 21 - 35
}

Like before, we need to create a new property and set it below. Great news! PhpStorm has a shortcut for this! With your cursor on $logger, press Alt+Enter, select "Initialize fields" and hit OK:

37 lines | src/Service/MarkdownHelper.php
// ... lines 1 - 5
use Psr\Log\LoggerInterface;
// ... lines 7 - 8
class MarkdownHelper
{
// ... lines 11 - 12
private $logger;
public function __construct(AdapterInterface $cache, MarkdownInterface $markdown, LoggerInterface $logger)
{
// ... lines 17 - 18
$this->logger = $logger;
}
// ... lines 21 - 35
}

Awesome! Down in parse(), if the source contains the word bacon... then of course, we need to know about that! Use $this->logger->info('They are talking about bacon again!'):

37 lines | src/Service/MarkdownHelper.php
// ... lines 1 - 8
class MarkdownHelper
{
// ... lines 11 - 21
public function parse(string $source): string
{
if (stripos($source, 'bacon') !== false) {
$this->logger->info('They are talking about bacon again!');
}
// ... lines 27 - 34
}
}

Ok, try it! This article does talk about bacon. Refresh! To see if it logged, open the profiler and go to "Logs". Yes! Here is our message. I love autowiring.

The Other Loggers

Go back to your terminal. The debug:autowiring output say that LoggerInterface is an alias to monolog.logger. That is the id of the service that's being passed to us. Fun fact: you can get a bit more info about a service by running:

./bin/console debug:container monolog.logger

This is cool - but you could also learn a lot by dumping it. Anyways, we normally use debug:container to list all of the services in the container. But we can also get a filtered list. Let's find all services that contain the word "log":

./bin/console debug:container --show-private log

There are about 6 services that I'm really interested in: these monolog.logger. something services.

Logging Channels

Here's what's going on. Symfony uses a library called Monolog for logging. And Monolog has a feature called channels, which are kind of like categories. Instead of having just one logger, you can have many loggers. Each has a unique name - called a channel - and each can do totally different things with their logs - like write them to different log files.

In the profiler, it even shows the channel. Apparently, the main logger uses a channel called app. But other parts of Symfony are using other channels, like request or event. If you look in config/packages/dev/monolog.yaml, you can see different behavior based on the channel:

monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event"]
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
# type: firephp
# level: info
#chromephp:
# type: chromephp
# level: info
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]

For example, most logs are saved to a dev.log file. But, thanks to this channels: ["!event"] config, which means "not event", anything logged to the "event" logger is not saved to this file.

This is a really cool feature. But mostly... I'm telling you about this because it's a great example of a new problem: how could we access one of these other Logger objects? I mean, when we use the LoggerInterface type-hint, it gives us the main logger. But what if we need a different Logger, like the "event" channel logger?

Creating a new Logger Channel

Actually, let's create our own new channel called markdown. I want anything in this channel to log to a different file.

To do this, inside config/packages, create a file: monolog.yaml. Monolog is interesting: it doesn't normally have a main configuration file: it only has environment-specific config files for dev and prod. That makes sense: we log things in completely different ways based on the environment.

But we're going to add some config that will create a new channel, and we want that to exist in all environments. Add monolog, then channels set to [markdown]:

monolog:
channels: ['markdown']

That's it!

Because of a Symfony bug - which, is now fixed (woo!) - but won't be available until the next version - Symfony 4.0.5 - we need to clear the cache manually when adding a new config file:

./bin/console cache:clear

As soon as that finishes, run debug:container again:

./bin/console debug:container log

Yea! Suddenly we have a new logger service - monolog.logger.markdown! So cool.

Go back to the "dev" monolog.yaml file. Copy the first log handler, paste, and give it a key called markdown_logging - that's just a meaningless internal name. Change the path to markdown.log and only log the markdown channel:

25 lines | config/packages/dev/monolog.yaml
monolog:
handlers:
// ... lines 3 - 7
markdown_logging:
type: stream
path: "%kernel.logs_dir%/markdown.log"
level: debug
channels: ["markdown"]
// ... lines 13 - 25

Ok! If you go to your browser now and refresh... it does work. But if you check the logs, we are - of course - still logging to the app channel Logger. Yep, there's no markdown.log file yet.

Fetching a Non-Standard Service

So how can we tell Symfony to not pass us the "main" logger, but instead to pass us the monolog.logger.markdown service? This is our first case where autowiring doesn't work.

That's no problem: when autowiring doesn't do what you want, just... correct it! Open config/services.yaml. Ignore all of the configuration on top for now. But notice that we're under a key called services. Yep, this is where we configure how our services work. At the bottom, add App\Service\MarkdownHelper, then below it, arguments:

32 lines | config/services.yaml
// ... lines 1 - 4
services:
// ... lines 6 - 25
App\Service\MarkdownHelper:
arguments:
// ... lines 28 - 32

The argument we want to configure is called $logger. Use that here: $logger. We are telling the container what value to pass to that argument. Use the service id: monolog.logger.markdown. Paste!

32 lines | config/services.yaml
// ... lines 1 - 4
services:
// ... lines 6 - 25
App\Service\MarkdownHelper:
arguments:
$logger: 'monolog.logger.markdown'
// ... lines 29 - 32

Find your browser and... try it! Bah! A big error:

Argument 3 passed to MarkdownHelper::__construct() must implement LoggerInterface, string given.

Ah! It's totally legal to set an argument to a string value. But we don't want to pass the string monolog.logger.markdown! We want to pass the service with this id!

To do that, use a special Symfony syntax: add an @ symbol:

32 lines | config/services.yaml
// ... lines 1 - 4
services:
// ... lines 6 - 25
App\Service\MarkdownHelper:
arguments:
$logger: '@monolog.logger.markdown'
// ... lines 29 - 32

This tells Symfony not to pass us that string, but to pass us the service with that id.

Try it again! It works! Check out the var/log directory... boom! We have a markdown.log file!

Next, I'll show you an even cooler way to configure this. And we'll learn more about what all this config in services.yaml does.