Integration of Monolog into Nette Framework
The best way to install Kdyby/Monolog is using Composer:
$ composer require kdyby/monolog
and enable it in config.neon
extensions:
monolog: Kdyby\Monolog\DI\MonologExtension
This extension creates new configuration section monolog
.
You have to setup only channel name and you're good to go.
monolog:
name: awesome-blog
Please keep in mind that when you're using syslog, the name will be used in the logs.
The adapter replaces the default logger instance and you should not tinker with it anymore. To put it simply: use Monolog handlers, do not change the logger anymore.
You can of course turn off the registration into Tracy
monolog:
hookToTracy: off
but then you have to handle the messages yourself and you're giving up the power of Monolog.
Also, the adapter disables writing the messages to disk. But don't worry, every message is piped to the Monolog and you can really easily send them anywhere you want - that's what handlers are for.
This is the default handler that simulates the default Tracy behaviour and is registered only when no other handler is registered. When you start registering custom handlers, it automatically disables itself (to be exact, the extension does that) because it expects you know what you're doing :)
When you wanna for example have syslog on production and still have messages written to disk on localhost, you can easily configure that
monolog:
registerFallback: %debugMode%
The adapter even helps handle messages with non-standard level name (standard are: info, warning, error...). You could have been, for example, using this in your application
use Tracy\Debugger;
Debugger::log(sprintf('Sent email to "%s" with subject "%s"', $message->to, $message->subject), 'emails');
What does this do? It creates file logs/emails.log
and writes the given message into it.
Because emails
is not a standard message level name, Monolog would have failed to process it.
Therefore messages like this are sent with level info
and the real name is set to context.priority
, you'll see why in a moment.
Of course, when you install Monolog, this becomes obsolete, as you can start using the Logger
object instead of static method.
Processors are special type of class, that has to implement one method only - __invoke($record)
- so they become callable.
They're called for every message, that is sent to Monolog so they can change it's contents.
When you're adding processor programatically in your application, you can also use closures.
Kdyby always registers it's custom PriorityProcessor
, which has two tasks
- when message
context
containschannel
name, change the realchannel
to that name and unset the value fromcontext
- or, when the
context
haspriority
defined and the priority is standard name of record level (info, warning, error, ...) change thechannel
name to that
You already know about the priority
, the other one is explained in the next chapter.
We all wanna have nice logs, right? That's what channels are for - organizing logs.
The default channel name can be set in monolog.name
, as we've seen in the Minimal Configuration section.
The documentation of Monolog suggest using several instances of Logger
class with different names.
This can be useful when you need complex logging setup with different handlers for different loggers.
You can still create new Logger
service and add specific handlers, but this extension won't help you with that, it can configure only one Logger
currently.
But you can still use custom channels for different classes!
Let's say you want to have an EmailQueue
class that sends emails and you wanna log what emails you've sent.
Thanks to the PriorityProcessor
, you can set the channel in context
parameter and it will be used instead of the default channel.
$this->logger->addInfo(sprintf('Sent email to "%s" with subject "%s"', $message->to, $message->subject), ['channel' => 'emails']);
But that's not very nice, let's use something nicer. Can we expect, that the entire class will log every time with the same channel? Well, you certainly can if you're following the Single Responsibility Principle!
Let's look at the EmailQueue
constructor
class EmailQueue
{
/** @var \Nette\Mail\IMailer */
private $mailer;
/** @var \Monolog\Logger */
private $logger;
public function __construct(\Nette\Mail\IMailer $mailer, \Monolog\Logger $logger)
{
$this->mailer = $mailer;
$this->logger = $logger;
}
As you can see, such class would have been obviously doing only one task (sending emails), so making it handle the logging itself is not a problem as it's not adding much of new code. With bigger classes, you might wanna consider using Kdyby/Events and moving the logger to some kind of listener. But let's get back to channels.
Let's change the signature a bit, so the autocompletion starts working as expected
public function __construct(\Nette\Mail\IMailer $mailer, \Kdyby\Monolog\Logger $logger)
The custom logger in Kdyby only adds one new method ->channel($name)
, which is a factory for custom Logger with custom channel, that does nothing but proxy the messages to it's parent.
If we now use the factory instead of simply assigning the value
$this->logger = $logger->channel('emails');
every message we send to the logger will have changed the channel to emails
, so we can simplify the addInfo
statement from above to this
$this->logger->addInfo(sprintf('Sent email to "%s" with subject "%s"', $message->to, $message->subject));
Much nicer, right?
Please be aware that the ->channel()
really does return new instance, don't do things like
$this->logger = $logger;
$this->logger->channel('emails');
it won't work!