【日志革新】在ThinkPHP5中实现高效TraceId集成,打造可靠的日志追踪系统

问题背景

最近接手了一个骨灰级的项目,然而在项目中遇到了一个普遍的挑战:由于公司采用 ELK(Elasticsearch、Logstash、Kibana)作为日志收集和分析工具,追踪生产问题成为了一大难题。尽管 ELK 提供了强大的日志分析功能,但由于项目历史悠久,日志输出不规范,缺乏唯一标识,导致在海量日志中准确定位问题变得异常困难。为了提升生产环境下的问题排查和故障诊断效率,迫切需要在项目中引入一种机制,能够为每个请求生成唯一的标识符(traceId),并将其与 ELK 集成,以便在日志中准确追踪请求的全链路过程。

系统默认日志格式
在这里插入图片描述

elk 对这种格式采集并不太友好,所以打算重新写一个日志log类。每一条日志都非常清晰,区分 error、info、sql 级别 log,排查起来也非常直观。

先看效果:
在这里插入图片描述

查看application/config.php配置文件,第一反应就是这个File到底在哪?OK,我们直接全局搜索 File.php,最终锁定文件路径:source/thinkphp/library/think/log/driver/File.php

在这里插入图片描述
基于自身业务改造,时间比较短哈,改写了一个初版(简单粗暴就是日志单行展示),可以短时间适配业务,改造后的代码如下:

<?phpnamespace app\common\library;use think\App;
use think\Request;class YopLog
{protected $config = ['time_format' => ' c ','single'      => false,'file_size'   => 2097152,'path'        => LOG_PATH,'apart_level' => [],'max_files'   => 0,'json'        => true,];// 实例化并传入参数public function __construct($config = []){if (is_array($config)) {$this->config = array_merge($this->config, $config);}}/*** 日志写入接口* @access public* @param array $log 日志信息* @param bool $append 是否追加请求信息* @return bool*/public function save(array $log = [], $append = false){$destination = $this->getMasterLogFile();$path = dirname($destination);!is_dir($path) && mkdir($path, 0755, true);$info = [];foreach ($log as $type => $val) {foreach ($val as $msg) {if (!is_string($msg)) {if (!$this->config['json']) {$msg = var_export($msg, true);}}$info[$type][] = $this->config['json'] ? $msg : '[ ' . $type . ' ] ' . $msg;}if (!$this->config['json'] && (true === $this->config['apart_level'] || in_array($type, $this->config['apart_level']))) {// 独立记录的日志级别$filename = $this->getApartLevelFile($path, $type);$this->write($info[$type], $filename, true, $append);unset($info[$type]);}}if ($info) {return $this->write($info, $destination, false, $append);}return true;}/*** 获取主日志文件名* @access public* @return string*/protected function getMasterLogFile(){if ($this->config['single']) {$name = is_string($this->config['single']) ? $this->config['single'] : 'single';$destination = $this->config['path'] . $name . '.log';} else {$cli = PHP_SAPI == 'cli' ? '_cli' : '';if ($this->config['max_files']) {$filename = date('Ymd') . $cli . '.log';$files    = glob($this->config['path'] . '*.log');try {if (count($files) > $this->config['max_files']) {unlink($files[0]);}} catch (\Exception $e) {}} else {$filename = date('Ym') . DIRECTORY_SEPARATOR . date('d') . $cli . '.log';}$destination = $this->config['path'] . $filename;}return $destination;}/*** 获取独立日志文件名* @access public* @param string $path 日志目录* @param string $type 日志类型* @return string*/protected function getApartLevelFile($path, $type){$cli = PHP_SAPI == 'cli' ? '_cli' : '';if ($this->config['single']) {$name = is_string($this->config['single']) ? $this->config['single'] : 'single';$name .= '_' . $type;} elseif ($this->config['max_files']) {$name = date('Ymd') . '_' . $type . $cli;} else {$name = date('d') . '_' . $type . $cli;}return $path . DIRECTORY_SEPARATOR . $name . '.log';}/*** 日志写入* @access protected* @param array $message 日志信息* @param string $destination 日志文件* @param bool $apart 是否独立文件写入* @param bool $append 是否追加请求信息* @return bool*/protected function write($message, $destination, $apart = false, $append = false){// 检测日志文件大小,超过配置大小则备份日志文件重新生成$this->checkLogSize($destination);// 日志信息封装$info['timestamp'] = date($this->config['time_format']);if ($this->config['json']) {foreach ($message as $type => $msg) {if (is_array($msg)) {for ($i = 0; $i < count($msg); $i++) {$info[$type][$i] = $msg[$i] ?? "";}} else {$info[$type] = $msg;}}} else {foreach ($message as $type => $msg) {$info[$type] = is_array($msg) ? implode("\r\n", $msg) : $msg;}}if (PHP_SAPI == 'cli') {$message = $this->parseCliLog($info);} else {// 添加调试日志$this->getDebugLog($info, $append, $apart);$message = $this->parseLog($info);}return error_log(sprintf("%s %s", date("Y-m-d H:i:s"), $message), 3, $destination);}/*** 检查日志文件大小并自动生成备份文件* @access protected* @param string $destination 日志文件* @return void*/protected function checkLogSize($destination){if (is_file($destination) && floor($this->config['file_size']) <= filesize($destination)) {try {rename($destination, dirname($destination) . DIRECTORY_SEPARATOR . time() . '-' . basename($destination));} catch (\Exception $e) {}}}/*** CLI日志解析* @access protected* @param array $info 日志信息* @return string*/protected function parseCliLog($info){if ($this->config['json']) {$message = json_encode($info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\r\n";} else {$now = $info['timestamp'];unset($info['timestamp']);$message = implode("\r\n", $info);$message = "[{$now}]" . $message . "\r\n";}return $message;}/*** 解析日志* @access protected* @param array $info 日志信息* @return string*/protected function parseLog($info){$request     = Request::instance();$requestInfo = ['traceId' => $_SERVER['traceId'] ?? "",'ip'      => $request->ip(),'method'  => $request->method(),'host'    => $request->host(),'uri'     => $request->url(),];if ($this->config['json']) {$info = $requestInfo + $info;return json_encode($info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\r\n";}array_unshift($info, "---------------------------------------------------------------\r\n[{$info['timestamp']}] {$requestInfo['ip']} {$requestInfo['method']} {$requestInfo['host']}{$requestInfo['uri']}");unset($info['timestamp']);return implode("\r\n", $info) . "\r\n";}protected function getDebugLog(&$info, $append, $apart){if (App::$debug && $append) {if ($this->config['json']) {// 获取基本信息$runtime = round(microtime(true) - THINK_START_TIME, 10);$reqs    = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞';$memory_use = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2);$info = ['runtime' => number_format($runtime, 6) . 's','reqs'    => $reqs . 'req/s','memory'  => $memory_use . 'kb','file'    => count(get_included_files()),] + $info;} elseif (!$apart) {// 增加额外的调试信息$runtime = round(microtime(true) - THINK_START_TIME, 10);$reqs    = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞';$memory_use = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2);$time_str   = '[运行时间:' . number_format($runtime, 6) . 's] [吞吐率:' . $reqs . 'req/s]';$memory_str = ' [内存消耗:' . $memory_use . 'kb]';$file_load  = ' [文件加载:' . count(get_included_files()) . ']';array_unshift($info, $time_str . $memory_str . $file_load);}}}
}

