You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Micrometer Observation:如何便捷为Span添加参数元数据?

Spring Observability + Micrometer 高效捕获Span参数元数据

问题描述

我正在使用Spring Observability和Micrometer,尝试为Span捕获参数键值对等元数据。我了解可以使用@Observed注解,但它似乎仅能捕获lowCardinalityValues,且不确定是否可添加参数值。目前我找到的捕获参数值的方法是手动将服务方法的核心逻辑用Observation包裹,代码如下:

public List<Recipes> findRecipes(
            String type,
            List<UUID> ownerIds,
            Boolean includeOwner,
            Boolean includeTested,
            String sort,
            Integer limit
) {
    
        KeyValues lowCardinalityKeyValues = KeyValues.of(
                "type", String.valueOf(type),
                "includeOwner", String.valueOf(includeOwner),
                "includeTested", String.valueOf(includeTested),
                "sort", String.valueOf(sort),
                "limit", String.valueOf(limit)
        );

        return Observation.createNotStarted("test", observationRegistry)
                .contextualName("findRecipes")
                .lowCardinalityKeyValues(lowCardinalityKeyValues)
                .highCardinalityKeyValue("ownerIds", String.valueOf(ownerIds))
                .observe(() -> recipeDao.findRecipes(
                        type,
                        ownerIds,
                        includeTested,
                        sort,
                        limit
                ));
    }

该方法可行但手动工作量大。此前使用Sleuth的@SpanTag可实现该功能,但现在已无法使用,请问是否有更优方案?


更优解决方案

方案1:利用@Observed的SpEL表达式直接绑定参数

Micrometer 1.10+版本的@Observed注解支持通过SpEL表达式直接绑定方法参数到低/高基数键值对,无需手动包裹Observation。

修改方法,直接在@Observed中配置参数映射:

@Observed(
    name = "test",
    contextualName = "findRecipes",
    lowCardinalityKeyValues = {
        "type", "#type",
        "includeOwner", "#includeOwner.toString()",
        "includeTested", "#includeTested.toString()",
        "sort", "#sort",
        "limit", "#limit.toString()"
    },
    highCardinalityKeyValues = {
        "ownerIds", "#ownerIds.toString()"
    }
)
public List<Recipes> findRecipes(
        String type,
        List<UUID> ownerIds,
        Boolean includeOwner,
        Boolean includeTested,
        String sort,
        Integer limit
) {
    return recipeDao.findRecipes(type, ownerIds, includeTested, sort, limit);
}

这种方式保留了@Observed的声明式风格,无需编写冗余的Observation初始化代码。

方案2:全局自定义ObservationConvention实现自动参数捕获

创建全局的ObservationConvention组件,自动为所有标记@Observed的方法提取参数并分类为低/高基数键值对,完全无需每个方法手动配置。

自定义Convention代码

@Component
public class MethodParameterObservationConvention implements ObservationConvention<MethodObservationContext> {

    @Override
    public boolean supportsContext(Observation.Context context) {
        return context instanceof MethodObservationContext;
    }

    @Override
    public KeyValues getLowCardinalityKeyValues(MethodObservationContext context) {
        Method method = context.getMethod();
        Object[] args = context.getArguments();
        Parameter[] parameters = method.getParameters();
        
        KeyValues.Builder builder = KeyValues.builder();
        for (int i = 0; i < parameters.length; i++) {
            Parameter param = parameters[i];
            Object value = args[i];
            // 将基础类型、字符串等归为低基数参数
            if (isLowCardinalityType(value.getClass())) {
                builder.add(param.getName(), String.valueOf(value));
            }
        }
        return builder.build();
    }

    @Override
    public KeyValues getHighCardinalityKeyValues(MethodObservationContext context) {
        Method method = context.getMethod();
        Object[] args = context.getArguments();
        Parameter[] parameters = method.getParameters();
        
        KeyValues.Builder builder = KeyValues.builder();
        for (int i = 0; i < parameters.length; i++) {
            Parameter param = parameters[i];
            Object value = args[i];
            // 将集合、复杂对象等归为高基数参数
            if (!isLowCardinalityType(value.getClass())) {
                builder.add(param.getName(), String.valueOf(value));
            }
        }
        return builder.build();
    }

    private boolean isLowCardinalityType(Class<?> clazz) {
        return clazz.isPrimitive() || clazz.equals(String.class) || clazz.equals(Boolean.class) 
               || Number.class.isAssignableFrom(clazz);
    }
}

Spring会自动扫描并注册这个Convention,之后只要方法标记了@Observed,就会自动提取参数并添加到Span元数据中。

方案3:结合AOP与SpanProcessor实现全局参数注入

如果需要更灵活的控制(比如自定义参数过滤、格式转换),可以通过AOP拦截方法调用,结合Micrometer Tracing的SpanProcessor自动将参数注入Span。

1. AOP拦截器存储参数

@Aspect
@Component
public class MethodParameterAspect {

    @Before("@annotation(org.springframework.boot.actuate.autoconfigure.observation.Observed)")
    public void beforeMethod(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Parameter[] parameters = signature.getMethod().getParameters();
        Object[] args = joinPoint.getArgs();
        
        Map<String, Object> params = new HashMap<>();
        for (int i = 0; i < parameters.length; i++) {
            params.put(parameters[i].getName(), args[i]);
        }
        MethodParameterHolder.setParams(params);
    }
}

// 线程本地存储参数的辅助类
class MethodParameterHolder {
    private static final ThreadLocal<Map<String, Object>> PARAMS = new ThreadLocal<>();

    public static void setParams(Map<String, Object> params) {
        PARAMS.set(params);
    }

    public static Map<String, Object> getParams() {
        return PARAMS.get();
    }

    public static void clear() {
        PARAMS.remove();
    }
}

2. 自定义SpanProcessor注入参数

@Component
public class ParameterSpanProcessor implements SpanProcessor {

    @Override
    public void onStart(ReadOnlySpan span, TraceContext context) {
        Map<String, Object> params = MethodParameterHolder.getParams();
        if (params != null) {
            params.forEach((key, value) -> {
                if (isLowCardinalityType(value.getClass())) {
                    // 低基数参数添加为Span Tag
                    span.tag(key, String.valueOf(value));
                } else {
                    // 高基数参数可添加为Span Event或高基数Tag(根据追踪系统支持)
                    span.addEvent(key, String.valueOf(value));
                }
            });
            MethodParameterHolder.clear();
        }
    }

    @Override
    public boolean isStartRequired() {
        return true;
    }

    // 实现其他SpanProcessor方法(默认返回即可)
    @Override
    public void onEnd(ReadOnlySpan span, TraceContext context) {}
    @Override
    public boolean isEndRequired() { return false; }

    private boolean isLowCardinalityType(Class<?> clazz) {
        return clazz.isPrimitive() || clazz.equals(String.class) || clazz.equals(Boolean.class) 
               || Number.class.isAssignableFrom(clazz);
    }
}

内容的提问来源于stack exchange,提问作者fatjojo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 16:55:10