PHP项目日志接口与多通道

wen PHP项目 3

本文目录导读:

PHP项目日志接口与多通道

  1. 日志接口设计
  2. 基础日志类实现
  3. 处理器实现
  4. 格式化器实现
  5. 使用示例
  6. 工厂类简化配置
  7. 最佳实践

我来为你设计PHP项目中的日志接口与多通道实现方案。

日志接口设计

<?php
// 日志级别常量
interface LogLevel {
    const EMERGENCY = 'emergency';
    const ALERT     = 'alert';
    const CRITICAL  = 'critical';
    const ERROR     = 'error';
    const WARNING   = 'warning';
    const NOTICE    = 'notice';
    const INFO      = 'info';
    const DEBUG     = 'debug';
}
// 日志记录器接口
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 = []);
}
// 格式化器接口
interface FormatterInterface {
    public function format(array $record);
}
// 处理器接口
interface HandlerInterface {
    public function handle(array $record);
    public function setFormatter(FormatterInterface $formatter);
    public function getFormatter();
}

基础日志类实现

<?php
// 基础日志记录器
class Logger implements LoggerInterface {
    protected $name;
    protected $handlers = [];
    protected $processors = [];
    protected $timezone;
    public function __construct($name, array $handlers = [], array $processors = []) {
        $this->name = $name;
        $this->handlers = $handlers;
        $this->processors = $processors;
        $this->timezone = new \DateTimeZone('Asia/Shanghai');
    }
    public function addHandler(HandlerInterface $handler) {
        $this->handlers[] = $handler;
    }
    public function addProcessor(callable $processor) {
        $this->processors[] = $processor;
    }
    public function emergency($message, array $context = []) {
        $this->log(LogLevel::EMERGENCY, $message, $context);
    }
    public function alert($message, array $context = []) {
        $this->log(LogLevel::ALERT, $message, $context);
    }
    public function critical($message, array $context = []) {
        $this->log(LogLevel::CRITICAL, $message, $context);
    }
    public function error($message, array $context = []) {
        $this->log(LogLevel::ERROR, $message, $context);
    }
    public function warning($message, array $context = []) {
        $this->log(LogLevel::WARNING, $message, $context);
    }
    public function notice($message, array $context = []) {
        $this->log(LogLevel::NOTICE, $message, $context);
    }
    public function info($message, array $context = []) {
        $this->log(LogLevel::INFO, $message, $context);
    }
    public function debug($message, array $context = []) {
        $this->log(LogLevel::DEBUG, $message, $context);
    }
    public function log($level, $message, array $context = []) {
        if (empty($this->handlers)) {
            throw new \RuntimeException('No handlers configured');
        }
        $record = $this->createRecord($level, $message, $context);
        // 应用处理器
        foreach ($this->processors as $processor) {
            $record = call_user_func($processor, $record);
        }
        // 传递到所有处理器
        foreach ($this->handlers as $handler) {
            $handler->handle($record);
        }
    }
    protected function createRecord($level, $message, array $context = []) {
        return [
            'message' => $this->interpolate($message, $context),
            'level' => $level,
            'level_name' => strtoupper($level),
            'context' => $context,
            'channel' => $this->name,
            'datetime' => new \DateTime('now', $this->timezone),
            'extra' => [],
        ];
    }
    protected function interpolate($message, array $context = []) {
        // 替换消息中的占位符
        $replace = [];
        foreach ($context as $key => $val) {
            if (is_string($val) || method_exists($val, '__toString')) {
                $replace['{' . $key . '}'] = $val;
            }
        }
        return strtr($message, $replace);
    }
}

处理器实现

