Files
2026-08-31 11:33:59 +08:00

293 lines
7.9 KiB
PHP

<?php
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: https://madong.tech
*/
namespace core\logger;
use core\logger\enum\LogLevel;
use Monolog\Logger as Monolog;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\LineFormatter;
final class LoggerService
{
private Monolog $logger;
private static ?array $configCache = null;
private static float $configCacheTime = 0;
private const CONFIG_CACHE_TTL = 60;
public function __construct()
{
$this->initialize();
}
private function initialize(): void
{
if (!self::getConfig('enable')) {
throw new \RuntimeException('Logger is disabled in config');
}
$this->logger = new Monolog(self::getConfig('base.channel'));
if (self::getConfig('handlers.stream')) {
$this->addStreamHandlers();
}
if (self::getConfig('handlers.syslog')) {
$this->addSyslogHandler();
}
}
private function addStreamHandlers(): void
{
$formatter = new LineFormatter(
self::getConfig('format.output'),
self::getConfig('format.date'),
true,
true
);
foreach (LogLevel::cases() as $level) {
if ($this->shouldRecord($level)) {
$handler = new StreamHandler(
$this->getLogFile($level),
$level->value,
true,
null,
true
);
$handler->setFormatter($formatter);
$handler->setLevel($level->value);
$handler->setBubble(false);
$this->logger->pushHandler($handler);
}
}
}
public function handleLog(string $levelName, array $arguments): void
{
$level = LogLevel::fromName(strtoupper($levelName));
if ($level === null) {
return;
}
if (!self::getConfig('enable') || !$this->shouldRecord($level)) {
return;
}
$this->logger->log(
$level->value,
$arguments[0] ?? '',
$arguments[1] ?? []
);
}
private function shouldRecord(LogLevel $level): bool
{
if (!self::getConfig('enable')) {
return false;
}
if ($level === LogLevel::DEBUG &&
false === self::getConfig('levels.debug', true)) {
return false;
}
$minLevel = LogLevel::fromName(
strtoupper(self::getConfig('levels.min_level', 'debug'))
);
return $level->value >= $minLevel->value;
}
private function addSyslogHandler(): void
{
// syslog处理器实现
}
/**
* @param string|null $key
* @param $default
*
* @return array|mixed|null
*/
public static function getConfig(string $key = null, $default = null): mixed
{
$now = microtime(true);
if (self::$configCache === null || ($now - self::$configCacheTime) > self::CONFIG_CACHE_TTL) {
self::$configCache = config('core.logger.app', []);
self::$configCacheTime = $now;
}
return data_get(self::$configCache, $key, $default);
}
/**
* 清除配置缓存
*/
public static function clearConfigCache(): void
{
self::$configCache = null;
self::$configCacheTime = 0;
}
/**
* 获取按通道和月份分类的日志文件路径
*/
private function getLogFile(LogLevel $level): string
{
$basePath = $this->getBaseLogPath();
$monthDir = date('Y-m');
$dayPrefix = date('d');
$fullPath = "{$basePath}/{$monthDir}";
if (!is_dir($fullPath) && !mkdir($fullPath, 0755, true)) {
throw new \RuntimeException("Cannot create directory: {$fullPath}");
}
return sprintf('%s/%s-%s.log', $fullPath, $dayPrefix, $level->name);
}
/**
* 获取基础日志路径(包含通道目录)
*/
private function getBaseLogPath(): string
{
$path = rtrim(self::getConfig('base.path'), '/');
$channel = self::getConfig('base.channel', 'app');
$fullPath = "{$path}/{$channel}";
if (!is_dir($fullPath) && !mkdir($fullPath, 0755)) {
throw new \RuntimeException("Channel directory creation failed: {$fullPath}");
}
return $fullPath;
}
public function cleanup(bool $forceAll = false, ?string $channel = null, bool $removeEmptyDirs = true): void
{
if (!self::getConfig('enable')) {
return;
}
$basePath = rtrim(self::getConfig('base.path'), '/');
$channel = $channel ?? self::getConfig('base.channel', 'app');
$logPath = "{$basePath}/{$channel}";
if (!is_dir($logPath)) {
return;
}
$this->deleteExpiredFiles($logPath, $forceAll);
if ($removeEmptyDirs) {
$this->deleteEmptyDirectories($logPath);
}
}
private function deleteExpiredFiles(string $logPath, bool $forceAll): void
{
$retentionDays = (int)self::getConfig('base.retention_days', 7);
$cutoffDate = strtotime("-{$retentionDays} days");
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($logPath, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $file) {
if (!$file->isFile() || $file->getExtension() !== 'log') {
continue;
}
$fileName = $file->getFilename();
$filePath = $file->getRealPath();
$dirPath = dirname($filePath);
$monthDir = basename($dirPath);
if (!preg_match('/^(\d{4})-(\d{2})$/', $monthDir, $monthMatches)) {
continue;
}
if (!preg_match('/^(\d{2})-(.+)\.log$/', $fileName, $matches)) {
continue;
}
$fileDay = $matches[1];
$logLevel = strtoupper($matches[2]);
$year = $monthMatches[1];
$month = $monthMatches[2];
$fileDateStr = "{$year}-{$month}-{$fileDay}";
$fileDate = strtotime($fileDateStr);
if ($fileDate === false) {
continue;
}
if (self::getConfig('debug') && $logLevel === 'INFO') {
continue;
}
if ($forceAll || $fileDate < $cutoffDate) {
$this->safeUnlink($filePath);
}
}
}
private function deleteEmptyDirectories(string $logPath): void
{
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($logPath, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $dir) {
if ($dir->isDir() && $this->isEmptyDirectory($dir->getRealPath())) {
$this->safeRmdir($dir->getRealPath());
}
}
}
private function isEmptyDirectory(string $dirPath): bool
{
if (!is_dir($dirPath)) {
return false;
}
$dir = new \FilesystemIterator($dirPath);
return !$dir->valid();
}
private function safeUnlink(string $filePath): bool
{
try {
return @unlink($filePath);
} catch (\Throwable $e) {
error_log("Failed to delete file: {$filePath} - " . $e->getMessage());
return false;
}
}
private function safeRmdir(string $dirPath): bool
{
try {
return @rmdir($dirPath);
} catch (\Throwable $e) {
error_log("Failed to remove directory: {$dirPath} - " . $e->getMessage());
return false;
}
}
}