Skip to content

Commit 2e67963

Browse files
onobcsnicoll
authored andcommitted
Add startup time metrics
See gh-27878
1 parent 32cfde0 commit 2e67963

File tree

20 files changed

+601
-30
lines changed

20 files changed

+601
-30
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.metrics.startup;
18+
19+
import io.micrometer.core.instrument.MeterRegistry;
20+
21+
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration;
22+
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
23+
import org.springframework.boot.actuate.metrics.startup.StartupTimeMetrics;
24+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
25+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
26+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
27+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
28+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
29+
import org.springframework.context.annotation.Bean;
30+
import org.springframework.context.annotation.Configuration;
31+
32+
/**
33+
* {@link EnableAutoConfiguration Auto-configuration} for the {@link StartupTimeMetrics}.
34+
*
35+
* @author Chris Bono
36+
* @since 2.6.0
37+
*/
38+
@Configuration(proxyBeanMethods = false)
39+
@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class })
40+
@ConditionalOnClass(MeterRegistry.class)
41+
@ConditionalOnBean(MeterRegistry.class)
42+
public class StartupTimeMetricsAutoConfiguration {
43+
44+
@Bean
45+
@ConditionalOnMissingBean
46+
StartupTimeMetrics startupTimeMetrics(MeterRegistry meterRegistry) {
47+
return new StartupTimeMetrics(meterRegistry);
48+
}
49+
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Auto-configuration for actuator startup time metrics.
19+
*/
20+
package org.springframework.boot.actuate.autoconfigure.metrics.startup;

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.mongo.MongoMetricsAutoCon
7575
org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa.HibernateMetricsAutoConfiguration,\
7676
org.springframework.boot.actuate.autoconfigure.metrics.r2dbc.ConnectionPoolMetricsAutoConfiguration,\
7777
org.springframework.boot.actuate.autoconfigure.metrics.redis.LettuceMetricsAutoConfiguration,\
78+
org.springframework.boot.actuate.autoconfigure.metrics.startup.StartupTimeMetricsAutoConfiguration,\
7879
org.springframework.boot.actuate.autoconfigure.metrics.task.TaskExecutorMetricsAutoConfiguration,\
7980
org.springframework.boot.actuate.autoconfigure.metrics.web.client.HttpClientMetricsAutoConfiguration,\
8081
org.springframework.boot.actuate.autoconfigure.metrics.web.jetty.JettyMetricsAutoConfiguration,\
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.metrics.startup;
18+
19+
import java.time.Duration;
20+
21+
import io.micrometer.core.instrument.Tags;
22+
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
23+
import org.junit.jupiter.api.Test;
24+
25+
import org.springframework.boot.SpringApplication;
26+
import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
27+
import org.springframework.boot.actuate.metrics.startup.StartupTimeMetrics;
28+
import org.springframework.boot.autoconfigure.AutoConfigurations;
29+
import org.springframework.boot.context.event.ApplicationReadyEvent;
30+
import org.springframework.boot.context.event.ApplicationStartedEvent;
31+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
32+
import org.springframework.context.annotation.Bean;
33+
import org.springframework.context.annotation.Configuration;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
37+
/**
38+
* Tests for {@link StartupTimeMetricsAutoConfiguration}.
39+
*
40+
* @author Chris Bono
41+
*/
42+
class StartupTimeMetricsAutoConfigurationTests {
43+
44+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple())
45+
.withConfiguration(AutoConfigurations.of(StartupTimeMetricsAutoConfiguration.class));
46+
47+
@Test
48+
void startupTimeMetricsAreRecorded() {
49+
this.contextRunner.run((context) -> {
50+
context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null,
51+
context.getSourceApplicationContext(), Duration.ofMillis(2500)));
52+
context.publishEvent(new ApplicationReadyEvent(new SpringApplication(), null,
53+
context.getSourceApplicationContext(), Duration.ofMillis(3000)));
54+
assertThat(context).hasSingleBean(StartupTimeMetrics.class);
55+
SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class);
56+
assertThat(registry.find("application.started.time").timeGauge()).isNotNull();
57+
assertThat(registry.find("application.ready.time").timeGauge()).isNotNull();
58+
});
59+
}
60+
61+
@Test
62+
void startupTimeMetricsCanBeDisabled() {
63+
this.contextRunner.withPropertyValues("management.metrics.enable.application.started.time:false",
64+
"management.metrics.enable.application.ready.time:false").run((context) -> {
65+
context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null,
66+
context.getSourceApplicationContext(), Duration.ofMillis(2500)));
67+
context.publishEvent(new ApplicationReadyEvent(new SpringApplication(), null,
68+
context.getSourceApplicationContext(), Duration.ofMillis(3000)));
69+
SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class);
70+
assertThat(registry.find("application.started.time").timeGauge()).isNull();
71+
assertThat(registry.find("application.ready.time").timeGauge()).isNull();
72+
});
73+
}
74+
75+
@Test
76+
void customStartupTimeMetricsAreRespected() {
77+
this.contextRunner.withUserConfiguration(CustomStartupTimeMetricsConfiguration.class)
78+
.run((context) -> assertThat(context).hasSingleBean(StartupTimeMetrics.class)
79+
.hasBean("customStartTimeMetrics"));
80+
}
81+
82+
@Configuration(proxyBeanMethods = false)
83+
static class CustomStartupTimeMetricsConfiguration {
84+
85+
@Bean
86+
StartupTimeMetrics customStartTimeMetrics() {
87+
return new StartupTimeMetrics(new SimpleMeterRegistry(), Tags.empty(), "myapp.started", "myapp.ready");
88+
}
89+
90+
}
91+
92+
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfigurationTests.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.metrics.web.jetty;
1818