探索日志追踪解决方案

1. 生成 traceId: 需要一个能够生成唯一 traceId 的方法,确保每个请求都有一个唯一的标识符。
2. 存储 traceId: 将生成的 traceId 存储在 $_SERVER 中,以便在整个请求处理过程中都能够方便地访问到它。
3. 添加到响应头中: 在每次请求的响应中都添加 traceId 到响应头中,以便客户端收到响应后可以通过 traceId 与请求对应起来。
4. 处理异步请求: 对于异步请求,需要在发送请求时将 traceId 包含在请求头中,以便日志也能够与对应的原始请求进行关联。

解决方案

1. 生成 traceId: 在 Tags.php 中的 app_begin 钩子中,执行以下操作:

<?php
return [// 应用开始'app_begin' => ['app\\api\\behavior\\TraceId'],
];

2. 存储 traceId: 将生成的 traceId 存储在 $_SERVER 中(或者存储在header中)。

为了简化获取 traceId 的代码,我选择将其存储在 $_SERVER 中。这样,只需要通过 $_SERVER[‘traceId’] 就能够轻松获取到 traceId,而不需要编写繁琐的获取代码。相比之下,如果将 traceId 存储在请求体的 header 中,获取代码则需要写成 (Request::instance()->header())[‘traceId’] ?? “”。此外,如果系统中存在原生调用,需要获取所有的 header 头,就需要使用到 getallheaders() 函数。然而,getallheaders() 函数只能获取到最初请求打到服务上的所有 header 内容,而手动设置的 header 是无法被 getallheaders() 函数获取到的。因此,将 traceId 存储在 $_SERVER 中可以更加方便地获取,并且不受限于原生调用的影响

3. 添加到响应头中: 在响应头中添加 traceId。

