php日志系统

1.日志系统

服务器日志是服务器运行过程中记录的各种信息的集合,它们对于系统管理员和开发人员来说具有重要的意义。例如, 调试,监控,行为分析等等。

php自带一个log库,但与java生态存在同样的窘境,就是被第三方工具盖住了锋芒。例如java日志系统一般使用的是slfj坐门面,log4j或log4j2或logback做实现。

php自带的日志功能主要侧重于错误处理,虽然有像E_ERROR(致命错误)、E_WARNING(警告)、E_NOTICE(通知)等错误级别,但在实际复杂的应用场景中,这些级别可能不够精细。

另外,php输出目标比较单一,日志格式不够丰富,缺乏高级的功能,例如日志的切割(当一个日志文件达到一定大小后,自动分割成多个文件)、日志的归档和清理(按照一定的时间周期或者日志级别删除旧的日志)等功能缺失。

2.monolog日志库

丰富的日志级别

Monolog 支持多种日志级别,包括 DEBUG、INFO、NOTICE、WARNING、ERROR、CRITICAL、ALERT、EMERGENCY。这种精细的级别划分可以满足不同场景下的日志记录需求。例如,在开发阶段,将日志级别设置为 DEBUG,可以记录详细的程序运行信息,如函数调用的参数和返回值、数据库查询语句等,帮助开发人员快速定位和解决问题。在生产环境中,将日志级别调整为 ERROR 或 CRITICAL,只记录严重影响系统运行的关键错误,有助于减少日志文件的大小和提高系统性能。

灵活的处理器(Handler)

  • 多渠道输出:Monolog 可以通过不同的处理器将日志输出到各种目标。它可以将日志记录到文件、标准输出(stdout)、数据库、电子邮件、消息队列(如 RabbitMQ、Kafka)等。
  • 例如,对于一个 Web 应用,你可以使用StreamHandler将 INFO 级别的日志记录到文件中,用于日常的运维查看;同时使用SwiftMailerHandler将 ERROR 级别的日志发送到开发人员的邮箱,以便及时发现和处理严重错误。
  • 自定义处理器:开发人员还可以创建自定义的处理器,根据特定的业务需求来处理日志。比如,你可以创建一个处理器,将日志数据发送到一个自定义的数据分析系统,用于统计用户行为或系统性能指标。

易于定制的日志格式

  • Monolog 允许轻松定制日志格式。可以使用内置的格式化器(Formatter)或者创建自己的格式化器来定义日志的外观。
  • 例如,使用LineFormatter可以将日志格式化为简单的文本行,包含日志级别、日期时间、消息等信息。如果需要将日志与其他系统集成,如日志分析工具(Elasticsearch - Kibana),可以使用JsonFormatter将日志转换为 JSON 格式,方便存储和查询。这种灵活性使得 Monolog 能够适应各种不同的日志使用场景。

支持上下文信息(Context)

  • Monolog 允许在日志记录中添加上下文信息。上下文信息可以是任何与当前日志相关的数据,如用户 ID、请求 ID、当前执行的模块名称等。
  • 例如,在一个用户认证的场景中,当记录一个登录失败的日志时,可以添加用户的 IP 地址、尝试登录的用户名等上下文信息。这对于后续的故障排查和安全审计非常有用,能够提供更全面的事件背景。

3.合理的日志分类

3.1.日志分类

在生产环境,主要有三大类日志,一种是系统日志,主要用于记录程序的行为,用于排查bug,行为监控等;一种则是运营日志,主要用于数据分析(如果是游戏服务器,当程序出现bug,可用于补偿或者回收)。最后一种是异常日志,用于修复bug。

对于系统日志,一般无需结构化输出,只有肉眼可分析即可。例如可以用下面的格式:

复制代码
2024-09-08 19:46:54 [info] ----test1---
2024-09-08 19:46:54 [info] game server is starting ...
2024-09-08 19:48:21 [info] ----test2---
2024-09-08 19:48:21 [info] game server is starting ...
2024-09-08 19:50:14 [info] ----test3---
2024-09-08 19:50:14 [info] game server is starting ...

对于运营日志,如果服务器是分布式部署,需要将不同进程产生的运营日志统一采集到指定的目录,例如通过 ELK(Elasticsearch、Logstash、Kibana)或者hadoop。因此,运营日志一定是结构化日志(类似于mysql的表,有统一的格式),例如可以用下面的格式:

复制代码
time|1725276165776|model|request|url|/var/queryUserGameVars|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276166035|model|request|url|/var/queryUserGameVars|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276166288|model|request|url|/array/queryUserGameVars|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276166541|model|request|url|/array/queryUserGameVars|remoteIp|103.167.134.39, 172.71.214.147|localIp|127.0.0.1
time|1725276188600|model|request|url|/player/getProgress|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276188852|model|request|url|/player/getProgress|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276195164|model|request|url|/player/getArchives|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276195421|model|request|url|/player/getArchives|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276197467|model|request|url|/player/getArchives|remoteIp|103.167.134.39, 172.71.214.147|localIp|127.0.0.1
time|1725276199553|model|request|url|/player/getArchives|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276206665|model|request|url|/template/create|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276206926|model|request|url|/template/create|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1

对于异常日志,则需要有完整的堆栈信息,能提供上下文情况。

3.2.系统日志与异常日志

系统日志与异常日志这两类日志比较类似,不同的只是格式不同,这里作统一的api入口

php 复制代码
<?php

namespace logger;

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

class LoggerSystem
{
    private static $instance = null;
    private $loggers = [];


