首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 寫日誌函數最佳實踐:單例模式、日誌級別、自動輪轉、PSR-3 兼容封裝

     編輯:PHP 相關知識

PHP 寫日誌函數從入門到生產級:單例、級別、輪轉、PSR-3、Monolog 集成完整指南

日誌是系統可觀測性的基石。本文從最簡單的 fwrite 封裝出發,逐步演進至單例模式、多級別、文件輪轉、PSR-3 標準介面、Monolog 庫集成,給出適合不同項目體量的日誌方案選型建議。

一、 最簡實現:过程式函數


function writeLog(string $message, string $level = 'INFO'): void
{
    $file = __DIR__ . '/logs/app-' . date('Y-m-d') . '.log';
    $dir = dirname($file);
    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }
    $line = sprintf("[%s] %s: %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message);
    file_put_contents($file, $line, FILE_APPEND | LOCK_EX);
}

// 使用
writeLog('用戶登錄成功', 'info');
writeLog('數據庫連接失敗', 'error');
  • 優點:零依賴、即寫即用
  • 缺點:無級別過濾、無輪轉、高併發性能差、難測試

二、 進階:單例 Logger 類


class Logger
{
    private static ?self $instance = null;
    private string $logDir;
    private int $maxFiles = 30;
    private array $levelMap = [
        'debug' => 100, 'info' => 200, 'notice' => 250,
        'warning' => 300, 'error' => 400, 'critical' => 500,
        'alert' => 550, 'emergency' => 600,
    ];
    private int $minLevel = 200; // 默認 INFO

    private function __construct(string $logDir = __DIR__ . '/logs')
    {
        $this->logDir = $logDir;
    }

    public static function getInstance(string $logDir = ''): self
    {
        if (self::$instance === null) {
            self::$instance = new self($logDir);
        }
        return self::$instance;
    }

    public function setMinLevel(string $level): void
    {
        $this->minLevel = $this->levelMap[strtolower($level)] ?? 200;
    }

    public function log(string $level, string $message, array $context = []): void
    {
        $level = strtolower($level);
        if (($this->levelMap[$level] ?? 200) < $this->minLevel) {
            return; // 級別不足,丟棄
        }

        $file = $this->logDir . '/app-' . date('Y-m-d') . '.log';
        $this->rotateIfNeeded();

        $contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
        $line = sprintf("[%s] %-8s: %s%s\n", date('Y-m-d H:i:s'), strtoupper($level), $message, $contextStr);

        file_put_contents($file, $line, FILE_APPEND | LOCK_EX);
    }

    // 魔術方法快捷調用
    public function __call(string $name, array $args): void
    {
        if (isset($this->levelMap[$name])) {
            $this->log($name, $args[0] ?? '', $args[1] ?? []);
        }
    }

    private function rotateIfNeeded(): void
    {
        $files = glob($this->logDir . '/app-*.log');
        if (count($files) > $this->maxFiles) {
            usort($files, fn($a, $b) => filemtime($a) - filemtime($b));
            for ($i = 0; $i < count($files) - $this->maxFiles; $i++) {
                @unlink($files[$i]);
            }
        }
    }
}

// 使用
$log = Logger::getInstance();
$log->setMinLevel('warning'); // 生產環境只記錄 WARNING 以上
$log->info('用戶登錄', ['uid' => 123, 'ip' => '1.2.3.4']);
$log->error('支付失敗', ['order_id' => 'OD123', 'code' => 'PAY_TIMEOUT']);
$log->debug('SQL', ['sql' => 'SELECT * FROM users WHERE id=1']);
// 等價於
$log->info('用戶登錄', ['uid' => 123]);

三、 PSR-3 標準介面兼容


use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;

