This is the fourth day of my participation in the November Gwen Challenge. Check out the details: The last Gwen Challenge 2021

This background also comes from the functional requirements of a recent project, which is sorted out in this paper to explain and extend the monitoring burying point of MicroMeter implementation.

Micrometer

Micrometer is a library of measurement tools for JVM-based applications. It provides a simple facade (similar to SLF4J) for the detection clients of the most popular monitoring systems. Micrometer is designed to provide maximum portability of metrics work for projects with minimal overhead (mainly due to the design of its facade patterns, as well as others like OpenTracing, However, there are fundamental differences in design patterns between OpenTracing and SLF4J or Micrometer. This article does not focus on the basics or concepts of Micrometer. More information can be found in the official Micrometer documentation.

background

Here’s the project context: We want to provide an SDK or starter that uses as simple an API or annotations as possible to get the core method buried. In fact, Micrometer itself has provided similar annotations like @timer, but it lacks tag and only has statistics. The use of API-based method supports tag capability, but it is very intrusive to business code. Therefore, based on native annotations and apis is not sufficient for current project needs, so it needs to be extended.

According to the characteristics of the project, our general appeal for expanding Micrometer is as follows:

  • Support for tag capabilities in annotations
  • Support for adding tag dimensions during method execution

These two appeals seem simple enough, but there are several issues that must be addressed:

  • Tags are easy to support, but the values in a tag are relatively fixed. If you want to support dynamic injection of a tag value, you need to support binding parameters to the value
  • The annotation-based approach is basically equivalent to binding AOP. How does AOP handle this application problem
  • Threadlocal is a threadLocal binding implementation. How to solve the problem of cross-thread transfer

The following is a practical introduction to the Micrometer extension.

Micrometer monitoring framework extension practices

Let’s tackle these problems one by one. The first is the ability to support tags in annotations, which is not very technical. The way to do this is to abandon Micrometer native annotations and use custom annotations. As follows:

/ * * *@ClassName MeterMetrics
 * @Description
 *
 *  @MeterMetrics(name = "xx", type = MeterMetricsEnum.COUNT, tags = {@MeterTag(key = "key", value = "value"),... }) * public void test() {... } * *@Author glmapper
 * @Date2021/11/4 now *@Version1.0 * /
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MeterMetrics {

    /**
     * describe tags name
     * @return* /
    String key(a) default "";

    /**
     * describe tags value
     * @return* /
    String value(a) default "";
}
Copy the code

Now that the annotation is provided, we need to deal with it. Here is a faceted implementation of the annotation.


@Pointcut("@annotation(com.glmapper.bridge.boot.anno.MeterMetrics)")
public void meterMetricsCut(a) {}@Around("meterMetricsCut()")
public Object meterMetricsAround(ProceedingJoinPoint joinPoint) throws Throwable {
    / /... Parse the tag on the annotation and report it
}

@AfterThrowing(value = "meterMetricsCut()", throwing = "ex")
public void meterMetricsThrowing(JoinPoint joinPoint, Throwable ex) throws Throwable {
   / /... Parse the tag on the annotation and report it
}
Copy the code

This is available for the initial model, but is far from practical and usable. Here are the extension details.

Support SPEL expression, tag value binding parameters

For example, expect a value in a parameter to be the value of a tag like this:

@GetMapping("/resp")
@MeterMetrics(key = "app", value = "#testModel.getName()")
public String getResp(TestModel testModel){
    return "SUCCESS";
}
Copy the code

In this code, #testModel.getName() is an SPEL expression designed to bind the key-value of a custom annotation to a method parameter. The core processing of the corresponding section method is roughly as follows:

private final SpelExpressionParser parser = new SpelExpressionParser();
private final DefaultParameterNameDiscoverer pmDiscoverer = new DefaultParameterNameDiscoverer();
/** * Support to extract the corresponding value */ from the parameter through the SPEL expression
public String getSpelContent(String spelKey, JoinPoint pjp) {
    Expression expression = parser.parseExpression(spelKey);
    EvaluationContext context = new StandardEvaluationContext();
    MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
    Object[] args = pjp.getArgs();
    String[] paramNames = pmDiscoverer.getParameterNames(methodSignature.getMethod());
    for(int i = 0 ; i < args.length ; i++) {
        context.setVariable(paramNames[i], args[i]);
    }
    // Note that there may be an NPE
    return expression.getValue(context).toString();
}
Copy the code

There are a few caveats here, first that the TestModel above must provide constructors (determined by spEL’s own mechanism), and second that the parsing process should not have any impact on the main business process, meaning that when exceptions are raised, they need to be eaten internally and not spread.

So here we have resolved the annotation support for tag and binding parameters. The following is the ability to add a tag inside a method.

Allows tag information to be added to methods

For example, your parameter is a token string, but when checking the token, you want to know some information after the token is resolved, such as user information, permission information, etc. So data that is only available within a method needs to be channelized when it needs to be added to a tag.

Here is an example of how to implement pass-through based on ThreadLocal:

public class TokenMetricHolder {
    // TAG_CACHE
    private static ThreadLocal<MetricTag> TAG_CACHE = new ThreadLocal<>();
    public static void metricToken(TokenModel tokenModel, String from) {
        try {
            MetricTag.MetricTagBuilder builder = MetricTag.builder();
            / /... Add properties for Builder
            MetricTag metricTag = builder.build();
            TAG_CACHE.set(metricTag);
        } catch (Exception ex)  {
            // do not block main process
            LOGGER.error("error to fill metric tags.", ex); }}// Pay attention to remove to avoid memory leak risk
    public static MetricTag getAndRemove(a) {
        MetricTag metricTag = TAG_CACHE.get();
        TENANT_CACHE.remove();
        return metricTag;
    }

    @Data
    @Builder
    public static class MetricTag {
        // Here are some tag metrics to collect, such as userName
        private String userName;
    }
Copy the code

With the TAG_CACHE maintained internally by TokenMetricHolder, the tag information populated internally by methods in a request context can be easily retrieved in AOP, thus enriching the metric statistical dimension.

Resolve the this reference problem

With AOP, when another method is called from within a method (b is called from a method), AOP on B is invalidated because the current object of B refers to this rather than a proxy object.

public void a(a){
    b();
}
public voidb(a){/ /... }
Copy the code

There are plenty of explanations on the Internet, but here we are:


/**
 * com.glmapper.bridge.boot.holder#ProxyHolder
 * get current proxy object, if get null, return default value with specified
 *
 * @param t
 * @return* /
public static <T> T getCurrentProxy(T t) {
    if (t == null) {
        throw new IllegalArgumentException("t should be current object(this).");
    }

    try {
        return (T) AopContext.currentProxy();
    } catch (IllegalStateException ex) {
        returnt; }}Copy the code

Amend the above direct call b method to ProxyHolder. GetCurrentProxy (this). B ().

Rich MeterMetrics support for multiple tag groups

MeterMetrics only has key and value, which is a bit thin. Now we can extend MeterMetrics’ ability to specify name, index type, and tag data, as follows:

public @interface MeterMetrics {
    MeterMetricsEnum type(a) default MeterMetricsEnum.TIMER;
    MeterTag[] tags() default {};
    String name(a) default "";
}

public @interface MeterTag {
    String key(a) default "";
    String value(a) default "";
    boolean spel(a) default false;
}
Copy the code

Use demonstration and summary

As for the code being hosted on Github later, there is currently a lack of starter encapsulation. The specific use effect is shown below

@MeterMetrics(name = "test", tags = {@MeterTag(key = "appName", value = "#requestModel.getApp_name()"), @MeterTag(key = "fixedKey", value = "fixedValue")}, type = MeterMetricsEnum.COUNTER)
public void test(@Validated RequestModel requestModel) {... }Copy the code

This paper provides a feasible implementation idea based on Micrometer buried point extension and uses it in actual business scenarios. Based on the native API, annotations and apis are extended to solve some problems that might be faced in a series of real business scenarios.

If this article helped you at all, please give it a thumbs up. Thanks!