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.
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.
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):
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:
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:
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:
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.
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):
$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
));Fatal error: Uncaught exception 'LoggerException' with message 'Log writer with name "mysqlx" is not registered!' in */core/logging/Logger.php on line 433By 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:
$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:
$logger = & Singleton::getInstance('Logger');
$logger->addEntry(new SimpleLogEntry(
'stdout',
'This is a log message',
LogEntry::SEVERITY_INFO
));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:
$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.
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:
$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.
In order to remove registered LogWriters please use removeLogWriter():
$logger->removeLogWriter('mysqlx');Please note, that existing log entries cannot be persisted after removing the appropriate log writer.
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:
foreach($logger->getRegisteredTargets() as $logTarget) {
$logger->addLogWriter($logTarget, new StdOutLogWriter());
}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:
Registry::register('apf::core', 'LogDir', '/path/to/my/log/dir');This path is used to place all log files of the entire application.
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
$log = &Singleton::getInstance('Logger');
$log->setMaxBufferLength(500);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:
$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:
The following code block describes how to define a custom threshold - or create a custom profile:
$log = &Singleton::getInstance('Logger');
$log->setLogThreshold(array(
LogEntry::SEVERITY_TRACE,
LogEntry::SEVERITY_INFO
));The Logger defines the following default thresholds:
Details on the definition can be taken from API documentation.
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:
$log = &Singleton::getInstance('Logger');
$log->setHostPrefix('node1');$log = &Singleton::getInstance('Logger');
$log->setHostPrefix($_SERVER['SERVER_ADDR']);Release 1.17 ships several LogWriter implementations described within the subsequent chapters.
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.
Defining the log directory the constructor (mandatory) or setLogDir() (optional) can be used. Die registration of the FileLogWriters can be done as follows:
$logger->addLogWriter('file', new FileLogWriter('/path/to/log/dir'));Configuration of existing instances is as shown below:
$fileWriter = $logger->getLogWriter('file');
$logger->setLogDir('/path/to/log/dir');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:
$fileWriter = $logger->getLogWriter('file');
$fileWriter->setLogFolderPermissions(0755);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:
$fileWriter = $logger->getLogWriter('file');
$fileWriter->setHostPrefix('node1');$fileWriter = $logger->getLogWriter('file');
$fileWriter->setHostPrefix($_SERVER['SERVER_ADDR']);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.
Registration of the StdOutLogWriter can be done within your bootstrap file as follows:
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.
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:
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:
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 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:
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:
$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
));// 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);To use the logger it must be imported using
import('core::logging','Logger');prior to use. After that the logger can be created by using the abstract singleton class:
$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
$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:
$log->addEntry(new SimpleLogEntry('FILENAME', 'MESSAGE'));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.
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:
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:
$logger->addLogWriter('syslog', new SysLogWriter());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.
As an implementation example we will create a CsvLogEntry that formats the applied content as comma separated values. The implementation is as follows:
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():
$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:
$logger->addLogWriter('csv-file', new FileLogWriter('../logs'));JetBRAINS supports the development of the APF with PHPStorm licenses and we feel confidential that PHPStorm strongly influences the APF's quality. Use PHPStorm!
Proud to useIntelligent PHP IDE for coding, testing and debugging with pleasure