Logger

1. Introduction

The class Logger is a tool to record application information easily and within a central store. It supports multiple log files and threshold configuration. Thresholds are useful to filter entries that are not meant for production environments.

Threshold definition has been introduced in release 1.15 and may be used since this version only.

To not influence the performance of an web application in a negative way the logger must be used in singleton flavour. The component ensures that every log entry is flushed into a logfile at the end of a request.

2. Architecture

Since version 1.17 the Logger implementation of the APF provides a separation between the Logger itself, the model of log entries (LogEntry), and the persistence (LogWriter):

Architecture of the APF Logger since version 1.17

The Logger is the central touch-point for the framework and applications built on the APF to store log entries. The appearance of a log entry is described by the LogEntry interface and the reference implementation SimpleLogEntry. The question where the log entry is written is answered by the LogWriter interface implementation. The Logger uses the registered writers to persist the internal buffer at the end of the request processing.

To register LogWriter implementations, the Logger provides the following methods:

  • addLogWriter($target, LogWriter $writer)
  • removeLogWriter($target)
  • getLogWriter($target)

Each LogWriter is registered with a unique target identifier ($target). The Logger uses this target to assign incoming log entries to the appropriate persistence layer. To support this mapping, each LogEntry is assigned to one dedicated log target, too.

The interface of a LogEntry is as follows:

PHP code
interface LogEntry { const SEVERITY_TRACE = 'TRACE'; const SEVERITY_DEBUG = 'DEBUG'; const SEVERITY_INFO = 'INFO'; const SEVERITY_WARNING = 'WARN'; const SEVERITY_ERROR = 'ERROR'; const SEVERITY_FATAL = 'FATAL'; public function __toString(); public function getLogTarget(); public function getSeverity(); }

getSeverity() is used by the Logger to check, whether the entry's severity is above the defined threshold or should be discarded. The return value of the getLogTarget() method is used by the Logger to assign the entry to the appropriate LogWriter.

The implementation of a LogWriter must comply with the following interface definition:

PHP code
interface LogWriter { public function writeLogEntries(array $entries); public function setTarget($target); }

Using the setTarget() method the Logger injects the target identifier. This key may be used to generate the log file name. writeLogEntries() is delegated the persistence of the applied list of log entries.

Compared to the implementation of the Logger up to version 1.16 in 1.17 the Logger provides the full feature set of the AdvancedLogger while being much more flexible. Besides the configuration of the persistence layers you can also define different formats of the log entries using different LogEntry implementations.

3. Configuration

3.1. LogWriter management

The Logger understands itself as a router for log entries as described in chapter 2 that delegates the persistence of the queued entries to registered LogWriters. For that reason, a LogWriter is necessary to persist the desired log entries to a file or a database.

Managing LogWriters with the Logger is described within the next chapters.

Please note, that the APF registers one FileLogWriter instance at shipping. Thus, the framework behaves as in pre-1.17 releases.

Moreover, the configuration of the standard log target is defined within the Registry using namespace apf::core and key InternalLogTarget as of 1.17. Please use the following code to add log entries using the standard log target (both alternatives possible):

PHP code
$logger->logEntry( Registry::retrieve('apf::core', 'InternalLogTarget'), 'My log message', LogEntry::SEVERITY_INFO ); $logger->addEntry(new SimpleLogEntry( Registry::retrieve('apf::core', 'InternalLogTarget'), 'My log message', LogEntry::SEVERITY_INFO ));
Using this method, you can ensure that the log entry is passed to the configured standard LogWriter and persisted within the standard log file.

In case no log target has been specified the Logger quits writing the log entries with an exception. Example:
Code
Fatal error: Uncaught exception 'LoggerException' with message 'Log writer with name "mysqlx" is not registered!' in */core/logging/Logger.php on line 433
The Logger does not include fallback mechanisms for security reasons. Fallback mechanisms - e.g. writing entries to stdout like the StdOutLogWriter does - may make sensible data available to the outside.
3.1.1. Adding a LogWriter

