Logging
Table of contents
Loading...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 loggedhasRecordThatMatches()
checks if a message matching a certain regex pattern has been loggedhasRecordThatPasses()
checks if a message passes a certain callback functionhasRecord()
checks if a certain message has been loggedgetRecords()
returns all logged messageshasRecords()
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
<?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));
}
}