本文目录导读:

我来为你设计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等多种输出方式,具有良好的扩展性和灵活性。