How to set up a proper logger in Symfony2

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.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s