PHP 项目里日志组件换底层

wen PHP项目 2

本文目录导读:

PHP 项目里日志组件换底层

  1. 评估现有实现
  2. 抽象接口层(推荐方案)
  3. 创建适配器
  4. 依赖注入容器配置
  5. 框架集成示例
  6. 数据迁移和兼容
  7. 配置管理
  8. 测试切换逻辑
  9. AOP 增强(可选)
  10. 运维兼容性
  11. 建议

在 PHP 项目中更换日志组件的底层库,通常意味着从一种日志实现(如 MonoLog)切换到另一种,或者更换日志的存储方式(如从文件到数据库),以下是系统化的替换方案:

评估现有实现

明确当前使用的日志组件和业务代码中的使用方式:

// 假设当前使用 Monolog
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
class UserService {
    private $logger;
    public function __construct(Logger $logger) {
        $this->logger = $logger;
    }
    public function createUser($data) {
        $this->logger->info('创建用户', $data);
        // 业务逻辑
    }
}

抽象接口层(推荐方案)

创建一个统一的日志接口,让业务代码不直接依赖具体实现:

// app/Contracts/LoggerInterface.php
namespace App\Contracts;
interface LoggerInterface {
    public function emergency($message, array $context = []);
    public function alert($message, array $context = []);
    public function critical($message, array $context = []);
    public function error($message, array $context = []);
    public function warning($message, array $context = []);
    public function notice($message, array $context = []);
    public function info($message, array $context = []);
    public function debug($message, array $context = []);
    public function log($level, $message, array $context = []);
}

创建适配器

为不同底层实现创建适配器:

// app/Logging/MonologAdapter.php
namespace App\Logging;
use Monolog\Logger as MonologLogger;
use App\Contracts\LoggerInterface;
class MonologAdapter implements LoggerInterface {
    private $logger;
    public function __construct(MonologLogger $logger) {
        $this->logger = $logger;
    }
    public function emergency($message, array $context = []) {
        return $this->logger->emergency($message, $context);
    }
    public function alert($message, array $context = []) {
        return $this->logger->alert($message, $context);
    }
    // ... 其他方法实现
    public function log($level, $message, array $context = []) {
        return $this->logger->log($level, $message, $context);
    }
}
// app/Logging/RedisAdapter.php - 假设换成 Redis 日志
namespace App\Logging;
use Redis;
use App\Contracts\LoggerInterface;
class RedisAdapter implements LoggerInterface {
    private $redis;
    private $key;
    public function __construct(Redis $redis, string $key = 'logs') {
        $this->redis = $redis;
        $this->key = $key;
    }
    public function emergency($message, array $context = []) {
        return $this->writeLog('EMERGENCY', $message, $context);
    }
    public function alert($message, array $context = []) {
        return $this->writeLog('ALERT', $message, $context);
    }
    public function error($message, array $context = []) {
        return $this->writeLog('ERROR', $message, $context);
    }
    // ... 其他方法
    private function writeLog($level, $message, array $context = []) {
        $logData = [
            'level' => $level,
            'message' => $message,
            'context' => $context,
            'timestamp' => time(),
            'url' => $_SERVER['REQUEST_URI'] ?? '',
            'ip' => $_SERVER['REMOTE_ADDR'] ?? ''
        ];
        // 使用 Redis 的 HSET 或 List 存储
        return $this->redis->lPush($this->key, json_encode($logData));
    }
}

依赖注入容器配置

使用 DI 容器来管理日志实例的切换:

// 方式一:传统 PHP 项目
class ServiceContainer {
    private $services = [];
    public function get($class) {
        if (isset($this->services[$class])) {
            return $this->services[$class];
        }
        switch ($class) {
            case LoggerInterface::class:
                // 这里可以方便地切换底层实现
                // return new MonologAdapter($this->createMonolog());
                return new RedisAdapter($this->createRedis());
                break;
        }
    }
}
// 方式二:使用 PHP-DI (PHP 依赖注入库)
use DI\ContainerBuilder;
use App\Contracts\LoggerInterface;
use App\Logging\MonologAdapter;
use App\Logging\RedisAdapter;
$containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions([
    LoggerInterface::class => function() {
        // 切换这里即可
        return new RedisAdapter(getRedis());
        // 或者
        // return new MonologAdapter(getMonolog());
    }
]);
$container = $containerBuilder->build();

框架集成示例

Laravel 框架

