【日志革新】在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,实现了请求的全链路追踪(包括异步请求,确保请求连贯性),从而提高了系统问题排查和诊断的效率。
相关文章:
【日志革新】在ThinkPHP5中实现高效TraceId集成,打造可靠的日志追踪系统
问题背景 最近接手了一个骨灰级的项目,然而在项目中遇到了一个普遍的挑战:由于公司采用 ELK(Elasticsearch、Logstash、Kibana)作为日志收集和分析工具,追踪生产问题成为了一大难题。尽管 ELK 提供了强大的日志分析功…...
英译汉早操练-(二十)
hello大家好,这篇跟随十九,继续真题学习。如果想看全部请返回到第十九篇。 英译汉早操练-(十九)-CSDN博客 The political upheaval in Libya and elsewhere in North Africa has opened the way for thousands of new migrants to…...
Go-Zero自定义goctl实战:定制化模板,加速你的微服务开发效率(四)
前言 上一篇文章带你实现了Go-Zero和goctl:解锁微服务开发的神器,快速上手指南,本文将继续深入探讨Go-Zero的强大之处,并介绍如何使用goctl工具实现模板定制化,并根据实际项目业务需求进行模板定制化实现。 通过本文…...
(五)STM32F407 cubemx IIC驱动OLED(1)IIC协议篇
(五)STM32F407 cubemx IIC驱动OLED(1)IIC协议篇 这篇文章主要是个人的学习经验,想分享出来供大家提供思路,如果其中有不足之处请批评指正哈。 废话不多说直接开始主题,本人是基于STM32F407V…...
OpenCV特征匹配总结
1.概述 在深度学习出现之前,图像中的特征匹配方法主要有 2.理论对比 3.代码实现 #include <iostream> #include <opencv2/opencv.hpp>int main(int argc, char** argv) {if(argc ! 3) {std::cerr << "Usage: " << argv[0] <…...
二叉树的四种遍历代码实现
二叉树的遍历大致能分为以下几种 1.前序:根 左 右 2.中序:左 根 右 3.后序:左 右 根 4.层序:从根开始一层一层的向下 如上图访问顺序: 前序:1 2 3 N N N 4 5 N N 6 N N 中序:N 3 N 2 N 1 N 5 N 4 N …...
系统和功能测试:确保软件的功能和易用性
目录 概述 功能测试 LOSED 模型 用例的设计 等价类划分 边界值分析 循环结构测试的综合方法 因果图 决策表 功能图 正交实验设计 易用性测试 内部易用性测试 外部易用性测试 功能性测试 正向功能性测试 负向功能性测试 功能性测试工具 结语 概述 在软件开发…...
关于服务端接口知识的汇总
大家好,今天给大家分享一下之前整理的关于接口知识的汇总,对于测试人员来说,深入了解接口知识能带来诸多显著的好处。 一、为什么要了解接口知识? 接口是系统不同模块之间交互的关键通道。只有充分掌握接口知识,才能…...
树(数据结构)
树的定义 一个根结点,其余结点分为 m 个不相交的集合, 其中每个集合本身又是一棵树,并且称为根的子树。 树的根结点没有前驱,其他结点有且仅有一个前驱。 所有结点可以有0个或多个后继。 基本术语 结点的度 树的度 : 树…...
Spring底层入门(十一)
1、条件装配 在上一篇中,我们介绍了Spring,Spring MVC常见类的自动装配,在源码中可见许多以Conditional...开头的注解: Conditional 注解是Spring 框架提供的一种条件化装配的机制,它可以根据特定的条件来控制 Bean 的…...
优质资料:大型制造企业等级保护安全建设整改依据,系统现状分析,网络安全风险分析
第1章 项目概述 XX 大型制造型企业是国内一家大型从事制造型出口贸易的大型综合企业集团,为了落实国家及集团的信息安全等级保护制度,提高信息系统的安全防护水平,细化各项信息网络安全工作措施,提升网络与信息系统工作的效率&am…...
几种监控工具学习
在Linux上有很多监控工具,比如Zabbix、Prometheus、APM和ELK 监控工具是确保系统稳定运行的关键组件之一,它可以帮助系统管理员和开发人员及时发现并解决问题。 以下是几种流行的监控工具的简要介绍: Zabbix: Zabbix 是一个企…...
树莓派python开发
树莓派自带thonny 点亮LED灯 import RPi.GPIO as GPIO import time# 设置GPIO模式为BCM GPIO.setmode(GPIO.BCM)# 设置LED引脚 led_pin 18# 设置LED引脚为输出 GPIO.setup(led_pin, GPIO.OUT)# 点亮LED GPIO.output(led_pin, GPIO.HIGH)# 延时2秒 time.sleep(2)# 关闭LED GPI…...
纯血鸿蒙APP实战开发——首页下拉进入二楼效果案例
介绍 本示例主要介绍了利用position和onTouch来实现首页下拉进入二楼、二楼上划进入首页的效果场景,利用translate和opacity实现动效的移动和缩放,并将界面沉浸式(全屏)显示。 效果图预览 使用说明 向下滑动首页页面超过触发距…...
苹果cms:开启高速缓存加快访问速度
由于苹果cms采集的影片数据过多,如果不设置缓存,可能会造成网站访问缓慢,或者CPU消耗过高。随着用户访问量的上升,添加缓存设置是有这个必要的。目前cms提供了四种缓存方式 1)file:以文件形式,通俗说直接访问Mysql,要达…...
实时数据推送——长轮询,短轮询,长连接
短轮询 短轮询是最简单的一种数据推送方式,客户端在固定的时间间隔(例如每隔5秒)向服务器发送请求,询问是否有更新的数据。服务器立即处理请求并返回数据,不论数据是否真的已经更新。 长轮询 长轮询是对短轮询的改进…...
七.音视频编辑-创建视频过渡-应用
引言 在上一篇博客中,我们已经介绍了创建视频过渡的实现方案,步骤非常繁琐,在生成AVMutableVideoCompositionInstruction和AVMutableVideoCompositionLayerInstruction的计算也十分复杂,但其实还有一个创建视频组合的捷径。不过我…...
Android11 InputManagerService启动流程分析
InputManagerService在systemserver进程中被启动 //frameworks\base\services\java\com\android\server\SystemServer.java t.traceBegin("StartInputManagerService"); inputManager new InputManagerService(context);//1 t.traceEnd(); //省略 //注册服务 Servi…...
【计算机网络篇】数据链路层(8)共享式以太网的退避算法和信道利用率
文章目录 🛸共享式以太网的退避算法🥚截断二进制指数算法 🍔共享式以太网的信道利用率 🛸共享式以太网的退避算法 在使用CSMA/CD协议的共享总线以太网中,正在发送帧的站点一边发送帧一边检测碰撞,当检测到…...
wordpress主题 7B2 PRO主题5.4.2免授权直接安装
内容目录 一、详细介绍二、效果展示1.部分代码2.效果图展示 三、学习资料下载 一、详细介绍 WordPress 资讯、资源、社交、商城、圈子、导航等多功能商用主题:B2 PRO 其设计风格专业且时尚,功能十分强大,包括多栏布局、自定义页面、强大的主…...
Chapter03-Authentication vulnerabilities
文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...
Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...
蓝牙 BLE 扫描面试题大全(2):进阶面试题与实战演练
前文覆盖了 BLE 扫描的基础概念与经典问题蓝牙 BLE 扫描面试题大全(1):从基础到实战的深度解析-CSDN博客,但实际面试中,企业更关注候选人对复杂场景的应对能力(如多设备并发扫描、低功耗与高发现率的平衡)和前沿技术的…...
在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module
1、为什么要修改 CONNECT 报文? 多租户隔离:自动为接入设备追加租户前缀,后端按 ClientID 拆分队列。零代码鉴权:将入站用户名替换为 OAuth Access-Token,后端 Broker 统一校验。灰度发布:根据 IP/地理位写…...
ETLCloud可能遇到的问题有哪些?常见坑位解析
数据集成平台ETLCloud,主要用于支持数据的抽取(Extract)、转换(Transform)和加载(Load)过程。提供了一个简洁直观的界面,以便用户可以在不同的数据源之间轻松地进行数据迁移和转换。…...
在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案
这个问题我看其他博主也写了,要么要会员、要么写的乱七八糟。这里我整理一下,把问题说清楚并且给出代码,拿去用就行,照着葫芦画瓢。 问题 在继承QWebEngineView后,重写mousePressEvent或event函数无法捕获鼠标按下事…...
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材)
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材) 这个项目能干嘛? 使用 gemini 2.0 的 api 和 google 其他的 api 来做衍生处理 简化和优化了文生图和图生图的行为(我的最主要) 并且有一些目标检测和切割(我用不到) 视频和 imagefx 因为没 a…...
C#学习第29天:表达式树(Expression Trees)
目录 什么是表达式树? 核心概念 1.表达式树的构建 2. 表达式树与Lambda表达式 3.解析和访问表达式树 4.动态条件查询 表达式树的优势 1.动态构建查询 2.LINQ 提供程序支持: 3.性能优化 4.元数据处理 5.代码转换和重写 适用场景 代码复杂性…...
为什么要创建 Vue 实例
核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...
