Update Actuator endpoints to use custom media type

Previously, the actuator's endpoints produced application/json and,
where appropriate, also consumed application/json. Without a custom,
versioned media type, it's impossible for us to make changes to the
endpoints without breaking clients.

This commit introduces a new media type,
application/spring-boot.actuator.v1+json, that is now produced by
default with application/json also being produced if requested.
Endpoints that consume JSON will now also accept content the uses
the new media type in addition to application/json.

Closes gh-7967
This commit is contained in:
Andy Wilkinson
2017-01-19 15:25:48 +00:00
parent bed545df64
commit b900a3efc8
23 changed files with 478 additions and 80 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* 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.
@@ -37,6 +37,8 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@@ -47,6 +49,7 @@ import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
@@ -70,6 +73,21 @@ public class AuditEventsMvcEndpointTests {
this.mvc = MockMvcBuilders.webAppContextSetup(this.context).build();
}
@Test
public void contentTypeDefaultsToActuatorV1Json() throws Exception {
this.mvc.perform(get("/auditevents")).andExpect(status().isOk())
.andExpect(header().string("Content-Type",
"application/vnd.spring-boot.actuator.v1+json;charset=UTF-8"));
}
@Test
public void contentTypeCanBeApplicationJson() throws Exception {
this.mvc.perform(get("/auditevents").header(HttpHeaders.ACCEPT,
MediaType.APPLICATION_JSON_VALUE)).andExpect(status().isOk())
.andExpect(header().string("Content-Type",
MediaType.APPLICATION_JSON_UTF8_VALUE));
}
@Test
public void invokeWhenDisabledShouldReturnNotFoundStatus() throws Exception {
this.context.getBean(AuditEventsMvcEndpoint.class).setEnabled(false);

View File

@@ -39,6 +39,8 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
@@ -49,6 +51,7 @@ import org.springframework.web.context.WebApplicationContext;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
@@ -76,6 +79,36 @@ public class EnvironmentMvcEndpointTests {
"foo:bar", "fool:baz");
}
@Test
public void homeContentTypeDefaultsToActuatorV1Json() throws Exception {
this.mvc.perform(get("/env")).andExpect(status().isOk())
.andExpect(header().string("Content-Type",
"application/vnd.spring-boot.actuator.v1+json;charset=UTF-8"));
}
@Test
public void homeContentTypeCanBeApplicationJson() throws Exception {
this.mvc.perform(
get("/env").header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk()).andExpect(header().string("Content-Type",
MediaType.APPLICATION_JSON_UTF8_VALUE));
}
@Test
public void subContentTypeDefaultsToActuatorV1Json() throws Exception {
this.mvc.perform(get("/env/foo")).andExpect(status().isOk())
.andExpect(header().string("Content-Type",
"application/vnd.spring-boot.actuator.v1+json;charset=UTF-8"));
}
@Test
public void subContentTypeCanBeApplicationJson() throws Exception {
this.mvc.perform(get("/env/foo").header(HttpHeaders.ACCEPT,
MediaType.APPLICATION_JSON_VALUE)).andExpect(status().isOk())
.andExpect(header().string("Content-Type",
MediaType.APPLICATION_JSON_UTF8_VALUE));
}
@Test
public void home() throws Exception {
this.mvc.perform(get("/env")).andExpect(status().isOk())

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* 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.
@@ -61,17 +61,13 @@ public class HealthMvcEndpointTests {
private MockEnvironment environment;
private HttpServletRequest user = createAuthenticationToken(
"ROLE_USER");
private HttpServletRequest user = createAuthenticationToken("ROLE_USER");
private HttpServletRequest actuator = createAuthenticationToken(
"ROLE_ACTUATOR");
private HttpServletRequest actuator = createAuthenticationToken("ROLE_ACTUATOR");
private HttpServletRequest hero = createAuthenticationToken(
"ROLE_HERO");
private HttpServletRequest hero = createAuthenticationToken("ROLE_HERO");
private HttpServletRequest createAuthenticationToken(
String role) {
private HttpServletRequest createAuthenticationToken(String role) {
MockServletContext servletContext = new MockServletContext();
servletContext.declareRoles(role);
return new MockHttpServletRequest(servletContext);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* 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.
@@ -38,6 +38,8 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@@ -47,6 +49,7 @@ import org.springframework.web.context.WebApplicationContext;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
@@ -80,6 +83,21 @@ public class InfoMvcEndpointTests {
"\"beanName2\":{\"key21\":\"value21\",\"key22\":\"value22\"}")));
}
@Test
public void contentTypeDefaultsToActuatorV1Json() throws Exception {
this.mvc.perform(get("/info")).andExpect(status().isOk())
.andExpect(header().string("Content-Type",
"application/vnd.spring-boot.actuator.v1+json;charset=UTF-8"));
}
@Test
public void contentTypeCanBeApplicationJson() throws Exception {
this.mvc.perform(
get("/info").header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk()).andExpect(header().string("Content-Type",
MediaType.APPLICATION_JSON_UTF8_VALUE));
}
@Import({ JacksonAutoConfiguration.class, AuditAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class,
EndpointWebMvcAutoConfiguration.class, WebMvcAutoConfiguration.class,

View File

@@ -39,6 +39,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
@@ -55,6 +56,7 @@ import static org.mockito.Mockito.verifyZeroInteractions;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
@@ -129,12 +131,35 @@ public class LoggersMvcEndpointTests {
}
@Test
public void setLoggerShouldSetLogLevel() throws Exception {
public void contentTypeForGetDefaultsToActuatorV1Json() throws Exception {
this.mvc.perform(get("/loggers")).andExpect(status().isOk())
.andExpect(header().string("Content-Type",
"application/vnd.spring-boot.actuator.v1+json;charset=UTF-8"));
}
@Test
public void contentTypeForGetCanBeApplicationJson() throws Exception {
this.mvc.perform(get("/loggers").header(HttpHeaders.ACCEPT,
MediaType.APPLICATION_JSON_VALUE)).andExpect(status().isOk())
.andExpect(header().string("Content-Type",
MediaType.APPLICATION_JSON_UTF8_VALUE));
}
@Test
public void setLoggerUsingApplicationJsonShouldSetLogLevel() throws Exception {
this.mvc.perform(post("/loggers/ROOT").contentType(MediaType.APPLICATION_JSON)
.content("{\"configuredLevel\":\"debug\"}")).andExpect(status().isOk());
verify(this.loggingSystem).setLogLevel("ROOT", LogLevel.DEBUG);
}
@Test
public void setLoggerUsingActuatorV1JsonShouldSetLogLevel() throws Exception {
this.mvc.perform(post("/loggers/ROOT")
.contentType(ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON)
.content("{\"configuredLevel\":\"debug\"}")).andExpect(status().isOk());
verify(this.loggingSystem).setLogLevel("ROOT", LogLevel.DEBUG);
}
@Test
public void setLoggerWhenDisabledShouldReturnNotFound() throws Exception {
this.context.getBean(LoggersEndpoint.class).setEnabled(false);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* 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.
@@ -38,6 +38,8 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
@@ -49,6 +51,7 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
@@ -80,6 +83,36 @@ public class MetricsMvcEndpointTests {
.andExpect(content().string(containsString("\"foo\":1")));
}
@Test
public void homeContentTypeDefaultsToActuatorV1Json() throws Exception {
this.mvc.perform(get("/metrics")).andExpect(status().isOk())
.andExpect(header().string("Content-Type",
"application/vnd.spring-boot.actuator.v1+json;charset=UTF-8"));
}
@Test
public void homeContentTypeCanBeApplicationJson() throws Exception {
this.mvc.perform(get("/metrics").header(HttpHeaders.ACCEPT,
MediaType.APPLICATION_JSON_VALUE)).andExpect(status().isOk())
.andExpect(header().string("Content-Type",
MediaType.APPLICATION_JSON_UTF8_VALUE));
}
@Test
public void specificMetricContentTypeDefaultsToActuatorV1Json() throws Exception {
this.mvc.perform(get("/metrics/foo")).andExpect(status().isOk())
.andExpect(header().string("Content-Type",
"application/vnd.spring-boot.actuator.v1+json;charset=UTF-8"));
}
@Test
public void specificMetricContentTypeCanBeApplicationJson() throws Exception {
this.mvc.perform(get("/metrics/foo").header(HttpHeaders.ACCEPT,
MediaType.APPLICATION_JSON_VALUE)).andExpect(status().isOk())
.andExpect(header().string("Content-Type",
MediaType.APPLICATION_JSON_UTF8_VALUE));
}
@Test
public void homeWhenDisabled() throws Exception {
this.context.getBean(MetricsEndpoint.class).setEnabled(false);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* 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.
@@ -16,34 +16,127 @@
package org.springframework.boot.actuate.endpoint.mvc;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration;
import org.springframework.boot.actuate.endpoint.ShutdownEndpoint;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for {@link ShutdownMvcEndpoint}.
*
* @author Dave Syer
*
*/
@SpringBootTest(properties = { "management.security.enabled=false",
"endpoints.shutdown.enabled=true" })
@RunWith(SpringRunner.class)
public class ShutdownMvcEndpointTests {
private ShutdownEndpoint endpoint = mock(ShutdownEndpoint.class);
@Autowired
private WebApplicationContext context;
private ShutdownMvcEndpoint mvc = new ShutdownMvcEndpoint(this.endpoint);
private MockMvc mvc;
@Before
public void setUp() {
this.mvc = MockMvcBuilders.webAppContextSetup(this.context).build();
}
@Test
public void disabled() {
@SuppressWarnings("unchecked")
ResponseEntity<Map<String, String>> response = (ResponseEntity<Map<String, String>>) this.mvc
.invoke();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
public void contentTypeDefaultsToActuatorV1Json() throws Exception {
this.mvc.perform(post("/shutdown")).andExpect(status().isOk())
.andExpect(header().string("Content-Type",
"application/vnd.spring-boot.actuator.v1+json;charset=UTF-8"));
assertThat(this.context.getBean(CountDownLatch.class).await(30, TimeUnit.SECONDS))
.isTrue();
}
@Test
public void contentTypeCanBeApplicationJson() throws Exception {
this.mvc.perform(post("/shutdown").header(HttpHeaders.ACCEPT,
MediaType.APPLICATION_JSON_VALUE)).andExpect(status().isOk())
.andExpect(header().string("Content-Type",
MediaType.APPLICATION_JSON_UTF8_VALUE));
assertThat(this.context.getBean(CountDownLatch.class).await(30, TimeUnit.SECONDS))
.isTrue();
}
@Configuration
@Import({ JacksonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class,
EndpointWebMvcAutoConfiguration.class, WebMvcAutoConfiguration.class,
ManagementServerPropertiesAutoConfiguration.class })
public static class TestConfiguration {
@Bean
public TestShutdownEndpoint endpoint() {
return new TestShutdownEndpoint(contextCloseLatch());
}
@Bean
public CountDownLatch contextCloseLatch() {
return new CountDownLatch(1);
}
}
private static class TestShutdownEndpoint extends ShutdownEndpoint {
private final CountDownLatch contextCloseLatch;
TestShutdownEndpoint(CountDownLatch contextCloseLatch) {
this.contextCloseLatch = contextCloseLatch;
}
@Override
public void setApplicationContext(ApplicationContext context)
throws BeansException {
ConfigurableApplicationContext mockContext = mock(
ConfigurableApplicationContext.class);
willAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
TestShutdownEndpoint.this.contextCloseLatch.countDown();
return null;
}
}).given(mockContext).close();
super.setApplicationContext(mockContext);
}
}
}