// config/logging.php
return [
    'default' => env('LOG_CHANNEL', 'stack'),
    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['daily'],
            'ignore_exceptions' => false,
        ],
        'monolog' => [
            'driver' => 'monolog',
            'handler' => \Monolog\Handler\StreamHandler::class,
            'level' => env('LOG_LEVEL', 'debug'),
            'path' => storage_path('logs/laravel.log'),
        ],
        'redis' => [
            'driver' => 'redis',
            'connection' => 'default',
            'key' => 'app_logs',
            'level' => 'debug',
        ],
    ],
];

Symfony 框架

# config/packages/monolog.yaml
monolog:
    handlers:
        main:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
        redis:
            type: service
            id: App\Logging\RedisLogHandler
            level: debug

数据迁移和兼容

如果数据格式不一致,需要做兼容处理:

// app/Logging/LogFormatter.php
class LogFormatter {
    public static function formatForExport(array $monologFormat): array {
        return [
            'level' => strtoupper($monologFormat['level_name']),
            'message' => $monologFormat['message'],
            'context' => $monologFormat['context'] ?? [],
            'datetime' => $monologFormat['datetime']->format('Y-m-d H:i:s'),
            'extra' => $monologFormat['extra'] ?? []
        ];
    }
    public static function formatRedisLog(array $redisLog): string {
        return json_encode([
            'level' => $redisLog['level'],
            'message' => $redisLog['message'],
            'context' => $redisLog['context'],
            'timestamp' => date('Y-m-d H:i:s', $redisLog['timestamp'])
        ]);
    }
}

配置管理

// config/logger.php
return [
    'driver' => env('LOG_DRIVER', 'monolog'),
    'drivers' => [
        'monolog' => [
            'path' => storage_path('logs/app.log'),
            'level' => 'debug',
        ],
        'redis' => [
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'port' => env('REDIS_PORT', 6379),
            'key_prefix' => env('LOG_KEY_PREFIX', 'app:logs:'),
            'list_length' => 10000,
        ],
        'database' => [
            'table' => 'app_logs',
            'max_rows' => 100000,
        ],
        'sentry' => [
            'dsn' => env('SENTRY_DSN'),
            'level' => 'error',
        ],
    ]
];

测试切换逻辑

class LogSystemTest extends PHPUnit\Framework\TestCase {
    public function testLoggerSwitching() {
        // 测试 Monolog
        $monologAdapter = new MonologAdapter(createMonolog());
        $this->assertInstanceOf(LoggerInterface::class, $monologAdapter);
        // 测试 Redis
        $redisAdapter = new RedisAdapter(createRedis());
        $this->assertInstanceOf(LoggerInterface::class, $redisAdapter);
        // 测试业务逻辑不受影响
        $userService = new UserService($redisAdapter);
        $result = $userService->createUser(['name' => 'test']);
        $this->assertTrue($result);
    }
    public function testLogFormatCompatibility() {
        $monologLog = ['level' => 'INFO', 'message' => 'test'];
        $redisLog = LogFormatter::formatForExport($monologLog);
        $this->assertArrayHasKey('level', $redisLog);
        $this->assertArrayHasKey('message', $redisLog);
        $this->assertArrayNotHasKey('datetime', $redisLog);
    }
}

AOP 增强(可选)

使用 AOP 技术减少代码侵入性:

// 使用 Aspect-Oriented Programming
class LoggerAspect {
    public function before($method, $params) {
        $this->logger->info('调用: ' . $method, $params);
    }
    public function after($method, $result) {
        $this->logger->info('返回: ' . $method, ['result' => $result]);
    }
    public function exception($method, \Throwable $e) {
        $this->logger->error('异常: ' . $method, ['message' => $e->getMessage()]);
    }
}

运维兼容性

// 添加运维辅助类
class LogRouter {
    public function route($level, $message, $context = []) {
        // 根据日志级别路由到不同存储
        if (in_array($level, ['EMERGENCY', 'ALERT', 'CRITICAL'])) {
            // 发送告警短信/邮件
            $this->notifyAdmin($level, $message);
            // 写入 MongoDB(高可用存储)
            $this->writeToMongo($level, $message, $context);
        } else {
            // 普通日志写入文件
            $this->writeToFile($level, $message, $context);
        }
    }
}

建议

  1. 渐进式切换:先在测试环境验证,再逐步在生产环境切换
  2. 保留旧日志:切换后保留一段时间旧日志,便于问题追踪
  3. 监控告警:配置日志丢失告警
  4. 性能测试:对比不同实现的 I/O 性能
  5. 文档更新:更新日志配置和使用文档

通过上面的抽象和适配器模式,可以在不修改业务代码的情况下轻松更换日志底层实现。

抱歉,评论功能暂时关闭!