Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Using Non-Standard Services: Logger Channels

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

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:

... 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:

... 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!'):

... 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:

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:

... 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!

... 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:

... 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.

Leave a comment!

26
Login or Register to join the conversation
Default user avatar
Default user avatar Putera Kahfi | posted 3 years ago

Thanks for the tutorial,
btw is there a way to change the log location, I change the path to /var/www/logs/
but did not work, the log still stored in var/logs

I have created new channel to log all rest request :


rest:
type: stream
handler: stream
# path: "/var/www/log/%kernel.environment%.rest.log"
path: "/var/www/logs/rest.log"
level: info
channels: ["rest"]

Thanks

15 Reply

Hey Putera,

Yes, you did it right! Just specify whatever you want path explicitly as you did. So, probably you just need to clear the cache to see it in action. Try to clear the cache first.

Cheers!

Reply
Richard Avatar
Richard Avatar Richard | posted 2 years ago

I'm confused as to the addition in packages/monolog.yaml. Why was it added there? since we need to add a dev and prod config anyway. The other channels in the prod and dev monolog.yaml files are not declared at the top level and don't appear to be anywhere else either (or my grep is failing in vendor)

1 Reply

Hey maxii123

When you declare a new channel at packages/monolog.yaml what it does is to create a new logger service under the hood but you still have to configure it, maybe for the dev environment you may want to log everything coming from that channel into a file but in the production environment you may want to send the log to a Slack channel, so the config will be different. That's why the channels configuration is separated in environments but the definition of channels is kept in a single file. I hope it helps making things clearer

Cheers!

1 Reply
Akavir S. Avatar
Akavir S. Avatar Akavir S. | posted 2 years ago

This tutorial is greaat !

1 Reply

Hey Virgile,

Thank you! We're happy you like it and it's useful for you :)

Cheers!

1 Reply
Default user avatar
Default user avatar Dzosef Dzozefiński | posted 1 year ago

Hello,

i'm using symfony 4.4.*. Why i cannot see the logs in the profiler? I made all the steps like you did, and this option is empty.

i used this command (shown below with the results)

λ php bin/console debug:container --tag=data_collector


Symfony Container Services Tagged with "data_collector" Tag
===========================================================


---------------------------- ---------------------------------------------- ------------- ---------- --------------------------------------------------------------------
Service ID template id priority Class name
---------------------------- ---------------------------------------------- ------------- ---------- --------------------------------------------------------------------
data_collector.request @WebProfiler/Collector/request.html.twig request 335 Symfony\Component\HttpKernel\DataCollector\RequestDataCollector
data_collector.time @WebProfiler/Collector/time.html.twig time 330 Symfony\Component\HttpKernel\DataCollector\TimeDataCollector
data_collector.memory @WebProfiler/Collector/memory.html.twig memory 325 Symfony\Component\HttpKernel\DataCollector\MemoryDataCollector
data_collector.ajax @WebProfiler/Collector/ajax.html.twig ajax 315 Symfony\Component\HttpKernel\DataCollector\AjaxDataCollector
data_collector.exception @WebProfiler/Collector/exception.html.twig exception 305 Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector
data_collector.logger @WebProfiler/Collector/logger.html.twig logger 300 Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector
data_collector.events @WebProfiler/Collector/events.html.twig events 290 Symfony\Component\HttpKernel\DataCollector\EventDataCollector
data_collector.router @WebProfiler/Collector/router.html.twig router 285 Symfony\Bundle\FrameworkBundle\DataCollector\RouterDataCollector
data_collector.cache @WebProfiler/Collector/cache.html.twig cache 275 Symfony\Component\Cache\DataCollector\CacheDataCollector
data_collector.twig @WebProfiler/Collector/twig.html.twig twig 257 Symfony\Bridge\Twig\DataCollector\TwigDataCollector
data_collector.http_client @WebProfiler/Collector/http_client.html.twig http_client 250 Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector
data_collector.dump @Debug/Profiler/dump.html.twig dump 240 Symfony\Component\HttpKernel\DataCollector\DumpDataCollector
data_collector.config @WebProfiler/Collector/config.html.twig config -255 Symfony\Component\HttpKernel\DataCollector\ConfigDataCollector
---------------------------- ---------------------------------------------- ------------- ---------- --------------------------------------------------------------------

So it seems it's working - the log is also appearing in the var/log/dev.log (the line with finding bacon)

How can i enable showing the logs in the profiler?

Reply

Hey Dzosef,

Hm, it should just work. Do you see "Logs" item in the left sidebar on this page: https://127.0.0.1:8000/_profiler/latest ? What if you go directly to this link: https://127.0.0.1:8000/_profiler/latest?panel=logger ? Do you see any errors or you can see log tabs? Btw, any missing installed packages? Please, run "composer install", clear the cache and try again. Does it help?

Cheers!

Reply
Hicham A. Avatar
Hicham A. Avatar Hicham A. | posted 2 years ago

I am totally lost, I am new to PHP. So I am not sure what I need to master before doing this tutorial on Symfony? Thank you

Reply

Hey Travel,

We're sorry to hear this! unfortunately, if you're a newbie to PHP - yes, it might be difficult to you to understand some concepts, that's why I strongly recommend you to learn about PHP itself first. After you feel confident in PHP - try to learn Symfony again. When you don't know PHP programming language but trying to learn Symfony is the same as learn how to cook in Spanish but when you don't know Spanish at all. I hope you understand what I mean here.

Btw, I don't see your reply on my previous comment, so I suppose you haven't seen it yet. I gave you some good tips where to start first before you deal with Symfony framework, see another my comment here: https://symfonycasts.com/sc...

