Ultimate Guide to Logging

Your open-source resource for understanding, analyzing, and troubleshooting system logs

PHP Framework Logging

PHP logging is usually handled through an applications framework interface. This guide includes examples from a few popular frameworks and explores how each one handles logging. Frameworks included are Laravel, Drupal, Zend Framework, Symfony, and Magento. Some of the examples included pertain to specific versions and others to the current version.

Laravel

Laravel is a popular full stack framework. True to its philosophy of reusing the best components available, Laravel chose Monolog as its logging functionality.

Configuration

Logging uses a concept of channels to send log messages to different outputs. Laravel creates a default channel named for the current environment (development, production, etc.). You can configure logging via the file config/logging.php.  Each channel can have one or more drivers, which are the destinations logs are sent to. For a complete list of channels, please see the documentation. Some of the available channel drivers are:

  • single (default) – A single file or path-based logger channel
  • daily – A rotating log file driver which rotates daily
  • syslog – Use Monolog’s Syslog driver
  • errorlog – Use Monolog’s Errorlog driver

Environments

Your application will log error details depending on the value of the APP_DEBUG environment level in laravel/.env . On your development environment, you’ll want this value set to true, so that errors are displayed along with output. However, for your production environment, you should set it to false, which will log errors depending on your configuration settings. For example, if you’re using single or daily and your code encounters an error or an exception and APP_DEBUG=false, your laravel.log would have a line that looks like this.

[2015-09-25 02:25:40] local.ERROR: exception 'Symfony\Component\Debug\Exception\FatalErrorException' with message 'syntax error, unexpected 'Log' (T_STRING), expecting ',' or ';'' in /var/www/laravel/app/Http/routes.php:20

Logging Custom Actions

You can log custom actions in Laravel very easily with the Log facade. It has different methods corresponding to severity levels. These correspond to PSR-3 Log Levels for emergency, alert, critical, error, warning, notice, info, and debug, which come from RFC 5424. (Read more about PSR-3.) In your route or controller, the following will log an informational item.

use Log;

 

// ...

 

public function login() {

 

// …

Log::info('User login attempt', ['user' => $username]);

 

}

When a user tries to log in, the following example JSON output will be written to your log file.

[2015-09-25 02:38:39] local.INFO: User logged in. {"user":"oscar"}

Similarly, you can log other severity levels:

// email validation failed

Log::info('Invalid email supplied for registration', ['email' => $email]);

 

// could not write a file

Log::error('Can not save file');

Drupal 7

Logging in Drupal 7 is totally different than Drupal 8, so jump to the Drupal 8 logging section below unless you’re running Drupal 7. Drupal 7 logging is done using the hook_watchdog function. It passes you an array containing the necessary logging details you need for your module. We can take the DBLog module as an example, which writes logs to the Drupal database.

/**

* Implements hook_watchdog().

*/

 

function dblog_watchdog(array $log_entry) {

Database::getConnection('default', 'default')->insert('watchdog')->fields(array(

'uid'       => $log_entry['uid'],

'type'      => substr($log_entry['type'], 0, 64),

'message'   => $log_entry['message'],

'variables' => serialize($log_entry['variables']),

'severity'  => $log_entry['severity'],

'link'      => substr($log_entry['link'], 0, 255),

'location'  => $log_entry['request_uri'],

'referer'   => $log_entry['referer'],

'hostname'  => substr($log_entry['ip'], 0, 128),

'timestamp' => $log_entry['timestamp'],

))

->execute();

}

See the Drupal documentation for more details on available fields.

Configuration

Logging modules like SysLog, FileLog, and MailLog provide a configuration page to personalize things like errors display, limits, logs formatting, and more. For the example below, you may give the user the ability to select the logging severity, custom system variables (user, timestamp), etc.

Building a Logger

We’re going to build a MailLog module for our example. It will only email log errors and above. (ERROR, CRITICAL, ALERT, EMERGENCY)

Inside our mailog.module file, we’re going to register our hook function. You can read more about building a Drupal module in the official documentation.

/**

* Implements hook_watchdog().

*/

 

function maillog_watchdog(array $log_entry) {

if( (int) $log_entry['severity'] <= WATCHDOG_ERROR ) {

$to = "developer@example.com";

$from = "admin@example.com";

$subject = "MySite logs";

$headers = "From: {$from}";

 

if (!isset($log_entry['variables'])) {

$message = $log_entry['message'];

} else {

$message = strtr($log_entry['message'], $log_entry['variables']);

}

 

mail($to, $subject, $message, $headers);

}

}

The severity attribute inside the log_entry array contains an integer from 0 to 7 describing error severity. So, the if condition will only log errors and above. If the log contains any context variables, we substitute them using the strtr PHP function.

Drupal 8

Drupal 8 loggers are PSR-3 compliant. You can resolve the Drupal logger class from the container like the following.

\Drupal::logger($channel)->info($message);

You may also pass some context variables as a second parameter.

