初始化本地仓库
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
<?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 Throwable;
|
||||
|
||||
/**
|
||||
* 日志处理门面
|
||||
* @method static void emergency(string $name, $arguments) 系统不可用(最高级别)
|
||||
* @method static void alert(string $name, $arguments) 需立即处理
|
||||
* @method static void critical(string $name, $arguments) 严重故障
|
||||
* @method static void error(string $name, $arguments) 运行错误
|
||||
* @method static void warning(string $name, $arguments) 警告信息
|
||||
* @method static void notice(string $name, $arguments) 重要事件
|
||||
* @method static void info(string $name, $arguments) 一般事件
|
||||
* @method static void debug(string $name, $arguments) 调试信息
|
||||
* @method static void log(string $name, $arguments) 通用日志(默认级别)
|
||||
* @method static void cleanup(bool $forceAll = false, ?string $channel = null, bool $removeEmptyDirs = true) 清除日志
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0
|
||||
*/
|
||||
final class Logger
|
||||
{
|
||||
private static ?LoggerService $service = null;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
// 禁止实例化
|
||||
}
|
||||
|
||||
private static function getService(): LoggerService
|
||||
{
|
||||
if (self::$service === null) {
|
||||
self::$service = new LoggerService();
|
||||
}
|
||||
return self::$service;
|
||||
}
|
||||
|
||||
public static function __callStatic(string $name, array $arguments): void
|
||||
{
|
||||
try {
|
||||
self::getService()->handleLog($name, $arguments);
|
||||
} catch (Throwable $e) {
|
||||
error_log('Logging failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: https://madong.tech
|
||||
*/
|
||||
|
||||
return [
|
||||
'enable' => true,
|
||||
// 基础配置
|
||||
'base' => [
|
||||
'path' => runtime_path('logs'), // 日志存储路径
|
||||
'channel' => 'core', // 日志通道名称
|
||||
'retention_days' => 7, // 默认保留7天
|
||||
'daily_rotation' => true, // 启用每日文件分割
|
||||
],
|
||||
// 格式配置
|
||||
'format' => [
|
||||
'date' => 'Y-m-d H:i:s.u', // 日期格式
|
||||
'output' => "[%datetime%] %channel%.%level_name%: %message% %context%\n",
|
||||
],
|
||||
|
||||
// 级别配置
|
||||
'levels' => [
|
||||
'debug' => config('app.debug', false), // 是否启用debug日志
|
||||
'min_level' => 'debug', // 最小记录级别
|
||||
],
|
||||
|
||||
// 处理器配置
|
||||
'handlers' => [
|
||||
'stream' => true, // 启用文件处理器
|
||||
'syslog' => false, // 是否启用syslog
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,119 @@
|
||||
<?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\enum;
|
||||
|
||||
use core\interface\IEnum;
|
||||
use Error;
|
||||
use ValueError;
|
||||
|
||||
/**
|
||||
* 日志级别
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0
|
||||
*/
|
||||
enum LogLevel: int implements IEnum
|
||||
{
|
||||
case DEBUG = 100;
|
||||
case INFO = 200;
|
||||
case NOTICE = 250;
|
||||
case WARNING = 300;
|
||||
case ERROR = 400;
|
||||
case CRITICAL = 500;
|
||||
case ALERT = 550;
|
||||
case EMERGENCY = 600;
|
||||
|
||||
/* 基础方法 */
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return strtolower($this->name);
|
||||
}
|
||||
|
||||
public function value(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/* 工厂方法 */
|
||||
|
||||
public static function fromName(string $name): ?self
|
||||
{
|
||||
try {
|
||||
return constant("self::" . strtoupper($name));
|
||||
} catch (Error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function tryFromName(string $name): self
|
||||
{
|
||||
return self::fromName($name) ?? throw new ValueError("Invalid log level: $name");
|
||||
}
|
||||
|
||||
/* 实用方法 */
|
||||
|
||||
public static function valuesArray(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
public static function namesArray(): array
|
||||
{
|
||||
return array_column(self::cases(), 'name');
|
||||
}
|
||||
|
||||
public static function isValid(string|int $level): bool
|
||||
{
|
||||
if (is_int($level)) {
|
||||
return in_array($level, self::valuesArray(), true);
|
||||
}
|
||||
|
||||
return self::fromName($level) !== null;
|
||||
}
|
||||
|
||||
public static function getMinLevel(): self
|
||||
{
|
||||
return self::DEBUG;
|
||||
}
|
||||
|
||||
public static function getMaxLevel(): self
|
||||
{
|
||||
return self::EMERGENCY;
|
||||
}
|
||||
|
||||
/* 比较方法 */
|
||||
|
||||
public function isHigherThan(self $other): bool
|
||||
{
|
||||
return $this->value > $other->value;
|
||||
}
|
||||
|
||||
public function isLowerThan(self $other): bool
|
||||
{
|
||||
return $this->value < $other->value;
|
||||
}
|
||||
|
||||
/* 转换方法 */
|
||||
|
||||
public function toPsr3Level(): string
|
||||
{
|
||||
return $this->label();
|
||||
}
|
||||
|
||||
public function toMonologLevel(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user