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

news2024/12/26 23:43:39

问题背景

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

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

elk 对这种格式采集并不太友好,所以打算重新写一个日志log类。

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

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

<?php

namespace app\common\library;

use think\App;
use think\Request;

class YeeLog
{
    protected $config = [
        'time_format' => ' c ',
        'single'      => false,
        'file_size'   => 2097152,
        'path'        => LOG_PATH,
        'apart_level' => [],
        'max_files'   => 0,
        'json'        => true,
        'trace_id'  => null,
        'log_format'  => 'json'
    ];

    // 实例化并传入参数
    public function __construct($config = [])
    {
        if (is_array($config)) {
            $this->config = array_merge($this->config, $config);
        }
        $this->config['trace_id'] = $_SERVER['traceId'] ?? "";
    }

    /**
     * 日志写入接口
     * @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['log_format'] == 'json') {
                        $msg = json_encode($msg, 320);
                    } else {
                        $msg = var_export($msg, true);
                    }
                }

                $info[$type][] = $this->config['json'] ? $msg : $this->getCurrentTime() . ' [ ' . $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';
    }

    /**
     * 获取当前时间戳
     * @return false|string
     */
    protected function getCurrentTime()
    {
        $customTimestamp = trim(config('log.timestamp'));
        return empty($customTimestamp) ? date($this->config['time_format']) : date($customTimestamp);
    }

