初始化本地仓库

This commit is contained in:
2026-08-31 11:33:59 +08:00
commit 663f74b633
2390 changed files with 402918 additions and 0 deletions
+58
View File
@@ -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());
}
}
}
+292
View File
@@ -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;
}
}
}
+39
View File
@@ -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
],
];
+119
View File
@@ -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;
}
}