<?php
// 文件处理器
class FileHandler implements HandlerInterface {
    protected $formatter;
    protected $filename;
    protected $maxFiles = 30;
    public function __construct($filename, $level = LogLevel::DEBUG) {
        $this->filename = $filename;
        $this->level = $level;
        $this->ensureDirectoryExists();
    }
    public function handle(array $record) {
        // 检查日志级别
        if (!$this->isHandling($record['level'])) {
            return false;
        }
        $formatted = $this->formatter->format($record);
        $this->write($formatted);
        return true;
    }
    public function setFormatter(FormatterInterface $formatter) {
        $this->formatter = $formatter;
    }
    public function getFormatter() {
        if (!$this->formatter) {
            $this->formatter = new LineFormatter();
        }
        return $this->formatter;
    }
    protected function isHandling($level) {
        $levels = [
            LogLevel::DEBUG => 100,
            LogLevel::INFO => 200,
            LogLevel::NOTICE => 250,
            LogLevel::WARNING => 300,
            LogLevel::ERROR => 400,
            LogLevel::CRITICAL => 500,
            LogLevel::ALERT => 550,
            LogLevel::EMERGENCY => 600,
        ];
        return $levels[$level] >= $levels[$this->level];
    }
    protected function write($message) {
        $this->rotateLogs();
        file_put_contents($this->filename, $message, FILE_APPEND | LOCK_EX);
    }
    protected function rotateLogs() {
        if (!file_exists($this->filename)) {
            return;
        }
        for ($i = $this->maxFiles - 1; $i > 0; $i--) {
            $oldFile = $this->filename . '.' . $i;
            $newFile = $this->filename . '.' . ($i + 1);
            if (file_exists($oldFile)) {
                if ($i == $this->maxFiles - 1) {
                    unlink($oldFile);
                } else {
                    rename($oldFile, $newFile);
                }
            }
        }
        rename($this->filename, $this->filename . '.1');
    }
    protected function ensureDirectoryExists() {
        $directory = dirname($this->filename);
        if (!is_dir($directory)) {
            mkdir($directory, 0777, true);
        }
    }
}
// 数据库处理器
class DatabaseHandler implements HandlerInterface {
    protected $db;
    protected $table;
    protected $formatter;
    public function __construct(PDO $db, $table = 'logs') {
        $this->db = $db;
        $this->table = $table;
        $this->initTable();
    }
    public function handle(array $record) {
        $formatted = $this->formatter->format($record);
        $stmt = $this->db->prepare(
            "INSERT INTO {$this->table} (level, message, context, created_at) 
             VALUES (:level, :message, :context, :created_at)"
        );
        return $stmt->execute([
            ':level' => $record['level_name'],
            ':message' => $formatted,
            ':context' => json_encode($record['context']),
            ':created_at' => $record['datetime']->format('Y-m-d H:i:s')
        ]);
    }
    public function setFormatter(FormatterInterface $formatter) {
        $this->formatter = $formatter;
    }
    public function getFormatter() {
        if (!$this->formatter) {
            $this->formatter = new LineFormatter();
        }
        return $this->formatter;
    }
    protected function initTable() {
        $sql = "CREATE TABLE IF NOT EXISTS {$this->table} (
            id INT AUTO_INCREMENT PRIMARY KEY,
            level VARCHAR(20) NOT NULL,
            message TEXT NOT NULL,
            context TEXT,
            created_at DATETIME NOT NULL,
            INDEX idx_level (level),
            INDEX idx_created_at (created_at)
        )";
        $this->db->exec($sql);
    }
}
// 邮件处理器
class MailHandler implements HandlerInterface {
    protected $to;
    protected $subject;
    protected $from;
    protected $formatter;
    public function __construct($to, $subject = 'Log Alert', $from = 'noreply@example.com') {
        $this->to = $to;
        $this->subject = $subject;
        $this->from = $from;
    }
    public function handle(array $record) {
        // 只发送ERROR及以上级别的日志
        $criticalLevels = [LogLevel::ERROR, LogLevel::CRITICAL, LogLevel::ALERT, LogLevel::EMERGENCY];
        if (!in_array($record['level'], $criticalLevels)) {
            return false;
        }
        $formatted = $this->formatter->format($record);
        $headers = "From: {$this->from}\r\n";
        $headers .= "Content-Type: text/html; charset=UTF-8\r\n";
        return mail($this->to, $this->subject, nl2br($formatted), $headers);
    }
    public function setFormatter(FormatterInterface $formatter) {
        $this->formatter = $formatter;
    }
    public function getFormatter() {
        if (!$this->formatter) {
            $this->formatter = new HtmlFormatter();
        }
        return $this->formatter;
    }
}
// Elasticsearch处理器
class ElasticsearchHandler implements HandlerInterface {
    protected $client;
    protected $index;
    protected $formatter;
    public function __construct($hosts = ['localhost:9200'], $index = 'logs') {
        $this->client = new \Elasticsearch\Client([
            'hosts' => $hosts
        ]);
        $this->index = $index . '_' . date('Y.m.d');
    }
    public function handle(array $record) {
        $params = [
            'index' => $this->index,
            'type' => '_doc',
            'body' => [
                'level' => $record['level_name'],
                'message' => $record['message'],
                'channel' => $record['channel'],
                'context' => $record['context'],
                'extra' => $record['extra'],
                'timestamp' => $record['datetime']->format('c')
            ]
        ];
        try {
            $this->client->index($params);
            return true;
        } catch (\Exception $e) {
            return false;
        }
    }
    public function setFormatter(FormatterInterface $formatter) {
        $this->formatter = $formatter;
    }
    public function getFormatter() {
        if (!$this->formatter) {
            $this->formatter = new JsonFormatter();
        }
        return $this->formatter;
    }
}