<?phpnamespace app\api\behavior;/*** TraceId 行为类** 此行为类用于在 API 请求的上下文中自动注入一个唯一的 traceId 到 HTTP 响应头。* traceId 主要用于链路追踪,有助于在日志中跟踪请求的全链路过程,* 提升系统问题排查和诊断的效率。*/
class TraceId
{/*** 执行行为** @return void*/public function run(){// 使用generateTraceId()函数生成一个唯一的traceId值$traceId = generateTraceId();// 将生成的唯一traceId值存储在$_SERVER全局变量中$_SERVER['traceId'] = $traceId;// 设置响应头header("X-Trace-Id: {$traceId}");}
}

4. 处理异步请求: 在异步请求中,确保在发送请求时将 traceId 包含在请求头中。
发送请求

public function exec_bce($method, $post)
{$config = new \stdClass();$config->secret = 'dz_mufeng';$sign = $this->make_sign($post, $config);$traceId = $_SERVER['traceId'] ?? "";// 获取数据$content = http_build_query($post, '', '&');$header = ["Content-type:application/x-www-form-urlencoded","Content-length:" . strlen($content),"traceId: " . $traceId];$context['http'] = ['timeout' => 60,'method' => 'POST','header' => implode("\r\n", $header),'content' => $content,];$url = config('bce_url').'/code.php?method=' . $method . '&sign=' . $sign;log_write('code_exec_context:' . json_encode($context), 'info');$contextStream = stream_context_create($context);$res = file_get_contents($url, false, $contextStream);log_write("执行返回结果:" . $res, 'info');$res = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $res);$res = json_decode($res, true);if ($res['result'] == 0) {return $this->renderError($res['data']);} else {return $this->renderSuccess($res['data']);}}

本系统原生代码在接收请求时,可直接使用 $_SERVER[‘HTTP_TRACEID’] 获取 traceId。

<?php
public function log($params, $type = 'info')
{if (!is_string($params)) {$params = json_encode($params, 320);}$requestId = $_SERVER['traceId'] ?? '';$traceId = $_SERVER['HTTP_TRACEID'] ?? "";!is_dir($this->logPath) && mkdir($this->logPath, 0755, true);$requestInfo = ['[trace_id]' => empty($traceId) ? $requestId : $traceId,'[request_ip]' => $this->getIp(),'[method]'     => $_SERVER['REQUEST_METHOD'],'[domain]'     => sprintf('%s://%s', $_SERVER['REQUEST_SCHEME'], $_SERVER['HTTP_HOST']),'[uri]'        => sprintf('%s://%s%s', $_SERVER['REQUEST_SCHEME'], $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI']),'[param]'      => $params . "\r\n",'[trace]'      => (new \Exception)->getTraceAsString()];$println = "---------------------------------------------------------------\r\n";$msg     = sprintf("%s%s [%s] ", $println, date("Y-m-d H:i:s"), $type);foreach ($requestInfo as $key => $value) {$msg .= sprintf("%s: %s ", $key, $value);}file_put_contents(sprintf("%s/%s_%s",$this->logPath, date("d"), "api.log"), $msg . "\r\n", FILE_APPEND);
}

修改完之后日志中心检索,发现日志好多报错,发现不规范获取数据索引导致。

在这里插入图片描述
这类错误暂时不影响主流程,可以屏蔽掉,那么 [8] 代表什么?他代表错误级别,可以针对性屏蔽掉。

详见: Core_d.php

在这里插入图片描述
可以在框架对应模块的基类中过滤掉

error_reporting(E_ERROR | E_PARSE);

这段代码是用来设置 PHP 错误报告级别的。在 PHP 中,你可以通过修改 error_reporting 指令来控制哪些类型的错误将被报告。在这个例子中,error_reporting 被设置为报告四种类型的错误:

E_ERROR: 严重错误,可能会导致脚本终止执行。
E_WARNING: 警告,不会导致脚本终止执行,但可能会表明存在问题。
E_PARSE: 语法解析错误,通常是由于语法错误而导致的解析失败。
E_NOTICE: 提示,指出可能会导致问题的非致命性错误。
通过将这些错误类型组合起来,这段代码将导致 PHP 报告这四种类型的错误,而忽略其它类型的错误。这通常有助于在开发和调试阶段发现潜在的问题,但在生产环境中,通常会调整为仅报告严重错误(如 E_ERROR)。

结论

以上解决方案有效地为 ThinkPHP5 的日志添加了 traceId,实现了请求的全链路追踪(包括异步请求,确保请求连贯性),从而提高了系统问题排查和诊断的效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/690156.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

信息收集(详细)

文章目录 网站要素IP信息资产架构信息服务信息网站信息cms信息源码信息中间件信息 个人信息收集其它 渗透的第一步就是信息收集&#xff0c;通常信息收集的结果如何&#xff0c;会直接影响后续能否渗透成功。好的信息收集可以让攻击面更广&#xff0c;思路更加广。 本文对信息收…

uni-segmented-control插件使用

dcloud插件市场 前端/uniapp 1.HBuildX打开目标项目 2.进入dcloud插件市场下载目标插件 3.看到如下提示(已经可以在目标项目中使用插件啦) 4.项目正式使用

docker(二):Centos安装docker

文章目录 1、安装docker2、启动docker3、验证 官方文档&#xff1a;https://docs.docker.com/engine/install/centos/ 1、安装docker 下载依赖包 yum -y install gcc yum -y install gcc-c yum install -y yum-utils设置仓库 yum-config-manager --add-repo http://mirrors…

多线程·线程状态

目录 1.等待一个线程 join 2.休眠当前线程 3.线程的所有状态 4.线程的状态转换 1.等待一个线程 join 有些场景&#xff0c;我们需要控制线程的执行顺序&#xff0c;这时候就需要用到 join 了 比如&#xff1a;把大象装进冰箱要几步&#xff1f; 第一步&#xff1a;打开冰…

ICode国际青少年编程竞赛- Python-4级训练场-嵌套for循环练习

ICode国际青少年编程竞赛- Python-4级训练场-嵌套for循环练习 1、 for i in range(3):Spaceship.step(4)for j in range(4):Dev.step(2)Dev.turnRight()Spaceship.turnLeft()Spaceship.step(4)Spaceship.turnRight()2、 for i in range(4):Spaceship.step(6)for j in range(3):…

JavaFX布局-HBox

JavaFX布局-HBox 常用属性alignmentspacingchildrenmarginpaddinghgrow 实现方式Java实现Xml实现 综合案例 HBox按照水平方向排列其子节点改变窗口大小,不会该部整体布局窗口太小会遮住内部元素&#xff0c;不会产生滚动条 常用属性 alignment 对齐方式 new HBox().setAlign…

Vue3:路由

1. 路由简介 在Vue3中&#xff0c;路由是一个核心概念&#xff0c;特别是在构建单页面应用程序&#xff08;SPA&#xff09;时。以下是Vue3中路由的基本概念&#xff1a; 1. **路由&#xff08;Route&#xff09;**&#xff1a;在Vue3中&#xff0c;路由是指根据特定的规则将用…

毕业论文凑字数——关于IVR自动语音应答交互式电话导航自动总机等等概念的一些剖析

目录 IVR毕业论文的讨巧思路IVR自动语音应答IVR的使用流程IVR的各种应用IVR的基本配置 一个小朋友的毕业论文要凑字数&#xff0c;所以推荐她讲一讲IVR&#xff0c;因为IVR可以翻译的名字很多&#xff0c;比如交互式语音应答&#xff0c;自动语音应答&#xff0c;自动语音服务&…

深度解析DPO及其变体在多种任务上的表现如何,该如何选择

深度学习自然语言处理 原创作者&#xff1a;wkk 单位&#xff1a;亚利桑那州立大学paper&#xff1a;Insights into Alignment:Evaluating DPO and its Variants Across Multiple TasksLink&#xff1a;https://arxiv.org/pdf/2404.14723 今天&#xff0c;我要带大家深入了解一…

高性能运营级流媒体服务框架:支持多协议互转 | 开源日报 No.250

ZLMediaKit/ZLMediaKit Stars: 12.6k License: NOASSERTION ZLMediaKit 是一个基于 C11 的高性能运营级流媒体服务框架。 使用 C11 开发&#xff0c;避免裸指针&#xff0c;代码稳定可靠&#xff0c;性能优越。支持多种协议 (RTSP/RTMP/HLS/HTTP-FLV/WebSocket-FLV/GB28181 等…

【软考高项】四十二、八大绩效域知识点

一、干系人绩效域 预期目标 建立高效的工作关系 检查&#xff1a;干系人参与的连续性 干系人认同项目目标 检查&#xff1a; 变更频率 支持项目的干系人提高了满意度&#xff0c;从中受益 检查&#xff1a;干系人行为、干系人满意度、干系人相…

使用xtuner微调InternLM-Chat-7B

1. 安装xtuner #激活环境 source activate test_llm # 安装xtuner pip install xtuner#还有一些依赖项需要安装 future>0.6.0 cython lxml>3.1.0 cssselect mmengine 2. 创建一个ft-oasst1 数据集的工作路径&#xff0c;进入 mkdir ft-oasst1 cd ft-oasst1 3.XTune…