Return objects from trace, audit event, and thread dump endpoints

Closes gh-7648
This commit is contained in:
Andy Wilkinson 2017-09-05 10:39:24 +01:00
parent a6b30a3aab
commit ad4ce9cf57
14 changed files with 100 additions and 185 deletions

View File

@ -18,15 +18,11 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web;
import org.springframework.boot.actuate.autoconfigure.ManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.ConditionalOnEnabledEndpoint;
import org.springframework.boot.actuate.endpoint.AuditEventsEndpoint;
import org.springframework.boot.actuate.endpoint.web.AuditEventsWebEndpointExtension;
import org.springframework.boot.actuate.endpoint.web.HeapDumpWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.LogFileWebEndpoint;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
@ -53,15 +49,6 @@ public class WebEndpointManagementContextConfiguration {
return new HeapDumpWebEndpoint();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnEnabledEndpoint
@ConditionalOnBean(value = AuditEventsEndpoint.class, search = SearchStrategy.CURRENT)
public AuditEventsWebEndpointExtension auditEventsWebEndpointExtension(
AuditEventsEndpoint delegate) {
return new AuditEventsWebEndpointExtension(delegate);
}
@Bean
@ConditionalOnMissingBean
@Conditional(LogFileCondition.class)

View File

@ -42,9 +42,28 @@ public class AuditEventsEndpoint {
}
@ReadOperation
public List<AuditEvent> eventsWithPrincipalDateAfterAndType(String principal,
public AuditEventsDescriptor eventsWithPrincipalDateAfterAndType(String principal,
Date after, String type) {
return this.auditEventRepository.find(principal, after, type);
return new AuditEventsDescriptor(
this.auditEventRepository.find(principal, after, type));
}
/**
* A description of an application's {@link AuditEvent audit events}. Primarily
* intended for serialization to JSON.
*/
public static final class AuditEventsDescriptor {
private final List<AuditEvent> events;
private AuditEventsDescriptor(List<AuditEvent> events) {
this.events = events;
}
public List<AuditEvent> getEvents() {
return this.events;
}
}
}

View File

@ -28,14 +28,32 @@ import org.springframework.boot.endpoint.ReadOperation;
* {@link Endpoint} to expose thread info.
*
* @author Dave Syer
* @author Andy Wilkinson
*/
@Endpoint(id = "threaddump")
public class ThreadDumpEndpoint {
@ReadOperation
public List<ThreadInfo> threadDump() {
return Arrays
.asList(ManagementFactory.getThreadMXBean().dumpAllThreads(true, true));
public ThreadDumpDescriptor threadDump() {
return new ThreadDumpDescriptor(Arrays
.asList(ManagementFactory.getThreadMXBean().dumpAllThreads(true, true)));
}
/**
* A description of a thread dump. Primarily intended for serialization to JSON.
*/
public static final class ThreadDumpDescriptor {
private final List<ThreadInfo> threads;
private ThreadDumpDescriptor(List<ThreadInfo> threads) {
this.threads = threads;
}
public List<ThreadInfo> getThreads() {
return this.threads;
}
}
}

View File

@ -44,8 +44,26 @@ public class TraceEndpoint {
}
@ReadOperation
public List<Trace> traces() {
return this.repository.findAll();
public TraceDescriptor traces() {
return new TraceDescriptor(this.repository.findAll());
}
/**
* A description of an application's {@link Trace} entries. Primarily intended for
* serialization to JSON.
*/
public static final class TraceDescriptor {
private final List<Trace> traces;
private TraceDescriptor(List<Trace> traces) {
this.traces = traces;
}
public List<Trace> getTraces() {
return this.traces;
}
}
}

View File

@ -17,10 +17,9 @@
package org.springframework.boot.actuate.endpoint.jmx;
import java.util.Date;
import java.util.List;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.boot.actuate.endpoint.AuditEventsEndpoint;
import org.springframework.boot.actuate.endpoint.AuditEventsEndpoint.AuditEventsDescriptor;
import org.springframework.boot.endpoint.ReadOperation;
import org.springframework.boot.endpoint.jmx.JmxEndpointExtension;
@ -41,12 +40,12 @@ public class AuditEventsJmxEndpointExtension {
}
@ReadOperation
public List<AuditEvent> eventsWithDateAfter(Date dateAfter) {
public AuditEventsDescriptor eventsWithDateAfter(Date dateAfter) {
return this.delegate.eventsWithPrincipalDateAfterAndType(null, dateAfter, null);
}
@ReadOperation
public List<AuditEvent> eventsWithPrincipalAndDateAfter(String principal,
public AuditEventsDescriptor eventsWithPrincipalAndDateAfter(String principal,
Date dateAfter) {
return this.delegate.eventsWithPrincipalDateAfterAndType(principal, dateAfter,
null);

View File

@ -1,51 +0,0 @@
/*
* 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.endpoint.web;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.boot.actuate.endpoint.AuditEventsEndpoint;
import org.springframework.boot.endpoint.ReadOperation;
import org.springframework.boot.endpoint.web.WebEndpointExtension;
/**
* Web-specific extension of the {@link AuditEventsEndpoint}.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
@WebEndpointExtension(endpoint = AuditEventsEndpoint.class)
public class AuditEventsWebEndpointExtension {
private final AuditEventsEndpoint delegate;
public AuditEventsWebEndpointExtension(AuditEventsEndpoint delegate) {
this.delegate = delegate;
}
@ReadOperation
public Map<String, List<AuditEvent>> eventsWithPrincipalDateAfterAndType(
String principal, Date after, String type) {
return Collections.singletonMap("events", this.delegate
.eventsWithPrincipalDateAfterAndType(principal, after, type));
}
}

View File

@ -24,7 +24,6 @@ import org.springframework.boot.actuate.audit.AuditEventRepository;
import org.springframework.boot.actuate.endpoint.AuditEventsEndpoint;
import org.springframework.boot.actuate.endpoint.HealthEndpoint;
import org.springframework.boot.actuate.endpoint.StatusEndpoint;
import org.springframework.boot.actuate.endpoint.web.AuditEventsWebEndpointExtension;
import org.springframework.boot.actuate.endpoint.web.HealthReactiveWebEndpointExtension;
import org.springframework.boot.actuate.endpoint.web.HealthWebEndpointExtension;
import org.springframework.boot.actuate.endpoint.web.HeapDumpWebEndpoint;
@ -143,9 +142,10 @@ public class WebEndpointManagementContextConfigurationTests {
public void reactiveHealthWebEndpointExtensionCanBeDisabled() {
reactiveWebContextRunner(HealthEndpointConfiguration.class)
.withPropertyValues("endpoints.health.enabled=false").run((context) -> {
assertThat(context).doesNotHaveBean(HealthReactiveWebEndpointExtension.class);
assertThat(context).doesNotHaveBean(HealthWebEndpointExtension.class);
});
assertThat(context)
.doesNotHaveBean(HealthReactiveWebEndpointExtension.class);
assertThat(context).doesNotHaveBean(HealthWebEndpointExtension.class);
});
}
@ -175,22 +175,10 @@ public class WebEndpointManagementContextConfigurationTests {
public void reactiveStatusWebEndpointExtensionCanBeDisabled() {
reactiveWebContextRunner(StatusEndpointConfiguration.class)
.withPropertyValues("endpoints.status.enabled=false").run((context) -> {
assertThat(context).doesNotHaveBean(StatusReactiveWebEndpointExtension.class);
assertThat(context).doesNotHaveBean(StatusWebEndpointExtension.class);
});
}
@Test
public void auditEventsWebEndpointExtensionIsAutoConfigured() {
beanIsAutoConfigured(AuditEventsWebEndpointExtension.class,
AuditEventsEndpointConfiguration.class);
}
@Test
public void auditEventsWebEndpointExtensionCanBeDisabled() {
beanIsNotAutoConfiguredWhenEndpointIsDisabled(
AuditEventsWebEndpointExtension.class, "auditevents",
AuditEventsEndpointConfiguration.class);
assertThat(context)
.doesNotHaveBean(StatusReactiveWebEndpointExtension.class);
assertThat(context).doesNotHaveBean(StatusWebEndpointExtension.class);
});
}
@Test
@ -209,7 +197,8 @@ public class WebEndpointManagementContextConfigurationTests {
@Test
public void logFileWebEndpointIsAutoConfiguredWhenExternalFileIsSet() {
webContextRunner().withPropertyValues("endpoints.logfile.external-file:external.log")
webContextRunner()
.withPropertyValues("endpoints.logfile.external-file:external.log")
.run((context) -> assertThat(
context.getBeansOfType(LogFileWebEndpoint.class)).hasSize(1));
}

View File

@ -47,8 +47,8 @@ public class AuditEventsEndpointTests {
public void eventsWithType() {
given(this.repository.find(null, null, "type"))
.willReturn(Collections.singletonList(this.event));
List<AuditEvent> result = this.endpoint.eventsWithPrincipalDateAfterAndType(null,
null, "type");
List<AuditEvent> result = this.endpoint
.eventsWithPrincipalDateAfterAndType(null, null, "type").getEvents();
assertThat(result).isEqualTo(Collections.singletonList(this.event));
}
@ -57,8 +57,8 @@ public class AuditEventsEndpointTests {
Date date = new Date();
given(this.repository.find(null, date, null))
.willReturn(Collections.singletonList(this.event));
List<AuditEvent> result = this.endpoint.eventsWithPrincipalDateAfterAndType(null,
date, null);
List<AuditEvent> result = this.endpoint
.eventsWithPrincipalDateAfterAndType(null, date, null).getEvents();
assertThat(result).isEqualTo(Collections.singletonList(this.event));
}
@ -67,7 +67,7 @@ public class AuditEventsEndpointTests {
given(this.repository.find("Joan", null, null))
.willReturn(Collections.singletonList(this.event));
List<AuditEvent> result = this.endpoint
.eventsWithPrincipalDateAfterAndType("Joan", null, null);
.eventsWithPrincipalDateAfterAndType("Joan", null, null).getEvents();
assertThat(result).isEqualTo(Collections.singletonList(this.event));
}

View File

@ -30,7 +30,8 @@ public class ThreadDumpEndpointTests {
@Test
public void dumpThreads() throws Exception {
assertThat(new ThreadDumpEndpoint().threadDump().size()).isGreaterThan(0);
assertThat(new ThreadDumpEndpoint().threadDump().getThreads().size())
.isGreaterThan(0);
}
}

View File

@ -38,7 +38,7 @@ public class TraceEndpointTests {
public void trace() throws Exception {
TraceRepository repository = new InMemoryTraceRepository();
repository.add(Collections.<String, Object>singletonMap("a", "b"));
Trace trace = new TraceEndpoint(repository).traces().get(0);
Trace trace = new TraceEndpoint(repository).traces().getTraces().get(0);
assertThat(trace.getInfo().get("a")).isEqualTo("b");
}

View File

@ -50,7 +50,7 @@ public class AuditEventsJmxEndpointExtensionTests {
Date date = new Date();
given(this.repository.find(null, date, null))
.willReturn(Collections.singletonList(this.event));
List<AuditEvent> result = this.extension.eventsWithDateAfter(date);
List<AuditEvent> result = this.extension.eventsWithDateAfter(date).getEvents();
assertThat(result).isEqualTo(Collections.singletonList(this.event));
}
@ -59,8 +59,8 @@ public class AuditEventsJmxEndpointExtensionTests {
Date date = new Date();
given(this.repository.find("Joan", date, null))
.willReturn(Collections.singletonList(this.event));
List<AuditEvent> result = this.extension.eventsWithPrincipalAndDateAfter("Joan",
date);
List<AuditEvent> result = this.extension
.eventsWithPrincipalAndDateAfter("Joan", date).getEvents();
assertThat(result).isEqualTo(Collections.singletonList(this.event));
}

View File

@ -33,8 +33,8 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* Integration tests for {@link AuditEventsEndpoint} and
* {@link AuditEventsWebEndpointExtension} exposed by Jersey, Spring MVC, and WebFlux.
* Integration tests for {@link AuditEventsEndpoint} exposed by Jersey, Spring MVC, and
* WebFlux.
*
* @author Vedran Pavic
* @author Andy Wilkinson
@ -95,11 +95,6 @@ public class AuditEventsEndpointWebIntegrationTests {
return new AuditEventsEndpoint(auditEventsRepository());
}
@Bean
public AuditEventsWebEndpointExtension auditEventsWebEndpointExtension() {
return new AuditEventsWebEndpointExtension(auditEventsEndpoint());
}
private AuditEvent createEvent(String instant, String principal, String type) {
return new AuditEvent(Date.from(Instant.parse(instant)), principal, type,
Collections.<String, Object>emptyMap());

View File

@ -1,60 +0,0 @@
/*
* 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.endpoint.web;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.boot.actuate.audit.AuditEventRepository;
import org.springframework.boot.actuate.endpoint.AuditEventsEndpoint;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link AuditEventsWebEndpointExtension}.
*
* @author Andy Wilkinson
*/
public class AuditEventsWebEndpointExtensionTests {
private final AuditEventRepository repository = mock(AuditEventRepository.class);
private final AuditEventsWebEndpointExtension extension = new AuditEventsWebEndpointExtension(
new AuditEventsEndpoint(this.repository));
private final AuditEvent event = new AuditEvent("principal", "type",
Collections.singletonMap("a", "alpha"));
@Test
public void delegatesResponseIsAvailableFromEventsKeyInMap() {
Date date = new Date();
given(this.repository.find("principal", date, "type"))
.willReturn(Collections.singletonList(this.event));
Map<String, List<AuditEvent>> result = this.extension
.eventsWithPrincipalDateAfterAndType("principal", date, "type");
assertThat(result).hasSize(1);
assertThat(result).containsEntry("events", Collections.singletonList(this.event));
}
}

View File

@ -163,35 +163,35 @@ public class SampleActuatorApplicationTests {
}
@Test
@SuppressWarnings("unchecked")
public void testTrace() throws Exception {
this.restTemplate.getForEntity("/health", String.class);
@SuppressWarnings("rawtypes")
ResponseEntity<List> entity = this.restTemplate
ResponseEntity<Map> entity = this.restTemplate
.withBasicAuth("user", getPassword())
.getForEntity("/application/trace", List.class);
.getForEntity("/application/trace", Map.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
@SuppressWarnings("unchecked")
List<Map<String, Object>> list = entity.getBody();
Map<String, Object> trace = list.get(list.size() - 1);
@SuppressWarnings("unchecked")
Map<String, Object> body = entity.getBody();
Map<String, Object> trace = ((List<Map<String, Object>>) body.get("traces"))
.get(0);
Map<String, Object> map = (Map<String, Object>) ((Map<String, Object>) ((Map<String, Object>) trace
.get("info")).get("headers")).get("response");
assertThat(map.get("status")).isEqualTo("200");
}
@Test
@SuppressWarnings("unchecked")
public void traceWithParameterMap() throws Exception {
this.restTemplate.withBasicAuth("user", getPassword())
.getForEntity("/application/health?param1=value1", String.class);
@SuppressWarnings("rawtypes")
ResponseEntity<List> entity = this.restTemplate
ResponseEntity<Map> entity = this.restTemplate
.withBasicAuth("user", getPassword())
.getForEntity("/application/trace", List.class);
.getForEntity("/application/trace", Map.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
@SuppressWarnings("unchecked")
List<Map<String, Object>> list = entity.getBody();
Map<String, Object> trace = list.get(0);
@SuppressWarnings("unchecked")
Map<String, Object> body = entity.getBody();
Map<String, Object> trace = ((List<Map<String, Object>>) body.get("traces"))
.get(0);
Map<String, Object> map = (Map<String, Object>) ((Map<String, Object>) trace
.get("info")).get("parameters");
assertThat(map.get("param1")).isNotNull();