Have you ever wanted to have the ability to dynamically, at run-time, control the logging in your application?

Let’s suppose you’re building some network-based application, like simple client-server chat application. Normally, your application will perform all sorts of tasks typical for chat applications like connect/disconnect, log in/log out, send message/receive message, etc. You probably want to record all or most of these significant events, that is to log them, just in case if you need them in the future, for whatever reason. However, soon you realize that your application is generating really a lots of logs, and you’re very quickly reaching your monthly plan at your logging service. Then you decide either to turn the logging off entirely, or to use some other strategy to log only some smaller percentage of all available logs (like Monolog\SamplingHandler).

What if you really don’t actually need any logs, most of the time? It would be super cool to keep the logging turned off all the time, and turn it on, only when you need it. Once you’re done, you turn it off again, until the next time you need the same thing.

Switchable Logger is a simple solution designed to solve the problem explained above. Its sole purpose is to log events only when enabled.

Let’s see how SwitchableLogger could be implemented. Credits for class name SwitchableLogger go to my colleague Ben.

<?php

declare(strict_types=1);

namespace App\Infrastructure\Logging;

use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;

final class SwitchableLogger extends AbstractLogger
{
    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var LoggingStateInterface
     */
    private $loggingState;

    public function __construct(LoggerInterface $logger, LoggingStateInterface $loggingState)
    {
        $this->logger = $logger;
        $this->loggingState = $loggingState;
    }

    public function log($level, $message, array $context = [])
    {
        if (!$this->loggingState->isEnabled()) {
            return;
        }

        $this->logger->log($level, $message, $context);
    }
}

SwitchableLogger simply decorates any PSR-3-compliance logger implementation and performs the real logging if its “logging state” is enabled. The sole purpose of “logging state” passed to this logger is to tell us whether or not logging is enabled.

You can notice that “logging state” is represented as an abstration named LoggingStateInterface:

<?php

declare(strict_types=1);

namespace App\Infrastructure\Logging;

interface LoggingStateInterface
{
    public function isEnabled(): bool;
}

This is how we encapsulate algorithms for determining whether or not logging should be enabled. We also decouple them from the client code SwitchableLogger. This is, in essence, simple Strategy pattern implementation. All we have to do is to implement LoggingStateInterface and decide when we want to enable logging. Let’s do some quickly!

<?php

declare(strict_types=1);

namespace App\Infrastructure\Logging;

use Redis;

class RedisControlledLoggingState implements LoggingStateInterface
{
    private const REDIS_KEY = 'logging.enabled';

    /**
     * @var Redis
     */
    private $redis;

    public function __construct(Redis $redis)
    {
        $this->redis = $redis;
    }

    public function isEnabled(): bool
    {
        return (bool) $this->redis->get(self::REDIS_KEY);
    }
}

RedisControlledLoggingState is a logging state that can be controlled with Redis. Simply set logging.enabled key in Redis and logging will be enabled. Delete the key and logging is disabled.

<?php

declare(strict_types=1);

namespace App\Infrastructure\Logging;

class RandomSelectionLoggingState implements LoggingStateInterface
{
    public function isEnabled(): bool
    {
        return (bool) random_int(0, 1);
    }
}

Ok, this is just a silly one for demonstration purposes. RandomSelectionLoggingState randomly turns the logging on or off.

<?php

declare(strict_types=1);

namespace App\Infrastructure\Logging;

class NightShiftLoggingState implements LoggingStateInterface
{
    private const START = 21;
    private const END = 7;

    public function isEnabled(): bool
    {
        return date('H') >= self::START || date('H') <= self::END;
    }
}

Yet another dumb example of possible logging state implementations. NightShiftLoggingState ensures logging is turned on while everyone’s sleeping.

Now, let’s see very quickly how to use all of these in the real world example. We’ll use Monolog as PSR-3 logger implementation, but it can be any other that conforms to PSR-3.

<?php

$logger = new SwitchableLogger(new Monolog\Logger(), new NightShiftLoggingState());
$logger->warning('No one is here during the night! Time for logging.');

$logger = new SwitchableLogger(new Monolog\Logger(), new RandomSelectionLoggingState());
$logger->info('This might or might not be logged...');

$logger = new SwitchableLogger(new Monolog\Logger(), new RedisControlledLoggingState());
// redis-cli> SET logger.enabled 1
$logger->debug(sprintf('Huge JSON %s received over the network.', $json));
// redis-cli> DEL logger.enabled
$logger->debug(sprintf('Again, enormous JSON %s received but the logging is turned off.', $json));

SwitchableLogger is a very simple yet powerful implementation that solves one concrete problem. The code presented in this blog post is also available on Github. Feel free to download and use it in your projects if you think it could be useful for you.