Add support for Spring HATEOAS hypermedia in Actuator endpoints

If spring-hateoas is on the classpath and an MvcEndpoint returns a
@ResponseBody it will be extended and wrapped into a Resource with links.
All the existing endpoints that return sensible JSON data can be extended
this way (i.e. not /logfile). The HAL browser will also be added as an
endpoint if available on the classpath. Finally, asciidocs for the
Actuator endpoints are available as a separate jar file, which if
included in an app will also generate a new (HTTP) endpoint.

Fixes gh-1390
This commit is contained in:
Dave Syer
2015-07-01 18:36:21 +01:00
parent 82fdb87a8c
commit 74e9e0749b
74 changed files with 3106 additions and 154 deletions

View File

@@ -0,0 +1,26 @@
=== /autoconfig
This endpoint is a report on the Spring Boot Autoconfiguration process
that happened when your application started up. It lists all the
`@Conditional` annotations that were evaluated as the context started
and in each case it gives an indication of if (and why) the condition
matched. A positive match results in a bean being included in the context,
and a negative result means the opposite (the beans's class may not even
be loaded).
The report is split into 2 parts, positive matches first, and then negative.
If the context is a hierarchy, there is also a separate report on the parent
context with the same format (and recursively up to the top of the hierarchy).
NOTE: the report is actually about `@Conditional` evaluation not autoconfiguration
per se, but most autoconfiguration features use `@Conditional` heavily, so there is
a lot of overlap.
Example curl request:
include::{generated}/autoconfig/curl-request.adoc[]
Example HTTP request:
include::{generated}/autoconfig/http-request.adoc[]
Example HTTP response:
include::{generated}/autoconfig/http-response.adoc[]

View File

@@ -0,0 +1,17 @@
=== /beans
This endpoint is a report on the Spring Boot `ApplicationContext`. It lists
the beans in the context and their dependencies, detailing the names and
concrete classes of each bean.
NOTE: some beans are pure configuration (any class that is annotated
`@Configuration`).
Example curl request:
include::{generated}/beans/curl-request.adoc[]
Example HTTP request:
include::{generated}/beans/http-request.adoc[]
Example HTTP response:
include::{generated}/beans/http-response.adoc[]

View File

@@ -0,0 +1,19 @@
=== /configprops
This endpoint is a report on the Spring Boot `@ConfigurationProperties`
beans. Beans with this annotation are bound to the `Environment` on
startup, so they reflect the externalised configuration of the application.
Beans are listed by name.
A bean that is added using `@EnableConfigurationProperties` will have
a conventional name: `<prefix>.CONFIGURATION_PROPERTIES`, where
`<prefix>` is the environment key prefix specified in the
`@ConfigurationProperties` annotation.
Example curl request:
include::{generated}/configprops/curl-request.adoc[]
Example HTTP request:
include::{generated}/configprops/http-request.adoc[]
Example HTTP response:
include::{generated}/configprops/http-response.adoc[]

View File

@@ -0,0 +1,19 @@
=== /dump
This endpoint is a thread dump: the result is a list of threads each with
their name, monitor state and stack. It is the same information as you would
get from `kill -3` of a running Java process. Can be very useful for detecting
issues at runtime, especially sluggish behaviour caused by threads blocked
by slow or unavailable I/O (e.g. if a connection pool is exhausted).
NOTE: some `SecurityManager` implementations might prevent this endpoint
from working.
Example curl request:
include::{generated}/dump/curl-request.adoc[]
Example HTTP request:
include::{generated}/dump/http-request.adoc[]
Example HTTP response:
include::{generated}/dump/http-response.adoc[]

View File

@@ -0,0 +1,17 @@
=== /env
This endpoint is a dump of the Spring `Environment`. It lists the active
profiles and all the `PropertySources` in the `Environment` (the ones that
are listed first take precedence when binding to `@ConfigurationProperties`
or `@Value`). Normally you will see the Java `System` properties and the
OS environment variables in their own `PropertySources` plus any `.properties`
or `.yml` files used to configure the application on start up.
Example curl request:
include::{generated}/env/curl-request.adoc[]
Example HTTP request:
include::{generated}/env/http-request.adoc[]
Example HTTP response:
include::{generated}/env/http-response.adoc[]

View File

