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!

28
Login or Register to join the conversation
Default user avatar
Default user avatar Putera Kahfi | posted 4 years ago | edited

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 3 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 Richard

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 3 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
Peter-K Avatar

Hi, I am on symfony 5.4 but did not find any related video regarding my query.

I want to have a variable in .env.local file something like DEBUG_EMAILS_ENABLED = TRUE/FALSE

Then I want to use this variable in monolog.yaml to disable sending error emails

Something like

    symfony_mailer:
        enabled: '%env(DEBUG_EMAILS_ENABLED)%'
        type: symfony_mailer
        from_email: '%env(MAILER_FROM)%'
        to_email: ['%env(SUPPORT_EMAIL)%']
        subject: 'An Error Occurred! %%message%%'
        level: notice
        formatter: monolog.formatter.email_error_formatter
        content_type: text/html

But there is no such option so how can I pass somehow an option that will disable this functionality? Would level: 1000000 work? So it will basically never send an email?

Reply

Yo @Peter-K!

Hmm. This is usually done, for logging, on an environment-by-environment basis. So, you have a set of Monolog config for dev that does NOT send error emails, and you have some config for prod that DOES send emails. I'm sure you've noticed your monolog.yaml is likely already set up in this way: https://github.com/symfony/recipes/blob/4031f688ba05b714093cf39a5795e51d2af27a73/symfony/monolog-bundle/3.7/config/packages/monolog.yaml#L40

However, if you need an actual environment variable (so you could switch this functionality on/off at-will on production), then it might be a bit more complex.

Would level: 1000000 work? So it will basically never send an email?

That's an interesting idea. I assume you're using the symfony_mailer monolog handler? Yea, you could try creating a MAILER_LOG_LEVEL environment variable and setting that to, I think, anything greater than 600 - 600 is the highest built-in level. I have no idea if this will work, but it's worth a try as it would be super easy.

Let me know if it works!

Cheers!

Reply
Default user avatar
Default user avatar Dzosef Dzozefiński | posted 2 years ago | edited

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 3 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 4 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 4 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/screencast/symfony/services#using-the-logger-service

Hope that helps!

Cheers!

Reply
Shakeel A. Avatar
Shakeel A. Avatar Shakeel A. | posted 4 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 ojtouch

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

Thanks for your quick reply sadikoff

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

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 5 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
Default user avatar toporovvv | elbarto | posted 5 years ago | edited

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
    }
}
userVoice