Add Quartz actuator endpoint

See gh-10364
This commit is contained in:
Vedran Pavic 2017-09-20 11:00:12 +02:00 committed by Stephane Nicoll
parent 095ff18854
commit 9795061360
12 changed files with 611 additions and 0 deletions

View File

@ -86,6 +86,7 @@ dependencies {
optional("org.mongodb:mongodb-driver-reactivestreams")
optional("org.mongodb:mongodb-driver-sync")
optional("org.neo4j.driver:neo4j-java-driver")
optional("org.quartz-scheduler:quartz")
optional("org.springframework:spring-jdbc")
optional("org.springframework:spring-jms")
optional("org.springframework:spring-messaging")

View File

@ -67,6 +67,7 @@ include::endpoints/loggers.adoc[leveloffset=+1]
include::endpoints/mappings.adoc[leveloffset=+1]
include::endpoints/metrics.adoc[leveloffset=+1]
include::endpoints/prometheus.adoc[leveloffset=+1]
include::endpoints/quartz.adoc[leveloffset=+1]
include::endpoints/scheduledtasks.adoc[leveloffset=+1]
include::endpoints/sessions.adoc[leveloffset=+1]
include::endpoints/shutdown.adoc[leveloffset=+1]

View File

@ -0,0 +1,45 @@
[[quartz]]
= Quartz (`quartz`)
The `quartz` endpoint provides information about the scheduled jobs that are managed by
Quartz Scheduler.
[[quartz-report]]
== Retrieving the Quartz report
To retrieve the Quartz, make a `GET` request to `/application/quartz`,
as shown in the following curl-based example:
include::{snippets}quartz-report/curl-request.adoc[]
The resulting response is similar to the following:
include::{snippets}quartz-report/http-response.adoc[]
[[quartz-job]]
== Retrieving the Quartz job details
To retrieve the Quartz, make a `GET` request to `/application/quartz/{group}/{name}`,
as shown in the following curl-based example:
include::{snippets}quartz-job/curl-request.adoc[]
The preceding example retrieves the job with the `group` of `groupOne` and `name` of
`jobOne`. The resulting response is similar to the following:
include::{snippets}quartz-job/http-response.adoc[]
[[quartz-job-response-structure]]
=== Response Structure
The response contains details of the scheduled job. The following table describes the
structure of the response:
[cols="2,1,3"]
include::{snippets}quartz-job/response-fields.adoc[]

View File

@ -0,0 +1,51 @@
/*
* Copyright 2012-2017 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
*
* http://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.actuate.autoconfigure.quartz;
import org.quartz.Scheduler;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.quartz.QuartzEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link QuartzEndpoint}.
*
* @author Vedran Pavic
* @since 2.0.0
*/
@Configuration
@ConditionalOnClass(Scheduler.class)
@AutoConfigureAfter(QuartzAutoConfiguration.class)
@ConditionalOnAvailableEndpoint(endpoint = QuartzEndpoint.class)
public class QuartzEndpointAutoConfiguration {
@Bean
@ConditionalOnBean(Scheduler.class)
@ConditionalOnMissingBean
public QuartzEndpoint quartzEndpoint(Scheduler scheduler) {
return new QuartzEndpoint(scheduler);
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Auto-configuration for actuator Quartz Scheduler concerns.
*/
package org.springframework.boot.actuate.autoconfigure.quartz;

View File

@ -80,6 +80,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat.TomcatMetricsA
org.springframework.boot.actuate.autoconfigure.mongo.MongoHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.mongo.MongoReactiveHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.quartz.QuartzEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.r2dbc.ConnectionFactoryHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.redis.RedisHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.redis.RedisReactiveHealthContributorAutoConfiguration,\

View File

@ -0,0 +1,128 @@
/*
* Copyright 2012-2017 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
*
* http://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.actuate.autoconfigure.endpoint.web.documentation;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.regex.Pattern;
import org.junit.Test;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.boot.actuate.quartz.QuartzEndpoint;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import static org.mockito.BDDMockito.given;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.replacePattern;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for generating documentation describing the {@link QuartzEndpoint}.
*
* @author Vedran Pavic
*/
public class QuartzEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
private static final JobDetail jobOne = JobBuilder.newJob(Job.class).withIdentity("jobOne", "groupOne")
.withDescription("My first job").build();
private static final JobDetail jobTwo = JobBuilder.newJob(Job.class).withIdentity("jobTwo", "groupOne").build();
private static final JobDetail jobThree = JobBuilder.newJob(Job.class).withIdentity("jobThree", "groupTwo").build();
private static final Trigger triggerOne = TriggerBuilder.newTrigger().forJob(jobOne).withIdentity("triggerOne")
.withDescription("My first trigger").modifiedByCalendar("myCalendar")
.startAt(Date.from(Instant.parse("2017-12-01T12:00:00Z")))
.endAt(Date.from(Instant.parse("2017-12-01T12:30:00Z")))
.withSchedule(SimpleScheduleBuilder.repeatMinutelyForever()).build();
private static final Trigger triggerTwo = TriggerBuilder.newTrigger().forJob(jobOne).withIdentity("triggerTwo")
.withDescription("My second trigger").modifiedByCalendar("myCalendar")
.startAt(Date.from(Instant.parse("2017-12-01T00:00:00Z")))
.endAt(Date.from(Instant.parse("2017-12-10T00:00:00Z")))
.withSchedule(SimpleScheduleBuilder.repeatHourlyForever()).build();
@MockBean
private Scheduler scheduler;
@Test
public void quartzReport() throws Exception {
String groupOne = jobOne.getKey().getGroup();
String groupTwo = jobThree.getKey().getGroup();
given(this.scheduler.getJobGroupNames()).willReturn(Arrays.asList(groupOne, groupTwo));
given(this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupOne)))
.willReturn(new HashSet<>(Arrays.asList(jobOne.getKey(), jobTwo.getKey())));
given(this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupTwo)))
.willReturn(Collections.singleton(jobThree.getKey()));
this.mockMvc.perform(get("/application/quartz")).andExpect(status().isOk()).andDo(document("quartz/report"));
}
@Test
public void quartzJob() throws Exception {
JobKey jobKey = jobOne.getKey();
given(this.scheduler.getJobDetail(jobKey)).willReturn(jobOne);
given(this.scheduler.getTriggersOfJob(jobKey)).willAnswer(invocation -> Arrays.asList(triggerOne, triggerTwo));
this.mockMvc.perform(get("/application/quartz/groupOne/jobOne")).andExpect(status().isOk()).andDo(document(
"quartz/job",
preprocessResponse(replacePattern(Pattern.compile("org.quartz.Job"), "com.example.MyJob")),
responseFields(fieldWithPath("jobGroup").description("Job group."),
fieldWithPath("jobName").description("Job name."),
fieldWithPath("description").description("Job description, if any."),
fieldWithPath("className").description("Job class."),
fieldWithPath("triggers.[].triggerGroup").description("Trigger group."),
fieldWithPath("triggers.[].triggerName").description("Trigger name."),
fieldWithPath("triggers.[].description").description("Trigger description, if any."),
fieldWithPath("triggers.[].calendarName").description("Trigger's calendar name, if any."),
fieldWithPath("triggers.[].startTime").description("Trigger's start time."),
fieldWithPath("triggers.[].endTime").description("Trigger's end time."),
fieldWithPath("triggers.[].nextFireTime").description("Trigger's next fire time."),
fieldWithPath("triggers.[].previousFireTime")
.description("Trigger's previous fire time, if any."),
fieldWithPath("triggers.[].finalFireTime").description("Trigger's final fire time, if any."))));
}
@Configuration
@Import(BaseDocumentationConfiguration.class)
static class TestConfiguration {
@Bean
public QuartzEndpoint endpoint(Scheduler scheduler) {
return new QuartzEndpoint(scheduler);
}
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright 2012-2017 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
*
* http://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.actuate.autoconfigure.quartz;
import org.junit.Test;
import org.quartz.Scheduler;
import org.springframework.boot.actuate.quartz.QuartzEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link QuartzEndpointAutoConfiguration}.
*
* @author Vedran Pavic
*/
public class QuartzEndpointAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(QuartzEndpointAutoConfiguration.class))
.withUserConfiguration(QuartzConfiguration.class);
@Test
public void runShouldHaveEndpointBean() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(QuartzEndpoint.class));
}
@Test
public void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointBean() throws Exception {
this.contextRunner.withPropertyValues("management.endpoint.quartz.enabled:false")
.run((context) -> assertThat(context).doesNotHaveBean(QuartzEndpoint.class));
}
@Configuration
static class QuartzConfiguration {
@Bean
public Scheduler scheduler() {
return mock(Scheduler.class);
}
}
}