@@ -0,0 +1,22 @@
=== /health
This endpoint is an indication of the health of the application.
It has an overall status ("UP", "DOWN" etc.), which is the only thing
you see unless either you are authenticated or the endpoint is marked
as `sensitive=false` (`endpoints.health.sensitive=false`).
The HTTP code in the response reflects the status (e.g. "UP" = 200,
"OUT_OF_SERVICE"=503, "DOWN"=503). The mappings can be changed by
configuring `endpoints.health.mapping.<STATUS>=XXX`.
Example curl request:
include::{generated}/health/curl-request.adoc[]
Example HTTP request:
include::{generated}/health/http-request.adoc[]
Example HTTP response:
include::{generated}/health/http-response.adoc[]
Example HTTP response with `endpoints.health.sensitive=false`:
include::{generated}/health/unsensitive/http-response.adoc[]

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

View File

@@ -0,0 +1,148 @@
= Spring Boot Actuator Endpoints
:toc: left
:idprefix: spring_boot_actuator_
Actuator endpoints allow you to monitor and interact with your application. Spring Boot
includes a number of built-in endpoints and you can also add your own. For example the
`health` endpoint provides basic application health information.
The way that endpoints are exposed will depend on the type of technology that you choose.
Most applications choose HTTP monitoring, where the ID of the endpoint is mapped
to a URL. For example, by default, the `health` endpoint will be mapped to `/health`.
== List of Endpoints
include::{generated}/endpoints.adoc[]
=== /logfile
This endpoint (if available) contains the plain text logfile configured by the user
using `logging.file` or `logging.path` (by default logs are only emitted on stdout
so one of these properties has to be set for this endpoint to be active).
Example curl request:
include::{generated}/logfile/curl-request.adoc[]
Example HTTP request:
include::{generated}/logfile/http-request.adoc[]
Example HTTP response:
include::{generated}/logfile/http-response.adoc[]
=== /docs
This endpoint (if available) contains HTML documemtation for the other endpoints. Its path
can be "/docs" (if there is an existing home page) or "/" (otherwise, including if the
HAL browser is not active).
== Hypermedia Support
If https://projects.spring.io/spring-hateoas[Spring HATEOAS] is enabled
(i.e. if it is on the classpath by default) then the Actuator
endpoint responses are enhanced with hypermedia in the form of "links". The default
media type for responses is http://stateless.co/hal_specification.html[HAL], resulting
in each resource having an extra property called "_links". You can change the
media type to another one supported by Spring HATEOAS by providing your own
`@EnableHypermedia` annotation and custom providers as necessary.
Example enhanced "/metrics" endpoint with additional "_links":
include::{generated}/metrics/hypermedia/http-response.adoc[]
WARNING: Beware of Actuator endpoint paths clashing with application endpoints.
The easiest way to avoid that is to use a `management.contextPath`, e.g. "/admin".
TIP: You can disable the hypermedia support in Actuator endpoints by setting
`endpoints.links.enabled=false`.
=== Default home page
If the `management.contextPath` is empty, or if the home page provided
by the application happens to be a response body of type `ResourceSupport`, then it will
be enhanced with links to the actuator endpoints. The latter would happen for instance
if you use Spring Data REST to expose `Repository` endpoints.
Example vanilla "/" endpoint if the `management.contextPath` is empty (the "/admin"
page would be the same with different links if `management.contextPath=/admin`):
include::{generated}/admin/http-response.adoc[]
=== Endpoints with format changes
Some endpoints in their "raw" form consist of an array (e.g. the "/beans" and the "/trace" endpoints).
These need to be converted to objects (maps) before they can be enhanced with
links, so their contents are inserted as a field named "content".
Example enhanced "/beans" endpoint with additional "_links":
include::{generated}/beans/hypermedia/http-response.adoc[]
== HAL Browser
If Hypermedia is enabled and the HAL format is in use (which is the default), then
you can provide a browser for the resources by including a dependency
on the https://github.com/mikekelly/hal-browser[HAL browser] webjar.
For example in Maven:
[source,xml]
----
<dependency>
<groupId>org.webjars</groupId>
<artifactId>hal-browser</artifactId>
</dependency>
----
or in Gradle
[source,groovy]
----
dependencies {
...
compile('org.webjars:hal-browser')
...
}
----
NOTE: if you are using Spring Data REST, then a dependency on the `spring-data-rest-hal-browser`
will have an equivalent effect.
If you do that then a new endpoint will appear at "/" or "/hal" (relative to the `management.contextPath`)
serving up a static HTML page with some JavaScript that lets you browse the available
resources. The default endpoint path depends on whether or not there is already a static home page
("index.html") - if there is not and the `management.contextPath` is empty, then the HAL browser
shows up on the home page. Example:
image::hal-browser.png[HAL Browser]
TIP: The endpoint path can always, as with all MVC endpoints, be overridden using
`endpoints.hal.path=/yourpath` (note the leading slash).
== Actuator Documentation Browser
You can also provide a browser for the standard generated documentation
for the Actuator endpoints by including a dependency on the documentation jar.
For example in Maven:
[source,xml]
----
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-hypermedia-docs</artifactId>
</dependency>
----
or in Gradle
[source,groovy]
----
dependencies {
...
compile('org.springframework.boot:spring-boot-hypermedia-docs')
...
}
----
If you do that then a new endpoint at "/" or "/docs" (relative to the `management.contextPath`)
will serve up a static HTML page with this documentation in it. The default endpoint path depends
on whether or not there is already a static home page
("index.html" or a HAL browser) - if there is not and the `management.contextPath` is empty,
then the docs browser shows up on the home page.

