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入口
<?phpnamespace 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.运营日志
对于运营日志,我们是需要区别模块的,比如监控,调式,请求以及各种功能模块
定义模块枚举
<?phpnamespace logger;enum LoggerFunction
{// url请求case REQUEST;// 调试数据case DEBUG;// 监控case MONITOR;// 定义方法返回枚举值的名称public function getName(): string{return $this->name;}
}
对于每一个模块,缓存名称与对应的logger对象,保证每一个模块只生成一个logger对象
<?phpnamespace 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参数,不定参数,成对出现
<?phpnamespace 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");