View File

@ -48,6 +48,7 @@ dependencies {
optional("org.mongodb:mongodb-driver-reactivestreams")
optional("org.mongodb:mongodb-driver-sync")
optional("org.neo4j.driver:neo4j-java-driver")
optional("org.quartz-scheduler:quartz")
optional("org.springframework:spring-jdbc")
optional("org.springframework:spring-messaging")
optional("org.springframework:spring-webflux")

View File

@ -0,0 +1,201 @@
/*
* Copyright 2012-2017 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
*
* http://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.actuate.quartz;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.util.Assert;
/**
* {@link Endpoint} to expose Quartz Scheduler info.
*
* @author Vedran Pavic
* @since 2.0.0
*/
@Endpoint(id = "quartz")
public class QuartzEndpoint {
private final Scheduler scheduler;
public QuartzEndpoint(Scheduler scheduler) {
Assert.notNull(scheduler, "Scheduler must not be null");
this.scheduler = scheduler;
}
@ReadOperation
public Map<String, Object> quartzReport() {
Map<String, Object> result = new LinkedHashMap<>();
try {
for (String groupName : this.scheduler.getJobGroupNames()) {
List<String> jobs = this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName)).stream()
.map(JobKey::getName).collect(Collectors.toList());
result.put(groupName, jobs);
}
}
catch (SchedulerException ignored) {
}
return result;
}
@ReadOperation
public QuartzJob quartzJob(@Selector String groupName, @Selector String jobName) {
try {
JobKey jobKey = JobKey.jobKey(jobName, groupName);
JobDetail jobDetail = this.scheduler.getJobDetail(jobKey);
List<? extends Trigger> triggers = this.scheduler.getTriggersOfJob(jobKey);
return new QuartzJob(jobDetail, triggers);
}
catch (SchedulerException e) {
return null;
}
}
/**
* Details of a {@link Job Quartz Job}.
*/
public static final class QuartzJob {
private final String jobGroup;
private final String jobName;
private final String description;
private final String className;
private final List<QuartzTrigger> triggers = new ArrayList<>();
QuartzJob(JobDetail jobDetail, List<? extends Trigger> triggers) {
this.jobGroup = jobDetail.getKey().getGroup();
this.jobName = jobDetail.getKey().getName();
this.description = jobDetail.getDescription();
this.className = jobDetail.getJobClass().getName();
triggers.forEach(trigger -> this.triggers.add(new QuartzTrigger(trigger)));
}
public String getJobGroup() {
return this.jobGroup;
}
public String getJobName() {
return this.jobName;
}
public String getDescription() {
return this.description;
}
public String getClassName() {
return this.className;
}
public List<QuartzTrigger> getTriggers() {
return this.triggers;
}
}
/**
* Details of a {@link Trigger Quartz Trigger}.
*/
public static final class QuartzTrigger {
private final String triggerGroup;
private final String triggerName;
private final String description;
private final String calendarName;
private final Date startTime;
private final Date endTime;
private final Date previousFireTime;
private final Date nextFireTime;
private final Date finalFireTime;
QuartzTrigger(Trigger trigger) {
this.triggerGroup = trigger.getKey().getGroup();
this.triggerName = trigger.getKey().getName();
this.description = trigger.getDescription();
this.calendarName = trigger.getCalendarName();
this.startTime = trigger.getStartTime();
this.endTime = trigger.getEndTime();
this.previousFireTime = trigger.getPreviousFireTime();
this.nextFireTime = trigger.getNextFireTime();
this.finalFireTime = trigger.getFinalFireTime();
}
public String getTriggerGroup() {
return this.triggerGroup;
}
public String getTriggerName() {
return this.triggerName;
}
public String getDescription() {
return this.description;
}
public String getCalendarName() {
return this.calendarName;
}
public Date getStartTime() {
return this.startTime;
}
public Date getEndTime() {
return this.endTime;
}
public Date getPreviousFireTime() {
return this.previousFireTime;
}
public Date getNextFireTime() {
return this.nextFireTime;
}
public Date getFinalFireTime() {
return this.finalFireTime;
}
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Actuator support for Quartz Scheduler.
*/
package org.springframework.boot.actuate.quartz;

View File

@ -0,0 +1,79 @@
/*
* Copyright 2012-2017 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
*
* http://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.actuate.quartz;
import java.util.Collections;
import java.util.Map;
import org.junit.Test;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJob;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link QuartzEndpoint}.
*
* @author Vedran Pavic
*/
public class QuartzEndpointTests {
private final Scheduler scheduler = mock(Scheduler.class);
private final QuartzEndpoint endpoint = new QuartzEndpoint(this.scheduler);
private final JobDetail jobDetail = JobBuilder.newJob(Job.class).withIdentity("testJob").build();
private final Trigger trigger = TriggerBuilder.newTrigger().forJob(this.jobDetail).withIdentity("testTrigger")
.build();
@Test
public void quartzReport() throws Exception {
String jobGroup = this.jobDetail.getKey().getGroup();
given(this.scheduler.getJobGroupNames()).willReturn(Collections.singletonList(jobGroup));
given(this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals(jobGroup)))
.willReturn(Collections.singleton(this.jobDetail.getKey()));
Map<String, Object> quartzReport = this.endpoint.quartzReport();
assertThat(quartzReport).hasSize(1);
}
@Test
public void quartzJob() throws Exception {
JobKey jobKey = this.jobDetail.getKey();
given(this.scheduler.getJobDetail(jobKey)).willReturn(this.jobDetail);
given(this.scheduler.getTriggersOfJob(jobKey))
.willAnswer(invocation -> Collections.singletonList(this.trigger));
QuartzJob quartzJob = this.endpoint.quartzJob(jobKey.getGroup(), jobKey.getName());
assertThat(quartzJob.getJobGroup()).isEqualTo(jobKey.getGroup());
assertThat(quartzJob.getJobName()).isEqualTo(jobKey.getName());
assertThat(quartzJob.getClassName()).isEqualTo(this.jobDetail.getJobClass().getName());
assertThat(quartzJob.getTriggers()).hasSize(1);
assertThat(quartzJob.getTriggers().get(0).getTriggerGroup()).isEqualTo(this.trigger.getKey().getGroup());
assertThat(quartzJob.getTriggers().get(0).getTriggerName()).isEqualTo(this.trigger.getKey().getName());
}
}