Skip to content

Logging

samuelgfeller edited this page Mar 22, 2024 · 10 revisions

Introduction

Logging is the process of recording events or messages that occur while a program is running. These events or messages are typically written to a log file and can be used for various purposes including debugging, monitoring application performance, security and auditing.

The PSR-3 standard defines a common interface for logging libraries.

The go-to library for logging in PHP is Monolog. It is PSR-3 compliant and supports a variety of handlers and formatters.

composer require monolog/monolog

Log levels

The log level is used to determine the severity of the message.
Monolog uses the RFC 5424 standard for log levels which correspond to Monolog\Logger enum cases.

  • Debug: detailed debug information, useful when diagnosing problems.
  • Info: Interesting events. Examples: User logs in, SQL logs
  • Notice: Uncommon events.
  • Warning: Exceptional occurrences that are not errors. Examples: Use of deprecated APIs, undesirable things that are not necessarily wrong.
  • Error: Runtime errors.
  • Critical: Critical conditions. Example: Application component unavailable, unexpected exception.
  • Alert: Action must be taken immediately. Example: Entire website down, database unavailable, etc.
  • Emergency: Urgent alert. System is unusable.

Configuration

The defaults.php file holds the default configuration values for the logger. It contains the path where the log file should be created and the minimal log level that should be recorded.

The folder path should be created manually and the webserver user (e.g. www-data) should have write permissions.

File: config/defaults.php

$settings['logger'] = [
    // Log file location in the logs folder in the project root
    'path' => dirname(__DIR__) . '/logs',
    // Default log level
    'level' => \Monolog\Level::Debug,
];

Disabling logging for testing

To prevent the logger from writing to the log file during testing, the test mode must be enabled in the testing configuration by setting the config key 'test' to true.

File: config/env.test.php

// ...

// Enable test mode for the logger
$settings['logger']['test'] = true;

The section setting the right environment of the Configuration chapter details how the test environment values are loaded during testing.

Container setup

To use the logger across the application via dependency injection, it has to be instantiated in the container with the configuration. That way, LoggerInterface resolves to the Monolog\Logger instance.

The RotatingFileHandler is used to create a new log file every day with the date in the filename.

File: config/container.php

use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Formatter\LineFormatter;

return [
    // ...
    LoggerInterface::class => function (ContainerInterface $container) {
        $loggerSettings = $container->get('settings')['logger'];

        $logger = new Logger('app');

        // When testing, 'test' value is true which means the monolog test handler should be used
        if (isset($loggerSettings['test']) && $loggerSettings['test'] === true) {
            return $logger->pushHandler(new \Monolog\Handler\TestHandler());
        }

        // Instantiate logger with rotating file handler
        $filename = sprintf('%s/app.log', $loggerSettings['path']);
        $level = $loggerSettings['level'];
        // With the RotatingFileHandler, a new log file is created every day
        $rotatingFileHandler = new RotatingFileHandler($filename, 0, $level, true, 0777);
        // The last "true" here tells monolog to remove empty []'s
        $rotatingFileHandler->setFormatter(new LineFormatter(null, 'Y-m-d H:i:s', false, true));
        return $logger->pushHandler($rotatingFileHandler);
    },
    // ...
];

Usage

To use the logger in the application, the PSR-3 LoggerInterface can be injected into the constructor of the class that needs it.

The interface defines a method for each log level with the name of the level as the method name. The first parameter is the message to be logged; the second one optionally accepts a context array.

File: src/Domain/ExampleClass.php

<?php

namespace App\Domain;

use Psr\Log\LoggerInterface;

final readonly class ExampleClass
{
    public function __construct(
        private LoggerInterface $logger
    ) {
    }

    public function exampleFunction(): void
    {
        $this->logger->debug('Example debug message', ['foo' => 'bar']);
        $this->logger->info('Example info message');
        $this->logger->notice('Example notice message');
        $this->logger->warning('Example warning message');
        $this->logger->error('Example error message');
        $this->logger->critical('Example critical message');
        $this->logger->alert('Example alert message');
        $this->logger->emergency('Example emergency message');
    }
}

Asserting logged messages

The monolog Monolog\Handler\TestHandler can be used to assert the logged messages during testing.

It stores the logged messages in memory and provides the following methods to assert the messages:

  • hasRecordThatContains() checks if a message containing a certain string has been logged
  • hasRecordThatMatches() checks if a message matching a certain regex pattern has been logged
  • hasRecordThatPasses() checks if a message passes a certain callback function
  • hasRecord() checks if a certain message has been logged
  • getRecords() returns all logged messages
  • hasRecords() checks if any messages have been logged

The TestHandler can be retrieved from the Monolog\Logger instance with the getHandlers() method.

Note: if another PSR-3 logger is used, the getHandlers() method and the TestHandler class are not available as they are monolog-specific.

File: tests/Integration/ExampleClassTest.php

namespace App\Test\Integration;

use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Monolog\Handler\TestHandler;
use Monolog\Level;
use App\Test\Trait\AppTestTrait;

class ExampleClassTest extends TestCase
{

    use AppTestTrait;
    // ...

    public function testExampleAction(): void
    {
        // ... 
        
        // Get the test handler
        /** @var TestHandler $testHandler */
        $testHandler = $this->container->get(LoggerInterface::class)->getHandlers()[0];
        // Assert that a certain notice has been logged
        self::assertTrue($testHandler->hasRecordThatContains('Example partial notice', Level::Notice));
    }
}
Clone this wiki locally