Trace Sql:打通全链路日志最后一里路

背景介绍

笔者之前遇到一个Bug,简单描述就是有一字段由笔者和同事共同维护,最后发现该字段与实际情况不符。绝大多数生产环境一定不会打印SQL语句,且我们各自负责的模块业务没有互相交叉理解,因此比较难判断问题根源。

后面我使用阿里云的SQL洞察,获取到该条记录的更新历史,才还笔者清白。原因是同事后续的操作将字段更新错误。当时还好两个操作时间有一定的时间差,才比较断定。所以当时我就想如果SQL语句也能携带上TraceId就方便回溯,铁证如山。

解决思路

恒等条件

笔者最开始想到的办法是使用WHERE子句 ,比如查询的时候我在WHERE的最后加一个恒等表达式,形如:#{traceId} = #{traceId}

sql 复制代码
SELECT
    id, age, name 
FROM 
    tbl_user
WHERE
    id = 1 
AND 
    '004b0307-2d71-466e-aedf-cb8009893881' = '004b0307-2d71-466e-aedf-cb8009893881';

这样我们就可以把链路中的TraceId带入SQL,可是这么搞局限性非常大:

  • SELECT UPDATE DELET都可以带WHERE字句,但是INSERT不可以
  • 需要处理的case复杂,比如子句前需不需要AND,比如有的SQL是ORDER或者LIMIT子句结尾,那你还要找到WHERE子句的位置然后添加恒等式,等等
  • 即使各种case全部都能覆盖,但是为了加上trace浪费挺多资源

综上所述,笔者最终放弃了这个方案。

SQL注释

我们必须要找到一种足够简单,不需要应对各种复杂case,代码好写好维护的方案。于是我想到可以把SQL都带上注释,然后注释里带上Trace信息。

sql 复制代码
/*X-ld:0002a4de-400f-40ae-ba46-9402f0eb46f4*/
SELECT
    id, age, name
FROM
    tbl_user
WHERE
    id = 1;

这个方案有很多好处:

  • 支持所有类型的SQL
  • 足够简单,不需要分析原SQL,仅仅只需要给原来的语句头部加上注释信息
  • 代码好写,基本上也没啥资源消耗

具体实现

使用MyBatis的Interceptor拦截StatementHandler,修改SQL。 MyBatis 允许开发者通过实现 org.apache.ibatis.plugin.Interceptor接口,拦截四大对象之一:

  • ​Executor​
  • ​ParameterHandler​
  • ​ResultSetHandler​
  • ​StatementHandler​ ← ​我们重点要用这个!​

我们要拦截的是:​​StatementHandler.prepare()方法​ ​,在这个方法执行之前或之后,可以拿到​​即将发送到数据库的原始 SQL​ ​,然后我们​​在 SQL 的头部或尾部拼接上 /* traceId = xxx */这种注释​​(笔者最终选择头部,因为一眼就能看到)。

java 复制代码
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;

import java.lang.reflect.Field;
import java.sql.Connection;

@Intercepts({
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
})
public class TraceSqlInterceptor implements Interceptor {

    /**
     * 这里没叫traceId是因为阿里云ARMS已经使用该名词
     * 换成别的已示区分
     */
    private static final String TRACE_NAME = "X-Id:";
    

    /**
     * @see BoundSql#sql
     */
    private static final String SQL = "sql";
    

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        if (target instanceof StatementHandler) {
            String sqlMark = buildMark();
            if (StringUtils.isNotBlank(sqlMark)) {
                StatementHandler stat = (StatementHandler) target;
                BoundSql boundSql = stat.getBoundSql();
                /* 原始SQL语句 */
                String sql = boundSql.getSql();
                setField(boundSql, sqlMark + sql);
            }
        }
        return invocation.proceed();
    }


    private void setField(BoundSql boundSql, String newSql) {
        try {
            Field field = BoundSql.class.getDeclaredField(SQL);
            field.setAccessible(true);
            field.set(boundSql, newSql);
        } catch (Exception e) {
            /* 忽略,因为失败不会有任何影响,无非SQL无法成功着色 */
        }
    }
    

    /**
     * 构造标记
     */
    private static String buildMark() 
        /* 这一行是获取业务系统的链路Id */
        String traceId = TraceContext.getTraceId();
        
        if (StringUtils.isBlank(traceId)) {
            return StringUtils.EMPTY;
        }

        return StringPool.SLASH
            + StringPool.ASTERISK
            + StringPool.SPACE
            + TRACE_NAME
            + traceId
            + StringPool.SPACE
            + StringPool.ASTERISK
            + StringPool.SLASH;
    }

}

同时不要忘了往Spring容器中注入拦截器实例

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration(proxyBeanMethods = false)
public class TraceSqlConfiguration {


    @Bean
    public TraceSqlInterceptor traceSqlInterceptor() {
        return new TraceSqlInterceptor();
    }
    

}

看看成果

可以看到阿里云ARMS采集到的SQL已经全部带上了业务系统的TraceId,目的达成

相关推荐
KING-WU5128 分钟前
Linux 工具之 yum、vim、gcc
linux·运维·服务器·后端
蜗牛互联网9 分钟前
GPT-6.1 Sol迁移指南:从token单价转向每任务成本门禁
java·人工智能·后端·gpt
IT_陈寒11 分钟前
Redis键过期失效?这个坑我踩得明明白白
前端·人工智能·后端
蜗牛互联网15 分钟前
HSTU在Dynamo-Triton中的AOTI与KV缓存验收方法
java·人工智能·后端·缓存
青山木18 分钟前
秒杀系统设计(一):需求拆解与流量治理
java·数据库·redis·后端·架构
沫璃染墨31 分钟前
《从零入门Linux系统篇(五十三):线程篇·六——线程互斥详解:从并发问题到mutex互斥锁》
linux·运维·服务器·开发语言·后端·架构·系统架构
魏码不凡1 小时前
Go 编译报错:undefined: webp.Encode 问题排查与解决
开发语言·后端·golang
tachibana23 小时前
与其他语言相比,使用 Go 有什么好处?
开发语言·后端·golang·go
蜗牛互联网12 小时前
WSL Containers GA:本地AI容器的生命周期、网络与治理验收
java·网络·人工智能·后端
Zhou14113613 小时前
SpringBoot_02_自动配置原理
java·spring boot·后端