By default, the FileLogWriter is registered with the key specified within the Registry key InternalLogTarget (namespace: apf::core) that is apf at delivery.

In order to add a custom LogWriter you may use the addLogWriter() function. This procedure can be used for shipped and custom implementations:

PHP code
$logger = & Singleton::getInstance('Logger'); import('core::logging::writer', 'StdOutLogWriter'); $logger->addLogWriter( 'stdout', new StdOutLogWriter() );

Afterwards, the LogWriter registered with stdout can be used as follows:

PHP code
$logger = & Singleton::getInstance('Logger'); $logger->addEntry(new SimpleLogEntry( 'stdout', 'This is a log message', LogEntry::SEVERITY_INFO ));
3.1.2. Configuration of existing LogWriter

In case you intend to configure a registered LogWriter you may use getLogWriter(). Please use getRegisteredTargets() to query a list of registered log writers.

To change the configuration of the standard LogWriter please use the following code:

PHP code
$standardWriter = $logger->getLogWriter( Registry::retrieve('apf::core', 'InternalLogTarget') ); $standardWriter->setHostPrefix($_SERVER['HTTP_HOST']);

Details on the configuration of LogWriters can be taken from chapter 4.

3.1.3. Additional configuration with existing LogWriter

Using the getLogWriter() method you can use previously registered LogWriter as a basis for your custom targets. Please use the following code as a template to register the standard LogWriter under another target identifier:

PHP code
$writer = clone $logger->getLogWriter( Registry::retrieve('apf::core', 'InternalLogTarget') ); $logger->addLogWriter('new-target', $writer);

As described within the previous chapters the clone'd instance of the LogWriter can be configured as desired.

3.1.4. Deleting LogWriter

In order to remove registered LogWriters please use removeLogWriter():

PHP code
$logger->removeLogWriter('mysqlx');

Please note, that existing log entries cannot be persisted after removing the appropriate log writer.

3.1.5. Configuration for development environments

In development environments it may be helpful to include all log entries to the HTML source code of your application. To redirect all existing log entries to the standard output channel of the web server, you may use the following source code:

PHP code
foreach($logger->getRegisteredTargets() as $logTarget) { $logger->addLogWriter($logTarget, new StdOutLogWriter()); }

3.2. Log directory

Please note, that the configuration of the log directory has changed in version 1.17. The content of this chapter is only valid for releases up to 1.16. Please note the description of configuring log targets in chapter 4.

The log directory is configured within the registry value LogDir from the registry namespace apf::core. The value is initialized with the absolute path to the current directory plus the logs sub-folder on startup. In case you intend to use another directory, you may adapt the path within your bootstrap file before creation of the front controller:

PHP code
Registry::register('apf::core', 'LogDir', '/path/to/my/log/dir');

This path is used to place all log files of the entire application.

To avoid issues with accessing the log directory the path should be defined as absolute!

3.3. Buffer length

As noted above, the Logger collects all entries to write them to the log file at the end of the request. Facing high log traffic, the memory consumption of your application may collide with PHP's system configuration (e.g. memory_limit).

To reduce memory consumption the maximum buffer length can be defined as desired. By default, 300 entries are collected until the buffer is flushed. To adapt this value, call

PHP code
$log = &Singleton::getInstance('Logger'); $log->setMaxBufferLength(500);

3.4. Threshold

Since release 1.15 thresholds - or profiles - for log entries can be defined. In case the severity of the log entry is above the threshold - or matches the profile - it is added to the log file. Otherwise it is skipped.

To define the threshold, please use the following lines:

PHP code
$log = &Singleton::getInstance('Logger'); $log->setLogThreshold(Logger::$LOGGER_THRESHOLD_WARN);

As you may take from the example, the Logger already defines standard thresholds. Furthermore, you can create custom profiles using the severity definitions of the LogEntry interface. These are:

  • SEVERITY_TRACE
  • SEVERITY_DEBUG
  • SEVERITY_INFO
  • SEVERITY_WARNING
  • SEVERITY_ERROR
  • SEVERITY_FATAL

The following code block describes how to define a custom threshold - or create a custom profile:

PHP code
$log = &Singleton::getInstance('Logger'); $log->setLogThreshold(array( LogEntry::SEVERITY_TRACE, LogEntry::SEVERITY_INFO ));
Please note, that defining thresholds with the setLogThreshold() method are not restricted to any hierarchy. This means, that you can define which log entries are written to the log file or not freely.

The Logger defines the following default thresholds:

  • $LOGGER_THRESHOLD_ALL
  • $LOGGER_THRESHOLD_WARN
  • $LOGGER_THRESHOLD_INFO
  • $LOGGER_THRESHOLD_ERROR

Details on the definition can be taken from API documentation.

3.5. Host prefix

Please note, that the configuration of the host prefix has changed in version 1.17. The content of this chapter is only valid for releases up to 1.16. Please note the description of configuring host prefixes in chapter 4.

In clustered web server environments log files are often written to a shared filesystem. This eases analysis and archiving but often causes concurrency issues and slows down the application. This is because waiting for write locks blocks the application and increases lock traffic.

One common solution is to split log files into host-dependent files. Often, this is done by adding the host name to the file name. Moreover, this enables you to analyze issues with a dedicated node.

Using 1.15's host prefix you can define a server-dependent prefix for each log file:

PHP code
$log = &Singleton::getInstance('Logger'); $log->setHostPrefix('node1');
To define a generic prefix without the need to create host-specific deployment tasks, you may want to use environment or server configuration values to define the prefix. This may be done using the $_SERVER super global variable:
PHP code
$log = &Singleton::getInstance('Logger'); $log->setHostPrefix($_SERVER['SERVER_ADDR']);

4. Existing LogWriter

Release 1.17 ships several LogWriter implementations described within the subsequent chapters.

4.1. FileLogWriter

The FileLogWriter persists the applied log entries to a file. The name of the file contains the current date, an optional host prefix and the log target the LogWriter is registered with (e.g. 2013_01_20__127_0_0_1__apf.log).

The following chapters contain a detailed description of the FileLogWriters.

4.1.1. Log directory

Defining the log directory the constructor (mandatory) or setLogDir() (optional) can be used. Die registration of the FileLogWriters can be done as follows:

PHP code
$logger->addLogWriter('file', new FileLogWriter('/path/to/log/dir'));

Configuration of existing instances is as shown below:

PHP code
$fileWriter = $logger->getLogWriter('file'); $logger->setLogDir('/path/to/log/dir');
In order to avoid issues with accessing log directories the log path should be be defined as an absolute path!
4.1.2. Log directory permission

In case the log directory applied to the constructor or to setLogDir() doesn't exist the FileLogWriter will create it lazily. To define the desired file mask the FileLogWriter provides the setLogFolderPermissions() function. Please use the method as follows to the define the umask:

PHP code
$fileWriter = $logger->getLogWriter('file'); $fileWriter->setLogFolderPermissions(0755);
4.1.3. Host prefix

In clustered web server environments log files are often written to a shared filesystem. This eases analysis and archiving but often causes concurrency issues and slows down the application. This is because waiting for write locks blocks the application and increases lock traffic.

One common solution is to split log files into host-dependent files. Often, this is done by adding the host name to the file name. Moreover, this enables you to analyze issues with a dedicated node.

Using 1.15's host prefix you can define a server-dependent prefix for each log file:

PHP code
$fileWriter = $logger->getLogWriter('file'); $fileWriter->setHostPrefix('node1');
To define a generic prefix without the need to create host-specific deployment tasks, you may want to use environment or server configuration values to define the prefix. This may be done using the $_SERVER super global variable:
PHP code
$fileWriter = $logger->getLogWriter('file'); $fileWriter->setHostPrefix($_SERVER['SERVER_ADDR']);

