Skip to content

Instantly share code, notes, and snippets.

@jason-napolitano
Created December 9, 2025 04:07
Show Gist options
  • Select an option

  • Save jason-napolitano/6e8e7ca7613e568f16970997a3f3dd0c to your computer and use it in GitHub Desktop.

Select an option

Save jason-napolitano/6e8e7ca7613e568f16970997a3f3dd0c to your computer and use it in GitHub Desktop.
A PHP 8.4 powered, PSR-3 compliant logger
<?php
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
/**
* The Logger Library Class is a utility class that assists in the
* logging application of activity.
*
* @requires PHP 8.4
*/
class Logger implements LoggerInterface
{
/**
* File name and path of log file.
*
* @var string
*/
private string $file;
/**
* Log channel--namespace for log lines.
* Used to identify and correlate groups of similar log lines.
*
* @var string
*/
private string $channel {
set {
$this->channel = $value;
}
}
/**
* Lowest log level to log.
*
* @var int
*/
private int $level;
/**
* Whether to log to standard out.
*
* @var bool
*/
private bool $stdout;
/**
* Log fields separated by tabs to form a TSV (CSV with tabs).
*/
protected const string TAB = "\t";
/**
* Special minimum log level which will not log any log levels.
*/
protected const string LOG_LEVEL_NONE = 'none';
/**
* Log level hierarchy
*/
protected const array LEVELS = [
self::LOG_LEVEL_NONE => -1,
LogLevel::DEBUG => 0,
LogLevel::INFO => 1,
LogLevel::NOTICE => 2,
LogLevel::WARNING => 3,
LogLevel::ERROR => 4,
LogLevel::CRITICAL => 5,
LogLevel::ALERT => 6,
LogLevel::EMERGENCY => 7,
];
/**
* Logger constructor
*
* @param string $file File name and path of log file.
* @param string $channel Logger channel associated with this logger.
* @param string $level (optional) Lowest log level to log.
*/
public function __construct(string $file, string $channel = 'event', string $level = LogLevel::DEBUG)
{
$this->file = $file;
$this->channel ??= $channel;
$this->stdout = false;
$this->setLevel($level);
}
/**
* Set the lowest log level to log.
*
* @param string $level
*/
public function setLevel(string $level): void
{
if ( !array_key_exists($level, self::LEVELS) ) {
throw new DomainException("Log level $level is not a valid log level. Must be one of (" . implode(', ', array_keys(self::LEVELS)) . ')');
}
$this->level = self::LEVELS[$level];
}
/**
* Set the standard out option on or off.
* If set to true, log lines will also be printed to standard out.
*
* @param bool $stdout
*/
public function setOutput(bool $stdout): void
{
$this->stdout = $stdout;
}
/**
* @inheritdoc
*/
public function debug($message = '', ?array $context = null): void
{
if ( $this->logAtThisLevel(LogLevel::DEBUG) ) {
$this->log(LogLevel::DEBUG, $message, $context);
}
}
/**
* @inheritdoc
*/
public function info($message = '', array $context = null): void
{
if ( $this->logAtThisLevel(LogLevel::INFO) ) {
$this->log(LogLevel::INFO, $message, $context);
}
}
/**
* @inheritdoc
*/
public function notice($message = '', array $context = null): void
{
if ( $this->logAtThisLevel(LogLevel::NOTICE) ) {
$this->log(LogLevel::NOTICE, $message, $context);
}
}
/**
* @inheritdoc
*/
public function warning($message = '', array $context = null): void
{
if ( $this->logAtThisLevel(LogLevel::WARNING) ) {
$this->log(LogLevel::WARNING, $message, $context);
}
}
/**
* @inheritdoc
*/
public function error($message = '', array $context = null): void
{
if ( $this->logAtThisLevel(LogLevel::ERROR) ) {
$this->log(LogLevel::ERROR, $message, $context);
}
}
/**
* @inheritdoc
*/
public function critical($message = '', array $context = null): void
{
if ( $this->logAtThisLevel(LogLevel::CRITICAL) ) {
$this->log(LogLevel::CRITICAL, $message, $context);
}
}
/**
* @inheritdoc
*/
public function alert($message = '', array $context = null): void
{
if ( $this->logAtThisLevel(LogLevel::ALERT) ) {
$this->log(LogLevel::ALERT, $message, $context);
}
}
/**
* @inheritdoc
*/
public function emergency($message = '', array $context = null): void
{
if ( $this->logAtThisLevel(LogLevel::EMERGENCY) ) {
$this->log(LogLevel::EMERGENCY, $message, $context);
}
}
/**
* @inheritdoc
*/
public function log($level, $message = '', array $context = null): void
{
// Build log line
$pid = getmypid();
[$exception, $context] = $this->handleException($context);
$context = $context ? json_encode($context, JSON_UNESCAPED_SLASHES) : '{}';
$context = $context ?: '{}'; // Fail-safe in case json_encode fails.
$log_line = $this->formatLogLine($level, $pid, $message, $context, $exception);
// Log to file
try {
$fh = fopen($this->file, 'a+b');
fwrite($fh, $log_line);
fclose($fh);
} catch ( Throwable $e ) {
throw new RuntimeException("Could not open log file $this->file for writing to \\Logger channel $this->channel!", 0, $e);
}
// Log to stdout if option set to do so.
if ( $this->stdout ) {
print($log_line);
}
}
/**
* Determine if the logger should log at a certain log level.
*
* @param string $level
*
* @return bool True if we log at this level; false otherwise.
*/
private function logAtThisLevel(string $level): bool
{
return self::LEVELS[$level] >= $this->level;
}
/**
* Handle an exception in the data context array.
* If an exception is included in the data context array, extract it.
*
* @param ?array $context
*
* @return array [exception, data (without exception)]
*
* @throws \JsonException
*/
private function handleException(?array $context = null): array
{
if ( isset($data['exception']) && $data['exception'] instanceof Throwable ) {
$exception = $data['exception'];
$exception_data = $this->buildExceptionData($exception);
unset($data['exception']);
} else {
$exception_data = '{}';
}
return [$exception_data, $context];
}
/**
* Build the exception log data.
*
* @param Throwable $e
*
* @return string JSON {message, code, file, line, trace}
*
* @throws \JsonException
*/
private function buildExceptionData(Throwable $e): string
{
$exceptionData = json_encode([
'message' => $e->getMessage(),
'code' => $e->getCode(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTrace(),
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
// Fail-safe in case json_encode failed
return $exceptionData ?: '{"message":"' . $e->getMessage() . '"}';
}
/**
* Format the log line.
* YYYY-mm-dd HH:ii:ss.uuuuuu [loglevel] [channel] [pid:##] Log message content {"Optional":"JSON
* Contextual Support Data"} {"Optional":"Exception Data"}
*
*
* @param string $level
* @param int $pid
* @param string $message
* @param string $data
* @param string $exception_data
*
* @return string
*/
private function formatLogLine(string $level, int $pid, string $message, string $data, string $exception_data): string
{
return
$this->getTime() . self::TAB .
'[' . strtoupper($level) . ']' . self::TAB .
"[$this->channel]" . self::TAB .
"[pid:$pid]" . self::TAB .
str_replace(PHP_EOL, ' ', trim($message)) . self::TAB .
str_replace(PHP_EOL, ' ', $data) . self::TAB .
str_replace(PHP_EOL, ' ', $exception_data) . PHP_EOL;
}
/**
* Get current date time.
* Format: YYYY-mm-dd HH:ii:ss.uuuuuu
* Microsecond precision for PHP 7.1 and greater
*
* @return string Date time
*/
private function getTime(): string
{
return new \DateTimeImmutable('now')->format('Y-m-d H:i:s.u');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment