当前位置: 首页 > article >正文

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

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

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

namespace logger;

enum LoggerFunction
{


        // url请求
    case REQUEST;

        // 调试数据
    case  DEBUG;

        // 监控
    case   MONITOR;

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

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

<?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

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.代码示例

// 记录常规日志
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");


http://www.kler.cn/a/409406.html

相关文章:

  • DataWhale—PumpkinBook(TASK05决策树)
  • java: itext8.05 create pdf
  • Windows 软件之 FFmpeg
  • SpringBoot+SpringCloud面试题整理附答案
  • 使用 LSTM(长短期记忆网络) 模型对时间序列数据(航空旅客人数数据集)进行预测
  • Android12 mtk设置插充电器自动开机
  • PICO VR串流调试Unity程序
  • [第五空间 2021]pklovecloud 详细题解
  • 跟着问题学5——深度学习中的数据集详解(1)
  • 【深度学习】Linux常见命令
  • web前端开发--创建百雀羚网站
  • C++11: STL之bind
  • 【MySQL】sql注入相关内容
  • 【开源风云】从若依系列脚手架汲取编程之道(八)
  • C#里怎么样使用正则表达式?
  • 动态规划—课堂笔记=>背包问题(2)
  • 东胜物流软件 GetDataListCA SQL注入漏洞复现
  • Laravel对接SLS日志服务
  • 如何快速将Excel数据导入到SQL Server数据库
  • 界面控件DevExpress WPF中文教程:网格视图数据布局的列和卡片字段
  • C++中定义类型名的方法
  • 【Golang】——Gin 框架与数据库集成详解
  • Python的tkinter如何把日志弄进文本框(Text)
  • 大事件管理系统项目总结(上)
  • 【Vscode】不同系统快捷键
  • 论防火墙对网络安全的重要性