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

问题背景

最近接手了一个骨灰级的项目,然而在项目中遇到了一个普遍的挑战:由于公司采用 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,实现了请求的全链路追踪(包括异步请求,确保请求连贯性),从而提高了系统问题排查和诊断的效率。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Sam Altman 在斯坦福大学演讲的 10 个要点

最近在斯坦福大学举行的问答环节中&#xff0c;OpenAI 富有远见的首席执行官 Sam Altman 分享了关于人工智能的未来及其对社会的潜在影响的宝贵见解。作为 GPT 和 DALL-E 等突破性人工智能模型背后的研究组织的联合创始人&#xff0c;Altman 的观点对于企业家、研究人员以及任何…

网盘应用:桌面端界面欣赏,这个赛道容不下小玩家。

网盘&#xff08;Cloud Storage&#xff09;是一种云存储服务&#xff0c;允许用户在互联网上存储、管理和共享文件。它提供了一个在线的虚拟硬盘&#xff0c;用户可以通过网络将文件上传到云端&#xff0c;并随时随地访问和管理这些文件。 阿里云盘

Redis集群.md

Redis集群 本章是基于 CentOS7 下的 Redis 集群教程&#xff0c;包括&#xff1a; 单机安装RedisRedis主从Redis分片集群 1.单机安装Redis 首先需要安装Redis所需要的依赖&#xff1a; yum install -y gcc tcl然后将课前资料提供的Redis安装包上传到虚拟机的任意目录&#xf…

为什么你创业总是失败?2024普通人如何创业?2024创业赛道!2024创业新风口!2024创业方向!2024普通人的机会!

为什么你做项目老是不赚钱&#xff0c;是你不够努力吗&#xff1f;是你运气不好吗&#xff1f; 如果都不是&#xff01;那一定是你的思维逻辑出了问题&#xff01; 先想一想你以前做的项目&#xff0c;有没有哪个符合以下条件&#xff1a;对客户有价值、寻找客源成本在可接受…

智慧文旅赋能旅游服务升级:以科技创新驱动行业变革,打造智慧化、个性化、高效化的旅游新体验,满足游客日益增长的多元化需求

目录 一、引言 二、智慧文旅的概念与内涵 三、智慧文旅在旅游服务升级中的应用 1、智慧旅游服务平台建设 2、智慧景区管理 3、智慧旅游营销 四、智慧文旅推动旅游行业变革的案例分析 案例一&#xff1a;某智慧旅游城市建设项目 案例二&#xff1a;某景区智慧化改造项目…

Linux进程状态与优先级

本篇详细的讲解了 Linux 中进程会出现的各种状态&#xff0c;以及出现这些状态的原因&#xff0c;其中进程的阻塞、挂起和运行就是进程状态的体现。接着科普了一下进程的切换&#xff0c;然后讲解了进程的优先级&#xff0c;以及如何调整进程的优先级。最后对进程的特点进行了总…

C语言自定义类型中结构体、结构体声明、结构体自引用、结构体变量的定义和初始化、结构体内存对齐,结构体传参,位段等的介绍

文章目录 前言一、结构体二、 结构体声明三、 特殊的声明----匿名结构体类型四、 结构体的自引用&#xff08;1&#xff09;数据结构&#xff08;2&#xff09;结构体的自引用 五、 结构体变量的定义和初始化六、 结构体内存对齐1. 结构体的对齐规则&#xff08;1&#xff09;结…

华为eNSP综合实验-网络地址转换

实验完成之后,在AR1的g0/0/1接口抓包,查看地址转换 实现私网pc访问公网pc 实验命令展示 SW1: vlan batch 12 #创建vlan interface e0/0/1 #进入接口配置vlan端口 port link-type access port default vlan 12 q interface e0/0/2 #进入接口配置vlan端口 port link-type ac…

iphone忘记锁屏密码怎么解锁?这些解锁方法你必须知道!

在使用iPhone的过程中经常会遇到很多问题&#xff0c;比如忘记了iPhone的锁屏密码。面对这样的情况&#xff0c;许多用户可能会感到手足无措。别担心&#xff0c;本文将为您详细介绍iPhone忘记锁屏密码的解锁方法&#xff0c;让您轻松解决这一烦恼。 一、使用iTunes备份恢复 如…

盘点一下4种常见的微信的广告类型,在微信上打广告要花多少钱?

