Skip to content

Commit fdf21da

Browse files
committed
Add buffering ApplicationStartup variant
As of spring-projects/spring-framework#24878, Spring Framework provides an `ApplicationStartup` infrastructure that applications can use to collect and track events during the application startup phase. This commit adds a new `BufferingApplicationStartup` implementation that buffer `StartupStep`s and tracks their execution time. Once buffered, these steps can be pushed to an external metrics system or drained through a web endpoint, to a file... Closes gh-22603
1 parent 17b6910 commit fdf21da

File tree

6 files changed

+593
-0
lines changed

6 files changed

+593
-0
lines changed

spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,37 @@ This feature could also be useful for any service wrapper implementation.
443443
TIP: If you want to know on which HTTP port the application is running, get the property with a key of `local.server.port`.
444444

445445

446+
[[boot-features-application-startup-tracking]]
447+
=== Application Startup tracking
448+
During the application startup, the `SpringApplication` and the `ApplicationContext` perform many tasks related to the application lifecycle,
449+
the beans lifecycle or even processing application events.
450+
With {spring-framework-api}/core/metrics/ApplicationStartup.html[ApplicationStartup`], Spring Framework {spring-framework-docs}/core.html#context-functionality-startup[allows you track the application startup sequence with `StartupStep`s].
451+
This data can be collected for profiling purposes, or just to have a better understanding of an application startup process.
452+
453+
You can choose a `ApplicationStartup` implementation when setting up the `SpringApplication` instance.
454+
For example, to use the `BufferingApplicationStartup`, you could write:
455+
456+
[source,java,indent=0]
457+
----
458+
public static void main(String[] args) {
459+
SpringApplication app = new SpringApplication(MySpringConfiguration.class);
460+
app.setApplicationStartup(new BufferingApplicationStartup(2048));
461+
app.run(args);
462+
}
463+
----
464+
465+
The first available implementation, `FlightRecorderApplicationStartup` is provided by Spring Framework.
466+
It adds Spring-specific startup events to a Java Flight Recorder session and is meant for profiling applications and correlating their Spring context lifecycle with JVM events (such as allocations, GCs, class loading...).
467+
Once configured, you can record data by running the application with the Flight Recorder enabled:
468+
469+
[source,bash,indent=0]
470+
----
471+
$ java -XX:StartFlightRecording:filename=recording.jfr,duration=10s -jar demo.jar
472+
----
473+
474+
Spring Boot ships with the `BufferingApplicationStartup` variant; this implementation is meant for buffering the startup steps and draining them into an external metrics system.
475+
Applications can ask for the bean of type `BufferingApplicationStartup` in any component.
476+
Additionally, Spring Boot Actuator will <<production-ready-features.adoc#production-ready-endpoints, expose a `startup` endpoint to expose this information as a JSON document>>.
446477

447478
[[boot-features-external-config]]
448479
== Externalized Configuration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
* Copyright 2002-2020 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.context.metrics.buffering;
18+
19+
import java.util.Iterator;
20+
import java.util.function.Consumer;
21+
import java.util.function.Supplier;
22+
23+
import org.springframework.core.metrics.StartupStep;
24+
25+
/**
26+
* {@link StartupStep} implementation to be buffered by
27+
* {@link BufferingApplicationStartup}. Its processing time is recorded using
28+
* {@link System#nanoTime()}.
29+
*
30+
* @author Brian Clozel
31+
*/
32+
class BufferedStartupStep implements StartupStep {
33+
34+
private final String name;
35+
36+
private final long id;
37+
38+
private final Long parentId;
39+
40+
private long startTime;
41+
42+
private long endTime;
43+
44+
private final DefaultTags tags;
45+
46+
private final Consumer<BufferedStartupStep> recorder;
47+
48+
BufferedStartupStep(long id, String name, Long parentId, Consumer<BufferedStartupStep> recorder) {
49+
this.id = id;
50+
this.parentId = parentId;
51+
this.tags = new DefaultTags();
52+
this.name = name;
53+
this.recorder = recorder;
54+
}
55+
56+
@Override
57+
public String getName() {
58+
return this.name;
59+
}
60+
61+
@Override
62+
public long getId() {
63+
return this.id;
64+
}
65+
66+
@Override
67+
public Long getParentId() {
68+
return this.parentId;
69+
}
70+
71+
@Override
72+
public Tags getTags() {
73+
return this.tags;
74+
}
75+
76+
@Override
77+
public StartupStep tag(String key, String value) {
78+
if (this.endTime != 0L) {
79+
throw new IllegalStateException("StartupStep has already ended.");
80+
}
81+
this.tags.add(key, value);
82+
return this;
83+
}
84+
85+
@Override
86+
public StartupStep tag(String key, Supplier<String> value) {
87+
return this.tag(key, value.get());
88+
}
89+
90+
@Override
91+
public void end() {
92+
this.recorder.accept(this);
93+
}
94+
95+
long getStartTime() {
96+
return this.startTime;
97+
}
98+
99+
void recordStartTime(long startTime) {
100+
this.startTime = startTime;
101+
}
102+
103+
long getEndTime() {
104+
return this.endTime;
105+
}
106+
107+
void recordEndTime(long endTime) {
108+
this.endTime = endTime;
109+
}
110+
111+
static class DefaultTags implements Tags {
112+
113+
private Tag[] tags = new Tag[0];
114+
115+
void add(String key, String value) {
116+
Tag[] newTags = new Tag[this.tags.length + 1];
117+
System.arraycopy(this.tags, 0, newTags, 0, this.tags.length);
118+
newTags[newTags.length - 1] = new DefaultTag(key, value);
119+
this.tags = newTags;
120+
}
121+
122+
@Override
123+
public Iterator<Tag> iterator() {
124+
return new TagsIterator();
125+
}
126+
127+
private class TagsIterator implements Iterator<Tag> {
128+
129+
private int idx = 0;
130+
131+
@Override
132+
public boolean hasNext() {
133+
return this.idx < DefaultTags.this.tags.length;
134+
}
135+
136+
@Override
137+
public Tag next() {
138+
return DefaultTags.this.tags[this.idx++];
139+
}
140+
141+
@Override
142+
public void remove() {
143+
throw new UnsupportedOperationException("tags are append only");
144+
}
145+
146+
}
147+
148+
}
149+
150+
static class DefaultTag implements Tag {
151+
152+
private final String key;
153+
154+
private final String value;
155+
156+
DefaultTag(String key, String value) {
157+
this.key = key;
158+
this.value = value;
159+
}
160+
161+
@Override
162+
public String getKey() {
163+
return this.key;
164+
}
165+
166+
@Override
167+
public String getValue() {
168+
return this.value;
169+
}
170+
171+
}
172+
173+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright 2002-2020 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.context.metrics.buffering;
18+
19+
import java.time.Instant;
20+
import java.util.ArrayDeque;
21+
import java.util.ArrayList;
22+
import java.util.Deque;
23+
import java.util.List;
24+
import java.util.concurrent.BlockingQueue;
25+
import java.util.concurrent.LinkedBlockingQueue;
26+
import java.util.function.Predicate;
27+
28+
import org.springframework.core.metrics.ApplicationStartup;
29+
import org.springframework.core.metrics.StartupStep;
30+
import org.springframework.util.Assert;
31+
32+
/**
33+
* {@link ApplicationStartup} implementation that buffers {@link StartupStep steps} and
34+
* records their timestamp as well as their processing time.
35+
* <p>
36+
* Once recording has been {@link #startRecording() started}, steps are buffered up until
37+
* the configured {@link #BufferingApplicationStartup(int) capacity}; after that, new
38+
* steps are not recorded.
39+
* <p>
40+
* There are several ways to keep the buffer size low:
41+
* <ul>
42+
* <li>configuring {@link #addFilter(Predicate) filters} to only record steps that are
43+
* relevant to us.
44+
* <li>{@link #drainBufferedTimeline() draining} the buffered steps.
45+
* </ul>
46+
*
47+
* @author Brian Clozel
48+
* @since 2.4.0
49+
*/
50+
public class BufferingApplicationStartup implements ApplicationStartup {
51+
52+
private Instant recordingStartTime;
53+
54+
private long recordingStartNanos;
55+
56+
private long currentSequenceId = 0;
57+
58+
private final Deque<Long> currentSteps;
59+
60+
private final BlockingQueue<BufferedStartupStep> recordedSteps;
61+
62+
private Predicate<StartupStep> stepFilters = (step) -> true;
63+
64+
/**
65+
* Create a new buffered {@link ApplicationStartup} with a limited capacity and starts
66+
* the recording of steps.
67+
* @param capacity the configured capacity; once reached, new steps are not recorded.
68+
*/
69+
public BufferingApplicationStartup(int capacity) {
70+
this.currentSteps = new ArrayDeque<>();
71+
this.currentSteps.offerFirst(this.currentSequenceId);
72+
this.recordedSteps = new LinkedBlockingQueue<>(capacity);
73+
startRecording();
74+
}
75+
76+
/**
77+
* Start the recording of steps and mark the beginning of the {@link StartupTimeline}.
78+
* The class constructor already implicitly calls this, but it is possible to reset it
79+
* as long as steps have not been recorded already.
80+
* @throws IllegalStateException if called and {@link StartupStep} have been recorded
81+
* already.
82+
*/
83+
public void startRecording() {
84+
Assert.state(this.recordedSteps.isEmpty(), "Cannot restart recording once steps have been buffered.");
85+
this.recordingStartTime = Instant.now();
86+
this.recordingStartNanos = getCurrentTime();
87+
}
88+
89+
/**
90+
* Add a predicate filter to the list of existing ones.
91+
* <p>
92+
* A {@link StartupStep step} that doesn't match all filters will not be recorded.
93+
* @param filter the predicate filter to add.
94+
*/
95+
public void addFilter(Predicate<StartupStep> filter) {
96+
this.stepFilters = this.stepFilters.and(filter);
97+
}
98+
99+
/**
100+
* Return the {@link StartupTimeline timeline} as a snapshot of currently buffered
101+
* steps.
102+
* <p>
103+
* This will not remove steps from the buffer, see {@link #drainBufferedTimeline()}
104+
* for its counterpart.
105+
* @return a snapshot of currently buffered steps.
106+
*/
107+
public StartupTimeline getBufferedTimeline() {
108+
return new StartupTimeline(this.recordingStartTime, this.recordingStartNanos, this.recordedSteps);
109+
}
110+
111+
/**
112+
* Return the {@link StartupTimeline timeline} by pulling steps from the buffer.
113+
* <p>
114+
* This removes steps from the buffer, see {@link #getBufferedTimeline()} for its
115+
* read-only counterpart.
116+
* @return buffered steps drained from the buffer.
117+
*/
118+
public StartupTimeline drainBufferedTimeline() {
119+
List<BufferedStartupStep> steps = new ArrayList<>(this.recordedSteps.size());
120+
this.recordedSteps.drainTo(steps);
121+
return new StartupTimeline(this.recordingStartTime, this.recordingStartNanos, steps);
122+
}
123+
124+
@Override
125+
public StartupStep start(String name) {
126+
BufferedStartupStep step = new BufferedStartupStep(++this.currentSequenceId, name,
127+
this.currentSteps.peekFirst(), this::record);
128+
step.recordStartTime(getCurrentTime());
129+
this.currentSteps.offerFirst(this.currentSequenceId);
130+
return step;
131+
}
132+
133+
private void record(BufferedStartupStep step) {
134+
step.recordEndTime(getCurrentTime());
135+
if (this.stepFilters.test(step)) {
136+
this.recordedSteps.offer(step);
137+
}
138+
this.currentSteps.removeFirst();
139+
}
140+
141+
private long getCurrentTime() {
142+
return System.nanoTime();
143+
}
144+
145+
}

0 commit comments

Comments
 (0)