19+
import java.time.Duration;
20+
1921
import io.micrometer.core.instrument.MeterRegistry;
2022
import io.micrometer.core.instrument.Tags;
2123
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
@@ -58,7 +60,7 @@ void autoConfiguresThreadPoolMetricsWithEmbeddedServletJetty() {
5860
.withUserConfiguration(ServletWebServerConfiguration.class, MeterRegistryConfiguration.class)
5961
.run((context) -> {
6062
context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null,
61-
context.getSourceApplicationContext()));
63+
context.getSourceApplicationContext(), Duration.ZERO));
6264
assertThat(context).hasSingleBean(JettyServerThreadPoolMetricsBinder.class);
6365
SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class);
6466
assertThat(registry.find("jetty.threads.config.min").meter()).isNotNull();
@@ -73,7 +75,7 @@ void autoConfiguresThreadPoolMetricsWithEmbeddedReactiveJetty() {
7375
.withUserConfiguration(ReactiveWebServerConfiguration.class, MeterRegistryConfiguration.class)
7476
.run((context) -> {
7577
context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null,
76-
context.getSourceApplicationContext()));
78+
context.getSourceApplicationContext(), Duration.ZERO));
7779
SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class);
7880
assertThat(registry.find("jetty.threads.config.min").meter()).isNotNull();
7981
});
@@ -95,7 +97,7 @@ void autoConfiguresConnectionMetricsWithEmbeddedServletJetty() {
9597
.withUserConfiguration(ServletWebServerConfiguration.class, MeterRegistryConfiguration.class)
9698
.run((context) -> {
9799
context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null,
98-
context.getSourceApplicationContext()));
100+
context.getSourceApplicationContext(), Duration.ZERO));
99101
assertThat(context).hasSingleBean(JettyConnectionMetricsBinder.class);
100102
SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class);
101103
assertThat(registry.find("jetty.connections.messages.in").meter()).isNotNull();
@@ -110,7 +112,7 @@ void autoConfiguresConnectionMetricsWithEmbeddedReactiveJetty() {
110112
.withUserConfiguration(ReactiveWebServerConfiguration.class, MeterRegistryConfiguration.class)
111113
.run((context) -> {
112114
context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null,
113-
context.getSourceApplicationContext()));
115+
context.getSourceApplicationContext(), Duration.ZERO));
114116
SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class);
115117
assertThat(registry.find("jetty.connections.messages.in").meter()).isNotNull();
116118
});
@@ -125,7 +127,7 @@ void allowsCustomJettyConnectionMetricsBinderToBeUsed() {
125127
MeterRegistryConfiguration.class)
126128
.run((context) -> {
127129
context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null,
128-
context.getSourceApplicationContext()));
130+
context.getSourceApplicationContext(), Duration.ZERO));
129131
assertThat(context).hasSingleBean(JettyConnectionMetricsBinder.class)
130132
.hasBean("customJettyConnectionMetricsBinder");
131133
SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class);
@@ -144,7 +146,7 @@ void autoConfiguresSslHandshakeMetricsWithEmbeddedServletJetty() {
144146
"server.ssl.key-store-password: secret", "server.ssl.key-password: password")
145147
.run((context) -> {
146148
context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null,
147-
context.getSourceApplicationContext()));
149+
context.getSourceApplicationContext(), Duration.ZERO));
148150
assertThat(context).hasSingleBean(JettySslHandshakeMetricsBinder.class);
149151
SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class);
150152
assertThat(registry.find("jetty.ssl.handshakes").meter()).isNotNull();
@@ -161,7 +163,7 @@ void autoConfiguresSslHandshakeMetricsWithEmbeddedReactiveJetty() {
161163
"server.ssl.key-store-password: secret", "server.ssl.key-password: password")
162164
.run((context) -> {
163165
context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null,
164-
context.getSourceApplicationContext()));
166+
context.getSourceApplicationContext(), Duration.ZERO));
165167
SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class);
166168
assertThat(registry.find("jetty.ssl.handshakes").meter()).isNotNull();
167169
});
@@ -178,7 +180,7 @@ void allowsCustomJettySslHandshakeMetricsBinderToBeUsed() {
178180
"server.ssl.key-store-password: secret", "server.ssl.key-password: password")
179181
.run((context) -> {
180182
context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null,
181-
context.getSourceApplicationContext()));
183+
context.getSourceApplicationContext(), Duration.ZERO));
182184
assertThat(context).hasSingleBean(JettySslHandshakeMetricsBinder.class)
183185
.hasBean("customJettySslHandshakeMetricsBinder");
184186
SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class);

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/tomcat/TomcatMetricsAutoConfigurationTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat;
1818

