Skip to content

Commit 2174833

Browse files
committed
Add configurable startup time format
Introduce a new configuration property 'spring.main.log-startup-time-format' to control how application startup times are displayed in logs. This addresses concerns about backward compatibility with existing log parsing tools. The property supports two formats: - 'decimal' (default): Displays time in seconds with millisecond precision (e.g., "3.456 seconds"). This maintains backward compatibility. - 'human-readable': Displays time in a more intuitive way using appropriate units (e.g., "1 minute 30 seconds" or "1 hour 15 minutes"). By defaulting to the existing decimal format, this change ensures no breaking changes for existing deployments while providing an opt-in mechanism for users who prefer more readable startup time logs. Signed-off-by: Huang Xiao <youngledo@qq.com>
1 parent 29d0299 commit 2174833

File tree

5 files changed

+169
-7
lines changed

5 files changed

+169
-7
lines changed

core/spring-boot/src/main/java/org/springframework/boot/ApplicationProperties.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ class ApplicationProperties {
6868
*/
6969
private boolean logStartupInfo = true;
7070

71+
/**
72+
* Format used to display application startup time in logs.
73+
*/
74+
private StartupTimeFormat logStartupTimeFormat = StartupTimeFormat.DECIMAL;
75+
7176
/**
7277
* Whether the application should have a shutdown hook registered.
7378
*/
@@ -139,6 +144,14 @@ void setLogStartupInfo(boolean logStartupInfo) {
139144
this.logStartupInfo = logStartupInfo;
140145
}
141146

147+
StartupTimeFormat getLogStartupTimeFormat() {
148+
return this.logStartupTimeFormat;
149+
}
150+
151+
void setLogStartupTimeFormat(StartupTimeFormat logStartupTimeFormat) {
152+
this.logStartupTimeFormat = logStartupTimeFormat;
153+
}
154+
142155
boolean isRegisterShutdownHook() {
143156
return this.registerShutdownHook;
144157
}

core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,8 @@ public ConfigurableApplicationContext run(String... args) {
322322
afterRefresh(context, applicationArguments);
323323
Duration timeTakenToStarted = startup.started();
324324
if (this.properties.isLogStartupInfo()) {
325-
new StartupInfoLogger(this.mainApplicationClass, environment).logStarted(getApplicationLog(), startup);
325+
new StartupInfoLogger(this.mainApplicationClass, environment, this.properties.getLogStartupTimeFormat())
326+
.logStarted(getApplicationLog(), startup);
326327
}
327328
listeners.started(context, timeTakenToStarted);
328329
callRunners(context, applicationArguments);

core/spring-boot/src/main/java/org/springframework/boot/StartupInfoLogger.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,16 @@ class StartupInfoLogger {
4444

4545
private final Environment environment;
4646

47+
private final StartupTimeFormat startupTimeFormat;
48+
4749
StartupInfoLogger(@Nullable Class<?> sourceClass, Environment environment) {
50+
this(sourceClass, environment, StartupTimeFormat.DECIMAL);
51+
}
52+
53+
StartupInfoLogger(@Nullable Class<?> sourceClass, Environment environment, StartupTimeFormat startupTimeFormat) {
4854
this.sourceClass = sourceClass;
4955
this.environment = environment;
56+
this.startupTimeFormat = startupTimeFormat;
5057
}
5158

5259
void logStarting(Log applicationLog) {
@@ -87,16 +94,18 @@ private CharSequence getStartedMessage(Startup startup) {
8794
message.append(startup.action());
8895
appendApplicationName(message);
8996
message.append(" in ");
90-
message.append(startup.timeTakenToStarted().toMillis() / 1000.0);
91-
message.append(" seconds");
97+
message.append(formatDuration(startup.timeTakenToStarted().toMillis()));
9298
Long uptimeMs = startup.processUptime();
9399
if (uptimeMs != null) {
94-
double uptime = uptimeMs / 1000.0;
95-
message.append(" (process running for ").append(uptime).append(")");
100+
message.append(" (process running for ").append(formatDuration(uptimeMs)).append(")");
96101
}
97102
return message;
98103
}
99104

105+
private String formatDuration(long millis) {
106+
return this.startupTimeFormat.format(millis);
107+
}
108+
100109
private void appendAotMode(StringBuilder message) {
101110
append(message, "", () -> AotDetector.useGeneratedArtifacts() ? "AOT-processed" : null);
102111
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2012-present 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;
18+
19+
import java.time.Duration;
20+
21+
/**
22+
* Format styles for displaying application startup time in logs.
23+
*
24+
* @author Huang Xiao
25+
* @since 3.5.0
26+
*/
27+
public enum StartupTimeFormat {
28+
29+
/**
30+
* Decimal format displays time in seconds with millisecond precision (e.g., "3.456
31+
* seconds"). This is the default format and maintains backward compatibility with
32+
* existing log parsing tools.
33+
*/
34+
DECIMAL {
35+
36+
@Override
37+
public String format(long millis) {
38+
return String.format("%.3f seconds", millis / 1000.0);
39+
}
40+
41+
},
42+
43+
/**
44+
* Human-readable format displays time in a more intuitive way using appropriate units
45+
* (e.g., "1 minute 30 seconds" or "1 hour 15 minutes"). Times under 60 seconds still
46+
* use the decimal format for consistency.
47+
*/
48+
HUMAN_READABLE {
49+
50+
@Override
51+
public String format(long millis) {
52+
Duration duration = Duration.ofMillis(millis);
53+
long seconds = duration.getSeconds();
54+
if (seconds < 60) {
55+
return String.format("%.3f seconds", millis / 1000.0);
56+
}
57+
long hours = duration.toHours();
58+
int minutes = duration.toMinutesPart();
59+
int secs = duration.toSecondsPart();
60+
if (hours > 0) {
61+
return String.format("%d hour%s %d minute%s", hours, (hours != 1) ? "s" : "", minutes,
62+
(minutes != 1) ? "s" : "");
63+
}
64+
return String.format("%d minute%s %d second%s", minutes, (minutes != 1) ? "s" : "", secs,
65+
(secs != 1) ? "s" : "");
66+
}
67+
68+
};
69+
70+
/**
71+
* Format the given duration in milliseconds according to this format style.
72+
* @param millis the duration in milliseconds
73+
* @return the formatted string
74+
*/
75+
public abstract String format(long millis);
76+
77+
}

core/spring-boot/src/test/java/org/springframework/boot/StartupInfoLoggerTests.java

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ void startedFormat() {
109109
new StartupInfoLogger(getClass(), this.environment).logStarted(this.log, new TestStartup(1345L, "Started"));
110110
then(this.log).should()
111111
.info(assertArg((message) -> assertThat(message.toString()).matches("Started " + getClass().getSimpleName()
112-
+ " in \\d+\\.\\d{1,3} seconds \\(process running for 1.345\\)")));
112+
+ " in \\d+\\.\\d{1,3} seconds \\(process running for 1\\.345 seconds\\)")));
113113
}
114114

115115
@Test
@@ -130,17 +130,79 @@ void restoredFormat() {
130130
.matches("Restored " + getClass().getSimpleName() + " in \\d+\\.\\d{1,3} seconds")));
131131
}
132132

