20 changed files with 601 additions and 30 deletions
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
/* |
||||
* 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.autoconfigure.metrics.startup; |
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry; |
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; |
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; |
||||
import org.springframework.boot.actuate.metrics.startup.StartupTimeMetrics; |
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter; |
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
|
||||
/** |
||||
* {@link EnableAutoConfiguration Auto-configuration} for the {@link StartupTimeMetrics}. |
||||
* |
||||
* @author Chris Bono |
||||
* @since 2.6.0 |
||||
*/ |
||||
@Configuration(proxyBeanMethods = false) |
||||
@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) |
||||
@ConditionalOnClass(MeterRegistry.class) |
||||
@ConditionalOnBean(MeterRegistry.class) |
||||
public class StartupTimeMetricsAutoConfiguration { |
||||
|
||||
@Bean |
||||
@ConditionalOnMissingBean |
||||
StartupTimeMetrics startupTimeMetrics(MeterRegistry meterRegistry) { |
||||
return new StartupTimeMetrics(meterRegistry); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,20 @@
@@ -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. |
||||
*/ |
||||
|
||||
/** |
||||
* Auto-configuration for actuator startup time metrics. |
||||
*/ |
||||
package org.springframework.boot.actuate.autoconfigure.metrics.startup; |
||||
@ -0,0 +1,92 @@
@@ -0,0 +1,92 @@
|
||||
/* |
||||
* 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.autoconfigure.metrics.startup; |
||||
|
||||
import java.time.Duration; |
||||
|
||||
import io.micrometer.core.instrument.Tags; |
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.boot.SpringApplication; |
||||
import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; |
||||
import org.springframework.boot.actuate.metrics.startup.StartupTimeMetrics; |
||||
import org.springframework.boot.autoconfigure.AutoConfigurations; |
||||
import org.springframework.boot.context.event.ApplicationReadyEvent; |
||||
import org.springframework.boot.context.event.ApplicationStartedEvent; |
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link StartupTimeMetricsAutoConfiguration}. |
||||
* |
||||
* @author Chris Bono |
||||
*/ |
||||
class StartupTimeMetricsAutoConfigurationTests { |
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) |
||||
.withConfiguration(AutoConfigurations.of(StartupTimeMetricsAutoConfiguration.class)); |
||||
|
||||
@Test |
||||
void startupTimeMetricsAreRecorded() { |
||||
this.contextRunner.run((context) -> { |
||||
context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, |
||||
context.getSourceApplicationContext(), Duration.ofMillis(2500))); |
||||
context.publishEvent(new ApplicationReadyEvent(new SpringApplication(), null, |
||||
context.getSourceApplicationContext(), Duration.ofMillis(3000))); |
||||
assertThat(context).hasSingleBean(StartupTimeMetrics.class); |
||||
SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); |
||||
assertThat(registry.find("application.started.time").timeGauge()).isNotNull(); |
||||
assertThat(registry.find("application.ready.time").timeGauge()).isNotNull(); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void startupTimeMetricsCanBeDisabled() { |
||||
this.contextRunner.withPropertyValues("management.metrics.enable.application.started.time:false", |
||||
"management.metrics.enable.application.ready.time:false").run((context) -> { |
||||
context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, |
||||
context.getSourceApplicationContext(), Duration.ofMillis(2500))); |
||||
context.publishEvent(new ApplicationReadyEvent(new SpringApplication(), null, |
||||
context.getSourceApplicationContext(), Duration.ofMillis(3000))); |
||||
SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); |
||||
assertThat(registry.find("application.started.time").timeGauge()).isNull(); |
||||
assertThat(registry.find("application.ready.time").timeGauge()).isNull(); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void customStartupTimeMetricsAreRespected() { |
||||
this.contextRunner.withUserConfiguration(CustomStartupTimeMetricsConfiguration.class) |
||||
.run((context) -> assertThat(context).hasSingleBean(StartupTimeMetrics.class) |
||||
.hasBean("customStartTimeMetrics")); |
||||
} |
||||
|
||||
@Configuration(proxyBeanMethods = false) |
||||
static class CustomStartupTimeMetricsConfiguration { |
||||
|
||||
@Bean |
||||
StartupTimeMetrics customStartTimeMetrics() { |
||||
return new StartupTimeMetrics(new SimpleMeterRegistry(), Tags.empty(), "myapp.started", "myapp.ready"); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,109 @@
@@ -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 @@
@@ -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 @@
@@ -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 { |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue