measure() {
}
@Override
- public void _manualRollover() {
- countTotal.manualRollover();
+ public void _closingRollover() {
+ countTotal.closingRollover();
}
}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepFunctionCounter.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepFunctionCounter.java
index ab43ee8eb2..b0f612a7c4 100644
--- a/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepFunctionCounter.java
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepFunctionCounter.java
@@ -51,9 +51,9 @@ public double count() {
}
@Override
- public void _manualRollover() {
+ public void _closingRollover() {
count(); // add any difference from last count
- count.manualRollover();
+ count.closingRollover();
}
}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepFunctionTimer.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepFunctionTimer.java
index 12d8259cec..f19c37cb1a 100644
--- a/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepFunctionTimer.java
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepFunctionTimer.java
@@ -119,9 +119,9 @@ public Type type() {
}
@Override
- public void _manualRollover() {
+ public void _closingRollover() {
accumulateCountAndTotal();
- countTotal.manualRollover();
+ countTotal.closingRollover();
}
}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepMeter.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepMeter.java
index c8d23df8fc..dafdb604f6 100644
--- a/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepMeter.java
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepMeter.java
@@ -23,8 +23,9 @@ interface StepMeter {
/**
* This is an internal method not meant for general use.
*
- * Force a rollover of the values returned by a step meter.
+ * Force a rollover of the values returned by a step meter and never roll over again
+ * after.
*/
- void _manualRollover();
+ void _closingRollover();
}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepMeterRegistry.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepMeterRegistry.java
index 7c3389d784..66435c9d0f 100644
--- a/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepMeterRegistry.java
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepMeterRegistry.java
@@ -107,10 +107,12 @@ protected DistributionStatisticConfig defaultHistogramConfig() {
@Override
public void close() {
stop();
- getMeters().stream()
- .filter(meter -> meter instanceof StepMeter)
- .map(meter -> (StepMeter) meter)
- .forEach(StepMeter::_manualRollover);
+ if (!isPublishing()) {
+ getMeters().stream()
+ .filter(StepMeter.class::isInstance)
+ .map(StepMeter.class::cast)
+ .forEach(StepMeter::_closingRollover);
+ }
super.close();
}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepTimer.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepTimer.java
index bbe06ddf41..11dffdd8dc 100644
--- a/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepTimer.java
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepTimer.java
@@ -80,8 +80,8 @@ public double max(final TimeUnit unit) {
}
@Override
- public void _manualRollover() {
- countTotal.manualRollover();
+ public void _closingRollover() {
+ countTotal.closingRollover();
}
}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepTuple2.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepTuple2.java
index fd3d4935af..b16415bb23 100644
--- a/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepTuple2.java
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepTuple2.java
@@ -77,7 +77,9 @@ private void rollCount(long now) {
* Intended for internal use. Rolls the values regardless of the clock or current
* time.
*/
- void manualRollover() {
+ void closingRollover() {
+ // ensure rollover does not happen again
+ lastInitPos.set(Long.MAX_VALUE);
t1Previous = t1Supplier.get();
t2Previous = t2Supplier.get();
}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepValue.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepValue.java
index 0fa287b01a..f48a4a77da 100644
--- a/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepValue.java
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/step/StepValue.java
@@ -76,7 +76,9 @@ public V poll() {
/**
* internal use only; intentionally left package-private
*/
- void manualRollover() {
+ void closingRollover() {
+ // make sure value does not roll over again if passing a step boundary
+ lastInitPos.set(Long.MAX_VALUE);
previous = valueSupplier().get();
}
diff --git a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/DefaultHttpClientObservationConvention.java b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/DefaultHttpClientObservationConvention.java
index 12454dc28e..91e4b9871c 100644
--- a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/DefaultHttpClientObservationConvention.java
+++ b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/DefaultHttpClientObservationConvention.java
@@ -18,6 +18,7 @@
import io.micrometer.common.KeyValues;
import io.micrometer.common.lang.NonNull;
import io.micrometer.common.lang.Nullable;
+import io.micrometer.core.instrument.binder.http.Outcome;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
@@ -47,8 +48,11 @@ public KeyValues getLowCardinalityKeyValues(HttpClientContext context) {
HttpClientObservationDocumentation.LowCardinalityKeys.URI
.withValue(getUriTag(httpRequest, context.getResponse(), context.getUriMapper())));
if (context.getResponse() != null) {
- keyValues = keyValues.and(HttpClientObservationDocumentation.LowCardinalityKeys.STATUS
- .withValue(String.valueOf(context.getResponse().statusCode())));
+ keyValues = keyValues
+ .and(HttpClientObservationDocumentation.LowCardinalityKeys.STATUS
+ .withValue(String.valueOf(context.getResponse().statusCode())))
+ .and(HttpClientObservationDocumentation.LowCardinalityKeys.OUTCOME
+ .withValue(Outcome.forStatus(context.getResponse().statusCode()).name()));
}
return keyValues;
}
diff --git a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientObservationDocumentation.java b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientObservationDocumentation.java
index 38b61f8bdf..63888b5f41 100644
--- a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientObservationDocumentation.java
+++ b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/HttpClientObservationDocumentation.java
@@ -60,6 +60,13 @@ public String asString() {
}
},
+ OUTCOME {
+ @Override
+ public String asString() {
+ return "outcome";
+ }
+ },
+
/**
* HTTP URI.
*/
diff --git a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClient.java b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClient.java
index 41b5c7a8dc..37c7a7a1cb 100644
--- a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClient.java
+++ b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClient.java
@@ -19,6 +19,7 @@
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
+import io.micrometer.core.instrument.binder.http.Outcome;
import io.micrometer.core.instrument.observation.ObservationOrTimerCompatibleInstrumentation;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
@@ -238,8 +239,11 @@ private void stopObservationOrTimer(
request.method(), HttpClientObservationDocumentation.LowCardinalityKeys.URI.asString(),
DefaultHttpClientObservationConvention.INSTANCE.getUriTag(request, res, uriMapper));
if (res != null) {
- tags = tags.and(Tag.of(HttpClientObservationDocumentation.LowCardinalityKeys.STATUS.asString(),
- String.valueOf(res.statusCode())));
+ tags = tags
+ .and(Tag.of(HttpClientObservationDocumentation.LowCardinalityKeys.STATUS.asString(),
+ String.valueOf(res.statusCode())))
+ .and(Tag.of(HttpClientObservationDocumentation.LowCardinalityKeys.OUTCOME.asString(),
+ Outcome.forStatus(res.statusCode()).name()));
}
return tags;
});
diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/MicrometerHttpRequestExecutorTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/MicrometerHttpRequestExecutorTest.java
index fd7a9b26ce..5dee8448b8 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/MicrometerHttpRequestExecutorTest.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/MicrometerHttpRequestExecutorTest.java
@@ -96,12 +96,18 @@ void httpStatusCodeIsTagged(boolean configureObservationRegistry, @WiremockResol
EntityUtils.consume(client.execute(new HttpGet(server.baseUrl() + "/ok")).getEntity());
EntityUtils.consume(client.execute(new HttpGet(server.baseUrl() + "/notfound")).getEntity());
EntityUtils.consume(client.execute(new HttpGet(server.baseUrl() + "/error")).getEntity());
- assertThat(registry.get(EXPECTED_METER_NAME).tags("method", "GET", "status", "200").timer().count())
- .isEqualTo(2L);
- assertThat(registry.get(EXPECTED_METER_NAME).tags("method", "GET", "status", "404").timer().count())
- .isEqualTo(1L);
- assertThat(registry.get(EXPECTED_METER_NAME).tags("method", "GET", "status", "500").timer().count())
- .isEqualTo(1L);
+ assertThat(registry.get(EXPECTED_METER_NAME)
+ .tags("method", "GET", "status", "200", "outcome", "SUCCESS")
+ .timer()
+ .count()).isEqualTo(2L);
+ assertThat(registry.get(EXPECTED_METER_NAME)
+ .tags("method", "GET", "status", "404", "outcome", "CLIENT_ERROR")
+ .timer()
+ .count()).isEqualTo(1L);
+ assertThat(registry.get(EXPECTED_METER_NAME)
+ .tags("method", "GET", "status", "500", "outcome", "SERVER_ERROR")
+ .timer()
+ .count()).isEqualTo(1L);
}
@ParameterizedTest
@@ -340,8 +346,10 @@ void httpStatusCodeIsTaggedWithIoError(boolean configureObservationRegistry,
assertThatThrownBy(
() -> EntityUtils.consume(client.execute(new HttpGet(server.baseUrl() + "/error")).getEntity()))
.isInstanceOf(ClientProtocolException.class);
- assertThat(registry.get(EXPECTED_METER_NAME).tags("method", "GET", "status", "IO_ERROR").timer().count())
- .isEqualTo(1L);
+ assertThat(registry.get(EXPECTED_METER_NAME)
+ .tags("method", "GET", "status", "IO_ERROR", "outcome", "UNKNOWN")
+ .timer()
+ .count()).isEqualTo(1L);
}
static class CustomGlobalApacheHttpConvention extends DefaultApacheHttpClientObservationConvention
diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/MicrometerHttpRequestExecutorTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/MicrometerHttpRequestExecutorTest.java
index 3b8019a4b0..fda6b81643 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/MicrometerHttpRequestExecutorTest.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/MicrometerHttpRequestExecutorTest.java
@@ -107,12 +107,18 @@ void httpStatusCodeIsTagged(boolean configureObservationRegistry, @WiremockResol
execute(client, new HttpGet(server.baseUrl() + "/ok"));
execute(client, new HttpGet(server.baseUrl() + "/notfound"));
execute(client, new HttpGet(server.baseUrl() + "/error"));
- assertThat(registry.get(EXPECTED_METER_NAME).tags("method", "GET", "status", "200").timer().count())
- .isEqualTo(2L);
- assertThat(registry.get(EXPECTED_METER_NAME).tags("method", "GET", "status", "404").timer().count())
- .isEqualTo(1L);
- assertThat(registry.get(EXPECTED_METER_NAME).tags("method", "GET", "status", "500").timer().count())
- .isEqualTo(1L);
+ assertThat(registry.get(EXPECTED_METER_NAME)
+ .tags("method", "GET", "status", "200", "outcome", "SUCCESS")
+ .timer()
+ .count()).isEqualTo(2L);
+ assertThat(registry.get(EXPECTED_METER_NAME)
+ .tags("method", "GET", "status", "404", "outcome", "CLIENT_ERROR")
+ .timer()
+ .count()).isEqualTo(1L);
+ assertThat(registry.get(EXPECTED_METER_NAME)
+ .tags("method", "GET", "status", "500", "outcome", "SERVER_ERROR")
+ .timer()
+ .count()).isEqualTo(1L);
}
@ParameterizedTest
@@ -334,8 +340,10 @@ void httpStatusCodeIsTaggedWithIoError(boolean configureObservationRegistry,
CloseableHttpClient client = client(executor(false, configureObservationRegistry));
assertThatThrownBy(() -> execute(client, new HttpGet(server.baseUrl() + "/error")))
.isInstanceOf(ClientProtocolException.class);
- assertThat(registry.get(EXPECTED_METER_NAME).tags("method", "GET", "status", "IO_ERROR").timer().count())
- .isEqualTo(1L);
+ assertThat(registry.get(EXPECTED_METER_NAME)
+ .tags("method", "GET", "status", "IO_ERROR", "outcome", "UNKNOWN")
+ .timer()
+ .count()).isEqualTo(1L);
}
static class CustomGlobalApacheHttpConvention extends DefaultApacheHttpClientObservationConvention
diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java
index 9063f95a26..4a14a91588 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java
@@ -72,8 +72,8 @@ void timeSuccessful(@WiremockResolver.Wiremock WireMockServer server) throws IOE
client.newCall(request).execute().close();
assertThat(registry.get("okhttp.requests")
- .tags("foo", "bar", "status", "200", "uri", URI_EXAMPLE_VALUE, "target.host", "localhost", "target.port",
- String.valueOf(server.port()), "target.scheme", "http")
+ .tags("foo", "bar", "status", "200", "outcome", "SUCCESS", "uri", URI_EXAMPLE_VALUE, "target.host",
+ "localhost", "target.port", String.valueOf(server.port()), "target.scheme", "http")
.timer()
.count()).isEqualTo(1L);
}
@@ -86,8 +86,8 @@ void timeNotFound(@WiremockResolver.Wiremock WireMockServer server) throws IOExc
client.newCall(request).execute().close();
assertThat(registry.get("okhttp.requests")
- .tags("foo", "bar", "status", "404", "uri", "NOT_FOUND", "target.host", "localhost", "target.port",
- String.valueOf(server.port()), "target.scheme", "http")
+ .tags("foo", "bar", "status", "404", "outcome", "CLIENT_ERROR", "uri", "NOT_FOUND", "target.host",
+ "localhost", "target.port", String.valueOf(server.port()), "target.scheme", "http")
.timer()
.count()).isEqualTo(1L);
}
@@ -114,7 +114,8 @@ void timeFailureDueToTimeout(@WiremockResolver.Wiremock WireMockServer server) {
}
assertThat(registry.get("okhttp.requests")
- .tags("foo", "bar", "uri", URI_EXAMPLE_VALUE, "status", "IO_ERROR", "target.host", "localhost")
+ .tags("foo", "bar", "uri", URI_EXAMPLE_VALUE, "status", "IO_ERROR", "outcome", "UNKNOWN", "target.host",
+ "localhost")
.timer()
.count()).isEqualTo(1L);
}
diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/logging/LoggingMeterRegistryTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/logging/LoggingMeterRegistryTest.java
index 8e4b7daefc..72fb2d42f2 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/instrument/logging/LoggingMeterRegistryTest.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/logging/LoggingMeterRegistryTest.java
@@ -21,6 +21,8 @@
import java.util.Arrays;
import java.util.Collections;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat;
@@ -29,6 +31,7 @@
*
* @author Jon Schneider
* @author Johnny Lim
+ * @author Matthieu Borgraeve
*/
class LoggingMeterRegistryTest {
@@ -43,6 +46,31 @@ void defaultMeterIdPrinter() {
assertThat(printer.id()).isEqualTo("my.gauage{tag-1=tag-2}");
}
+ @Test
+ void providedSinkFromConstructorShouldBeUsed() {
+ String expectedString = "my.gauage{tag-1=tag-2} value=1";
+ AtomicReference actual = new AtomicReference<>();
+ AtomicInteger gaugeValue = new AtomicInteger(1);
+ LoggingMeterRegistry registry = new LoggingMeterRegistry(LoggingRegistryConfig.DEFAULT, Clock.SYSTEM,
+ actual::set);
+ registry.gauge("my.gauage", Tags.of("tag-1", "tag-2"), gaugeValue);
+
+ registry.publish();
+ assertThat(actual.get()).isEqualTo(expectedString);
+ }
+
+ @Test
+ void providedSinkFromConstructorShouldBeUsedWithDefaults() {
+ String expectedString = "my.gauage{tag-1=tag-2} value=1";
+ AtomicReference actual = new AtomicReference<>();
+ AtomicInteger gaugeValue = new AtomicInteger(1);
+ LoggingMeterRegistry registry = new LoggingMeterRegistry(actual::set);
+ registry.gauge("my.gauage", Tags.of("tag-1", "tag-2"), gaugeValue);
+
+ registry.publish();
+ assertThat(actual.get()).isEqualTo(expectedString);
+ }
+
@Test
void customMeterIdPrinter() {
LoggingMeterRegistry registry = LoggingMeterRegistry.builder(LoggingRegistryConfig.DEFAULT)
diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/push/PushMeterRegistryTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/push/PushMeterRegistryTest.java
index 7c5e51f93f..2ffb100ce4 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/instrument/push/PushMeterRegistryTest.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/push/PushMeterRegistryTest.java
@@ -15,18 +15,27 @@
*/
package io.micrometer.core.instrument.push;
-import io.micrometer.core.instrument.MockClock;
+import io.micrometer.core.Issue;
+import io.micrometer.core.instrument.*;
+import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
+import io.micrometer.core.instrument.distribution.pause.PauseDetector;
import io.micrometer.core.instrument.step.StepMeterRegistry;
import io.micrometer.core.instrument.step.StepRegistryConfig;
import io.micrometer.core.instrument.util.NamedThreadFactory;
-import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.time.Duration;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.TimeUnit;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.Map;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.ToDoubleFunction;
+import java.util.function.ToLongFunction;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
@@ -56,15 +65,9 @@ public String get(String key) {
CountDownLatch latch = new CountDownLatch(2);
- PushMeterRegistry pushMeterRegistry = new ThrowingPushMeterRegistry(config, latch);
-
- @AfterEach
- void cleanUp() {
- pushMeterRegistry.close();
- }
-
@Test
void whenUncaughtExceptionInPublish_taskStillScheduled() throws InterruptedException {
+ PushMeterRegistry pushMeterRegistry = new ThrowingPushMeterRegistry(config, latch);
pushMeterRegistry.start(threadFactory);
assertThat(latch.await(500, TimeUnit.MILLISECONDS))
.as("publish should continue to be scheduled even if an uncaught exception is thrown")
@@ -73,9 +76,163 @@ void whenUncaughtExceptionInPublish_taskStillScheduled() throws InterruptedExcep
@Test
void whenUncaughtExceptionInPublish_closeRegistrySuccessful() {
+ PushMeterRegistry pushMeterRegistry = new ThrowingPushMeterRegistry(config, latch);
assertThatCode(() -> pushMeterRegistry.close()).doesNotThrowAnyException();
}
+ @Test
+ @Issue("#3712")
+ void publishOnlyHappensOnceWithMultipleClose() {
+ CountingPushMeterRegistry pushMeterRegistry = new CountingPushMeterRegistry(config, Clock.SYSTEM);
+ pushMeterRegistry.close();
+ assertThat(pushMeterRegistry.publishCount.get()).isOne();
+ pushMeterRegistry.close();
+ assertThat(pushMeterRegistry.publishCount.get()).isOne();
+ }
+
+ @Test
+ @Issue("#3711")
+ void scheduledPublishOverlapWithPublishOnClose() throws InterruptedException {
+ MockClock clock = new MockClock();
+ CyclicBarrier barrier = new CyclicBarrier(2);
+ OverlappingStepMeterRegistry overlappingStepMeterRegistry = new OverlappingStepMeterRegistry(config, clock,
+ barrier);
+ Counter c1 = overlappingStepMeterRegistry.counter("c1");
+ Counter c2 = overlappingStepMeterRegistry.counter("c2");
+ c1.increment();
+ c2.increment(2.5);
+ clock.add(config.step());
+
+ // simulated scheduled publish
+ Thread scheduledPublishingThread = new Thread(
+ () -> ((PushMeterRegistry) overlappingStepMeterRegistry).publishSafely(),
+ "scheduledMetricsPublisherThread");
+ scheduledPublishingThread.start();
+ // publish on shutdown
+ Thread onClosePublishThread = new Thread(overlappingStepMeterRegistry::close, "shutdownHookThread");
+ onClosePublishThread.start();
+ scheduledPublishingThread.join();
+ onClosePublishThread.join();
+
+ assertThat(overlappingStepMeterRegistry.publishes).as("only one publish happened").hasSize(1);
+ Deque firstPublishValues = overlappingStepMeterRegistry.publishes.get(0);
+ assertThat(firstPublishValues.pop()).isEqualTo(1);
+ assertThat(firstPublishValues.pop()).isEqualTo(2.5);
+ }
+
+ private static class OverlappingStepMeterRegistry extends StepMeterRegistry {
+
+ private final AtomicInteger numberOfPublishes = new AtomicInteger();
+
+ private final Map> publishes = new ConcurrentHashMap<>();
+
+ private final CyclicBarrier barrier;
+
+ OverlappingStepMeterRegistry(StepRegistryConfig config, Clock clock, CyclicBarrier barrier) {
+ super(config, clock);
+ this.barrier = barrier;
+ }
+
+ @Override
+ protected TimeUnit getBaseTimeUnit() {
+ return SECONDS;
+ }
+
+ @Override
+ protected void publish() {
+ try {
+ barrier.await(100, MILLISECONDS);
+ }
+ catch (InterruptedException | BrokenBarrierException | TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ int publishIndex = numberOfPublishes.getAndIncrement();
+ getMeters().stream()
+ .filter(meter -> meter instanceof Counter)
+ .map(meter -> (Counter) meter)
+ .forEach(counter -> publishes.merge(publishIndex, new ArrayDeque<>(Arrays.asList(counter.count())),
+ (l1, l2) -> {
+ l1.addAll(l2);
+ return l1;
+ }));
+ }
+
+ @Override
+ public void close() {
+ try {
+ barrier.await(100, MILLISECONDS);
+ }
+ catch (InterruptedException | BrokenBarrierException | TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ super.close();
+ }
+
+ }
+
+ static class CountingPushMeterRegistry extends PushMeterRegistry {
+
+ AtomicInteger publishCount = new AtomicInteger();
+
+ protected CountingPushMeterRegistry(PushRegistryConfig config, Clock clock) {
+ super(config, clock);
+ }
+
+ @Override
+ protected Gauge newGauge(Meter.Id id, T obj, ToDoubleFunction valueFunction) {
+ return null;
+ }
+
+ @Override
+ protected Counter newCounter(Meter.Id id) {
+ return null;
+ }
+
+ @Override
+ protected Timer newTimer(Meter.Id id, DistributionStatisticConfig distributionStatisticConfig,
+ PauseDetector pauseDetector) {
+ return null;
+ }
+
+ @Override
+ protected DistributionSummary newDistributionSummary(Meter.Id id,
+ DistributionStatisticConfig distributionStatisticConfig, double scale) {
+ return null;
+ }
+
+ @Override
+ protected Meter newMeter(Meter.Id id, Meter.Type type, Iterable measurements) {
+ return null;
+ }
+
+ @Override
+ protected FunctionTimer newFunctionTimer(Meter.Id id, T obj, ToLongFunction countFunction,
+ ToDoubleFunction totalTimeFunction, TimeUnit totalTimeFunctionUnit) {
+ return null;
+ }
+
+ @Override
+ protected FunctionCounter newFunctionCounter(Meter.Id id, T obj, ToDoubleFunction countFunction) {
+ return null;
+ }
+
+ @Override
+ protected TimeUnit getBaseTimeUnit() {
+ return null;
+ }
+
+ @Override
+ protected DistributionStatisticConfig defaultHistogramConfig() {
+ return null;
+ }
+
+ @Override
+ protected void publish() {
+ publishCount.incrementAndGet();
+ }
+
+ }
+
static class ThrowingPushMeterRegistry extends StepMeterRegistry {
final CountDownLatch countDownLatch;
diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepCounterTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepCounterTest.java
index 20fcb78ae3..f170429891 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepCounterTest.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepCounterTest.java
@@ -69,12 +69,15 @@ void count() {
}
@Test
- void manualRolloverPartialStep() {
+ void closingRolloverPartialStep() {
StepCounter counter = (StepCounter) registry.counter("my.counter");
counter.increment(2.5);
assertThat(counter.count()).isZero();
- counter._manualRollover();
+ counter._closingRollover();
+ assertThat(counter.count()).isEqualTo(2.5);
+
+ clock.add(config.step());
assertThat(counter.count()).isEqualTo(2.5);
}
diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepDistributionSummaryTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepDistributionSummaryTest.java
index 67d43105b7..9964494241 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepDistributionSummaryTest.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepDistributionSummaryTest.java
@@ -50,7 +50,7 @@ void meanShouldWorkIfTotalNotCalled() {
}
@Test
- void manualRolloverPartialStep() {
+ void closingRolloverPartialStep() {
Duration stepDuration = Duration.ofMillis(10);
StepDistributionSummary summary = new StepDistributionSummary(mock(Meter.Id.class), clock,
DistributionStatisticConfig.builder().expiry(stepDuration).bufferLength(2).build(), 1.0,
@@ -61,7 +61,13 @@ void manualRolloverPartialStep() {
assertThat(summary.count()).isZero();
- summary._manualRollover();
+ summary._closingRollover();
+
+ assertThat(summary.count()).isEqualTo(2);
+ assertThat(summary.totalAmount()).isEqualTo(300);
+ assertThat(summary.mean()).isEqualTo(150);
+
+ clock.add(stepDuration);
assertThat(summary.count()).isEqualTo(2);
assertThat(summary.totalAmount()).isEqualTo(300);
diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepFunctionCounterTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepFunctionCounterTest.java
index 4975da64b2..61c90c835b 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepFunctionCounterTest.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepFunctionCounterTest.java
@@ -65,14 +65,19 @@ void count() {
}
@Test
- void manualRolloverPartialStep() {
+ void closingRolloverPartialStep() {
AtomicInteger n = new AtomicInteger(3);
+ @SuppressWarnings({ "rawtypes", "unchecked" })
StepFunctionCounter counter = (StepFunctionCounter) registry.more()
.counter("my.counter", Tags.empty(), n);
assertThat(counter.count()).isZero();
- counter._manualRollover();
+ counter._closingRollover();
+
+ assertThat(counter.count()).isEqualTo(3);
+
+ clock.add(config.step());
assertThat(counter.count()).isEqualTo(3);
}
diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepFunctionTimerTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepFunctionTimerTest.java
index 6c9d6471c0..5a5b11c244 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepFunctionTimerTest.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepFunctionTimerTest.java
@@ -77,7 +77,7 @@ void meanShouldWorkIfTotalNotCalled() {
}
@Test
- void manualRolloverPartialStep() {
+ void closingRolloverPartialStep() {
Queue counts = new ArrayDeque<>();
counts.add(2L);
counts.add(5L);
@@ -95,7 +95,12 @@ void manualRolloverPartialStep() {
assertThat(timer.count()).isZero();
assertThat(timer.totalTime(TimeUnit.SECONDS)).isZero();
- timer._manualRollover();
+ timer._closingRollover();
+
+ assertThat(timer.count()).isEqualTo(2);
+ assertThat(timer.totalTime(TimeUnit.SECONDS)).isEqualTo(150);
+
+ clock.add(stepDuration);
assertThat(timer.count()).isEqualTo(2);
assertThat(timer.totalTime(TimeUnit.SECONDS)).isEqualTo(150);
diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepMeterRegistryTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepMeterRegistryTest.java
index c2c09ab755..4aeb7b4ec2 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepMeterRegistryTest.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepMeterRegistryTest.java
@@ -15,6 +15,7 @@
*/
package io.micrometer.core.instrument.step;
+import io.micrometer.common.lang.Nullable;
import io.micrometer.core.Issue;
import io.micrometer.core.instrument.*;
import org.junit.jupiter.api.Test;
@@ -293,6 +294,54 @@ void finalPushHasPartialStep() {
assertThat(registry.publishedFunctionTimerTotals.pop()).isEqualTo(24);
}
+ @Issue("#3720")
+ @Test
+ void publishOnCloseCrossesStepBoundary() {
+ Counter counter = Counter.builder("counter").register(registry);
+ counter.increment();
+ Timer timer = Timer.builder("timer").register(registry);
+ timer.record(5, MILLISECONDS);
+ DistributionSummary summary = DistributionSummary.builder("summary").register(registry);
+ summary.record(7);
+ FunctionCounter functionCounter = FunctionCounter.builder("counter.function", this, obj -> 15)
+ .register(registry);
+ FunctionTimer functionTimer = FunctionTimer.builder("timer.function", this, obj -> 3, obj -> 53, MILLISECONDS)
+ .register(registry);
+
+ // before rollover
+ assertThat(counter.count()).isZero();
+ assertThat(timer.count()).isZero();
+ assertThat(timer.totalTime(MILLISECONDS)).isZero();
+ assertThat(summary.count()).isZero();
+ assertThat(summary.totalAmount()).isZero();
+ assertThat(functionCounter.count()).isZero();
+ assertThat(functionTimer.count()).isZero();
+ assertThat(functionTimer.totalTime(MILLISECONDS)).isZero();
+
+ // before publishing, simulate a step boundary being crossed after forced rollover
+ // on close and before/during publishing
+ registry.setPrePublishAction(() -> clock.add(config.step()));
+ // force rollover and publish on close
+ registry.close();
+
+ assertThat(registry.publishedCounterCounts).hasSize(1);
+ assertThat(registry.publishedCounterCounts.pop()).isOne();
+ assertThat(registry.publishedTimerCounts).hasSize(1);
+ assertThat(registry.publishedTimerCounts.pop()).isOne();
+ assertThat(registry.publishedTimerSumMilliseconds).hasSize(1);
+ assertThat(registry.publishedTimerSumMilliseconds.pop()).isEqualTo(5.0);
+ assertThat(registry.publishedSummaryCounts).hasSize(1);
+ assertThat(registry.publishedSummaryCounts.pop()).isOne();
+ assertThat(registry.publishedSummaryTotals).hasSize(1);
+ assertThat(registry.publishedSummaryTotals.pop()).isEqualTo(7);
+ assertThat(registry.publishedFunctionCounterCounts).hasSize(1);
+ assertThat(registry.publishedFunctionCounterCounts.pop()).isEqualTo(15);
+ assertThat(registry.publishedFunctionTimerCounts).hasSize(1);
+ assertThat(registry.publishedFunctionTimerCounts.pop()).isEqualTo(3);
+ assertThat(registry.publishedFunctionTimerTotals).hasSize(1);
+ assertThat(registry.publishedFunctionTimerTotals.pop()).isEqualTo(53);
+ }
+
private class MyStepMeterRegistry extends StepMeterRegistry {
Deque publishedCounterCounts = new ArrayDeque<>();
@@ -311,12 +360,22 @@ private class MyStepMeterRegistry extends StepMeterRegistry {
Deque publishedFunctionTimerTotals = new ArrayDeque<>();
+ @Nullable
+ Runnable prePublishAction;
+
MyStepMeterRegistry() {
super(StepMeterRegistryTest.this.config, StepMeterRegistryTest.this.clock);
}
+ void setPrePublishAction(Runnable prePublishAction) {
+ this.prePublishAction = prePublishAction;
+ }
+
@Override
protected void publish() {
+ if (prePublishAction != null) {
+ prePublishAction.run();
+ }
publishes.incrementAndGet();
getMeters().stream()
.map(meter -> meter.match(null, this::publishCounter, this::publishTimer, this::publishSummary, null,
diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepTimerTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepTimerTest.java
index 95bbcf2d2b..b434f97c25 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepTimerTest.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepTimerTest.java
@@ -52,7 +52,7 @@ void meanShouldWorkIfTotalTimeNotCalled() {
}
@Test
- void manualRolloverPartialStep() {
+ void closingRolloverPartialStep() {
Duration stepDuration = Duration.ofMillis(10);
StepTimer timer = new StepTimer(mock(Meter.Id.class), clock,
DistributionStatisticConfig.builder().expiry(stepDuration).bufferLength(2).build(),
@@ -63,7 +63,12 @@ void manualRolloverPartialStep() {
assertThat(timer.count()).isZero();
assertThat(timer.totalTime(TimeUnit.MILLISECONDS)).isZero();
- timer._manualRollover();
+ timer._closingRollover();
+
+ assertThat(timer.count()).isEqualTo(2);
+ assertThat(timer.totalTime(TimeUnit.MILLISECONDS)).isEqualTo(100);
+
+ clock.add(stepDuration);
assertThat(timer.count()).isEqualTo(2);
assertThat(timer.totalTime(TimeUnit.MILLISECONDS)).isEqualTo(100);
diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepValueTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepValueTest.java
index 59a54eedc0..39972f111d 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepValueTest.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/step/StepValueTest.java
@@ -15,10 +15,12 @@
*/
package io.micrometer.core.instrument.step;
+import io.micrometer.core.Issue;
import io.micrometer.core.instrument.MockClock;
import org.junit.jupiter.api.Test;
import java.time.Duration;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
@@ -69,8 +71,39 @@ public Long noValue() {
aLong.set(27);
assertThat(stepValue.poll()).isEqualTo(24L);
- stepValue.manualRollover();
+ stepValue.closingRollover();
assertThat(stepValue.poll()).isEqualTo(27L);
}
+ @Test
+ @Issue("#3720")
+ void closingRolloverShouldNotDropDataOnStepCompletion() {
+ final MockClock clock = new MockClock();
+ final long stepTime = 60;
+ final AtomicLong aLong = new AtomicLong(10);
+ final StepValue stepValue = new StepValue(clock, stepTime) {
+ @Override
+ public Supplier valueSupplier() {
+ return () -> aLong.getAndSet(0);
+ }
+
+ @Override
+ public Long noValue() {
+ return 0L;
+ }
+ };
+ clock.add(Duration.ofMillis(1));
+ assertThat(stepValue.poll()).isZero();
+ clock.add(Duration.ofMillis(59));
+ assertThat(stepValue.poll()).isEqualTo(10);
+ clock.add(Duration.ofMillis(stepTime - 1));
+ aLong.set(5);
+ stepValue.closingRollover();
+ assertThat(stepValue.poll()).isEqualTo(5);
+ clock.add(Duration.ofMillis(1));
+ assertThat(stepValue.poll()).isEqualTo(5L);
+ clock.add(stepTime, TimeUnit.MILLISECONDS);
+ assertThat(stepValue.poll()).isEqualTo(5L);
+ }
+
}
diff --git a/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ClassPathExclusions.java b/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ClassPathExclusions.java
index e9b0f627d8..f6292ea5b1 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ClassPathExclusions.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ClassPathExclusions.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2019 the original author or authors.
+ * Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,7 +27,7 @@
* @author Andy Wilkinson
*/
@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.TYPE)
+@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@ExtendWith(ModifiedClassPathExtension.class)
public @interface ClassPathExclusions {
diff --git a/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ClassPathOverrides.java b/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ClassPathOverrides.java
index eaec561d57..941566569e 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ClassPathOverrides.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ClassPathOverrides.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2019 the original author or authors.
+ * Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,7 +26,7 @@
* @author Andy Wilkinson
*/
@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.TYPE)
+@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@ExtendWith(ModifiedClassPathExtension.class)
public @interface ClassPathOverrides {
diff --git a/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ForkedClassPath.java b/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ForkedClassPath.java
index 70f591d395..5a401a682e 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ForkedClassPath.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ForkedClassPath.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2020 the original author or authors.
+ * Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,14 +21,14 @@
import java.lang.annotation.*;
/**
- * Annotation used to fork the classpath. This can be helpful where neither
- * {@link ClassPathExclusions} or {@link ClassPathOverrides} are needed, but just a copy
- * of the classpath.
+ * Annotation used to fork the classpath. This can be helpful when using annotations on
+ * parameterized tests, or where neither {@link ClassPathExclusions} or
+ * {@link ClassPathOverrides} are needed, but just a copy of the classpath.
*
* @author Christoph Dreis
*/
@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.TYPE)
+@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@ExtendWith(ModifiedClassPathExtension.class)
public @interface ForkedClassPath {
diff --git a/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ModifiedClassPathClassLoader.java b/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ModifiedClassPathClassLoader.java
index f6189ea4d5..afd69e9071 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ModifiedClassPathClassLoader.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/testsupport/classpath/ModifiedClassPathClassLoader.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2022 the original author or authors.
+ * Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,26 @@
package io.micrometer.core.testsupport.classpath;
+import java.io.File;
+import java.lang.management.ManagementFactory;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Method;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
@@ -32,23 +52,14 @@
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
import org.eclipse.aether.transport.http.HttpTransporterFactory;
+
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ConcurrentReferenceHashMap;
+import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
-import java.io.File;
-import java.lang.management.ManagementFactory;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.util.*;
-import java.util.jar.Attributes;
-import java.util.jar.JarFile;
-import java.util.regex.Pattern;
-import java.util.stream.Stream;
-
/**
* Custom {@link URLClassLoader} that modifies the class path.
*
@@ -57,7 +68,7 @@
*/
final class ModifiedClassPathClassLoader extends URLClassLoader {
- private static final Map, ModifiedClassPathClassLoader> cache = new ConcurrentReferenceHashMap<>();
+ private static final Map, ModifiedClassPathClassLoader> cache = new ConcurrentReferenceHashMap<>();
private static final Pattern INTELLIJ_CLASSPATH_JAR_PATTERN = Pattern.compile(".*classpath(\\d+)?\\.jar");
@@ -79,19 +90,45 @@ public Class> loadClass(String name) throws ClassNotFoundException {
return super.loadClass(name);
}
- static ModifiedClassPathClassLoader get(Class> testClass) {
- return cache.computeIfAbsent(testClass, ModifiedClassPathClassLoader::compute);
+ static ModifiedClassPathClassLoader get(Class> testClass, Method testMethod, List