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");