怎样在PHP项目中实现日志分级?

wen java案例 2

本文目录导读:

怎样在PHP项目中实现日志分级?

  1. 使用PSR-3日志接口标准(推荐)
  2. 手动实现的日志分级类
  3. 使用环境变量控制日志级别
  4. 配置文件驱动的高级方案
  5. 实际项目中的最佳实践
  6. 日志分级的最佳实践建议
  7. 集成到框架中
  8. 总结建议

在PHP项目中实现日志分级,我推荐几种实用方案,从简单到完整逐步介绍。

使用PSR-3日志接口标准(推荐)

PSR-3定义了8个日志级别,是目前PHP生态的标准做法:

<?php
// 使用 monolog/monolog 库(最流行)
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\FirePHPHandler;
// 创建日志通道
$log = new Logger('my_app');
// 添加处理器,设置不同级别
$log->pushHandler(new StreamHandler(__DIR__.'/logs/debug.log', Logger::DEBUG));
$log->pushHandler(new StreamHandler(__DIR__.'/logs/error.log', Logger::ERROR));
// 使用不同级别记录日志
$log->debug('调试信息', ['user_id' => 123]);
$log->info('用户登录成功');
$log->notice('需要注意的情况');
$log->warning('警告:磁盘空间不足');
$log->error('数据库连接失败', ['db' => 'mysql']);
$log->critical('系统严重错误');
$log->alert('需要立即处理');
$log->emergency('系统不可用');

手动实现的日志分级类

如果你不想引入外部库:

<?php
class Logger {
    const DEBUG = 100;
    const INFO = 200;
    const NOTICE = 250;
    const WARNING = 300;
    const ERROR = 400;
    const CRITICAL = 500;
    const ALERT = 550;
    const EMERGENCY = 600;
    private $minLevel;
    private $logPath;
    private $levelNames = [
        100 => 'DEBUG',
        200 => 'INFO',
        250 => 'NOTICE',
        300 => 'WARNING',
        400 => 'ERROR',
        500 => 'CRITICAL',
        550 => 'ALERT',
        600 => 'EMERGENCY'
    ];
    public function __construct($logPath, $minLevel = self::DEBUG) {
        $this->logPath = $logPath;
        $this->minLevel = $minLevel;
        if (!is_dir(dirname($logPath))) {
            mkdir(dirname($logPath), 0755, true);
        }
    }
    public function log($level, $message, array $context = []) {
        if ($level < $this->minLevel) {
            return; // 低于最低级别,不记录
        }
        // 替换上下文占位符 {key}
        if (!empty($context)) {
            $message = preg_replace_callback('/\{([^}]+)\}/', function($matches) use ($context) {
                return isset($context[$matches[1]]) ? $context[$matches[1]] : $matches[0];
            }, $message);
        }
        $levelName = $this->levelNames[$level] ?? 'UNKNOWN';
        $timestamp = date('Y-m-d H:i:s');
        $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
        $file = $trace[1]['file'] ?? 'unknown';
        $line = $trace[1]['line'] ?? 0;
        $logEntry = sprintf(
            "[%s] %s: %s (in %s:%d)\n",
            $timestamp,
            $levelName,
            $message,
            $file,
            $line
        );
        // 按级别分文件存储
        $levelFile = $this->logPath . '/' . strtolower($levelName) . '.log';
        file_put_contents($levelFile, $logEntry, FILE_APPEND | LOCK_EX);
        // 同时写入综合日志
        file_put_contents($this->logPath . '/app.log', $logEntry, FILE_APPEND | LOCK_EX);
    }
    public function debug($message, array $context = []) {
        $this->log(self::DEBUG, $message, $context);
    }
    public function info($message, array $context = []) {
        $this->log(self::INFO, $message, $context);
    }
    public function warning($message, array $context = []) {
        $this->log(self::WARNING, $message, $context);
    }
    public function error($message, array $context = []) {
        $this->log(self::ERROR, $message, $context);
    }
}
// 使用示例
$logger = new Logger(__DIR__ . '/logs', Logger::INFO);
$logger->debug('这个不会记录'); // 因为最低级别是INFO
$logger->info('用户 #{user_id} 登录成功', ['user_id' => 123]);
$logger->error('数据库查询失败: {error}', ['error' => 'Connection refused']);

使用环境变量控制日志级别

<?php
class EnvironmentLogger {
    private $environment;
    private $levelMap;
    public function __construct() {
        $this->environment = getenv('APP_ENV') ?: 'production';
        // 不同环境设置不同级别
        $this->levelMap = [
            'development' => Logger::DEBUG,
            'staging' => Logger::INFO,
            'production' => Logger::WARNING
        ];
    }
    public function getMinLevel() {
        return $this->levelMap[$this->environment] ?? Logger::WARNING;
    }
}
// 使用
$envLogger = new EnvironmentLogger();
$logger = new Logger(__DIR__ . '/logs', $envLogger->getMinLevel());

