Initial Version
This commit is contained in:
125
core/lib/Logger/FileLogger.php
Normal file
125
core/lib/Logger/FileLogger.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace KTXC\Logger;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Simple file-based PSR-3 logger.
|
||||
*/
|
||||
class FileLogger implements LoggerInterface
|
||||
{
|
||||
private string $logFile;
|
||||
private bool $useMicroseconds;
|
||||
private string $channel;
|
||||
|
||||
/**
|
||||
* @param string $logDir Directory where log files are written
|
||||
* @param string $channel Logical channel name (used in filename)
|
||||
* @param bool $useMicroseconds Whether to include microseconds in timestamp
|
||||
*/
|
||||
public function __construct(string $logDir, string $channel = 'app', bool $useMicroseconds = true)
|
||||
{
|
||||
$this->useMicroseconds = $useMicroseconds;
|
||||
$this->channel = $channel;
|
||||
if (!is_dir($logDir)) {
|
||||
@mkdir($logDir, 0775, true);
|
||||
}
|
||||
$this->logFile = rtrim($logDir, '/').'/'.$channel.'.log';
|
||||
}
|
||||
|
||||
public function emergency($message, array $context = []): void { $this->log(LogLevel::EMERGENCY, $message, $context); }
|
||||
public function alert($message, array $context = []): void { $this->log(LogLevel::ALERT, $message, $context); }
|
||||
public function critical($message, array $context = []): void { $this->log(LogLevel::CRITICAL, $message, $context); }
|
||||
public function error($message, array $context = []): void { $this->log(LogLevel::ERROR, $message, $context); }
|
||||
public function warning($message, array $context = []): void { $this->log(LogLevel::WARNING, $message, $context); }
|
||||
public function notice($message, array $context = []): void { $this->log(LogLevel::NOTICE, $message, $context); }
|
||||
public function info($message, array $context = []): void { $this->log(LogLevel::INFO, $message, $context); }
|
||||
public function debug($message, array $context = []): void { $this->log(LogLevel::DEBUG, $message, $context); }
|
||||
|
||||
public function log($level, $message, array $context = []): void
|
||||
{
|
||||
$timestamp = $this->formatTimestamp();
|
||||
$interpolated = $this->interpolate((string)$message, $context);
|
||||
$payload = [
|
||||
'time' => $timestamp,
|
||||
'level' => strtolower((string)$level),
|
||||
'channel' => $this->channel,
|
||||
'message' => $interpolated,
|
||||
'context' => $this->sanitizeContext($context),
|
||||
];
|
||||
$json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
if ($json === false) {
|
||||
// Fallback stringify if encoding fails (should be rare)
|
||||
$json = json_encode([
|
||||
'time' => $timestamp,
|
||||
'level' => strtolower((string)$level),
|
||||
'channel' => $this->channel,
|
||||
'message' => $interpolated,
|
||||
'context_error' => 'failed to encode context: '.json_last_error_msg(),
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '{"error":"logging failure"}';
|
||||
}
|
||||
$this->write($json);
|
||||
}
|
||||
|
||||
private function formatTimestamp(): string
|
||||
{
|
||||
if ($this->useMicroseconds) {
|
||||
$dt = \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', microtime(true)));
|
||||
return $dt?->format('Y-m-d H:i:s.u') ?? date('Y-m-d H:i:s');
|
||||
}
|
||||
return date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
private function interpolate(string $message, array $context): string
|
||||
{
|
||||
if (!str_contains($message, '{')) {
|
||||
return $message;
|
||||
}
|
||||
$replace = [];
|
||||
foreach ($context as $key => $val) {
|
||||
if (is_array($val) || is_object($val)) {
|
||||
continue; // don't inline complex values
|
||||
}
|
||||
$replace['{'.$key.'}'] = (string)$val;
|
||||
}
|
||||
return strtr($message, $replace);
|
||||
}
|
||||
|
||||
private function sanitizeContext(array $context): array
|
||||
{
|
||||
if (empty($context)) { return []; }
|
||||
$clean = [];
|
||||
foreach ($context as $k => $v) {
|
||||
if ($v instanceof \Throwable) {
|
||||
$clean[$k] = [
|
||||
'type' => get_class($v),
|
||||
'message' => $v->getMessage(),
|
||||
'code' => $v->getCode(),
|
||||
'file' => $v->getFile(),
|
||||
'line' => $v->getLine(),
|
||||
'trace' => explode("\n", $v->getTraceAsString()),
|
||||
];
|
||||
} elseif (is_resource($v)) {
|
||||
$clean[$k] = 'resource('.get_resource_type($v).')';
|
||||
} elseif (is_object($v)) {
|
||||
// Try to extract serializable data
|
||||
if (method_exists($v, '__toString')) {
|
||||
$clean[$k] = (string)$v;
|
||||
} else {
|
||||
$clean[$k] = ['object' => get_class($v)];
|
||||
}
|
||||
} else {
|
||||
$clean[$k] = $v;
|
||||
}
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
|
||||
private function write(string $line): void
|
||||
{
|
||||
$line = rtrim($line)."\n"; // newline-delimited JSON (JSONL)
|
||||
@file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user