terrazza / logger
logger
Requires
- php: >=7.4
- psr/log: 1.1.*
Requires (Dev)
- phpunit/phpunit: ^9.3
- vimeo/psalm: 4.x-dev
README
This component is an implementation of PSR/Log standard with some extensions.
Structure
- the Logger component
has to be initialized with 0-n ChannelHandler
and provides the common known methods:- warning
- error
- notice
- ...
- the ChannelHandler component
is responsible to determine- the target/writer (required)
- the recordFormatter (required)
- a channelFilter (optional)
- all related LogHandler for this channel
- the LogHandler component
determines- the logLevel
- the format (optional, default: from ChannelHandler->recordFormatter)
The Terrazza/Logger component differ to the common PSR/Log implementation in handling the "Format".
The Writer Component handles multiple "rows" and combines it. Within this difference its possible to forward a transformed format and keep his keys per row.
For example: write the message/format into a json object or db.
Object/Classes
- Logger
- Handler
- LogRecord
- LogRecordFormatter
- LogHandlerFilter
- Converter
- Install
- Requirements
- Examples
Logger
The Logger object/methods is/are close to the common PSR implementations but!
The Logger is initialized with channelHandler(s) and not Handler
each channelHandler executes only one logHandler
method: registerChannelHandler
adds a channelHandler to the logger (not immutable).
method: registerExceptionHandler
register a callback for php exception handler.
well-developed projects should handle/cover all exceptions by themselves, but ;-)
method: registerErrorHandler
register a callback for php error handler.
sometime this kind of errors can't be catched without this workaround
method: registerFatalHandler
register a callback for php shutdown.
sometime this kind of errors can't be catched without this workaround
method: setExceptionFileName
The method:addMessage itself is covered with try/catch.
The catch handler writes the Exception.Message to a file which can be set with the method setExceptionFileName.
notice:
default: php://stderr
constructor: context (array)
The logger can be initialized, next to the name, with an initialized context.
This context can be addressed separately.
example of usage
a class is injected with the component and inside the constructor
$logger = new Logger("name", ["user" => "Value"]);
$logger->notice("hello", ["my" => "value"]);
$format = ["{Context.my} {iContext.user}"];
Handler
ChannelHandler
A ChannelHandler collects LogHandler to the same channel and provides
- the same writer
- the same formatter
for each LogHandler.
A ChannelHandler can be registered through the Logger with
- the method: registerChannelHandler
- the __constructor (3rd argument as variadic)
method: getWriter
method: getFormatter
method: getFilter
method: getLogHandler (LogHandlerInterface[])
method: pushLogHandler
Method to add a new LogHandler.
The logHandler-array will be key-sorted, to prevent multiple write transaction for different LogLevels.
method: getEffectedHandler
return the matched LogHandler for a given LogRecord.
method: writeRecord
for a passed LogHandler the record will be
- formatted
- and written to the Writer
LogHandler
The SingleHandler provides the common way to create a handler for a Logger. The only difference to the common implementation:
- instead of logLevel
- the SingleHandler has to be injected within a Channel
LogRecord
Against the common PSR implementation our component deals with an object and not an array.
LogRecord properties:
- logDate (\Datetime)
- loggerName (string)
- logLevel (int)
- logMessage (string)
- memUsed (int)
- memAllocated (int)
- LogRecordTrace
- context (array)
- initContext (array)
additional, the object provides
- logLevelName (string)
method/static createRecord
this method is used inside Logger to create a new LogRecord object.
method: getToken()
this method is used in the LogRecordFormatterInterface to get the LogRecord "encoded".
Every element can be accessed through his "format" e.g. {Level}{LevelName}{Context.name}
return [
'Date' => $this->getLogDate(),
'Level' => $this->getLogLevel(),
'LevelName' => $this->getLogLevelName(),
'LoggerName' => $this->getLoggerName(),
'MemUsed' => $this->getMemUsed(),
'MemAllocated' => $this->getMemAllocated(),
'Message' => $this->getLogMessage(),
'Context' => $this->getContext(),
'iContext' => $this->getInitContext(),
'Trace' => [
"Namespace" => $this->getTrace()->getNamespace(),
"Line" => $this->getTrace()->getLine(),
"Classname" => $this->getTrace()->getClassname(),
"Function" => $this->getTrace()->getFunction(),
"Method" => $this->getTrace()->getClassname()."::".$this->getTrace()->getFunction(),
"sMethod" => basename($this->getTrace()->getClassname())."::".$this->getTrace()->getFunction(),
]
]
LogRecordTrace
The LogRecordTrace object is generated in the Logger during a LogRecord is created.
Base on debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) every Record get additional properties:
- Namespace
- Classname
- Function
- Line
LogRecordFormatter
The LogRecordFormatter converts/maps a record to an array
.
Initialized properties:
- NonScalarConverterInterface
- format (array)
- valueConverter (array, optional)
NonScalarConverter (NonScalarConverterInterface)
The NonScalarConverter convert a nonScalar value (e.g. from Context) into a string.
Actually the provided class NonScalarJsonEncode use json_encode and prefix it with the attribute name.
The NonScalarConverter is used, when a formatter-line includes a nonScalar and a scalar content.
example of usage:
use Terrazza\Component\Logger\Converter\NonScalar\NonScalarJsonConverter;
$record = ["message" => "myMessage", "key" => ["value1", "value2"]];
echo (new NonScalarJsonEncode())->getValue($context["key"]); // key:{"value1", "value2"}
// in context of the formatter it will be
$format = ["{Message}:{Context.key}"]; // ... myMessage:key:{"value1", "value2"}
$format = ["Context" => "{Message}:{Context.key}"]; // ... myMessage:key:{"value1", "value2"}
a ValueConverter (LogRecordValueConverterInterface) can be used to convert special value based on his key.
method: pushConverter
to map/convert a special value, based on his key, push a special converter.
This converter has to fulfill LogRecordValueConverterInterface.
method: formatRecord
Maps the Record against the $format and returns a mapped array.
Unknown patterns (e.g. {undefined}) are removed from the response.
Empty "Lines" are also removed.
example
The example uses an additional ValueConverter for the Record value "Date".
use DateTime;
use Terrazza\Component\Logger\LogRecordValueConverterInterface;
use Terrazza\Component\Logger\LOgRecord;
use Terrazza\Component\Logger\Formatter\LogRecordFormatter;
use Terrazza\Component\Logger\Converter\NonScalar\NonScalarJsonEncode;
class RecordTokenValueDate implements LogRecordValueConverterInterface {
private string $dateFormat;
public function __construct(string $dateFormat="Y-m-d H:i:s.u") {
$this->dateFormat = $dateFormat;
}
public function getValue($value) {
return $value->format($this->dateFormat);
}
}
$formatter = new LogRecordFormatter(
new NonScalarJsonEncode,
["Date", "Message"]
);
$formatter->pushConverter("Date", new RecordTokenValueDate);
$record = LogRecord::create("LoggerName", 100, "myMessage");
var_dump($formatter->formatRecord($record));
/*
[
"Date" => 2022-12-31 23:59:01,
"Message" => "myMessage"
]
*/
LogHandlerFilter
LogHandler can have a LogHandlerFilter.
Properties:
- include (array, optional)
- exclude (array, optional)
- start (array, optional)
method: isHandling (string $callerNamespace) : bool
include
preg_match callerNamespace against include patterns
exclude
preg_match callerNamespace against exclude patterns
start
preg_match callerNamespace against start patterns
if preg_match is true all further isHandling will be true.
(exclude filter overrules start)
Converter
A LogRecord will be first converted/mapped with the RecordFormatter
Afterwards, it depends on the target/writer the array has to be formatted again.
We actually include/provide two Converter:
- convert to a string, json type
- convert to a string, e.g. for console logging In any case the Converter is injected into the Writer.
FormattedRecordFlat
Converts the mapped LogRecord into a string within a delimiter for each row.
For nonScalar values we use json_encode to convert the value.
method: setNonScalarPrefix(string $delimiter)
by using this method nonScalar values will be prefixed with the dataKey and the delimiter.
arguments:
- delimiter (string, required)
- encodingFlags (int, optional)
example of usage:
use Terrazza\Component\Logger\Converter\FormattedRecord\FormattedRecordFlat;
$formatter = new FormattedRecordFlat("|",0);
echo $formatter->convert(["message" => "myMessage", "context" => ["k" => "v"]);
//myMessage|{"k":"v"}
$formatter->setNonScalarPrefix(":");
echo $formatter->convert(["message" => "myMessage", "context" => ["k" => "v"]);
//myMessage|context:{"k":"v"}
FormattedRecordJson
Converts the mapped LogRecord into a string by using json_encode.
arguments:
- encodingFlags (int, optional)
example of usage:
use Terrazza\Component\Logger\Converter\FormattedRecord\FormattedRecordJson;
$formatter = new FormattedRecordJson(0);
echo $formatter->convert(["message" => "myMessage", "context" => ["k" => "v"]);
//{"message" : "myMessage", "context": {"k":"v"}}
Writer
StreamFile
Save converted record to a file.
arguments:
- converter (IFormattedRecordConverter, required)
- filename (string required)
- flags (int, optional, default: 0)
the converter should convert the formatted LogRecord into a string.
use Terrazza\Component\Logger\Writer\StreamFile;
use Terrazza\Component\Logger\Converter\FormattedRecord\FormattedRecordFlat;
$logFile = "log.txt";
@unlink($logFile);
$converter = new FormattedRecordFlat("|",0);
$writer = new StreamFile($converter, $logFile);
$writer->write(["message" => "myMessage", "context" => ["k" => "v"]);
$logContent = file_get_contents($logFile);
echo $logContent;
//{"message" : "myMessage", "context": {"k":"v"}}
How to install
Install via composer
composer require terrazza/logger
Requirements
- php >= 7.4
composer packages
- psr/log
Examples
1. create a ChannelHandler
use Terrazza\Component\Logger\Converter\FormattedRecord\FormattedRecordFlat;
use Terrazza\Component\Logger\Writer\StreamFile;
use Terrazza\Component\Logger\Formatter\RecordFormatter;
use Terrazza\Component\Logger\Converter\NonScalar\NonScalarJsonEncode;
use Terrazza\Component\Logger\Handler\ChannelHandler;
$writeConverter = new FormattedRecordFlat("|",0);
$writer = new StreamFile($writeConverter, "test.log");
$formatter = new RecordFormatter(new NonScalarJsonEncode(), [
"Message" => "{Level}-{Message}-{Context.pid}"
]);
$channelHandler = new ChannelHandler($writer, $formatter);
2. create a LogHandler
notice
next code lines depends on previous example... (create a ChannelHandler)
use Terrazza\Component\Logger\Handler\LogHandler;
use Terrazza\Component\Logger\Logger;
$handler = new LogHandler(Logger::WARNING);
// push handler into previouse create channelHandler
$channelHandler->pushLogHandler($handler);
3. create within a handler
notice:
next code lines depends on previous example... (create a LogHandler)
use Terrazza\Component\Logger\Logger;
// additinal we initialize the Context with pid = getmypid
// the formatter uses {Context.pid} and will print it
$logger = new Logger("loggerName", ["pid" => "myPID"], $channelHandler);
$logger->error($message = "message");
create and registerChannelHandler
use Terrazza\Component\Logger\Logger;
$logger = new Logger("loggerName", ["pid" => getmypid()]);
$logger = $logger->registerChannelHandler($channelHandler);
$logger->error($message = "message");
Usage of Logger
produce a NOTICE
$logger->notice("myMessage");
// by using our examples above this message will not be printed
// ...cause the logLevel for the Handler is WARNING
produce an ERROR
$logger->error("myMessage");
// output to file will be: 400-myMessage-myPID