Add startup time metrics
See gh-27878
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2012-2021 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.metrics.startup;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import io.micrometer.core.instrument.TimeGauge;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.event.SmartApplicationListener;
|
||||
|
||||
/**
|
||||
* Binds application startup metrics in response to {@link ApplicationStartedEvent} and
|
||||
* {@link ApplicationReadyEvent}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @since 2.6.0
|
||||
*/
|
||||
public class StartupTimeMetrics implements SmartApplicationListener {
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
private final String applicationStartedTimeMetricName;
|
||||
|
||||
private final String applicationReadyTimeMetricName;
|
||||
|
||||
private final Iterable<Tag> tags;
|
||||
|
||||
public StartupTimeMetrics(MeterRegistry meterRegistry) {
|
||||
this(meterRegistry, Collections.emptyList(), "application.started.time", "application.ready.time");
|
||||
}
|
||||
|
||||
public StartupTimeMetrics(MeterRegistry meterRegistry, Iterable<Tag> tags, String applicationStartedTimeMetricName,
|
||||
String applicationReadyTimeMetricName) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
this.tags = (tags != null) ? tags : Collections.emptyList();
|
||||
this.applicationStartedTimeMetricName = applicationStartedTimeMetricName;
|
||||
this.applicationReadyTimeMetricName = applicationReadyTimeMetricName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
|
||||
return ApplicationStartedEvent.class.isAssignableFrom(eventType)
|
||||
|| ApplicationReadyEvent.class.isAssignableFrom(eventType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEvent event) {
|
||||
if (event instanceof ApplicationStartedEvent) {
|
||||
onApplicationStarted((ApplicationStartedEvent) event);
|
||||
}
|
||||
if (event instanceof ApplicationReadyEvent) {
|
||||
onApplicationReady((ApplicationReadyEvent) event);
|
||||
}
|
||||
}
|
||||
|
||||
private void onApplicationStarted(ApplicationStartedEvent event) {
|
||||
if (event.getStartupTime() == null) {
|
||||
return;
|
||||
}
|
||||
TimeGauge
|
||||
.builder(this.applicationStartedTimeMetricName, () -> event.getStartupTime().toMillis(),
|
||||
TimeUnit.MILLISECONDS)
|
||||
.tags(maybeDcorateTagsWithApplicationInfo(event.getSpringApplication()))
|
||||
.description("Time taken (ms) to start the application").register(this.meterRegistry);
|
||||
}
|
||||
|
||||
private void onApplicationReady(ApplicationReadyEvent event) {
|
||||
if (event.getStartupTime() == null) {
|
||||
return;
|
||||
}
|
||||
TimeGauge
|
||||
.builder(this.applicationReadyTimeMetricName, () -> event.getStartupTime().toMillis(),
|
||||
TimeUnit.MILLISECONDS)
|
||||
.tags(maybeDcorateTagsWithApplicationInfo(event.getSpringApplication()))
|
||||
.description("Time taken (ms) for the application to be ready to serve requests")
|
||||
.register(this.meterRegistry);
|
||||
}
|
||||
|
||||
private Iterable<Tag> maybeDcorateTagsWithApplicationInfo(SpringApplication springApplication) {
|
||||
Class<?> mainClass = springApplication.getMainApplicationClass();
|
||||
if (mainClass == null) {
|
||||
return this.tags;
|
||||
}
|
||||
return Tags.concat(this.tags, "main-application-class", mainClass.getName());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2021 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Actuator support for startup metrics.
|
||||
*/
|
||||
package org.springframework.boot.actuate.metrics.startup;
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2012-2021 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.metrics.startup;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link StartupTimeMetrics}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
class StartupTimeMetricsTests {
|
||||
|
||||
private static final long APP_STARTED_TIME_MS = 2500;
|
||||
|
||||
private static final long APP_RUNNING_TIME_MS = 2900;
|
||||
|
||||
private MeterRegistry registry;
|
||||
|
||||
private StartupTimeMetrics metrics;
|
||||
|
||||
@BeforeEach
|
||||
void prepareUnit() {
|
||||
this.registry = new SimpleMeterRegistry();
|
||||
this.metrics = new StartupTimeMetrics(this.registry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void metricsRecordedWithoutCustomTags() {
|
||||
this.metrics.onApplicationEvent(applicationStartedEvent(APP_STARTED_TIME_MS));
|
||||
this.metrics.onApplicationEvent(applicationReadyEvent(APP_RUNNING_TIME_MS));
|
||||
assertMetricExistsWithValue("application.started.time", APP_STARTED_TIME_MS);
|
||||
assertMetricExistsWithValue("application.ready.time", APP_RUNNING_TIME_MS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void metricsRecordedWithCustomTagsAndMetricNames() {
|
||||
Tags tags = Tags.of("foo", "bar");
|
||||
this.metrics = new StartupTimeMetrics(this.registry, tags, "m1", "m2");
|
||||
this.metrics.onApplicationEvent(applicationStartedEvent(APP_STARTED_TIME_MS));
|
||||
this.metrics.onApplicationEvent(applicationReadyEvent(APP_RUNNING_TIME_MS));
|
||||
assertMetricExistsWithCustomTagsAndValue("m1", tags, APP_STARTED_TIME_MS);
|
||||
assertMetricExistsWithCustomTagsAndValue("m2", tags, APP_RUNNING_TIME_MS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void metricsRecordedWithoutMainAppClassTagWhenMainAppClassNotAvailable() {
|
||||
this.metrics.onApplicationEvent(applicationStartedEvent(APP_STARTED_TIME_MS));
|
||||
this.metrics.onApplicationEvent(applicationReadyEvent(APP_RUNNING_TIME_MS));
|
||||
assertThat(this.registry.find("application.started.time").timeGauge()).isNotNull();
|
||||
assertThat(this.registry.find("application.ready.time").timeGauge()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void metricsNotRecordedWhenStartupTimeNotAvailable() {
|
||||
this.metrics.onApplicationEvent(applicationStartedEvent(null));
|
||||
this.metrics.onApplicationEvent(applicationReadyEvent(null));
|
||||
assertThat(this.registry.find("application.started.time").timeGauge()).isNull();
|
||||
assertThat(this.registry.find("application.ready.time").timeGauge()).isNull();
|
||||
}
|
||||
|
||||
private ApplicationStartedEvent applicationStartedEvent(Long startupTimeMs) {
|
||||
SpringApplication application = mock(SpringApplication.class);
|
||||
doReturn(TestMainApplication.class).when(application).getMainApplicationClass();
|
||||
return new ApplicationStartedEvent(application, null, null,
|
||||
(startupTimeMs != null) ? Duration.ofMillis(startupTimeMs) : null);
|
||||
}
|
||||
|
||||
private ApplicationReadyEvent applicationReadyEvent(Long startupTimeMs) {
|
||||
SpringApplication application = mock(SpringApplication.class);
|
||||
doReturn(TestMainApplication.class).when(application).getMainApplicationClass();
|
||||
return new ApplicationReadyEvent(application, null, null,
|
||||
(startupTimeMs != null) ? Duration.ofMillis(startupTimeMs) : null);
|
||||
}
|
||||
|
||||
private void assertMetricExistsWithValue(String metricName, double expectedValueInMillis) {
|
||||
assertMetricExistsWithCustomTagsAndValue(metricName, Tags.empty(), expectedValueInMillis);
|
||||
}
|
||||
|
||||
private void assertMetricExistsWithCustomTagsAndValue(String metricName, Tags expectedCustomTags,
|
||||
double expectedValueInMillis) {
|
||||
assertThat(this.registry.find(metricName)
|
||||
.tags(Tags.concat(expectedCustomTags, "main-application-class", TestMainApplication.class.getName()))
|
||||
.timeGauge()).isNotNull().extracting((m) -> m.value(TimeUnit.MILLISECONDS))
|
||||
.isEqualTo(expectedValueInMillis);
|
||||
}
|
||||
|
||||
static class TestMainApplication {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user