微信是一款社交媒体应用&#xff0c;集Facebook、Instagram和Snapchat的功能于一身。该应用拥有超过12亿的月活跃用户&#xff0c;其中约7亿为日活跃用户。由于其在中国网民中的成功和广泛的通信工具&#xff0c;微信是推广您业务的绝佳平台。 在这篇博客文章中&#xff0c;我…

Centos7完全卸载与安装mysql8.0+

Centos7卸载与安装mysql8.0 1、mysql8完全卸载2、mysql8安装 1、mysql8完全卸载 sudo systemctl stop mysqldsudo yum remove mysql-community-serversudo rm -rf /var/lib/mysql sudo rm -rf /etc/my.cnfsudo groupdel mysql sudo userdel mysqlsudo rm -rf /var/log/mysql s…

nginx代理原理(端口复用)探究

前言&#xff1a;对于一些常用的插件&#xff0c;我们应该学会如何使用。同时&#xff0c;其实现原理也要进行深究&#xff0c;可以为其他的项目开发做借鉴。 探究方案&#xff1a; 一、发布两个不同的服务&#xff0c;这两个服务的端口不致 二、配置nginx&#xff0c;让这两…

3W 3KVDC 隔离单、双输出 DC/DC 电源模块——TPH-3W 系列

TPH-3W系列是一款3W,单、双输出隔离电源模块&#xff0c;特别适合板上只有一种电压而要求有正负电源的场合&#xff0c;工业级温度范围–40℃到105℃&#xff0c;在此温度范围内都可以稳定输出2W&#xff0c;并且效率非常高&#xff0c;高达86%&#xff0c;温升非常低&#xff…

剁手党必看——转转红包使用规则与最优组合计算全解析

​ 1、省钱攻略基础之“了解平台红包使用规则” 2、举个栗子 3、最优红包组合计算方法进化过程 3.1、初代“笛卡尔乘积”版 3.2、二代“边算边比较Map聚合”版 3.3、三代“边算边比较数组索引定位”版 4、总结 1、省钱攻略基础之“了解平台红包使用规则” 规则一&#x…

浙大×移动云,携手点亮AI新时代

近年来&#xff0c;中国移动依托强大的算网资源优势&#xff0c;围绕大模型训练、推理和应用三大场景&#xff0c;打造了一站式智算产品体系。该体系旨在为客户提供覆盖资源、平台、应用的AI全链路服务。目前&#xff0c;一站式智算产品体系已在浙江大学智算中心和许昌中原智算…

后端常用技能:基于easy-poi实现excel一对多、多对多导入导出【附带源码】

0. 引言 在业务系统开发中&#xff0c;我们经常遇到excel导入导出的业务场景&#xff0c;普通的excel导入导出我们可以利用 apache poi、jxl以及阿里开源的easyexcel来实现&#xff0c;特别easyexcel更是将excel的导入导出极大简化&#xff0c;但是对于一些负载的表格形式&…

新能源汽车热管理方案现状与未来发展趋势

前言 新能源汽车的热管理技术在提高电池寿命、提高能量利用效率和确保车辆运行安全方面起着至关重要的作用。 一 新能源汽车热管理技术方案 1 电池热管理系统 电池热管理系统是电动汽车中至关重要的一部分&#xff0c;它通过冷却液循环、加热器、散热片等方式控制电池温度&…

【解决Android Studio】cmake报错找不到vulkan包

1 报错信息 CMake Error at D:/Android/project/cmake/3.10.2.4988404/share/cmake-3.10/Modules/FindPackageHandleStandardArgs.cmake:137 (message): Could NOT find Vulkan (missing: Vulkan_LIBRARY) Call Stack (most recent call first): 2. 错误原因 minSdk版本不对&am…

【Linux网络编程】DNS、ICMP、NAT技术、代理服务器+网络通信各层协议总结

DNS、ICMP、NAT技术、代理服务器网络通信总结 1.DNS2.ICMP协议2.1ping命令2.2traceroute命令 3.NAT技术4.NAT和代理服务器5.网线通信各层协议总结 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&…

uniapp 小程序图片懒加载组件 ImageLazyLoad

预览图 组件【ImageLazyLoad】代码 <template><viewclass"image-lazy-load":style"{opacity: opacity,borderRadius: borderRadius rpx,background: background,transition: opacity ${time / 1000}s ease-in-out,}":class"image-lazy-loa…