class PsrLogger implements LoggerInterface
{
    private Logger $logger; // 組合上述單例

    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }

    public function emergency($message, array $context = []): void { $this->logger->log('emergency', $message, $context); }
    public function alert($message, array $context = []): void    { $this->logger->log('alert', $message, $context); }
    public function critical($message, array $context = []): void { $this->logger->log('critical', $message, $context); }
    public function error($message, array $context = []): void    { $this->logger->log('error', $message, $context); }
    public function warning($message, array $context = []): void  { $this->logger->log('warning', $message, $context); }
    public function notice($message, array $context = []): void   { $this->logger->log('notice', $message, $context); }
    public function info($message, array $context = []): void     { $this->logger->log('info', $message, $context); }
    public function debug($message, array $context = []): void    { $this->logger->log('debug', $message, $context); }
    public function log($level, $message, array $context = []): void
    {
        $this->logger->log($level, $message, $context);
    }
}
  • 實現 LoggerInterface 即可無縫接入遵循 PSR-3 的框架(Laravel、Symfony、Slim、Yii2 等)
  • 框架自動注入 LoggerInterface,業務代碼依賴介面而非具體實現

四、 生產環境首選:Monolog 庫


composer require monolog/monolog

use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Formatter\LineFormatter;
use Monolog\Processor\IntrospectionProcessor;

$log = new Logger('app');
$handler = new RotatingFileHandler(__DIR__ . '/logs/app.log', 30, Logger::WARNING);
$handler->setFormatter(new LineFormatter(
    "[%datetime%] %level_name%: %message% %context% %extra%\n",
    "Y-m-d H:i:s"
));
$handler->pushProcessor(new IntrospectionProcessor(Logger::DEBUG, ['Monolog\\', 'Psr\\']));
$log->pushHandler($handler);

// 多 Handler 鏈:錯誤單獨文件、Slack 通知、Elasticsearch 索引
$log->pushHandler(new \Monolog\Handler\StreamHandler(__DIR__ . '/logs/error.log', Logger::ERROR));
$log->pushHandler(new \Monolog\Handler\SlackHandler('webhook_url', Logger::CRITICAL));

$log->info('訂單創建', ['order_id' => 'OD123']);
$log->error('支付回調失敗', ['exception' => $e]);
Handler用途
RotatingFileHandler按日期輪轉,保留 N 天
StreamHandler單文件/錯誤專用文件
SyslogHandler系統日誌
SlackHandler異常告警推送
ElasticsearchHandler集中式日誌分析
FingersCrossedHandler緩衝日誌,觸發閾值才寫入(高性能)

五、 選型決策表

項目體量推薦方案理由
個人/微型項目單例 Logger 類零依賴、易部署、滿足基本級別/輪轉
中型項目/團隊協作PSR-3 + Monolog標準化、可測試、框架無縫集成、Handler 鏈靈活
大型/微服務Monolog + ELK/EFK + 鏈路追蹤集中式存儲、全文檢索、關聯請求 ID、告警體系

六、 關鍵最佳實踐

  1. 結構化日誌:$log->info('User login', ['uid'=>123, 'ip'=>'...']),而非字符串拼接
  2. 敏感數據脫敏:Processor 自動過濾 password、token、credit_card 字段
  3. 請求 ID 透傳:Middleware 生成 X-Request-ID,Processor 自動注入所有日誌行
  4. 異步寫入:高併發場景使用 BufferHandler + SwiftMailer 異步落盤
  5. 日誌級別語義:DEBUG(開發細節) → INFO(業務里程碑) → WARNING(潛在問題) → ERROR(功能失敗) → CRITICAL(組件不可用) → ALERT/EMERGENCY(需立即人工介入)

七、 結語

從 file_put_contents 到 Monolog,日誌演進的本質是「結構化、標準化、異步化、集中化」。小項目用單例類足夠,中型以上項目請直接上 Monolog 實現 PSR-3,享受生態紅利。記住:沒有日誌的系統是盲目的,沒有級別的日誌是噪音,沒有輪轉的日誌是炸彈。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved