Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ class ApplicationProperties {
*/
private boolean logStartupInfo = true;

/**
* Format used to display application startup time in logs.
*/
private StartupTimeFormat logStartupTimeFormat = StartupTimeFormat.DEFAULT;

/**
* Whether the application should have a shutdown hook registered.
*/
Expand Down Expand Up @@ -139,6 +144,14 @@ void setLogStartupInfo(boolean logStartupInfo) {
this.logStartupInfo = logStartupInfo;
}

StartupTimeFormat getLogStartupTimeFormat() {
return this.logStartupTimeFormat;
}

void setLogStartupTimeFormat(StartupTimeFormat logStartupTimeFormat) {
this.logStartupTimeFormat = logStartupTimeFormat;
}

boolean isRegisterShutdownHook() {
return this.registerShutdownHook;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,8 @@ public ConfigurableApplicationContext run(String... args) {
afterRefresh(context, applicationArguments);
Duration timeTakenToStarted = startup.started();
if (this.properties.isLogStartupInfo()) {
new StartupInfoLogger(this.mainApplicationClass, environment).logStarted(getApplicationLog(), startup);
new StartupInfoLogger(this.mainApplicationClass, environment, this.properties.getLogStartupTimeFormat())
.logStarted(getApplicationLog(), startup);
}
listeners.started(context, timeTakenToStarted);
callRunners(context, applicationArguments);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,16 @@ class StartupInfoLogger {

private final Environment environment;

private final StartupTimeFormat startupTimeFormat;

StartupInfoLogger(@Nullable Class<?> sourceClass, Environment environment) {
this(sourceClass, environment, StartupTimeFormat.DEFAULT);
}

StartupInfoLogger(@Nullable Class<?> sourceClass, Environment environment, StartupTimeFormat startupTimeFormat) {
this.sourceClass = sourceClass;
this.environment = environment;
this.startupTimeFormat = startupTimeFormat;
}

void logStarting(Log applicationLog) {
Expand Down Expand Up @@ -87,16 +94,18 @@ private CharSequence getStartedMessage(Startup startup) {
message.append(startup.action());
appendApplicationName(message);
message.append(" in ");
message.append(startup.timeTakenToStarted().toMillis() / 1000.0);
message.append(" seconds");
message.append(formatDuration(startup.timeTakenToStarted().toMillis()));
Long uptimeMs = startup.processUptime();
if (uptimeMs != null) {
double uptime = uptimeMs / 1000.0;
message.append(" (process running for ").append(uptime).append(")");
message.append(" (process running for ").append(formatDuration(uptimeMs)).append(")");
}
return message;
}

private String formatDuration(long millis) {
return this.startupTimeFormat.format(millis);
}

private void appendAotMode(StringBuilder message) {
append(message, "", () -> AotDetector.useGeneratedArtifacts() ? "AOT-processed" : null);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2012-present 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;

import java.time.Duration;

/**
* Format styles for displaying application startup time in logs.
*
* @author Huang Xiao
* @since 3.5.0
*/
public enum StartupTimeFormat {

/**
* Default format displays time in seconds with millisecond precision (e.g., "3.456
* seconds"). This maintains backward compatibility with existing log parsing tools.
*/
DEFAULT {

@Override
public String format(long millis) {
return String.format("%.3f seconds", millis / 1000.0);
}

},

/**
* Human format displays time in a more intuitive way using appropriate units (e.g.,
* "1 minute 30 seconds" or "1 hour 15 minutes"). Times under 60 seconds still use the
* default format for consistency.
*/
HUMAN {

@Override
public String format(long millis) {
Duration duration = Duration.ofMillis(millis);
long seconds = duration.getSeconds();
if (seconds < 60) {
return String.format("%.3f seconds", millis / 1000.0);
}
long hours = duration.toHours();
int minutes = duration.toMinutesPart();
int secs = duration.toSecondsPart();
if (hours > 0) {
return String.format("%d hour%s %d minute%s", hours, (hours != 1) ? "s" : "", minutes,
(minutes != 1) ? "s" : "");
}
return String.format("%d minute%s %d second%s", minutes, (minutes != 1) ? "s" : "", secs,
(secs != 1) ? "s" : "");
}

};

/**
* Format the given duration in milliseconds according to this format style.
* @param millis the duration in milliseconds
* @return the formatted string
*/
public abstract String format(long millis);

}
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ void startedFormat() {
new StartupInfoLogger(getClass(), this.environment).logStarted(this.log, new TestStartup(1345L, "Started"));
then(this.log).should()
.info(assertArg((message) -> assertThat(message.toString()).matches("Started " + getClass().getSimpleName()
+ " in \\d+\\.\\d{1,3} seconds \\(process running for 1.345\\)")));
+ " in \\d+\\.\\d{1,3} seconds \\(process running for 1\\.345 seconds\\)")));
}

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

@Test
void startedFormatWithHumanMinutes() {
given(this.log.isInfoEnabled()).willReturn(true);
new StartupInfoLogger(getClass(), this.environment, StartupTimeFormat.HUMAN).logStarted(this.log,
new TestStartup(90000L, "Started", 90000L));
then(this.log).should()
.info(assertArg(
(message) -> assertThat(message.toString()).isEqualTo("Started " + getClass().getSimpleName()
+ " in 1 minute 30 seconds (process running for 1 minute 30 seconds)")));
}

@Test
void startedFormatWithHumanHours() {
given(this.log.isInfoEnabled()).willReturn(true);
new StartupInfoLogger(getClass(), this.environment, StartupTimeFormat.HUMAN).logStarted(this.log,
new TestStartup(4500000L, "Started", 4500000L));
then(this.log).should()
.info(assertArg((message) -> assertThat(message.toString()).isEqualTo("Started "
+ getClass().getSimpleName() + " in 1 hour 15 minutes (process running for 1 hour 15 minutes)")));
}

@Test
void startedFormatWithHumanSingularUnits() {
given(this.log.isInfoEnabled()).willReturn(true);
new StartupInfoLogger(getClass(), this.environment, StartupTimeFormat.HUMAN).logStarted(this.log,
new TestStartup(61000L, "Started", 61000L));
then(this.log).should()
.info(assertArg((message) -> assertThat(message.toString()).isEqualTo("Started "
+ getClass().getSimpleName() + " in 1 minute 1 second (process running for 1 minute 1 second)")));
}

@Test
void startedFormatWithHumanZeroSeconds() {
given(this.log.isInfoEnabled()).willReturn(true);
new StartupInfoLogger(getClass(), this.environment, StartupTimeFormat.HUMAN).logStarted(this.log,
new TestStartup(300000L, "Started", 300000L));
then(this.log).should()
.info(assertArg(
(message) -> assertThat(message.toString()).isEqualTo("Started " + getClass().getSimpleName()
+ " in 5 minutes 0 seconds (process running for 5 minutes 0 seconds)")));
}

@Test
void startedFormatWithDefaultDecimalFormat() {
given(this.log.isInfoEnabled()).willReturn(true);
new StartupInfoLogger(getClass(), this.environment).logStarted(this.log,
new TestStartup(90000L, "Started", 90000L));
then(this.log).should()
.info(assertArg((message) -> assertThat(message.toString()).matches("Started " + getClass().getSimpleName()
+ " in \\d+\\.\\d{1,3} seconds \\(process running for \\d+\\.\\d{1,3} seconds\\)")));
}

static class TestStartup extends Startup {

private final long startTime = System.currentTimeMillis();
private long startTime;

private final @Nullable Long uptime;

private final String action;

TestStartup(@Nullable Long uptime, String action) {
this(uptime, action, null);
}

TestStartup(@Nullable Long uptime, String action, @Nullable Long timeTaken) {
this.uptime = uptime;
this.action = action;
if (timeTaken != null) {
this.startTime = System.currentTimeMillis() - timeTaken;
}
else {
this.startTime = System.currentTimeMillis();
}
started();
}

Expand Down
Loading