I hope this helps! And I believe that strategy will help you a lot in learning Symfony as a result.

P.S. But if you didn't get some concept we're talking about in this chapter - please, give us a bit more context of it, like asking more specific questions, and we would try to help you. Unfortunately, hearing "I am totally lost" does not say much to us, I don't know what exactly try to explain to you in different words to make you get it.

Cheers!

Reply
Default user avatar
Default user avatar Wazir Khan | posted 3 years ago

Hi all,
A quick question, I configured my custom logger as in the tutorial, but now the messages are logged in both files dev.log and markdown.log. Is it normal or there's something wrong?

Reply

Hey Wazir,

Yes, that's should be OK, because I see in this chapter the "main" handler log every channel except "event", so it looks like we just duplicate logs from markdown channel in markdown.log.

Cheers!

1 Reply
Shakeel A. Avatar
Shakeel A. Avatar Shakeel A. | posted 3 years ago

Great Work Men.
You passed custom markdown object in the service but how can we pass it in the controller?

Reply

Hey @Shakeel!

You want to get the MarkdownHelper service from in a controller? Just add it as an argument to your method/action - that's a super-power of controllers:


use App\Service\MarkdownHelper;

public function newProduct(MarkdownHelper $markdownHelper)
{
// and use it
}

We talk a bit about this in episode 1 of this series: https://symfonycasts.com/sc...

Hope that helps!

Cheers!

Reply
Shakeel A. Avatar
Shakeel A. Avatar Shakeel A. | posted 3 years ago

Great work men.
You passed custom markdown logger in the services but how can we pass in the controller?

Reply

Hello,

In the Symfony Profiler (Logs section, under "Deprecations" tab), I have this message :
A tree builder without a root node is deprecated since Symfony 4.2 and will not be supported anymore in 5.0.

{
/var/www/html/perso/the_spacebar/vendor/symfony/config/Definition/Builder/TreeBuilder.php:30 {
› if (null === $name) {
› @trigger_error('A tree builder without a root node is deprecated since Symfony 4.2 and will not be supported anymore in 5.0.', E_USER_DEPRECATED);
› } else {
}
/var/www/html/perso/the_spacebar/vendor/knplabs/knp-markdown-bundle/DependencyInjection/Configuration.php:17 {▼
› {
› $treeBuilder = new TreeBuilder();

}
}

Reply

Hello Camille Seuvin

It's normal that you see that deprecation message. You are using Symfony 4.2 and course code uses version 4.1. You can ignore this message, soon knplabs/knp-markdown-bundle will be updated and this message will fly away

Cheers!

Reply

I just updated the bundle! If you run "composer update knplabs/knp-markdown-bundle" it should go away!

Cheers!

Reply
Burke C. Avatar
Burke C. Avatar Burke C. | posted 3 years ago

I believe...as far as I can tell...that I've followed the lesson exactly. However, when I modify services.yaml as indicated, I get the following error:

"The definition for "App\Service\MarkdownHelper\" has no class. If you intend to inject this service dynamically at runtime, please mark it as synthetic=true. If this is an abstract definition solely used by child definitions, please add abstract=true, otherwise specify a class to get rid of this error."

In PhpStorm, hovering over App\Service\MarkdownHelper\: in services.yaml gives the error: "Missing Class".

But the class is in MarkdownHelp.php in the Service directory, with namespace App\Service; at the top.

Any help would be greatly appreciated.

Reply

Hey Burke Cox

Your MarkdownHelper class exists and lives inside "src/Service/MarkdownHelper.php" right? (Double check it's namespace)

Oh, probably you are getting that error because of that ending backslash in your service definition name "App\Service\MarkdownHelper\"

Cheers!

Reply
Default user avatar
Default user avatar elbarto | posted 4 years ago

Hi there !
I wanted to know : how would you do when you need to use the entityManager inside your own service ?
Dependency Injection through the constructor doesn't seem to work (typehints issues for example). What whould be the best solution in order to be able to call findAll() on an entity in my service ?

Thanks, keep up the good work, love symfony 4 so far.

Reply
Default user avatar

Would you say it's a good solution to inject ManagerRegistry into the service's constructor in order to getConnection() and getRepository() ?

Reply
Default user avatar

Try to inject an Entity Manager to your service like this (in services.yaml):


App\Service\MyServiceName:
arguments:
- '@doctrine.orm.default_entity_manager'


And get $em as usual in your service constructor.

1 Reply

I haven't had the need of using directly the ManagerRegistry, if you want to make use of the connection, you can do it through the "EntityManager" as well.

Reply

Hey @elbarto

Injecting the "EntityManager" into your services is totally ok. You can do it by type hinting the interface "EntityManagerInterface"

> Dependency Injection through the constructor doesn't seem to work

Can you tell me what error do you see?

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.1.4
        "symfony/asset": "^4.0", // v4.0.4
        "symfony/console": "^4.0", // v4.0.14
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.0.14
        "symfony/lts": "^4@dev", // dev-master
        "symfony/twig-bundle": "^4.0", // v4.0.4
        "symfony/web-server-bundle": "^4.0", // v4.0.4
        "symfony/yaml": "^4.0" // v4.0.14
    },
    "require-dev": {
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.4
        "symfony/debug-bundle": "^3.3|^4.0", // v4.0.4
        "symfony/dotenv": "^4.0", // v4.0.14
        "symfony/maker-bundle": "^1.0", // v1.0.2
        "symfony/monolog-bundle": "^3.0", // v3.1.2
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.0.4
    }
}