具有动态标记值的Spring Boot指标


指标是每个可扩展应用程序必不可少的工具。

Spring Boot 2.0在Micrometer库中引入了新级别的度量标准,使我们的代码与Prometheus和Graphite等监视系统的集成比以往任何时候都更加简单。

我们发现缺少的功能之一是动态标记值。标签名称和值在创建计数器时声明。实际上,标记值被视为名称修饰符:具有相同名称和标记名称的标记,但是不同的标记值是两个不同的独立计数器。

Counter counterWithTag1 = Counter.builder(name).tags(tagName, tagValue1).register(registry);
Counter counterWithTag2 = Counter.builder(name).tags(tagName, tagValue2).register(registry);

我们的系统是多租户的:它服务于来自多个客户的消息。我们想知道每个客户处理的消息的比率。活跃客户的名称列表是动态的,因为它们可以连接和断开连接。我们不想为系统中存在但不发送数据的客户保留计数器,我们不想将计数器与保存客户列表的数据库同步以检测新添加的客户。仅当客户正在发送数据时,我们才希望有一个计数器。

可以在https://github.com/firedome/dynamic-actuator-metrics上找到以下描述的完整代码, 并在MIT许可下使用。

对于这种情况(以及许多其他情况),我们实现了TaggedCounter:

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import java.util.HashMap;
import java.util.Map;
public class TaggedCounter {
    private String name;
    private String tagName;
    private MeterRegistry registry;
    private Map<String, Counter> counters = new HashMap<>();

    public TaggedCounter(String name, String tagName, MeterRegistry registry) {
        this.name = name;
        this.tagName = tagName;
        this.registry = registry;
    }
    public void increment(String tagValue){
        Counter counter = counters.get(tagValue);
        if(counter == null) {
            counter = Counter.builder(name).tags(tagName, tagValue).register(registry);
            counters.put(tagValue, counter);
        }
        counter.increment();
    }
}

在我们的消息接收器构造函数中,我们创建一个TaggedCounter,记下计数器的名称和标签的名称。没有值。请注意,MeterRegistry可以自动接线,您不必显式创建它。

private TaggedCounter perCustomerMessages;
@Autowired
public ReaderMessageReceiver(MeterRegistry meterRegistry) {
    this.perCustomerMessages = new TaggedCounter("per-customer-messages", "customer", meterRegistry);
}

稍后,当收到一条消息(在本例中为pubsub消息)时,我们增加计数器,现在注意标记值。

public void receiveMessage(PubsubMessage pubsubMessage, AckReplyConsumer ackReplyConsumer) {
        String customer = pubsubMessage.getAttributesMap().get("customer_id");
        perCustomerMessages.increment(customer);
    try {
      //process the message...
    } finally {
        ackReplyConsumer.ack();
    }
}

就是这样。我们的监视服务器是Prometheus,这是我们在/ actuator / prometheus端点中获得的信息(请注意,计数器名称或标记名称中的所有定界符均由下划线替换):

# TYPE per_customer_messages_total counter
per_customer_messages_total{customer="0f43e152",} 1291460.0
per_customer_messages_total{customer="93c2adbb",} 118899.0
per_customer_messages_total{customer="1eab1589",} 301311.0
per_customer_messages_total{customer="270e5ca0",} 1710188.0

在Grafana中,我们可以通过查询看到每个客户的消息图

sum by (customer)(rate(per_customer_messages_total[1m]))

随着{{客户}}在联想格式领域,我们得到我们想要的图形:每个客户一行。

per-cutomer-messages.jpg

另一个用例是处理不同类型的消息。每种类型都有其自己的处理时间,我们希望通过计时器来衡量每种类型的处理延迟。(尽管消息类型在系统中是预定义的,但其中一些未使用:已弃用或仅很少使用。我们希望仅在使用计数器时才触发计数器。)

因此,我们有一个类似的TaggedTimer类:

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import java.util.HashMap;
import java.util.Map;
public class TaggedTimer {
    private String name;
    private String tagName;
    private MeterRegistry registry;
    private Map<String, Timer> timers = new HashMap<>();

    public TaggedTimer(String name, String tagName, MeterRegistry registry) {
        this.name = name;
        this.tagName = tagName;
        this.registry = registry;
    }
    public Timer getTimer(String tagValue){
        Timer timer = timers.get(tagValue);
        if(timer == null) {
            timer = Timer.builder(name).tags(tagName, tagValue).register(registry);
            timers.put(tagValue, timer);
        }
        return timer;
    }
}

用法类似:在构造函数中使用名称和标记名创建

TaggedTimer perTypeTimer = new TaggedTimer("per-type-processing-timer", "message-type", meterRegistry);

然后在以下情况下将其用于“心跳”消息类型:

perTypeTimer.getTimer("heartbeat").record(() -> {
    //process the message...
});

在Grafana中,我们按以下方式显示每种消息类型的处理延迟

sum by (message_type)(rate(per_type_processing_timer_seconds_sum[1m])/rate(rate(per_type_processing_timer_seconds_count[1m]))

(我们没有创建不需要的带标记的量规,但是可以用相同的方式来创建。)

上面的TaggedCounter和TaggedTimer正在解决带有单个标签的计数器的最常见情况。但是,如果您需要按多个标签计数怎么办?例如,为了描述客户之间的功能差异,我们希望获得每个客户特定类型的消息的比率。

为此,我们创建了MultiTaggedCounter:

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.ImmutableTag;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import java.util.*;
public class MultiTaggedCounter {
    private String name;
    private String[] tagNames;
    private MeterRegistry registry;
    private Map<String, Counter> counters = new HashMap<>();
    public MultiTaggedCounter(String name, MeterRegistry registry, String ... tags) {
        this.name = name;
        this.tagNames = tags;
        this.registry = registry;
    }
    public void increment(String ... tagValues){
        String valuesString = Arrays.toString(tagValues);
        if(tagValues.length != tagNames.length) {
            throw new IllegalArgumentException("Counter tags mismatch! Expected args are "+Arrays.toString(tagNames)+", provided tags are "+valuesString);
        }
        Counter counter = counters.get(valuesString);
        if(counter == null) {
            List<Tag> tags = new ArrayList<>(tagNames.length);
            for(int i = 0; i<tagNames.length; i++) {
                tags.add(new ImmutableTag(tagNames[i], tagValues[i]));
            }
            counter = Counter.builder(name).tags(tags).register(registry);
            counters.put(valuesString, counter);
        }
        counter.increment();
    }

}

构建计数器时,我们仅注意标签名称

MultiTaggedCounter perCustomerPerTypeMessages = new MultiTaggedCounter("per-customer-per-type", meterRegistry, "customer", "message-type");

然后使用计数器,我们增加标记值

perCustomerPerTypeMessages.increment(message.getCustomerName(), message.getType());

在Grafana中,我们同时按customer和message_type标签进行求和:

sum by (customer, message_type)
(rate(per_customer_per_type_total[1m]))

(图例格式{{customer}}-{{message_type}} 是图例中该值的缩写)

我们还可以通过以下方式获取特定客户或特定类型的数据:

sum by (message_type)
(rate(per_customer_per_type_total{customer="89450f"}[1m]))

希望对您有所帮助!


原文链接:http://codingdict.com