View File

@@ -0,0 +1,16 @@
=== /info
This endpoint is empty and marked as `sensitive=false`
by default (so it is unauthenticated by default if Spring
Security is in use). It reflects the content of the `info.*` properties
in the `Environment`, as well as the properties in `git.properties`
if such a file exists in the root of the classpath.
Example curl request:
include::{generated}/info/curl-request.adoc[]
Example HTTP request:
include::{generated}/info/http-request.adoc[]
Example HTTP response:
include::{generated}/info/http-response.adoc[]

View File

@@ -0,0 +1,14 @@
=== /mappings
This endpoint lists the Spring MVC request mappings, so users can
see the handlers registered for requests by path, method, media type,
etc.
Example curl request:
include::{generated}/mappings/curl-request.adoc[]
Example HTTP request:
include::{generated}/mappings/http-request.adoc[]
Example HTTP response:
include::{generated}/mappings/http-response.adoc[]

View File

@@ -0,0 +1,17 @@
=== /metrics
This endpoint lists the public metrics exposed by the application.
By default this includes all the counters in the `CounterService`
and all the gauges in the `GaugeService`, plus a few JVM metrics about
memory and uptime. Users can register additional sources by creating
beans of type `PublicMetrics` and/or by registering counters and
gauges.
Example curl request:
include::{generated}/metrics/curl-request.adoc[]
Example HTTP request:
include::{generated}/metrics/http-request.adoc[]
Example HTTP response:
include::{generated}/metrics/http-response.adoc[]

View File

@@ -0,0 +1,16 @@
=== /trace
This endpoint lists contents of the `TraceRepository` (which
users can override by providing a bean of that type, or by
injecting that bean and adding stuff to it). By default
it is the last 100 HTTP requests, including all headers in the
request and response, and the path and HTTP status.
Example curl request:
include::{generated}/trace/curl-request.adoc[]
Example HTTP request:
include::{generated}/trace/http-request.adoc[]
Example HTTP response:
include::{generated}/trace/http-response.adoc[]

View File

