An Interest In:
Web News this Week
- April 20, 2024
- April 19, 2024
- April 18, 2024
- April 17, 2024
- April 16, 2024
- April 15, 2024
- April 14, 2024
Investigating an incident: how to log effectively (PHP)
Why do we need loggers?
It's common for humans to make mistakes. That's so not only in the relation of developers but also users. And if while developing we control the process totally and can see the wrong behavior through with simple debugging, the same case on the productions environment wouldn't be so easy to investigate. In such cases the thing to help us are journals. But to gain a maximum value of its usage we should do it properly.
Logging the process of maintaining such journals, and it helps to detect hidden errors, understand user problems and simply sort out what really happened. In the simplest implementation logs are written to the text files and contain an exact time and description of the event. There is a bunch of methods to do that, and what is good for us the best practices were defined long ago.
In this article, we'll sort out how to organize logging in a PHP app, how to use it efficiently, and what libraries might be useful.
PSR-3 Standard. Log levels
PSR is a collection of general recommendations for PHP developers. It contains tips for code style, some interfaces, and other recommendations. One of its documents (PSR-3) is dedicated to a logger implementation.
I suggest that you start getting acquainted with these recommendations with the logging levels that it offers.
<?phpnamespace Psr\Log;class LogLevel{ const EMERGENCY = 'emergency'; const ALERT = 'alert'; const CRITICAL = 'critical'; const ERROR = 'error'; const WARNING = 'warning'; const NOTICE = 'notice'; const INFO = 'info'; const DEBUG = 'debug';}
PSR-3 defines 8 different message levels. Proper usage of them will simplify searching for errors and will boost up an accident reaction speed. Let's go deeper and figure out when to use them.
- DEBUG debugging information that reveals the details of the event in detail;
- INFO any interesting events. For instance: user has signed in;
- NOTICE important events within the expected behavior;
- WARNING exceptional cases which are still not errors. For example usage of a deprecated method or wrong API request;
- ERROR errors to be monitored, but which don't require an urgent fixing;
- CRITICAL critical state or an event. For instance: unavailability of a component or an unhandled exception;
- ALERT error and an event to be solved in the shortest time. For example, the database is unavailable;
- EMERGENCY whole app/system is completely out of order.
Use of these levels means just adding it as some kind of prefix to your log message. For instance:
[2021-01-01 12:10:35] website.INFO: User has changed his password
Such levels as ALERT
and EMERGENCY
are commonly handled with an additional notification (SMS, Email, and so on). INFO
allows you to reproduce the whole user's action sequence, while DEBUG
will simplify exact values and function's results fetching.
PSR-3. Logger Interface
In addition to the class with levels, PSR-3 offers us an interface for implementing our own loggers - LoggerInterface
. Its implementation is very useful because most existing libraries support it, and if you decide to replace your logger with another one, just connect a new class instead.
LoggerInterface
requires the implementation of logging methods, in accordance with the previously reviewed levels. Let's create our own logger class that will correspond to this interface and writes messages to a text file.
But first, let's download the PSR-3 code with Composer.
composer req psr/log
The package we downloaded contains several classes, traits, and interfaces, among which the LogLevel
we previously reviewed, and the LoggerInterface
that is currently of our interest. Let's create a new class that implements that interface. Important: make sure that you have the autoloader class included (vendor/autoload.php).
<?php// index.php// Composer's autoloaderrequire_once 'vendor/autoload.php';// Our new Logger classrequire_once 'src/FileLogger.php';
<?php// src/FileLogger.php// If `use` was not added automatically, // be sure to add it yourselfuse Psr\Log\LoggerInterface;// Implement a downloadclass FileLogger implements LoggerInterface{ // ...}
We created the class, but in order for it to meet the requirements of the standard, you need to write all the methods described in the interface. The most important of them is log
. The main logic of logging into the file will be written in it.
class FileLogger implements LoggerInterface{ // ... public function log($level, $message, array $context = []): void { // Current date in 1970-12-01 23:59:59 format $dateFormatted = (new \DateTime())->format('Y-m-d H:i:s'); // Build the message with the current date, log level, // and the string from the arguments $message = sprintf( '[%s] %s: %s%s', $dateFormatted, $level, $message, PHP_EOL // Line break ); // Writing to the file `devto.log` file_put_contents('devto.log', $message, FILE_APPEND); // FILE_APPEND flag prevents flushing the file content on each call // and simply adds a new string to it } // ...}
To fully satisfy the LoggerInterface
, we need to write an implementation for the emergency
, alert
, critical
, error
, warning
, notice
, info
and debug
methods, which correspond to the levels we have previously reviewed. Their implementation boils down to a very simple principle we call the log
method, passing the necessary level to it.
class FileLogger implements LoggerInterface{ // ... public function emergency($message, array $context = []): void { // Use the level from LogLevel class $this->log(LogLevel::EMERGENCY, $message, $context); } public function alert($message, array $context = []): void { $this->log(LogLevel::ALERT, $message, $context); } // and so on... // ...}
Use of logger
Now that our class implements the interface proposed by the PSR-3 standard, we can easily use it anywhere. For example, in the file index.php:
<?php// index.php// Composer's autoloaderrequire_once 'vendor/autoload.php';// Our new Logger classrequire_once 'src/FileLogger.php';$logger = new FileLogger();$logger->debug('Message from index.php');
Or in any other class:
<?phpuse Psr\Log\LoggerInterface;class ExampleService{ /** @var LoggerInterface */ private $logger; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } public function doSomeAction(): void { // do some work $this->logger->debug('Message from ExampleService'); }}
Please note that we do not specify the final implementation (FileLogger
) as the type of the constructor argument, but the interface of the PSR-3 standard. This is very convenient because it allows us to very easily replace the used logger with any other one that supports this interface.
Context
You may have noticed that all methods of the LoggerInterface
contain the $context
argument. Why is it needed?
The context is intended for transmitting auxiliary and, often, dynamic information. For example, if you are making a debug entry (debug level), you can pass the value of a variable to the context.
In order to apply this argument, we need to support it in the log method. Let's refine it, given that $context
is an array.
class FileLogger implements LoggerInterface{ // ... public function log($level, $message, array $context = []): void { $dateFormatted = (new \DateTime())->format('Y-m-d H:i:s'); // Let's convert $context array into json $contextString = json_encode($context); $message = sprintf( '[%s] %s: %s %s%s', $dateFormatted, $level, $message, $contextString, // Add context string PHP_EOL ); file_put_contents('devto.log', $message, FILE_APPEND); } // ...}
Now, at any place of the logger call, we can pass an array of additional information with the second argument.
<?php// index.php//...$userName = 'Elijah';$userEmail = '[email protected]';$logger = new FileLogger();$logger->debug('Message from index.php', [ 'user_name' => $userName, 'user_email' => $userEmail,]);
As a result, we will get a record of the following form:
[2021-09-02 13:00:24] debug: Message from index.php {"user_name":"Elijah","user_email":"[email protected]"}
Regarding the context, there is one very simple rule: any dynamic information should be transmitted in it, but not in the message. That is, if you form a message into a log using sprintf
or concatenation of string variables, most likely this information can be put into the context. Following this rule simplifies the search in the log because it eliminates the need to predict (or calculate) the values of variables.
The Monolog library
Despite the simplicity of the logging principle, there is a very wide scope for modifications in this area. We could support other record formats, implement SMS sending, or simply allow you to change the name of the final log file.
Fortunately, all this has been implemented in most libraries for a long time, and one of the most common is monolog.
Among the most significant advantages of this package are:
- Full support for PSR-3;
- Support for different principles of log handling, depending on the level;
- Support for channel names (logger names);
- Very wide support of frameworks.
To start using this beautiful tool, we will install it using Composer.
composer req monolog/monolog
The Monolog usage
The main idea of the monolog library is handlers. They allow you to set specific behavior for logging events. For example, to write a message to a text file we'll use StreamHandler
. Let's replace the use of our class with the loaded library.
<?php// index.phpuse Monolog\Logger;use Monolog\Handler\StreamHandler;// Composer's autoloaderrequire_once 'vendor/autoload.php';// We pass a channel name as the argument for Logger$logger = new Logger('devto-demo');// Connect the handler. Argument for StreamHandler is a path to a file$logger->pushHandler(new StreamHandler('devto.log'));// Then we leave everything as it was$userName = 'Elijah';$userEmail = '[email protected]';$logger->debug('Message from index.php', [ 'user_name' => $userName, 'user_email' => $userEmail,]);
If we run this code, the following entry will appear in the devto.log file:
[2021-09-02T13:16:14.122686+00:00] devto-demo.DEBUG: Message from index.php {"user_name":"Elijah","user_email":"[email protected]"} []
This is very similar to what we had earlier, except for adding the channel name (devto-demo).
An important feature of Monolog handlers is that they can be set the level at which they work. For example, we can write all the errors in a separate file.
<?php// index.phpuse Monolog\Handler\StreamHandler;use Monolog\Logger;use Psr\Log\LogLevel;// ...$logger = new Logger('devto-demo');$logger->pushHandler(new StreamHandler('devto.log'));$logger->pushHandler(new StreamHandler('errors.log', LogLevel::ERROR));// ...$logger->emergency('It is not even an error. It is EMERGENCY!!!');
The handler connected to the ERROR
level will take overall records of the ERROR
level and higher. Therefore, the call to the emergency
method falls into both files: devto.log and errors.log
This simple division of records into levels greatly simplifies our reaction to errors. After all, we no longer need to look for them among all the other entries in the log. This is a very simple and useful function.
Log records of the request
In the process of developing our project, the logs are read very simply. They are consistent and clear. But when several people use the product, the logs can get mixed up and just confuse to a greater extent than they help. There is a very simple trick to solve this problem. Instead of the channel name, use a unique session ID. You can get it using the built-in function session_id()
. (The session must be started using session_start()
)
Let's look at an example of the implementation of such a technique:
<?php// index.php// Start the sessionsession_start();// ...// Pass the session id as a channel name$logger = new Logger(session_id());// ...
What does such a simple refactor give us? A very important feature is to group all records by user requests.
// First request records[2021-09-02T13:35:54.155043+00:00] b30m8k1fvmf638au7ph0edb3o5.DEBUG: Message from index.php {"user_name":"Elijah","user_email":"[email protected]"} [][2021-09-02T13:35:54.156800+00:00] b30m8k1fvmf638au7ph0edb3o5.EMERGENCY: It is not even an error. It is EMERGENCY!!! [] []// Another request records. They have different ids[2021-09-02T13:36:03.528474+00:00] u7fi04mn99h0timg148rles1um.DEBUG: Message from index.php {"user_name":"Elijah","user_email":"[email protected]"} [][2021-09-02T13:36:03.529421+00:00] u7fi04mn99h0timg148rles1um.EMERGENCY: It is not even an error. It is EMERGENCY!!! [] []
What's next?
The Monolog supports a lot of very useful ready-made handlers that are worth paying attention to:
- TelegramBotHandler Sends logs via Telegram Bot. Very useful for high levels of logging;
- SlackHandler Very similar to the previous one, but sends records to Slack;
- SwiftMailerHandler Allows to send logs with email;
- ChromePHPHandler Gives access to logs directly from the Chrome browser in Live mode!
Conclusion
Logging is a simple and important tool. It will help you fix errors at an early stage, be sure that nothing is broken in the new version of the code, investigate the cases of your users and have general visibility on the project.
The main thing is to remember the simple rules:
- Following PSR-3 will make it easier for you to replace logger classes in your code and will allow you to use external libraries;
- Different logging levels will help you focus on what is important;
- Separation of dynamic information into context will simplify the search through logs;
- The Monolog library implements almost all possible programmer's requirements. Be sure to take the time to study it;
- Using the session ID, you can separate log entries for each request;
- It is better to write a lot of extra logs than not to add one important one.
Original Link: https://dev.to/he110/investigating-an-incident-how-to-log-effectively-php-105o
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To