133+
@Test
134+
void startedFormatWithHumanReadableMinutes() {
135+
given(this.log.isInfoEnabled()).willReturn(true);
136+
new StartupInfoLogger(getClass(), this.environment, StartupTimeFormat.HUMAN_READABLE).logStarted(this.log,
137+
new TestStartup(90000L, "Started", 90000L));
138+
then(this.log).should()
139+
.info(assertArg(
140+
(message) -> assertThat(message.toString()).isEqualTo("Started " + getClass().getSimpleName()
141+
+ " in 1 minute 30 seconds (process running for 1 minute 30 seconds)")));
142+
}
143+
144+
@Test
145+
void startedFormatWithHumanReadableHours() {
146+
given(this.log.isInfoEnabled()).willReturn(true);
147+
new StartupInfoLogger(getClass(), this.environment, StartupTimeFormat.HUMAN_READABLE).logStarted(this.log,
148+
new TestStartup(4500000L, "Started", 4500000L));
149+
then(this.log).should()
150+
.info(assertArg((message) -> assertThat(message.toString()).isEqualTo("Started "
151+
+ getClass().getSimpleName() + " in 1 hour 15 minutes (process running for 1 hour 15 minutes)")));
152+
}
153+
154+
@Test
155+
void startedFormatWithHumanReadableSingularUnits() {
156+
given(this.log.isInfoEnabled()).willReturn(true);
157+
new StartupInfoLogger(getClass(), this.environment, StartupTimeFormat.HUMAN_READABLE).logStarted(this.log,
158+
new TestStartup(61000L, "Started", 61000L));
159+
then(this.log).should()
160+
.info(assertArg((message) -> assertThat(message.toString()).isEqualTo("Started "
161+
+ getClass().getSimpleName() + " in 1 minute 1 second (process running for 1 minute 1 second)")));
162+
}
163+
164+
@Test
165+
void startedFormatWithHumanReadableZeroSeconds() {
166+
given(this.log.isInfoEnabled()).willReturn(true);
167+
new StartupInfoLogger(getClass(), this.environment, StartupTimeFormat.HUMAN_READABLE).logStarted(this.log,
168+
new TestStartup(300000L, "Started", 300000L));
169+
then(this.log).should()
170+
.info(assertArg(
171+
(message) -> assertThat(message.toString()).isEqualTo("Started " + getClass().getSimpleName()
172+
+ " in 5 minutes 0 seconds (process running for 5 minutes 0 seconds)")));
173+
}
174+
175+
@Test
176+
void startedFormatWithDefaultDecimalFormat() {
177+
given(this.log.isInfoEnabled()).willReturn(true);
178+
new StartupInfoLogger(getClass(), this.environment).logStarted(this.log,
179+
new TestStartup(90000L, "Started", 90000L));
180+
then(this.log).should()
181+
.info(assertArg((message) -> assertThat(message.toString()).matches("Started " + getClass().getSimpleName()
182+
+ " in \\d+\\.\\d{1,3} seconds \\(process running for \\d+\\.\\d{1,3} seconds\\)")));
183+
}
184+
133185
static class TestStartup extends Startup {
134186

135-
private final long startTime = System.currentTimeMillis();
187+
private long startTime;
136188

137189
private final @Nullable Long uptime;
138190

139191
private final String action;
140192

141193
TestStartup(@Nullable Long uptime, String action) {
194+
this(uptime, action, null);
195+
}
196+
197+
TestStartup(@Nullable Long uptime, String action, @Nullable Long timeTaken) {
142198
this.uptime = uptime;
143199
this.action = action;
200+
if (timeTaken != null) {
201+
this.startTime = System.currentTimeMillis() - timeTaken;
202+
}
203+
else {
204+
this.startTime = System.currentTimeMillis();
205+
}
144206
started();
145207
}
146208

0 commit comments

Comments
 (0)