Properly setting up a logging system for your application is essential for its maintainability, debugging and monitoring.
That being said, what we ideally want in a logging system is the following:
- Only log our application messages, don’t log symphony stuff;
- Log to a different file per environment (dev.log, test.log, prod.log);
- Log to the console, when we run the app through the command line. This way we see feedback immediately, we don’t need to have another window open with tail -f dev.log. With this we can also automate the feedback the app gives back tot he user;
- Also log specific jobs to a specific file, so we have a log per job;
- On Production it should also send us an email if something goes wrong.
Before configuring the logs per environment, if we need custom channels we need to configure them in the main config.yml file:
monolog:
channels: [job]
Then, for each environment the logs are set up in the file /app/config/config_.yml
monolog:
handlers:
# The default logger handler
# This just sends all received log messages to all handlers in the group
main:
type: group
# This prevents symfony logging to be logged. This way it logs only the app logging.
channels: [app,doctrine,job]
# All these handlers receive the log message and decide what to do with it
# The buffered stream should only be here for production config
members: [streamed, console, job, mail]
# writes the log messages, above debug level, to a file
streamed:
type: stream
# The file name depends on the environment
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug # Only messages above debug level
# Writes log messages to the console, if we are running the app from cli
console:
type: console
verbosity_levels:
VERBOSITY_NORMAL: INFO
level: info # Only messages above info level
# Writes log messages sent to the 'job' channel, into the
# currently configured file. This needs a custom handler,
# set up as a service named 'app.jobs_logger_handler'
job:
type: service
id: app.jobs_logger_handler
level: info
channels: [job]
# This its triggered when finds a message with level
# and logs all messages after that, even if it has a lower level.
# This way the email will have a piece of the log in it.
mail:
type: fingers_crossed
action_level: error
handler: buffered
# Buffers all messages in the current request and, in the end,
# passes them all to the swift handler. If we use directly
# the swift handler, each message will be sent in a separate email
buffered:
type: buffer
handler: swift
# Sends messages by email, if the messages came from the
# buffered handler, it sends all of the request messages
# in one email
swift:
type: swift_mailer
from_email: notifications@somehost.com
to_email: notifications@somehost.com
subject: "[] [%kernel.environment%] An Error Occurred!"
# The email will contain all messages above, or equal to, this level
level: debug
For the job log handler to work, first we need to create it.
Its basically a subclass of the default StreamHandler but with extra methods so that we are able to change the destination file at runtime:
<?php
/**
* DynamicStreamHandler.php
*
* @author Herberto Graça
*/
namespace BundleApplicationBundleDomainLogHandler;
use MonologHandlerStreamHandler;
use MonologLogger;
class DynamicStreamHandler extends StreamHandler
{
public function __construct($level = Logger::DEBUG, $bubble = true)
{
parent::__construct("/dev/null", $level, $bubble);
}
/**
* Resets the stream so it logs to null (it doesn't log)
*/
public function resetStream()
{
$this->setStream("/dev/null");
}
/**
* @param string $stream
*
* @return self
*/
public function setStream($stream)
{
if (is_resource($stream)) {
$this->stream = $stream;
} else {
$this->url = $stream;
$this->stream = null;
}
return $this;
}
/**
* @return string
*/
public function getStream()
{
return $this->stream;
}
}
Next we need to set this handler as a service:
app.jobs_logger_handler.class: BundleFooBundleDomainLogHandlerDynamicStreamHandler
# CUSTOM LOG HANDLER for the individual job logging
app.jobs_logger_handler:
class: %app.jobs_logger_handler.class%
And finally we can use it in our code as:
$jobsLoggerHandler = $this->get('app.jobs_logger_handler');
$jobsLoggerHandler->setStream('/a/file/path/to/log/file');
$logger = $this->get('monolog.logger.job'); // Automatically recognized as the job channel
$job->doSomeWork($logger);
$jobsLoggerHandler->resetStream();
Of course, we could even have the function doSomeWork() fire an event and have an event listener set the stream file, and another event to reset the stream file.