Skip to content

Commit e8391f1

Browse files
committed
Add execution metadata to scheduled tasks actuator endpoint
As of spring-projects/spring-framework#24560, Spring provides additional metadata for scheduled tasks: * next execution time * last execution outcome (including status, time and raised exception) This commit leverages this information to enhance the existing `scheduledtasks` Actuator endpoint. Closes gh-17585
1 parent f1e98d0 commit e8391f1

File tree

3 files changed

+235
-72
lines changed

3 files changed

+235
-72
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ScheduledTasksEndpointDocumentationTests.java

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727
import org.springframework.context.annotation.Configuration;
2828
import org.springframework.context.annotation.Import;
2929
import org.springframework.restdocs.payload.FieldDescriptor;
30+
import org.springframework.restdocs.payload.JsonFieldType;
3031
import org.springframework.scheduling.Trigger;
3132
import org.springframework.scheduling.TriggerContext;
3233
import org.springframework.scheduling.annotation.EnableScheduling;
3334
import org.springframework.scheduling.annotation.Scheduled;
3435
import org.springframework.scheduling.annotation.SchedulingConfigurer;
36+
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
3537
import org.springframework.scheduling.config.ScheduledTaskHolder;
3638

3739
import static org.assertj.core.api.Assertions.assertThat;
@@ -57,9 +59,12 @@ void scheduledTasks() {
5759
"com.example.Processor")),
5860
responseFields(fieldWithPath("cron").description("Cron tasks, if any."),
5961
targetFieldWithPrefix("cron.[]."),
62+
nextExecutionWithPrefix("cron.[].").description("Time of the next scheduled execution."),
6063
fieldWithPath("cron.[].expression").description("Cron expression."),
6164
fieldWithPath("fixedDelay").description("Fixed delay tasks, if any."),
6265
targetFieldWithPrefix("fixedDelay.[]."), initialDelayWithPrefix("fixedDelay.[]."),
66+
nextExecutionWithPrefix("fixedDelay.[].")
67+
.description("Time of the next scheduled execution."),
6368
fieldWithPath("fixedDelay.[].interval")
6469
.description("Interval, in milliseconds, between the end of the last"
6570
+ " execution and the start of the next."),
@@ -68,9 +73,15 @@ void scheduledTasks() {
6873
fieldWithPath("fixedRate.[].interval")
6974
.description("Interval, in milliseconds, between the start of each execution."),
7075
initialDelayWithPrefix("fixedRate.[]."),
76+
nextExecutionWithPrefix("fixedRate.[].")
77+
.description("Time of the next scheduled execution."),
7178
fieldWithPath("custom").description("Tasks with custom triggers, if any."),
7279
targetFieldWithPrefix("custom.[]."),
73-
fieldWithPath("custom.[].trigger").description("Trigger for the task."))));
80+
fieldWithPath("custom.[].trigger").description("Trigger for the task."))
81+
.andWithPrefix("*.[].",
82+
fieldWithPath("lastExecution").description("Last execution of this task, if any.")
83+
.optional())
84+
.andWithPrefix("*.[].lastExecution.", lastExecution())));
7485
}
7586

7687
private FieldDescriptor targetFieldWithPrefix(String prefix) {
@@ -81,6 +92,22 @@ private FieldDescriptor initialDelayWithPrefix(String prefix) {
8192
return fieldWithPath(prefix + "initialDelay").description("Delay, in milliseconds, before first execution.");
8293
}
8394

95+
private FieldDescriptor nextExecutionWithPrefix(String prefix) {
96+
return fieldWithPath(prefix + "nextExecution.time").description("Time of the next scheduled execution.");
97+
}
98+
99+
private FieldDescriptor[] lastExecution() {
100+
return new FieldDescriptor[] {
101+
fieldWithPath("status").description("Status of the last execution (STARTED, SUCCESS, ERROR)."),
102+
fieldWithPath("time").description("Time of the last execution.").type(JsonFieldType.STRING),
103+
fieldWithPath("exception.type").description("Exception type thrown by the task, if any.")
104+
.type(JsonFieldType.STRING)
105+
.optional(),
106+
fieldWithPath("exception.message").description("Message of the exception thrown by the task, if any.")
107+
.type(JsonFieldType.STRING)
108+
.optional() };
109+
}
110+
84111
@Configuration(proxyBeanMethods = false)
85112
@EnableScheduling
86113
@Import(BaseDocumentationConfiguration.class)
@@ -96,7 +123,7 @@ void processOrders() {
96123

97124
}
98125

99-
@Scheduled(fixedDelay = 5000, initialDelay = 5000)
126+
@Scheduled(fixedDelay = 5000, initialDelay = 0)
100127
void purge() {
101128

102129
}
@@ -108,7 +135,10 @@ void retrieveIssues() {
108135

109136
@Bean
110137
SchedulingConfigurer schedulingConfigurer() {
111-
return (registrar) -> registrar.addTriggerTask(new CustomTriggeredRunnable(), new CustomTrigger());
138+
return (registrar) -> {
139+
registrar.setTaskScheduler(new TestTaskScheduler());
140+
registrar.addTriggerTask(new CustomTriggeredRunnable(), new CustomTrigger());
141+
};
112142
}
113143

114144
static class CustomTrigger implements Trigger {
@@ -124,7 +154,18 @@ static class CustomTriggeredRunnable implements Runnable {
124154

125155
@Override
126156
public void run() {
157+
throw new IllegalStateException("Failed while running custom task");
158+
}
159+
160+
}
161+
162+
static class TestTaskScheduler extends SimpleAsyncTaskScheduler {
127163

164+
TestTaskScheduler() {
165+
setThreadNamePrefix("test-");
166+
// do not log task errors
167+
setErrorHandler((throwable) -> {
168+
});
128169
}
129170

130171
}

0 commit comments

Comments
 (0)