Logger Channel Setup and Autowiring
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeHere's our goal... and the end result is going to be pretty cool: leverage our middleware - and the fact that we're adding this unique id to every message - to log the entire lifecycle of a message to a file. I want to see when a message was originally dispatched, when it was sent to the transport, when it was received from the transport and when it was handled.
Adding a Log Handler
Before we get into the middleware stuff, let's configure a new logger channel that logs to a new file. Open up config/packages/dev/monolog.yaml
and add a new channels
key. Wait... that's not right. A logging channel is, sort of a "category", and you can control how log messages for each category are handled. We don't want to add it here because then that new channel would only exist in the dev environment. Nope, we want the channel to exist in all environments... even if we decide to only give those messages special treatment in dev
.
To do that, directly inside config/packages
, create a new file called monolog.yaml
... though... remember - the names of these config files aren't important. What is important is to add a monolog
key, then channels
set to an array with one new one - how about messenger_audit
.
monolog: | |
channels: [messenger_audit] |
Thanks to this, we now have a new logger service in the container for this channel. Let's find it: at your terminal, run:
php bin/console debug:container messenger_audit
There it is: monolog.logger.messenger_audit
- we'll use that in a minute. But first, I want to make any logs to this channel save to a new file in the dev
environment. Back up in config/packages/dev/monolog.yaml
, copy the main
handler, paste and change the key to messenger
... though that could be anything. Update the file to be called messenger.log
and - here's the magic - instead of saying: log all messages except those in the event
channel, change this to only log messages that are in that messenger_audit
channel.
monolog: | |
handlers: | |
// ... lines 3 - 7 | |
messenger: | |
type: stream | |
path: "%kernel.logs_dir%/messenger.log" | |
level: debug | |
channels: ["messenger_audit"] | |
// ... lines 13 - 25 |
Autowiring the Channel Logger
Cool! To use this service, we can't just autowire it by type-hinting the normal LoggerInterface
... because that will give us the main logger. This is one of those cases where we have multiple services in the container that all use the same class or interface.
To make it wirable, back in services.yaml
, add a new global bind: $messengerAuditLogger
that points to the service id: copy that from the terminal, then paste as @monolog.logger.messenger_audit
.
// ... lines 1 - 7 | |
services: | |
// ... line 9 | |
_defaults: | |
// ... lines 11 - 12 | |
bind: | |
// ... lines 14 - 15 | |
$messengerAuditLogger: '@monolog.logger.messenger_audit' | |
// ... lines 17 - 34 |
Thank to this, if we use an argument named $messengerAuditLogger
in the constructor of a service or in a controller, Symfony will pass us that service. By the way, starting in Symfony 4.2, instead of binding only to the name of the argument, you can also bind to the name and type by saying Psr\Log\LoggerInterface $messengerAuditLogger
. That just makes things more specific: Symfony would pass us this service for any arguments that have this name and the LoggerInterface
type-hint.
Anyways, we have a new logger channel, that channel will log to a special file, and the logger service for that channel is wirable. Time to get to work!
Close up the monolog config files and go to AuditMiddleware
. Add a public function __construct()
with one argument LoggerInterface $messengerAuditLogger
- the same name we used in the config. I'll call the property itself $logger
, and finish this with $this->logger = $messengerAuditLogger
.
// ... lines 1 - 4 | |
use Psr\Log\LoggerInterface; | |
// ... lines 6 - 10 | |
class AuditMiddleware implements MiddlewareInterface | |
{ | |
private $logger; | |
public function __construct(LoggerInterface $messengerAuditLogger) | |
{ | |
$this->logger = $messengerAuditLogger; | |
} | |
// ... lines 19 - 40 | |
} |
Setting up the Context
Down in handle()
, remove the dump()
and create a new variable called $context
. In addition to the actual log message, it's a little-known fact that you can pass extra information to the logger... which is super handy! Let's create a key called id
set to the unique id, and another called class
that's set to the class of the original message class. We can get that with get_class($envelope->getMessage())
.
// ... lines 1 - 10 | |
class AuditMiddleware implements MiddlewareInterface | |
{ | |
// ... lines 13 - 19 | |
public function handle(Envelope $envelope, StackInterface $stack): Envelope | |
{ | |
// ... lines 22 - 28 | |
$context = [ | |
'id' => $stamp->getUniqueId(), | |
'class' => get_class($envelope->getMessage()) | |
]; | |
// ... lines 33 - 39 | |
} | |
} |
Let's do the logging next! It's a bit more interesting than you might expect. How can we figure out if the current message was just dispatched or was just received asynchronously from a transport? And if it was just dispatched, how can we find out whether or not the message will be handled right now or sent to a transport for later? The answer... lies in the stamps!
I'm working on a project with Symfony 6 and I have two questions:
(1) I spent more time than I even want to admit struggling to configure a logger (or do I mean channel? or handler? or all the above?) that would write to its own file and only there, with a view to recording certain application events for posterity -- not errors or debugging stuff. Might eventually want to store these messages in a database. But I digress. I called the handler "app" and the channel "app", and it simply would not work no matter how hard I tried -- even banging my head on the table didn't work. When I ran
container:debug monolog
, the servicemonolog.handler.app
would appear, but notmonolog.logger.app
. I managed to bind this thing to a variable so that__construct(LoggerInterface $appLogger)
looked like it might work -- but $appLogger turned out to be an instance of the handler, not the logger. (In retrospect this doesn't seem so surprising.) So Symfony got upset and exploded. I then changed the name "app" to something else wherever it occured, and lo and behold, everything works! So, the question: is there something sacred about the word "app"?(2) And now that I have it working pretty well, I wonder if I can make my logger write exclusively to my file, or other destination that I decide on later -- because I see that my nice new logger has three handlers: two Streamhandlers and one ConsoleHandler. One Streamhandler logs to my custom path, one writes to dev.log, and the console handler? Is that for use with a
Symfony\Component\Console\Command\Command
? Anyway, the question is: how do you configure a logger to write to a particular destination exclusively?