19+
import java.time.Duration;
1920
import java.util.Collections;
2021
import java.util.concurrent.atomic.AtomicInteger;
2122

@@ -62,7 +63,7 @@ void autoConfiguresTomcatMetricsWithEmbeddedServletTomcat() {
6263
.withUserConfiguration(ServletWebServerConfiguration.class, MeterRegistryConfiguration.class)
6364
.withPropertyValues("server.tomcat.mbeanregistry.enabled=true").run((context) -> {
6465
context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null,
65-
context.getSourceApplicationContext()));
66+
context.getSourceApplicationContext(), Duration.ZERO));
6667
assertThat(context).hasSingleBean(TomcatMetricsBinder.class);
6768
SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class);
6869
assertThat(registry.find("tomcat.sessions.active.max").meter()).isNotNull();
@@ -79,7 +80,7 @@ void autoConfiguresTomcatMetricsWithEmbeddedReactiveTomcat() {
7980
.withUserConfiguration(ReactiveWebServerConfiguration.class, MeterRegistryConfiguration.class)
8081
.withPropertyValues("server.tomcat.mbeanregistry.enabled=true").run((context) -> {
8182
context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null,
82-
context.getSourceApplicationContext()));
83+
context.getSourceApplicationContext(), Duration.ZERO));
8384
SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class);
8485
assertThat(registry.find("tomcat.sessions.active.max").meter()).isNotNull();
8586
assertThat(registry.find("tomcat.threads.current").meter()).isNotNull();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.metrics.startup;
18+
19+
import java.util.Collections;
20+
import java.util.concurrent.TimeUnit;
21+
22+
import io.micrometer.core.instrument.MeterRegistry;
23+
import io.micrometer.core.instrument.Tag;
24+
import io.micrometer.core.instrument.Tags;
25+
import io.micrometer.core.instrument.TimeGauge;
26+
27+
import org.springframework.boot.SpringApplication;
28+
import org.springframework.boot.context.event.ApplicationReadyEvent;
29+
import org.springframework.boot.context.event.ApplicationStartedEvent;
30+
import org.springframework.context.ApplicationEvent;
31+
import org.springframework.context.event.SmartApplicationListener;
32+
33+
/**
34+
* Binds application startup metrics in response to {@link ApplicationStartedEvent} and
35+
* {@link ApplicationReadyEvent}.
36+
*
37+
* @author Chris Bono
38+
* @since 2.6.0
39+
*/
40+
public class StartupTimeMetrics implements SmartApplicationListener {
41+
42+
private final MeterRegistry meterRegistry;
43+
44+
private final String applicationStartedTimeMetricName;
45+
46+
private final String applicationReadyTimeMetricName;
47+
48+
private final Iterable<Tag> tags;
49+
50+
public StartupTimeMetrics(MeterRegistry meterRegistry) {
51+
this(meterRegistry, Collections.emptyList(), "application.started.time", "application.ready.time");
52+
}
53+
54+
public StartupTimeMetrics(MeterRegistry meterRegistry, Iterable<Tag> tags, String applicationStartedTimeMetricName,
55+
String applicationReadyTimeMetricName) {
56+
this.meterRegistry = meterRegistry;
57+
this.tags = (tags != null) ? tags : Collections.emptyList();
58+
this.applicationStartedTimeMetricName = applicationStartedTimeMetricName;
59+
this.applicationReadyTimeMetricName = applicationReadyTimeMetricName;
60+
}
61+
62+
@Override
63+
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
64+
return ApplicationStartedEvent.class.isAssignableFrom(eventType)
65+
|| ApplicationReadyEvent.class.isAssignableFrom(eventType);
66+
}
67+
68+
@Override
69+
public void onApplicationEvent(ApplicationEvent event) {
70+
if (event instanceof ApplicationStartedEvent) {
71+
onApplicationStarted((ApplicationStartedEvent) event);
72+
}
73+
if (event instanceof ApplicationReadyEvent) {
74+
onApplicationReady((ApplicationReadyEvent) event);
75+
}
76+
}
77+
78+
private void onApplicationStarted(ApplicationStartedEvent event) {
79+
if (event.getStartupTime() == null) {
80+
return;
81+
}
82+
TimeGauge
83+
.builder(this.applicationStartedTimeMetricName, () -> event.getStartupTime().toMillis(),
84+
TimeUnit.MILLISECONDS)
85+
.tags(maybeDcorateTagsWithApplicationInfo(event.getSpringApplication()))
86+
.description("Time taken (ms) to start the application").register(this.meterRegistry);
87+
}
88+
89+
private void onApplicationReady(ApplicationReadyEvent event) {
90+
if (event.getStartupTime() == null) {
91+
return;
92+
}
93+
TimeGauge
94+
.builder(this.applicationReadyTimeMetricName, () -> event.getStartupTime().toMillis(),
95+
TimeUnit.MILLISECONDS)
96+
.tags(maybeDcorateTagsWithApplicationInfo(event.getSpringApplication()))
97+
.description("Time taken (ms) for the application to be ready to serve requests")
98+
.register(this.meterRegistry);
99+
}
100+
101+
private Iterable<Tag> maybeDcorateTagsWithApplicationInfo(SpringApplication springApplication) {
102+
Class<?> mainClass = springApplication.getMainApplicationClass();
103+
if (mainClass == null) {
104+
return this.tags;
105+
}
106+
return Tags.concat(this.tags, "main-application-class", mainClass.getName());
107+
}
108+
109+
}

0 commit comments

Comments
 (0)