    /**
     * 日志写入
     * @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['time'] = $this->getCurrentTime();

        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($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['time'];
            unset($info['time']);

            $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 = [
            '[trace_id]'      => $this->config['trace_id'],
            '[request_ip]'      => getIp(),
            '[method]'          => $request->method(),
            '[domain]'          => $request->domain(),
            '[uri]'             => $request->url(),
            '[param]'           => json_encode($request->post(), 320),
            '[x-forwarded-for]' => $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '--',
            '[http_x_real_ip]'  => $_SERVER['HTTP_X_REAL_IP'] ?? '--',
            '[remote_addr]'     => $_SERVER['REMOTE_ADDR'] ?? '--'
        ];

        if ($this->config['json']) {
            $info    = $requestInfo + $info;
            $println = "---------------------------------------------------------------\r\n";
            $msg     = sprintf("%s%s ", $println, $this->getCurrentTime());
            foreach ($info as $key => $value) {
                $msg .= sprintf("%s: %s ", $key, $value);
            }
            return $msg . "\r\n";
        }

        array_unshift($info, "---------------------------------------------------------------\r\n{$info['time']} [ hit ] {$this->config['trace_id']} {$requestInfo['ip']} {$requestInfo['method']} {$requestInfo['host']}{$requestInfo['uri']}");
        unset($info['time']);

        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。

<?php

namespace 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);
}

结论

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在时间同步应用上节省大量时间!德思特GNSS模拟器是怎么做到的?

​ 作者介绍 德思特Safran GNSS模拟器是一款综合解决方案&#xff0c;专为精确的PNT&#xff08;位置、导航和时间&#xff09;仿真与测试设计。它超越了传统GNSS定位导航仿真&#xff0c;也能提供极高的授时精度。 这款模拟器对于评估和提升GNSS接收机及同步系统的整体性能至…

前端JS必用工具【js-tool-big-box】,验证是否是Unicode字符,获取一个字符串的字节长度,以及新增发送JSONP跨域请求的方法

js-tool-big-box&#xff0c;目前已经收集到了用户需求&#xff0c;希望可以添加一些公用方法&#xff0c;我觉得这很好&#xff0c;我们一起把这个前端通用工具做大一些&#xff0c;帮助更多的小伙伴少些util代码&#xff0c;更多的关注于自己的业务开发&#xff0c;真是不错。…

OpenCV与AI深度学习 | 如何使用YOLOv9检测图片和视频中的目标

本文来源公众号“OpenCV与AI深度学习”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;如何使用YOLOv9检测图片和视频中的目标 1 介绍 在之前的文章中&#xff0c;我们探索了使用 YOLOv8 进行对象检测。现在&#xff0c;我们很高兴…

【仪酷LabVIEW AI工具包案例】使用LabVIEW AI工具包+YOLOv5结合Dobot机械臂实现智能垃圾分类

‍‍&#x1f3e1;博客主页&#xff1a; virobotics(仪酷智能)&#xff1a;LabVIEW深度学习、人工智能博主 &#x1f384;所属专栏&#xff1a;『仪酷LabVIEW AI工具包案例』 &#x1f4d1;上期文章&#xff1a;『【YOLOv9】实战二&#xff1a;手把手教你使用TensorRT实现YOLOv…

Spring - 7 ( 13000 字 Spring 入门级教程 )

一&#xff1a;Spring Boot 日志 1.1 日志概述 日志对我们来说并不陌生&#xff0c;我们可以通过打印日志来发现和定位问题, 或者根据日志来分析程序的运行过程&#xff0c;但随着项目的复杂度提升, 我们对日志的打印也有了更高的需求, 而不仅仅是定位排查问题 比如有时需要…

关于 c++ 中字符串 string 及 常量字符串的换行与使用

&#xff08;1&#xff09;例如 cout << " ddddddddddddddddddd" 。当输出字符太长&#xff0c;就需要换行。疑问是如何写代码&#xff0c;才可以保证源代码中的字符串换行被正确编译呢&#xff1f;测试一下&#xff0c;如下图可见&#xff0c;如此换行&#x…

STM32:GPIO输入输出

文章目录 1、GPIO介绍1.1 GPIO的基本结构1.1 GPIO的位结构 2、 GPIO工作模式3、GPIO标准外设库接口函数3.1 RCC接口函数3.2 GPIO接口函数3.2.1 GPIO的读取函数3.2.1 GPIO的写入函数 4、GPIO的初始化 1、GPIO介绍 GPIO&#xff08;General Purpose Input Output&#xff09;通用…

【MQTT】服务端、客户端工具使用记录

目录 一、服务端 1.1 下载 1.2 相关命令 &#xff08;1&#xff09;启动 &#xff08;2&#xff09;关闭 &#xff08;3&#xff09;修改用户名和密码 1.3 后台管理 &#xff08;1&#xff09;MQTT配置 &#xff08;2&#xff09;集群概览 &#xff08;3&#xff09;…

场外期权个股怎么对冲?

今天期权懂带你了解场外期权个股怎么对冲&#xff1f;场外个股期权是一种在非交易所市场进行的期权交易&#xff0c;它允许投资者针对特定的股票获得未来买入或卖出的权利。 场外期权个股怎么对冲&#xff1f; 持有相反方向的期权&#xff1a;这是最直接的对冲方法&#xff0c…

今晚 19:00 | 从这两个问题入手,带你了解数据要素相关税务问题

五一假期已经结束&#xff0c;返工后当然是继续劳动啦~数据要素系列直播《星光对话》第三期也将在今晚19:00&#xff0c;继续跟大家见面。 本期直播&#xff0c;依然由 星光数智咨询总监 刘靖 主讲&#xff0c;带来&#xff1a;《数据要素相关税务问题解读》。 主要围绕两个问题…

怎么快速分享视频文件?用二维码看视频的方法

怎样不通过传输下载分享视频内容呢&#xff1f;以前分享视频内容&#xff0c;大多会通过微信、QQ、邮箱、网盘等形式来传递。但是这种方式需要下载后才可以观看&#xff0c;不仅占用手机内存&#xff0c;而且效率也比较低&#xff0c;所以现在很多人会采用视频生成二维码的方式…

为 Flutter 应用设置主题:ThemeData 和 ColorScheme 指南

在媒体和其他来源中有许多关于这个主题的文章&#xff0c;那么这篇文章的必要性是什么&#xff1f; 在本文中&#xff0c;我计划仅关注 ThemeData 小部件的关键点以及我的开发经验中最常用的参数&#xff0c;并且您将获得有关每个参数如何对您的应用程序执行操作的简要说明。 …

LeetCode70:爬楼梯

题目描述 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 解题思想 1.确定dp数组以及下标的含义 dp[i]&#xff1a; 爬到第i层楼梯&#xff0c;有dp[i]种方法 2.确定递推公式 从dp[i]的定义可以…

旋转测径仪的常见故障和排除方法

关键字: 旋转测径仪,测径仪常见故障,测径仪故障排除方法,测径仪维护,测径仪较零 点击 “开始测量”按钮时提示“初始化失败&#xff01;”无法进行测量。 ◆ 检查控制柜面板“工作”指示灯&#xff08;绿&#xff09;是否点亮&#xff1b; ◆ 最小化软件窗口&#xff0c;查看…

解密某游戏的数据加密

前言 最近有个兄弟通过我的视频号加我&#xff0c;咨询能否将这个dubo游戏游戏开始前就将数据拿到从而进行押注&#xff0c;于是通过抓包工具测试了下&#xff0c;发现数据有时候是明文&#xff0c;有时候确实密文&#xff0c;大致看了下有这几种加密&#xff1a;Md5aes、Md5&a…

用Pycharm对图片中表格进行文字识别,并导出到xlsx文件中

需要使用到百度API对图片文字进行识别 在百度智能云官网中注册一个账号百度智能云-登录https://login.bce.baidu.com/ 之后在管理中心里创建应用 创建完成后会得到&#xff1a; 记下一下AppID&API Key&Secret Key这三个值&#xff0c;调用接口时使用。 示例图片&…

字符编码(十六进制)

题目描述 假设一个简易的变长编码规则XUTF:每个字符有一个唯一编号值 unicodeVal(如汉字“华”的编号十进制值是21326),使用1~6个字节进行编码,编码规则如下: 编码格式: 编号值范围编码后字节长度n二进制格式(x 表示有效位,其它为固定位)[0, 2^7)1字节1xxxxxxx[2^7,…

数智算网,链启未来 | 算力网络子链诚邀各方加入

4月28日&#xff0c;在中国移动算力网络大会期间&#xff0c;由中国移动集团主办&#xff0c;中国移动研究院和云能力中心联合承办的“数智算网&#xff0c;链启未来”共链行动算力网络专场会议成功召开。中国移动研究院副院长段晓东&#xff0c;中国移动集团首席专家、云能力中…

Tomcat、MySQL、Redis最大支持说明

文章目录 一、Tomcat二、MySQL三、Redis1、最大连接数2、TPS、QPS3、key和value最大支持 一、Tomcat 查看SpringBoot内置Tomcat的源码&#xff0c;如下&#xff1a; 主要就是看抽象类AbstractEndpoint&#xff0c;可以看到默认的核心线程数10&#xff0c;最大线程数200 通过…

百元挂耳式耳机哪款好?五款高品质一流机型不容错过

开放式耳机以其独特的不入耳设计&#xff0c;大大提升了佩戴的舒适度。相较于传统的入耳式耳机&#xff0c;它巧妙地避免了对耳朵的压迫&#xff0c;降低了中耳炎等潜在风险。不仅如此&#xff0c;开放式耳机还能让你保持对周边声音的灵敏度&#xff0c;无论是户外跑步还是骑行…