$channel = "general";

$message = "User %username has been deleted.";

$context = [

'%username' => 'younes'

];

\Drupal::logger($channel)->info($message, $context);

Because the DBLog module is activated by default, messages are logged to the watchdog table in your database.

// Output

mysql> select wid, type, message, variables from watchdog;
wid type message variables
81 general Just an info message a:0:{}
82 general User %username has been deleted. a:1:{s:9:”%username”;s:6:”younes”;}

2 rows in set (0.00 sec)

You can install other loggers from the modules page on the dashboard to log to different destinations. As an example, let’s use the Syslog module. You can go ahead and install it on the modules dashboard.

If you run the same code samples from above, you can check the log message on the /var/log/syslog file.

sudo cat /var/log/syslog

// Output

Sep 27 23:16:04 vagrant-ubuntu-trusty-64 drupal: https://vaprobash.dev|1443395764|general|192.168.22.1|https://vaprobash.dev/admin/modules|https://vaprobash.dev/admin/modules|1||Just an info message

Sep 27 23:16:21 vagrant-ubuntu-trusty-64 drupal: https://vaprobash.dev|1443395781|general|192.168.22.1|https://vaprobash.dev/admin/modules|https://vaprobash.dev/admin/modules|1||User younes has been deleted.

Configuration

You can configure logging through the Configuration > Development > Logging and Errors. The interface lets you specify errors display level, database log records limit, Syslog format (when Syslog module is installed), etc.

Note: Don’t forget to turn off errors display when moving to production.

Building a Logger

You can read all about defining a logger for Drupal 8 in the Logging API documentation. Our logger class should implement the Psr\Log\LoggerInterface interface and define the nine parent methods (emergency, alert, critical, error, warning, notice, info, debug, and log). You can check the PSR-3 Logging Standard for more details on how to use the Psr\Log package.

For example, I’m going to build a MailLog class that will log to the website admin. We aren’t going to cover how to create Drupal modules and settings pages. Here’s a stripped-down example:

use Psr\Log\LoggerInterface;

use Psr\Log\LoggerTrait;

 

class MailLog extends LoggerInterface

{

use LoggerTrait;

/**

* The parser will replace context variables inside the log message.

*/

protected $parser;

 

public function __construct(LogMessageParserInterface $parser)

{

$this->parser = $parser;

}

 

public function log($level, $message, array $context = array())

{

$to = "developer@example.com";

$from = "admin@example.com";

$subject = "MySite logs";

$headers = "From: {$from}";

$message =  $this->parser->parseMessagePlaceholders($message, $context);

 

mail($to, $subject, $message, $headers);

}

}

Zend Framework

The Zend Framework is a modular framework where each component is split into independent packages. One such component is the framework’s logging functionality, Zend\Log.

Zend\Log

The Zend\Log component is comprised of a group of logging classes that provide general logging functionality within a Zend Framework application, or can be used as a standalone component in another framework or application.

The base class for logging is the Zend\Log\Logger. Once instantiated, the logger object needs at least one writer to log data. You can have as many logger objects as needed without concerns about interoperability.

 

Once the logger object is instantiated, add writers as needed using the addWriter() method. The writer objects do the heavy lifting of writing to various data sources.

 

Optional filter objects are added to logger objects as needed to provide granular control over what is logged.

Optional formatter objects change the formatting of log entries. These objects are added to the writer objects, which in turn use them to format the log entries.

 

Once a logger object is set up with a writer, a call to the log() method writes the log data.

 

Note: The following examples assume loaded classes.

 

Example: Using a logger with a stream writer to a file path.

$logger = new Zend\Log\Logger;

$writer = new Zend\Log\Writer\Stream('/path/to/logfile');

$logger->addWriter($writer);

$logger->log(Zend\Log\Logger::CRIT, 'Critical stuff happening');

 

Available filters include: Priority, Regex, Timestamp, SuppressFilter, and Validator.

Severity Levels (called priorities in Zend) include: DEBUG, INFO, NOTICE, WARN, ERR, CRIT, ALERT, and EMERG.

Example: Adding a filter to a stream writer. In this case, a priority filter of critical.

$logger = new Zend\Log\Logger;

$writer = new Zend\Log\Writer\Stream('/path/to/critical/logfile');

$filter = new Zend\Log\Filter\Priority($logger::CRIT);

$logger->addWriter($writer);

$logger->log(Zend\Log\Logger::CRIT, ‘Critical stuff happening’);

Example: Adding a filter to a stream writer allowing for different priorities. The available writers are included in the Zend documentation.

$logger = new Zend\Log\Logger;

$criticalLog = new Zend\Log\Writer\Stream('/path/to/critical/logfile');

$filter = new Zend\Log\Filter\Priority(Zend\Log\Logger::CRIT);

$criticalLog->addFilter($filter); // Add filter to the critical stream writer

$logger->addWriter($criticalLog); // Add the writer to the logger

 