配置文件驱动的高级方案

<?php
// config/logging.php
return [
    'default' => 'stack',
    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['daily', 'slack']
        ],
        'daily' => [
            'driver' => 'daily',
            'path' => storage_path('logs/app.log'),
            'level' => 'debug',
            'days' => 14
        ],
        'slack' => [
            'driver' => 'slack',
            'url' => env('LOG_SLACK_WEBHOOK_URL'),
            'level' => 'critical'
        ],
        'emergency' => [
            'path' => storage_path('logs/emergency.log')
        ]
    ]
];
// 使用(类似 Laravel 的实现)
class LogManager {
    private $channels = [];
    private $config;
    public function __construct($config) {
        $this->config = $config;
    }
    public function channel($name = null) {
        $name = $name ?: $this->config['default'];
        if (!isset($this->channels[$name])) {
            $this->channels[$name] = $this->createChannel($name);
        }
        return $this->channels[$name];
    }
    private function createChannel($name) {
        $config = $this->config['channels'][$name];
        switch ($config['driver']) {
            case 'daily':
                return new DailyLogger($config['path'], $config['level'], $config['days']);
            case 'slack':
                return new SlackLogger($config['url'], $config['level']);
            default:
                return new Logger($config['path'], $config['level']);
        }
    }
}

实际项目中的最佳实践

<?php
// 1. 创建全局日志实例(单例)
class AppLogger {
    private static $instance = null;
    private $logger;
    private function __construct() {
        $logLevel = $this->determineLogLevel();
        $this->logger = new Logger(__DIR__ . '/logs', $logLevel);
    }
    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance->logger;
    }
    private function determineLogLevel() {
        // 从环境变量或配置文件读取
        $env = getenv('APP_ENV') ?: 'production';
        $levels = [
            'development' => Logger::DEBUG,
            'testing' => Logger::DEBUG,
            'staging' => Logger::INFO,
            'production' => Logger::WARNING
        ];
        return $levels[$env] ?? Logger::WARNING;
    }
}
// 2. 实际使用
$logger = AppLogger::getInstance();
try {
    // 业务逻辑
    $logger->info('开始处理请求', ['url' => $_SERVER['REQUEST_URI']]);
    // ...
    $logger->info('请求完成', ['duration' => $duration]);
} catch (Exception $e) {
    $logger->error('请求处理失败', [
        'error' => $e->getMessage(),
        'trace' => $e->getTraceAsString()
    ]);
    if ($e instanceof DatabaseException) {
        $logger->critical('数据库不可用', [
            'db_host' => DB_HOST,
            'error' => $e->getMessage()
        ]);
    }
}

日志分级的最佳实践建议

// 日志级别使用建议表
$usageGuide = [
    'debug' => [
        '用途' => '开发调试,详细运行信息',
        '建议' => '只在开发环境启用'
    ],
    'info' => [
        '用途' => '常规操作日志,用户行为记录',
        '建议' => '开发环境全开,生产环境选择性记录'
    ],
    'notice' => [
        '用途' => '正常但重要的事件',
        '建议' => '所有环境都记录'
    ],
    'warning' => [
        '用途' => '潜在问题,不影响运行',
        '建议' => '生产环境必须记录'
    ],
    'error' => [
        '用途' => '运行时错误,需要处理',
        '建议' => '立即通知开发团队'
    ],
    'critical' => [
        '用途' => '系统级严重错误',
        '建议' => '触发报警机制'
    ],
    'alert' => [
        '用途' => '需要立即处理',
        '建议' => '发送短信/电话通知'
    ],
    'emergency' => [
        '用途' => '系统不可用',
        '建议' => '全员紧急通知'
    ]
];

集成到框架中

// 以 Slim 框架为例
$container['logger'] = function($c) {
    $logger = new Logger('slim_app');
    // 文件处理器
    $fileHandler = new StreamHandler(__DIR__ . '/logs/app.log', Logger::DEBUG);
    $logger->pushHandler($fileHandler);
    // 错误邮件处理器(仅生产环境)
    if ($c->get('settings')['environment'] === 'production') {
        $mailHandler = new SwiftMailerHandler(
            new Swift_Mailer($transport),
            'admin@example.com',
            '应用错误通知',
            Logger::ERROR
        );
        $logger->pushHandler($mailHandler);
    }
    return $logger;
};

总结建议

  1. 使用 PSR-3 标准:便于与其他库集成
  2. 配置驱动:通过环境变量控制日志级别
  3. 区分环境:开发环境记录所有级别,生产环境只记录 WARNING 以上
  4. 合理使用上下文:记录相关数据便于调试
  5. 考虑性能:使用异步写入或队列处理高并发场景
  6. 轮转日志:避免单个日志文件过大

选择哪种方案取决于项目规模:

  • 小项目:手动实现简单类即可
  • 中型项目:使用 Monolog
  • 大型项目:使用框架内置日志系统或 ELK 等专业方案

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