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
相关产品推荐
相关产品推荐

