英文:
Spring Boot Metrics.counter("x").count() always return 0 for Influxdb
问题
拥有一个Spring Boot (2.7.10)应用程序,包括:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-influx</artifactId>
</dependency>
成功连接到InfluxDB。
现在,执行以下操作:
Metrics.counter("registration.upsigned").increment();
LOG.info("Size of registration-counter now: " + Metrics.counter("registration.upsigned").count());
成功在Grafana中创建一个计数:
但是日志结果仍然为0:
19-Jun-2023 07:25:15.282 INFO [https-jsse-nio2-14xx26-443-exec-123] xxx.register Size of registration-counter now: 0.0
如何在应用程序重启后继续递增计数?
英文:
Having a spring-boot (2.7.10) application include:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-influx</artifactId>
</dependency>
Connecting successfully to InfluxDB.
Now, executing
Metrics.counter("registration.upsigned").increment();
LOG.info("Size of registration-counter now: " + Metrics.counter("registration.upsigned").count());
Create successfully a tick in grafana
But the logging-result remains 0:
19-Jun-2023 07:25:15.282 INFO [https-jsse-nio2-14xx26-443-exec-123] xxx.register Size of registration-counter now: 0.0
How to keep increment the counter and surviving reboots of the application?
答案1
得分: 2
你描述的行为可以通过以下方式来解释。
如我的原始回答所示,InfluxMeterRegistry 在底层使用StepCounter
来实现计数器。
要获取实际的count
,StepCounter
使用了以下代码:
@Override
public double count() {
return value.poll();
}
其中value
实际上是StepValue
类的一个方便的实现,用于处理double
值。
请注意,当描述poll
时,它们在此处指示:
/**
* @return 最后一个已完成的间隔的值。
*/
public V poll() {
rollCount(clock.wallTime());
return previous;
}
即它报告的是上一个已完成间隔的值,而不是当前的值。
考虑查看此测试,下面是完整的内容,我认为它非常清楚地描述了编程行为:
@Test
void poll() {
final AtomicLong aLong = new AtomicLong(42);
final long stepTime = 60;
final StepValue<Long> stepValue = new StepValue<Long>(clock, stepTime) {
@Override
public Supplier<Long> valueSupplier() {
return () -> aLong.getAndSet(0);
}
@Override
public Long noValue() {
return 0L;
}
};
assertThat(stepValue.poll()).isEqualTo(0L);
clock.add(Duration.ofMillis(1));
assertThat(stepValue.poll()).isEqualTo(0L);
clock.add(Duration.ofMillis(59));
assertThat(stepValue.poll()).isEqualTo(42L);
clock.add(Duration.ofMillis(60));
assertThat(stepValue.poll()).isEqualTo(0L);
clock.add(Duration.ofMillis(60));
assertThat(stepValue.poll()).isEqualTo(0L);
aLong.set(24);
assertThat(stepValue.poll()).isEqualTo(0L);
clock.add(Duration.ofMillis(60));
assertThat(stepValue.poll()).isEqualTo(24L);
clock.add(Duration.ofMillis(stepTime / 2));
aLong.set(27);
assertThat(stepValue.poll()).isEqualTo(24L);
stepValue._closingRollover();
assertThat(stepValue.poll()).isEqualTo(27L);
}
请注意,为了这个目的,increment
在执行上与前面测试代码中的不同的aLong.set
相当。
我认为这个测试也可能有助于描述报告的值是上一个已完成间隔的值,而不是当前值:
@Test
void count() {
Counter counter = registry.counter("my.counter");
assertThat(counter).isInstanceOf(StepCounter.class);
assertThat(counter.count()).isEqualTo(0);
counter.increment();
clock.add(config.step());
assertThat(counter.count()).isEqualTo(1);
counter.increment();
counter.increment(5);
clock.add(config.step());
assertThat(counter.count()).isEqualTo(6);
clock.add(config.step());
assertThat(counter.count()).isEqualTo(0);
}
总之,我尝试的意思是,你代码片段中的日志:
Metrics.counter("registration.upsigned").increment();
LOG.info("Size of registration-counter now: " + Metrics.counter("registration.upsigned").count());
将始终打印出上一个已完成间隔的值,而不是当前间隔的值,具体取决于你执行increment
的间隔是否已过。
这不仅仅适用于InfluxDB,而是适用于基于步进概念的任何度量注册表实现。
无论如何,你可以获得的最有价值的信息是实际发布到你的后端的信息,无论是InfluxDB还是你定义的其他后端 - 这将导致步进相关的值被轮询。
我不知道是否会为你的问题提供额外的帮助,但你可以在Micrometer文档中阅读关于这种基于步进的速率数据聚合机制的信息,当他们描述客户端端速率聚合时 - 请注意,InfluxBD 是这种客户端端速率聚合监控系统之一。从文档中可以看到:
Micrometer通过积累当前发布间隔的数据的步进值来高效地维护速率数据。当步进值轮询(例如在发布时)时,如果步进值检测到当前间隔已经过去,它将当前数据移到“previous”状态。这个previous状态就是在下次当前数据覆盖它之前报告的状态。下面的图显示了当前和previous状态以及轮询的交互:
* 从我的理解来看,箭头表示发布事件。
如上所述,你的日志(正如你可以在上面提供的源代码中看到的,当执行count
时会进行poll
)将
英文:
The behavior you described could be explained by the following.
As indicated in my original answer InfluxMeterRegistry under the hood uses StepCounter
s to implement counters.
To obtain the actual count
StepCounter
uses the following code:
@Override
public double count() {
return value.poll();
}
where value turns out to be a convenient implementation for handling double
values of the StepValue
class.
Please, be aware that when describing poll
, they indicate:
/**
* @return The value for the last completed interval.
*/
public V poll() {
rollCount(clock.wallTime());
return previous;
}
i.e. it is reporting the value for the last completed interval, not the actual one.
Consider review this test, reproduced here for completeness, I think it describes the programmed behavior in a very clear way:
@Test
void poll() {
final AtomicLong aLong = new AtomicLong(42);
final long stepTime = 60;
final StepValue<Long> stepValue = new StepValue<Long>(clock, stepTime) {
@Override
public Supplier<Long> valueSupplier() {
return () -> aLong.getAndSet(0);
}
@Override
public Long noValue() {
return 0L;
}
};
assertThat(stepValue.poll()).isEqualTo(0L);
clock.add(Duration.ofMillis(1));
assertThat(stepValue.poll()).isEqualTo(0L);
clock.add(Duration.ofMillis(59));
assertThat(stepValue.poll()).isEqualTo(42L);
clock.add(Duration.ofMillis(60));
assertThat(stepValue.poll()).isEqualTo(0L);
clock.add(Duration.ofMillis(60));
assertThat(stepValue.poll()).isEqualTo(0L);
aLong.set(24);
assertThat(stepValue.poll()).isEqualTo(0L);
clock.add(Duration.ofMillis(60));
assertThat(stepValue.poll()).isEqualTo(24L);
clock.add(Duration.ofMillis(stepTime / 2));
aLong.set(27);
assertThat(stepValue.poll()).isEqualTo(24L);
stepValue._closingRollover();
assertThat(stepValue.poll()).isEqualTo(27L);
}
Note that for this purpose increment
is doing something "equivalent" to the different aLong.set
presented in the previous test code.
I think this other test could be of help as well for describing how the value reported is the one for the last completed interval, not the actual one:
@Test
void count() {
Counter counter = registry.counter("my.counter");
assertThat(counter).isInstanceOf(StepCounter.class);
assertThat(counter.count()).isEqualTo(0);
counter.increment();
clock.add(config.step());
assertThat(counter.count()).isEqualTo(1);
counter.increment();
counter.increment(5);
clock.add(config.step());
assertThat(counter.count()).isEqualTo(6);
clock.add(config.step());
assertThat(counter.count()).isEqualTo(0);
}
In conclusion, what I tried meaning is that the log in your code fragment:
Metrics.counter("registration.upsigned").increment();
LOG.info("Size of registration-counter now: " + Metrics.counter("registration.upsigned").count());
will always print out the value for the last completed interval, not for the current one, and it may be or not the value you expect depending on whether or not the interval in which you performed the increment
elapsed.
That is not specific for InfluxDB but for any Metrics registry implementation based on the step concept.
In any case, the most valuable information you can obtain is the one that is actually published to your backend, InfluxDB or the one you defined - which causes among other things the step related values to be polled.
I don't know if it will shed some additional light or not, but you can read about this rate data aggregation step based mechanism in the Micrometer documentation when they describe client side rate aggregation - please, note that InfluxBD is one of this client-side rate aggregation monitoring system. From the docs:
> Micrometer efficiently maintains rate data by means of a step value
> that accumulates data for the current publishing interval. When the
> step value is polled (when publishing, for example), if the step
> value detects that the current interval has elapsed, it moves
> current data to “previous” state. This previous state is what is
> reported until the next time current data overwrites it. The
> following image shows the interaction of current and previous
> state, along with polling:
* From my understanding the arrows signal publish events.
As indicated, your log (which, as you can see in the source code presented above, poll
s when performing count
) will always report the previous value presented in the image which may or not be the same you expect according to your increment
s invocations and step intervals (your log may behave as the first pollAsRate
presented in the image, in which it will print out 0
despite the fact two increment
s were performed).
If you prefer, here is a simplified version of the process, more focused in your example:
Please, take in mind that a step based counter:
- Always maintains two values under the hood, the current one, internal, not visible outside the class, and a previous one, visible, the one that is taking into account when publishing data.
- Every step is configured with a time interval for rate aggregation.
- The previous value is updated with the current value when any of two things happen:
- A poll is requested, either by invoking
count
manually or usually when publishing information to your metrics registry, and the step interval is elapsed. - Micrometer shutdowns and claims for a closing rollover, i.e., send the last acquired data to your metrics repository.
- A poll is requested, either by invoking
To illustrate it, please, consider the following test case, I think it could be of help:
import java.time.Duration;
import org.junit.jupiter.api.Test;
import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.simple.CountingMode;
import io.micrometer.core.instrument.simple.SimpleConfig;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
public class MicrometerTest {
@Test
public void testStepCounterBehavior() {
Clock clock = Clock.SYSTEM;
MeterRegistry registry = new SimpleMeterRegistry(new SimpleConfig() {
@Override
public String get(String key) {
return null;
}
@Override
public CountingMode mode() {
return CountingMode.STEP;
}
@Override
public Duration step() {
return Duration.ofMillis(1000);
}
}, clock);
double value = -1, previousValue = -1;
Tags tags = Tags.empty();
Counter counter = registry.counter("test.counter", tags);
long start = clock.wallTime();
counter.increment();
int i = 0;
while (clock.wallTime() < start + 5000) {
value = counter.count();
if (value != previousValue) {
System.out.printf("Current value: %.0f. Elapsed (millis): %d.\n", value, clock.wallTime() - start);
previousValue = value;
if (i++ == 2) {
counter.increment(3);
}
}
}
}
}
It will output, with some random bias, something similar to:
Current value: 0. Elapsed (millis): 0.
Current value: 1. Elapsed (millis): 601.
Current value: 0. Elapsed (millis): 1600.
Current value: 3. Elapsed (millis): 2600.
Current value: 0. Elapsed (millis): 3600.
Note that except for the first spurious output which reports a time less than the configured 1
second step interval, I think motivated by the different initialization stuff, the values and time printed are consistent with the explanation provided.
答案2
得分: 0
目前无法实现(2023年12月07日)
将来可能会解决,但目前没有100%的解决方案。目前最好不要在Java中使用micrometer,而是直接连接influxdb可能会更有帮助。
这在将来可能会改变,您可以查看其他回答此问题的答案。如果没有其他答案,这个问题可能已经得到解决。
英文:
Not possible at the moment (12.07.2023)
It might be solved in the future but there was no 100% solution at the moment. It might be better to not use micrometer in Java at the moment, instead a direct influxdb connection might help.
This might change in the future, you could check other answers to this question. If there is no other answer this issue might already be solved.
答案3
得分: -1
为了在应用程序重新启动时增加和持续保留计数器的值,您需要将计数器值存储在持久存储中,例如数据库或文件中。由于您正在使用InfluxDB,您可以将计数器值存储在单独的测量中,并在应用程序启动时检索它:
- 在数据库中创建它
一个简单的查询可以起作用,例如:
INSERT counter_values,counter_name=registration.upsigned value=0
- 在应用程序启动时检索它:
// 注解注入
@AutoWired
private InfluxDB influxDB;
private double getCounterValueFromInfluxDB(String counterName) {
Query query = new Query("SELECT last(value) FROM counter_values WHERE counter_name = '" + counterName + "'", "your_database_name");
QueryResult queryResult = influxDB.query(query);
List<QueryResult.Result> results = queryResult.getResults();
if (!results.isEmpty()) {
List<QueryResult.Series> seriesList = results.get(0).getSeries();
if (!seriesList.isEmpty()) {
List<List<Object>> values = seriesList.get(0).getValues();
if (!values.isEmpty()) {
return Double.parseDouble(values.get(0).get(1).toString());
}
}
}
return 0;
}
- 使用该值初始化计数器..
private Counter counter;
@PostConstruct
public void initCounter() {
double initialValue = getCounterValueFromInfluxDB("registration.upsigned");
counter = Counter.builder("registration.upsigned")
.register(Metrics.globalRegistry)
.increment(initialValue);
}
- 在增加时更新数据库中的计数器:
private void incrementCounterAndPersist() {
counter.increment();
double newValue = counter.count();
Point point = Point.measurement("counter_values")
.time(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.tag("counter_name", "registration.upsigned")
.addField("value", newValue)
.build();
influxDB.write("your_database_name", "autogen", point);
}
设置完成后,计数器值应该在数据库中持久保留,并在应用程序重新启动时保持不变。
英文:
To increment and persist the counter value across application reboots, you need to store the counter value in a persistent storage like a database or a file. Since you are using InfluxDB, you can store the counter value in a separate measurement and retrieve it when your application starts :
-
Create it in database
A simple query will work such as :
INSERT counter_values,counter_name=registration.upsigned value=0
-
Retrieve it on app startup :
// annotation injection
@AutoWired
private InfluxDB influxDB;
private double getCounterValueFromInfluxDB(String counterName) {
Query query = new Query("SELECT last(value) FROM counter_values WHERE counter_name = '" + counterName + "'", "your_database_name");
QueryResult queryResult = influxDB.query(query);
List<QueryResult.Result> results = queryResult.getResults();
if (!results.isEmpty()) {
List<QueryResult.Series> seriesList = results.get(0).getSeries();
if (!seriesList.isEmpty()) {
List<List<Object>> values = seriesList.get(0).getValues();
if (!values.isEmpty()) {
return Double.parseDouble(values.get(0).get(1).toString());
}
}
}
return 0;
}
- Init the counter using the value..
private Counter counter;
@PostConstruct
public void initCounter() {
double initialValue = getCounterValueFromInfluxDB("registration.upsigned");
counter = Counter.builder("registration.upsigned")
.register(Metrics.globalRegistry)
.increment(initialValue);
}
- Update the counter in db when incrementing :
private void incrementCounterAndPersist() {
counter.increment();
double newValue = counter.count();
Point point = Point.measurement("counter_values")
.time(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.tag("counter_name", "registration.upsigned")
.addField("value", newValue)
.build();
influxDB.write("your_database_name", "autogen", point);
}
After that setup, the counter value should persist in database and then survive to app reboots
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论