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");
相关推荐
Smile灬凉城66625 分钟前
robots协议
安全·php·robots
数据小爬虫@1 小时前
如何利用PHP爬虫获取速卖通(AliExpress)商品评论
开发语言·爬虫·php
fendouweiqian2 小时前
查看php已安装扩展命令
php
柒烨带你飞3 小时前
路由器的原理
网络·智能路由器·php
vvw&5 小时前
如何在 Ubuntu 22.04 上安装和使用 Composer
linux·运维·服务器·前端·ubuntu·php·composer
hking1116 小时前
upload-labs关卡记录5
web安全·php
2401_857617628 小时前
“无缝购物体验”:跨平台网上购物商城的设计与实现
java·开发语言·前端·安全·架构·php
2401_857439698 小时前
智慧社区电商系统:提升用户体验的界面设计
前端·javascript·php·ux
我是高手高手高高手8 小时前
ThinkPHP8多应用配置及不同域名访问不同应用的配置
linux·服务器·前端·php
vvw&11 小时前
如何在 Ubuntu 22.04 上安装 phpMyAdmin
linux·运维·服务器·mysql·ubuntu·php·phpmyadmin