格式化器实现

<?php
// 行格式化器
class LineFormatter implements FormatterInterface {
    protected $format;
    public function __construct($format = "[%datetime%] %channel%.%level_name%: %message% %context%\n") {
        $this->format = $format;
    }
    public function format(array $record) {
        $output = $this->format;
        foreach ($record as $key => $val) {
            if ($key === 'datetime') {
                $val = $val->format('Y-m-d H:i:s');
            } elseif (is_array($val)) {
                $val = !empty($val) ? json_encode($val, JSON_UNESCAPED_UNICODE) : '';
            }
            $output = str_replace('%' . $key . '%', $val, $output);
        }
        return $output;
    }
}
// JSON格式化器
class JsonFormatter implements FormatterInterface {
    public function format(array $record) {
        $record['datetime'] = $record['datetime']->format('Y-m-d H:i:s');
        return json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
    }
}
// HTML格式化器
class HtmlFormatter implements FormatterInterface {
    public function format(array $record) {
        $levelColors = [
            'DEBUG' => '#808080',
            'INFO' => '#0000FF',
            'NOTICE' => '#90EE90',
            'WARNING' => '#FFA500',
            'ERROR' => '#FF0000',
            'CRITICAL' => '#8B0000',
            'ALERT' => '#FF00FF',
            'EMERGENCY' => '#FF1493',
        ];
        $color = isset($levelColors[$record['level_name']]) ? $levelColors[$record['level_name']] : '#000000';
        return sprintf(
            '<div style="color: %s; margin: 5px 0; padding: 10px; border: 1px solid %s;">
                <strong>[%s]</strong> %s.%s: %s<br>
                <small>Context: %s</small>
            </div>',
            $color,
            $color,
            $record['datetime']->format('Y-m-d H:i:s'),
            $record['channel'],
            $record['level_name'],
            htmlspecialchars($record['message']),
            !empty($record['context']) ? json_encode($record['context']) : 'None'
        );
    }
}

使用示例

