【日志革新】在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 其设计风格专业且时尚,功能十分强大,包括多栏布局、自定义页面、强大的主…...

Dubbo基本使用
Dubbo基本使用 1.项目介绍2.开发步骤2.1 启动注册中心2.2 初始化项目2.3 添加 Maven 依赖2.3.1 父pom.xml2.3.1 consumer模块和provider模块pom.xml 2.4 定义服务接口2.5 定义服务端的实现2.6 配置服务端 Yaml 配置文件2.7 配置消费端 Yaml 配置文件2.8 基于 Spring 配置服务端…...

JS解密之新js加密实战(二)
前言 上次发了一篇关于新加密的,只解了前边两层,这中间家里各种事情因素影响,没有继续进一步研究,今天百忙之中抽空发布第二篇,关于其中的一小段加密片段,我认为分割成多个小片段是更容易被理解的。逻辑相…...

tsconfig 备忘清单
前言 ❝ Nealyang/blog0 使用 ts 已多年,但是貌似对于 tsconfig 总是记忆不清,每次都是 cv 历史项目,所以写了这篇备忘录,希望能帮助到大家。 本文总结整理自 Matt Pocock 的一篇文章3,加以个人理解,并做了…...

jmeter后置处理器提取到的参数因为换行符导致json解析错误
现象: {"message":"JSON parse error: Illegal unquoted character ((CTRL-CHAR, code 10)): has to be escaped using backslash to be included in string value; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Ill…...

栈与队列的实现
前言 本次博客将要实现一下栈和队列,好吧 他们两个既可以使用动态数组也可以使用链表来实现 本次会有详细的讲解 栈的实现 栈的基础知识 什么是栈呢? 栈的性质是后进先出 来画个图来理解 当然可不可以出一个进一个呢,当然可以了 比如…...

线性集合:ArrayList,LinkedList,Vector/Stack
共同点:都是线性集合 ArrayList ArrayList 底层是基于数组实现的,并且实现了动态扩容(当需要添加新元素时,如果 elementData 数组已满,则会自动扩容,新的容量将是原来的 1.5 倍),来…...

llama3 发布!大语言模型新选择 | 开源日报 No.251
meta-llama/llama Stars: 53.0k License: NOASSERTION llama 是用于 Llama 模型推理的代码。 提供了预训练和微调的 Llama 语言模型,参数范围从 7B 到 70B。可以通过下载脚本获取模型权重和 tokenizer。支持在本地快速运行推理,并提供不同规格的模型并…...

SpringBoot 具体是做什么的?
Spring Boot是一个用于构建独立的、生产级别的、基于Spring框架的应用程序的开源框架。它的目标是简化Spring应用程序的开发和部署过程,通过提供一种快速、便捷的方式来创建Spring应用程序,同时保持Spring的灵活性和强大特性。 1. 简化Spring应用程序开…...

Debian常用命令
Debian是一个开源的Unix-like操作系统,提供了大量的软件包供用户安装和使用。在Debian系统中,命令行界面(CLI)是用户与系统进行交互的重要工具。以下是Debian中一些常用的命令及其详细解释: 文件和目录操作命令&#x…...

常见的前端框架
常用的前端框架有以下几种: 模型 React:由Facebook开发的一款前端框架,采用虚拟DOM的概念,可高效地更新页面。Vue.js:一款轻量级的前端框架,易学易用,支持组件化开发和双向数据绑定。AngularJ…...