4.2. StdOutWriter

The StdOutLogWriter writes the log entries to the standard output channel of the web server. This means that your visitor will see the log entries.

Log entries may contain sensible data of your application. For security reasons the StdOutWriter should only be used in development environments!

Registration of the StdOutLogWriter can be done within your bootstrap file as follows:

PHP code
import('core::logging::writer', 'StdOutLogWriter'); $logger->addLogWriter('stdout', new StdOutLogWriter());

Log entries with the stdout target identifier will be displayed within the HTML source code of your application.

4.3. DatabaseLogWriter

The DatabaseLogWriter enables you to flush the collected entries to a database table. For this reason, please create a table within any database with the following SQL statement:

SQL statement
CREATE TABLE IF NOT EXISTS `{$this->logTable}` ( `target` varchar(10) NOT NULL default '', `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP, `severity` varchar(10) NOT NULL default '', `message` text NOT NULL );

Thereby, {$this->logTable} is the name of the table the DatabaseLogWriter is registered with. Having this virtualization in place you are provided the possibility to register different instances of the DatabaseLogWriter pointing to different log tables.

In order to use this LogWriter please use the following code sample:

PHP code
import('core::logging::writer', 'DatabaseLogWriter'); $logger->addlogWriter('db', new DatabaseLogWriter('my-connection', 'app_logs'));

The first argument of the constructor refers to the database connection that is created with the ConnectionManager, the second parameter names the log table.

The DatabaseLogWriter uses the target identifier as one content within the log table. This allows you to re-use one log table for different use cases (if desired).

4.4. GraphiteLogWriter

The GraphiteLogWriter - in combination with the GraphiteLogEntry - is an implementation for the system monitoring tool Graphite. Using the GraphiteLogEntry you can easily send monitoring metricts to a statsd daemon.

Registering the GraphiteLogWriter is as follows:

PHP code
import('core::logging::writer', 'GraphiteLogWriter'); $logger->addlogWriter('graphite', new GraphiteLogWriter('my-graphite-host', '1234'));

Due to the fact that the implementation of the GraphiteLogEntry already does the necessary formatting, the GraphiteLogWriter takes the entry separator as an optional configuration attribute. Please use setEntrySeparator() to apply the desired entry separator (default: \n).

Using the GraphiteLogWriter is as follows:

PHP code
$t = Singleton::getInstance('BenchmarkTimer'); $renderingTime = round(floatval($t->getTotalTime() * 1000)); import('core::logging::entry', 'GraphiteLogEntry'); $logger->addEntry(new GraphiteLogEntry( 'graphite', 'rendering-time', 'ms', $renderingTime, LogEntry::SEVERITY_INFO ));
Using the Python implementation of statds you may be facing issues with sending multiple metrics at once. To solve that, you may deactivate the bulk-send mode of the GraphiteLogWriter. Please use the third constructor argument creating the LogWriter or pass false to the setBatchWrites() method:
PHP code
// configuration per constructor import('core::logging::writer', 'GraphiteLogWriter'); $logger->addlogWriter( 'graphite', new GraphiteLogWriter('my-graphite-host', '1234', false) ); // configuration per method call $writer = $logger->getLogWriter('graphite'); $writer->setBatchWrites(false);

5. Usage

To use the logger it must be imported using

PHP code
import('core::logging','Logger');

prior to use. After that the logger can be created by using the abstract singleton class:

PHP code
$log = &Singleton::getInstance('Logger');

$log now contains a reference on the logger in the current scope of application. To add a log message with the content MESSAGE to the log file FILENAME the code snippet

PHP code
$log->logEntry('FILENAME','MESSAGE');

can be used. The third parameter of the method logEntry() (optional) specifies the SEVERITY of the message provided. Please find the available severity levels within the LogEntry interface.

Since 1.17 you can also use the addEntry() method. It takes an instance of an LogEntry interface implementation:

PHP code
$log->addEntry(new SimpleLogEntry('FILENAME', 'MESSAGE'));
In case the application does excessive logging it may be good to save memory to flush the log buffer more than one time within the request. For this reason, since 1.14 the Logger includes a threshold that causes the log buffer to auto-flush when the number of entries reaches this number. Using setMaxBufferLength() you can configure the value that has initially been set within the class. After flushing, the log buffer is empty again.

6. LogWriter implementation

The interface described in chapter 2 includes the methods writeLogEntries() and setTarget(). The first one takes care about the persistence of the applied entries, the second one receives the target identifier.

During the registration process of the LogWriter the Logger::addLogWriter() injects the target identifier using the LogWriter::setTarget() to the current instance.

The LogWriter interface does not define a constructor to be able to choose the signature according to the purpose of the implementation.

It is recommended to define the constructor to take all required parameters that are required for the LogWriter implementation. This ensures that no errors occur during the execution.

As an implementation example we will use a LogWriter that passes the applied entries to the syslog daemon. To do so, we can use the syslog(int $priority, string $message) PHP function. The implementation is as follows:

PHP code
class SysLogWriter implements LogWriter { private $target; public function writeLogEntries(array $entries) { define_syslog_variables(); openlog($this->target, LOG_PID, LOG_LOCAL0); foreach ($entries as $entry) { syslog( $this->getMappedSeverity($entry->getSeverity()), $entry ); } closelog(); } public function setTarget($target) { $this->target = $target; } private function getMappedSeverity($severity) { if ($severity == LogEntry::SEVERITY_DEBUG) { return LOG_DEBUG; } if ($severity == LogEntry::SEVERITY_ERROR) { return LOG_ERR; } if ($severity == LogEntry::SEVERITY_FATAL) { return LOG_EMERG; } if ($severity == LogEntry::SEVERITY_TRACE) { return LOG_NOTICE; } if ($severity == LogEntry::SEVERITY_WARNING) { return LOG_WARNING; } return LOG_INFO; } }

In order to use this implementation, please register it with the Logger as follows:

PHP code
$logger->addLogWriter('syslog', new SysLogWriter());

7. LogEntry implementation

The interface described in chapter 2 contains the methods getLogTarget(), getSeverity(), and __toString(). The first one returns the log target, the second one the severity definition of the log entry and the last one creates the format of the log message content.

Adding a log entry using logEntry() (uses the SimpleLogEntry implementation internally) or addEntry() (any kind of implementations possible) the Logger pre-sorts the applied entries according to the log target (return value of the getLogTarget() method) for processing the entries at the end of the request.

The LogEntry interface does not define a constructor to be able to choose the signature according to the purpose of the implementation.

It is recommended to define the constructor to take all required parameters that are required for generating the log entry format.

As an implementation example we will create a CsvLogEntry that formats the applied content as comma separated values. The implementation is as follows:

PHP code
class CsvLogEntry implements LogEntry { private $target; private $severity; private $message; private $date; private $time; public function __construct($target, $message, $severity) { $this->date = date('Y-m-d'); $this->time = date('H:i:s'); $this->target = $target; $this->message = $message; $this->severity = $severity; } public function __toString() { return $this->date . ';' . $this->time . ';' . $this->severity . ';' . $this->message; } public function getLogTarget() { return $this->target; } public function getSeverity() { return $this->severity; } }

In case you want to use a CsvLogEntry instead of the standard implementation SimpleLogEntry you can use the Logger::addEntry():

PHP code
$logger->addEntry(new CsvLogEntry( 'csv-file', 'There is something wrong!', LogEntry::SEVERITY_ERROR ));

In order to create a CSV file with the above implementation an appropriate LogWriter must be registered for the csv-file target:

PHP code
$logger->addLogWriter('csv-file', new FileLogWriter('../logs'));

Comments

Do you want to add a comment to the article above, or do you want to post additional hints? So please click here. Comments already posted can be found below.
There are no comments belonging to this article.