@@ -0,0 +1,170 @@
package org.springframework.boot.actuate.hypermedia.test;
import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.RestDocumentation.documentationConfiguration;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import groovy.text.Template;
import groovy.text.TemplateEngine;
import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.StringUtils;
import org.springframework.web.context.WebApplicationContext;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class)
@WebAppConfiguration
@TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true",
"endpoints.health.sensitive=true", "endpoints.links.enabled=false" })
@DirtiesContext
public class EndpointDocumentation {
@Autowired
private WebApplicationContext context;
@Autowired
private MvcEndpoints mvcEndpoints;
@Autowired
@Qualifier("metricFilter")
private Filter metricFilter;
@Autowired
@Qualifier("webRequestLoggingFilter")
private Filter traceFilter;
@Autowired
private TemplateEngine templates;
@Value("${org.springframework.restdocs.outputDir:${user.dir}/target/generated-snippets}")
private String restdocsOutputDir;
private MockMvc mockMvc;
@Before
public void setUp() {
System.setProperty("org.springframework.restdocs.outputDir",
this.restdocsOutputDir);
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.addFilters(this.metricFilter, this.traceFilter)
.apply(documentationConfiguration()).build();
}
@Test
public void logfile() throws Exception {
this.mockMvc.perform(get("/logfile").accept(MediaType.TEXT_PLAIN))
.andExpect(status().isOk())
.andDo(document("logfile"));
}
@Test
public void endpoints() throws Exception {
final File docs = new File("src/main/asciidoc");
final Map<String, Object> model = new LinkedHashMap<String, Object>();
final List<EndpointDoc> endpoints = new ArrayList<EndpointDoc>();
model.put("endpoints", endpoints);
for (MvcEndpoint endpoint : getEndpoints()) {
final String endpointPath = StringUtils.hasText(endpoint.getPath()) ? endpoint
.getPath() : "/";
if (!endpointPath.equals("/docs") && !endpointPath.equals("/logfile")) {
String output = endpointPath.substring(1);
output = output.length() > 0 ? output : "./";
this.mockMvc
.perform(get(endpointPath).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andDo(document(output))
.andDo(new ResultHandler() {
@Override
public void handle(MvcResult mvcResult) throws Exception {
EndpointDoc endpoint = new EndpointDoc(docs, endpointPath);
endpoints.add(endpoint);
}
});
}
}
File file = new File(this.restdocsOutputDir + "/endpoints.adoc");
file.getParentFile().mkdirs();
PrintWriter writer = new PrintWriter(file, "UTF-8");
try {
Template template = this.templates.createTemplate(new File(
"src/test/resources/templates/endpoints.adoc.tpl"));
template.make(model).writeTo(writer);
}
finally {
writer.close();
}
}
private Collection<? extends MvcEndpoint> getEndpoints() {
List<? extends MvcEndpoint> endpoints = new ArrayList<MvcEndpoint>(
this.mvcEndpoints.getEndpoints());
Collections.sort(endpoints, new Comparator<MvcEndpoint>() {
@Override
public int compare(MvcEndpoint o1, MvcEndpoint o2) {
return o1.getPath().compareTo(o2.getPath());
}
});
return endpoints;
}
public static class EndpointDoc {
private String path;
private String custom;
private String title;
public EndpointDoc(File rootDir, String path) {
this.title = path;
this.path = path.equals("/") ? "" : path;
String custom = path.substring(1) + ".adoc";
if (new File(rootDir, custom).exists()) {
this.custom = custom;
}
}
public String getTitle() {
return this.title;
}
public String getPath() {
return this.path;
}
public String getCustom() {
return this.custom;
}
}
}

View File

@@ -0,0 +1,63 @@
package org.springframework.boot.actuate.hypermedia.test;
import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.RestDocumentation.documentationConfiguration;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import groovy.text.TemplateEngine;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class)
@WebAppConfiguration
@TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true",
"endpoints.health.sensitive=false" })
@DirtiesContext
public class HealthEndpointDocumentation {
@Autowired
private WebApplicationContext context;
@Autowired
private MvcEndpoints mvcEndpoints;
@Autowired
private TemplateEngine templates;
@Value("${org.springframework.restdocs.outputDir:target/generated-snippets}")
private String restdocsOutputDir;
private MockMvc mockMvc;
@Before
public void setUp() {
System.setProperty("org.springframework.restdocs.outputDir",
this.restdocsOutputDir);
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration())
.build();
}
@Test
public void health() throws Exception {
this.mockMvc.perform(get("/health").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("health/unsensitive"));
}
}

View File

@@ -0,0 +1,76 @@
package org.springframework.boot.actuate.hypermedia.test;
import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.RestDocumentation.documentationConfiguration;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import groovy.text.TemplateEngine;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class)
@WebAppConfiguration
@TestPropertySource(properties = "spring.jackson.serialization.indent_output=true")
@DirtiesContext
public class HypermediaEndpointDocumentation {
@Autowired
private WebApplicationContext context;
@Autowired
private MvcEndpoints mvcEndpoints;
@Autowired
private TemplateEngine templates;
@Value("${org.springframework.restdocs.outputDir:target/generated-snippets}")
private String restdocsOutputDir;
private MockMvc mockMvc;
@Before
public void setUp() {
System.setProperty("org.springframework.restdocs.outputDir",
this.restdocsOutputDir);
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration())
.build();
}
@Test
public void beans() throws Exception {
this.mockMvc.perform(get("/beans").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("beans/hypermedia"));
}
@Test
public void metrics() throws Exception {
this.mockMvc.perform(get("/metrics").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("metrics/hypermedia"));
}
@Test
public void home() throws Exception {
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("admin"));
}
}

View File

@@ -0,0 +1,21 @@
package org.springframework.boot.actuate.hypermedia.test;
import groovy.text.GStringTemplateEngine;
import groovy.text.TemplateEngine;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class SpringBootHypermediaApplication {
@Bean
public TemplateEngine groovyTemplateEngine() {
return new GStringTemplateEngine();
}
public static void main(String[] args) {
SpringApplication.run(SpringBootHypermediaApplication.class, args);
}
}

View File

@@ -0,0 +1,2 @@
# management.contextPath=/admin
logging.path: target/logs

View File

@@ -0,0 +1,16 @@
<% endpoints.each { endpoint ->
if (endpoint.custom) { %>
include::{docs}/${endpoint.custom}[]
<% } else { %>
=== ${endpoint.title}
Example curl request:
include::{generated}${endpoint.path}/curl-request.adoc[]
Example HTTP request:
include::{generated}${endpoint.path}/http-request.adoc[]
Example HTTP response:
include::{generated}${endpoint.path}/http-response.adoc[]
<% }
} %>