<?php
// 创建日志记录器
$logger = new Logger('my_app');
// 添加文件处理器
$fileHandler = new FileHandler('/var/log/my_app.log', LogLevel::DEBUG);
$fileHandler->setFormatter(new LineFormatter());
$logger->addHandler($fileHandler);
// 添加数据库处理器
$db = new PDO('mysql:host=localhost;dbname=logs', 'user', 'password');
$dbHandler = new DatabaseHandler($db, 'application_logs');
$dbHandler->setFormatter(new JsonFormatter());
$logger->addHandler($dbHandler);
// 添加邮件处理器(仅错误级别)
$mailHandler = new MailHandler(
    'admin@example.com',
    'Application Error Alert',
    'error-noreply@example.com'
);
$mailHandler->setFormatter(new HtmlFormatter());
$logger->addHandler($mailHandler);
// 添加Elasticsearch处理器
$esHandler = new ElasticsearchHandler(
    ['localhost:9200'],
    'application_logs'
);
$esHandler->setFormatter(new JsonFormatter());
$logger->addHandler($esHandler);
// 添加处理器(上下文信息)
$logger->addProcessor(function ($record) {
    $record['extra']['ip'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
    $record['extra']['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown';
    $record['extra']['request_uri'] = $_SERVER['REQUEST_URI'] ?? '/';
    return $record;
});
// 使用日志记录器
$logger->info('用户登录成功', [
    'user_id' => 12345,
    'username' => 'john_doe',
    'ip' => '192.168.1.1'
]);
$logger->error('数据库连接失败', [
    'error_code' => 1045,
    'error_message' => 'Access denied for user'
]);
try {
    // 某些操作
    throw new \Exception('测试异常');
} catch (\Exception $e) {
    $logger->critical('系统异常', [
        'exception' => [
            'message' => $e->getMessage(),
            'file' => $e->getFile(),
            'line' => $e->getLine(),
            'trace' => $e->getTraceAsString()
        ]
    ]);
}

工厂类简化配置

<?php
class LoggerFactory {
    protected static $config = [
        'default' => 'stack',
        'channels' => [
            'stack' => [
                'driver' => 'stack',
                'channels' => ['single', 'daily']
            ],
            'single' => [
                'driver' => 'single',
                'path' => '/var/log/app.log',
                'level' => LogLevel::DEBUG
            ],
            'daily' => [
                'driver' => 'daily',
                'path' => '/var/log/app',
                'level' => LogLevel::DEBUG,
                'days' => 14
            ],
            'slack' => [
                'driver' => 'slack',
                'url' => 'https://hooks.slack.com/services/...',
                'level' => LogLevel::ERROR
            ],
            'syslog' => [
                'driver' => 'syslog',
                'level' => LogLevel::DEBUG
            ],
            'errorlog' => [
                'driver' => 'errorlog',
                'level' => LogLevel::DEBUG
            ],
            'mail' => [
                'driver' => 'mail',
                'to' => 'admin@example.com',
                'level' => LogLevel::ERROR
            ],
            'redis' => [
                'driver' => 'redis',
                'connection' => 'default',
                'level' => LogLevel::DEBUG
            ]
        ]
    ];
    public static function createLogger($channel = null) {
        $channel = $channel ?: self::$config['default'];
        if (!isset(self::$config['channels'][$channel])) {
            throw new \InvalidArgumentException("Logger channel '{$channel}' not found");
        }
        $config = self::$config['channels'][$channel];
        switch ($config['driver']) {
            case 'stack':
                return self::createStackLogger($config['channels']);
            case 'single':
                return self::createSingleLogger($config);
            case 'daily':
                return self::createDailyLogger($config);
            case 'slack':
                return self::createSlackLogger($config);
            case 'syslog':
                return self::createSyslogLogger($config);
            case 'errorlog':
                return self::createErrorlogLogger($config);
            default:
                throw new \InvalidArgumentException("Unknown driver: {$config['driver']}");
        }
    }
    protected static function createStackLogger(array $channels) {
        $logger = new Logger('app');
        foreach ($channels as $channelName) {
            $channelLogger = self::createLogger($channelName);
            $logger->addHandler(... $channelLogger->handlers); // 简化处理
        }
        return $logger;
    }
    // ... 其他创建方法
}

最佳实践

日志级别使用建议

  • DEBUG: 开发调试信息
  • INFO: 正常业务流程信息
  • NOTICE: 正常但重要的事件
  • WARNING: 可能出现问题的状况
  • ERROR: 运行时错误
  • CRITICAL: 临界错误
  • ALERT: 需要立即修复的问题
  • EMERGENCY: 系统不可用

性能优化

// 在记录前检查级别
if ($logger->isHandling(LogLevel::INFO)) {
    $logger->info('记录信息');
}
// 批量日志处理
class BufferedHandler extends AbstractHandler {
    protected $buffer = [];
    protected $bufferSize = 100;
    public function handle(array $record) {
        $this->buffer[] = $record;
        if (count($this->buffer) >= $this->bufferSize) {
            $this->flush();
        }
    }
    public function flush() {
        // 批量写入数据库或文件
    }
    public function __destruct() {
        $this->flush();
    }
}

这个系统设计实现了多通道日志记录,支持文件、数据库、邮件、Elasticsearch等多种输出方式,具有良好的扩展性和灵活性。

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