$infoLog = new Zend\Log\Writer\Stream(‘/path/to/information/logfile’);

$logger->addWriter($infoLog);

$logger->info(‘cool stuff happening’); //logged to informational logfile, blocked to critical log file due to the filter above

$logger->crit(‘critical stuff happening’); //logged to both informational and critical logfiles

Symfony

Symfony is another well-regarded, full stack framework. While framework components can be used separately, this section assumes you are using the standard edition for your application as indicated in the installation docs. The standard edition also uses the Monolog package to handle logging.

Configuring a Logger

Depending on your environment, Monolog is configured as a service in app/config/config_dev.yml or app/config/config_prod.yml. In the development environment, you’ll see a section with the following content (note that the level is set to debug, which can be quite verbose).

monolog:

handlers:

main:

type: stream

path: "%kernel.logs_dir%/%kernel.environment%.log"

level: debug

If you look at config_prod.yml, you’ll see a different handler and action_level are used.

monolog:

handlers:

main:

type: fingers_crossed

action_level: error

The FingersCrossedHandler class from Monolog stores messages in a buffer and only logs them if a message reaches the action_level specified. This keeps your production logs from getting cluttered with log messages you aren’t interested in.

Monolog service configuration includes specifying a logging format as a console service in app/config/services.yml. If you want to see errors logged to the console, here is an example. The formatting are placeholders for passed arguments.

monolog:

my_formatter:

class: Symfony\Bridge\Monolog\Formatter\ConsoleFormatter

arguments:

- "[%%datetime%%] %%start_tag%%%%message%%%%end_tag%% (%%level_name%%) %%context%% %%extra%%\n"

Environments

For a production environment, a typical Symfony app will use Monolog to save messages to app/logs/prod.log . Similarly, if you’re in a dev environment, the log file is app/logs/env.log.

Log a Message

To log a message use dependency injection in your controller method.

Use Psr\Log\LoggerInterface;

public function indexAction(LoggerInterface $logger)

{

$logger->info('User logged in', ['user' => $username]);

}

In your log file, you’ll find a line with:

[2015-09-24 23:05:42] app.INFO: User logged in {"user":"janeDoe"}

You can also use different severity levels.

// email validation failed

$logger->info('Invalid email supplied for registration', ['email' => $email]);

// could not write a file

$logger->error('Can not save file');

Magento 1.9*

Magento is an eCommerce platform written in PHP. You can configure its logging capabilities via the admin interface at <your/site/url/admin>. Logging is not enabled by default and requires configuration settings adjustments.

Enable Logging

Within the Admin panel, select the top menu “System”. On the left menu under configuration, select Advanced>Developer to display the Developer configuration options.

In the main content, select the “Log Settings” option to display the options for enabling logging and define the log paths for the system and exception logs. The path default settings are {{base_dir}}/var/log. The base_dir in this case is the install directory for the source code on the server. The default log for system logging is the /var/log/system.log, and the default for exception logging is the /var/log/exception.log.

Once logging is enabled and the log paths set, select the “Save Config” button. A message is displayed to confirm the saved configuration.

The Mage class is used to log the two types of messages: the Mage::log() method for general system logging and the Mage::logExceptions() method for exceptions.

The log method defines four parameters: a required message to log, an optional severity level, an optional file location path, and a forceLog boolean.

Mage::log($message, $level = null, $file = ‘’, $forceLog = false);

The logException defines a single parameter—an exception object.

Mage::logException(Exception $e);

Example – Log a message to the system log.

Mage::log(‘Message to log’);

Example – Log a message to the system log with a severity.

Mage::log(‘Message to log’, 4);

Example – Log a message to a custom log location with a severity and log path.

Mage::log(‘Message to log’, 4, <path/to/severity/4/file>);

There are quite a few additional logging options available in the system configuration. Settings such as dates, times, and cleaning are found here and are configurable by system administrators.  Within the Admin panel, select the top menu “System”. On the left menu under configuration, select Advanced>System to display the Log accordion option. These settings have global system impact.

Magento 2

Magento 2 is a full stack e-commerce platform. It integrates and uses the Monolog logging framework as its main logging functionality rather than the previous base class Mage::log() method. This makes it compliant with the PSR-3 logging implementation standard, as well as namespaces.

Magento 2 uses Monolog as a standard framework implementation in the form of Magento\Framework\Logger\Monolog. This means when a class injects the \Psr\Logger\LoggerInterface instance, it will receive an instance of the Monolog logger.

This example class uses dependency injection to ensure logging is available via a class implementing the standard. To use the logger, grab the injected instance and call one of the standard methods passing an exception.

Use \Psr\Logger\LoggerInterface;

 

class SomeClass {

protected $logger;

 

public function __construct(LoggerInterface $logger) {

$this->logger = $logger;

}

 

public function doMethod() {

//business logic ...

try {

//Something that throws an exception

} catch (\Exception $e) {

$this->logger->critical($e);

}

}

}