Zubair Mohsin

How Logging works in Laravel using `monolog/monolog` package

From the GitHub About section of monolog package:

  • Sends your logs to files, sockets, inboxes, databases and various web services.

Take a look at Handlers directory of this package to see how many services it supports out of the box 🤯

Code Dive

In this blogpost, we'll start from the example so that we can get a taste of diving, instead of directly jumping to ServiceProvider. Let's start with an example usage of logging in Laravel.

use Illuminate\Support\Facades\Log;

Log::info('hello world', ['data' => 'hello world again']);

First of all, let's jump to Log facade where we can see its returning a string log.

  • I am assuming reader knows how Facade works in Laravel.

Log facade

class Log extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'log';
    }
}

We know that this string log has been bound already to a concrete implementation inside service container using a dedicated ServiceProvider. By following the convention we can safely assume that it must be LogServiceProvider.

Let's take another approach this time and ask the container to tell us the concrete class behind log string.

How do we do it? Pretty easy, using the app() helper.

app('log');

If we dd() the result, we get:

Illuminate\Log\LogManager {#178 â–¼
#app: Illuminate\Foundation\Application {#1 â–¶}
#channels: array:2 [â–¶]
#customCreators: array:1 [â–¶]
#dateFormat: "Y-m-d H:i:s"
#levels: array:8 [â–¶] }

There we have it, class behind log string. To verify that we got the correct class, let's take a look at LogServiceProvider too.

<?php

namespace Illuminate\Log;

use Illuminate\Support\ServiceProvider;

class LogServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('log', function ($app) {
            return new LogManager($app);
        });
    }
}

Log::info()

Now that we know Log facade uses LogManager class under-the-hood, we can safely assume that info() must exist on this class.

/**
 * Interesting events.
 *
 * Example: User logs in, SQL logs.
 *
 * @param  string  $message
 * @param  array  $context
 *
 * @return void
 */
public function info($message, array $context = [])
{
    $this->driver()->info($message, $context);
}

info() method gets the driver() and calls info() method on that driver. Let's dig into driver() method and all its related methods.

        /**
     * Get a log driver instance.
     *
     * @param  string|null  $driver
     * @return \Psr\Log\LoggerInterface
     */
    public function driver($driver = null)
    {
        return $this->get($driver ?? $this->getDefaultDriver());
    }

        /**
     * Get the default log driver name.
     *
     * @return string
     */
    public function getDefaultDriver()
    {
        return $this->app['config']['logging.default'];
    }

        /**
     * Attempt to get the log from the local cache.
     *
     * @param  string  $name
     * @return \Psr\Log\LoggerInterface
     */
    protected function get($name)
    {
        try {
            return $this->channels[$name] ?? with(
                $this->resolve($name),
                function ($logger) use ($name) {
                    return $this->channels[$name] = $this->tap(
                        $name,
                        new Logger($logger, $this->app['events'])
                    );
                }
            );
        } catch (Throwable $e) {
            return tap(
                $this->createEmergencyLogger(),
                function ($logger) use ($e) {
                    $logger->emergency(
                        'Using emergency logger.',
                        ['exception' => $e,]
                    );
                }
            );
        }
    }

The driver() method is getting the default driver from config/logging.php file. Laravel supports multi-channel logging, and each channel has its own driver. For the sake of simplicity, we'll consider single as our default channel with below config:

'single' => [
            'driver' => 'single',
            'path' => storage_path('logs/laravel.log'),
            'level' => env('LOG_LEVEL', 'debug'),
        ],

Inside the get() method above, it's resolving the given channel driver by name and applying taps on it. (Read more about taps in the docs). If it can't resolve the channel driver, it creates an emergency logger which writes to laravel.log file. How can this happen? If you misspelled your channel name or driver name.

resolve() method

/**
 * Resolve the given log instance by name.
 *
 * @param  string  $name
 * @return \Psr\Log\LoggerInterface
 *
 * @throws \InvalidArgumentException
 */
protected function resolve($name)
{
    $config = $this->configurationFor($name);

    if (is_null($config)) {
        throw new InvalidArgumentException("Log [{$name}] is not defined.");
    }

    $driverMethod = 'create' . ucfirst($config['driver']) . 'Driver';

    if (method_exists($this, $driverMethod)) {
        return $this->{$driverMethod}($config);
    }

    throw new InvalidArgumentException(
        "Driver [{$config['driver']}] is not supported."
    );
}

It's getting the configuration for given driver and then creating a method name dynamically. In our case $name = 'single' so the $driverMethod = 'createSingleDriver' . After that, its calling that method with its configuration.

use Monolog\Logger as Monolog;
use Monolog\Handler\StreamHandler;

/**
 * Create an instance of the single file log driver.
 *
 * @param  array  $config
 * @return \Psr\Log\LoggerInterface
 */
protected function createSingleDriver(array $config)
{
    return new Monolog($this->parseChannel($config), [
        $this->prepareHandler(
            new StreamHandler(
                $config['path'],
                $this->level($config),
                $config['bubble'] ?? true,
                $config['permission'] ?? null,
                $config['locking'] ?? false
            ),
            $config
        ),
    ]);
}

Here we can see that a new instance of Monolog\Logger is returned with channel name and a handler which also provided by monolog package.

For the sake of understanding, in the end input() method is called on Monolog\Logger class.

/**
 * Adds a log record at the INFO level.
 *
 * This method allows for compatibility with common interfaces.
 *
 * @param string  $message The log message
 * @param mixed[] $context The log context
 */
public function info($message, array $context = []): void
{
    $this->addRecord(static::INFO, (string) $message, $context);
}

addRecord method is responsible for writing log record in the file. How it writes to the file is out of scope of this blogpost.

Interesting facts about Monolog in Laravel context

  • This package has been part of Laravel since the beginning ( maybe ).
  • Link to very first commit of laravel/framework on GitHub made by Taylor Otwell on 11 Jan, 2013 confirms the above statement.
  • On GitHub, we only have access to version 4.0 of laravel/framework, so, not sure if this package was part of Laravel in versions earlier than 4.0.

I hope you enjoyed this post. Next, we will see how Laravel uses nesbot/carbon package. You can follow me on Twitter or join my newsletter for more content.