    /**
     * 获取 LoggerSystem 单例实例
     *
     * @return LoggerSystem
     */
    public static function getInstance()
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * 获取 Logger 实例
     *
     * @param string $type 日志类型 ('exception' 或 'console')
     * @return Logger 返回对应的日志记录器
     */
    public function getLogger($type)
    {
        if (!isset($this->loggers[$type])) {
            // 创建新的 Logger 实例
            $logger = new Logger($type);


            // 根据不同类型配置不同的处理器
            if ($type === 'exception') {
                // 异常日志处理器 (RotatingFileHandler,按日期分割文件,保留 30 天)
                $handler = new RotatingFileHandler($_SERVER['DOCUMENT_ROOT'] . '/logs/exception.log', 30, Logger::ERROR);
            } else {
                // 常规日志处理器 (RotatingFileHandler,按日期分割文件,保留 30 天)
                $output = "[%datetime%] %channel%.%level_name%: %message%\n";
                $formatter = new LineFormatter($output);
                $handler = new RotatingFileHandler($_SERVER['DOCUMENT_ROOT'] . '/logs/app.log', 30, Logger::INFO);
                $handler->setFormatter($formatter);
            }

            // 将处理器加入到 Logger 中
            $logger->pushHandler($handler);

            // 缓存该 Logger 实例,避免重复创建
            $this->loggers[$type] = $logger;
        }

        // 返回缓存的 Logger 实例
        return $this->loggers[$type];
    }
}

门面api

php 复制代码
namespace logger;

class LoggerUtil
{
    /**
     * 记录异常日志
     *
     * @param string $message
     * @param Throwable $e
     */
    public static function logException($message, \Throwable $e)
    {
        $logger = LoggerSystem::getInstance()->getLogger('exception');
        $logger->error($message, ['exception' => $e]);
    }

    /**
     * 记录常规日志
     *
     * @param string $message
     */
    public static function logInfo($message)
    {
        $logger = LoggerSystem::getInstance()->getLogger('console');
        $logger->info($message);
    }
}

3.3.运营日志

对于运营日志,我们是需要区别模块的,比如监控,调式,请求以及各种功能模块

定义模块枚举

php 复制代码
<?php

namespace logger;

enum LoggerFunction
{


        // url请求
    case REQUEST;

        // 调试数据
    case  DEBUG;

        // 监控
    case   MONITOR;

    // 定义方法返回枚举值的名称
    public function getName(): string
    {
        return $this->name;
    }
}

对于每一个模块,缓存名称与对应的logger对象,保证每一个模块只生成一个logger对象

php 复制代码
<?php

namespace logger;

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

class LoggerBuilder
{
    // 日志实例容器
    private static $container = [];

    /**
     * 根据名称获取日志实例
     * 
     * @param string $name 日志名称
     * @return Logger
     */
    public static function getLogger($name)
    {
        if (isset(self::$container[$name])) {
            return self::$container[$name];
        }

        // 保证线程安全(这里 PHP 是单线程环境,锁可以省略)
        return self::build($name);
    }

    /**
     * 构建 Logger 对象
     * 
     * @param string $name 日志名称
     * @return Logger
     */
    private static function build($name)
    {
        // 创建 Logger 实例
        $logger = new Logger($name);

        // 文件路径
        $fileName = strtolower($name);
        $filePath = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . 'logs' . '/' . $fileName . '/' . $fileName . '.log';

        // 创建一个 RotatingFileHandler 实例
        $handler = new RotatingFileHandler($filePath, 15, Logger::INFO);

        // 设置日志格式,只输出消息内容
        $output = "%message%\n";
        $formatter = new LineFormatter($output);
        $handler->setFormatter($formatter);

        // 将 handler 加入到 logger 中
        $logger->pushHandler($handler);

        // 保存到容器中
        self::$container[$name] = $logger;

        return $logger;
    }
}

门面api,传入的参数为模块名称,以及对应的key,value参数,不定参数,成对出现

php 复制代码
<?php

namespace logger;

class LoggerUtil
{

    // 信息日志记录
    public static function info(LoggerFunction $logger, ...$args)
    {
        if (empty($args)) {
            return;
        }

        // 如果参数数量不是偶数,抛出异常
        if (count($args) % 2 !== 0) {
            throw new \InvalidArgumentException(sprintf("Logger %s, args %s", $logger, $args));
        }

        $sb = [];
        $sb[] = "time|" . time() . "|";
        $sb[] = "date|" . date('Y-m-d H:i:s') . "|";

        // 构建键值对日志信息
        for ($i = 0, $n = count($args); $i < $n; $i += 2) {
            $key = $args[$i];
            $value = $args[$i + 1];
            $sb[] = "$key|$value|";
        }

        // 将最后一个多余的 | 去掉
        $logMessage = rtrim(implode("", $sb), "|");
        // 记录信息日志
        LoggerBuilder::getLogger($logger->getName())->info($logMessage);
    }
}

3.4.代码示例

php 复制代码
// 记录常规日志
logger\LoggerUtil::logInfo('This is a regular info log.');

// 捕获异常并记录异常日志
try {
	throw new Exception("Something went wrong!");
} catch (Throwable $e) {
	logger\LoggerUtil::logException('An error occurred', $e);
}

// 记录运营日志
logger\LoggerUtil::info(logger\LoggerFunction::DEBUG, "key1", "value1", "key2", "value2");
相关推荐
BingoGo11 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack11 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo1 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack1 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack2 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo2 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack3 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理4 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
QQ5110082854 天前
python+springboot+django/flask的校园资料分享系统
spring boot·python·django·flask·node.js·php
WeiXin_DZbishe4 天前
基于django在线音乐数据采集的设计与实现-计算机毕设 附源码 22647
javascript·spring boot·mysql·django·node.js·php·html5