Spring actuator CompositeHealthIndicator - how to automatically use/display all indicators on /health [duplicate] - spring-boot

This question already has answers here:
spring-boot health not showing details (withDetail info)
(7 answers)
Closed 4 years ago.
Spring being all auto auto-magic, I was a bit suprised at how I am observing the HealthIndicator to function.
I expected that, when I have a HealthIndicator bean in context that it will aggregate it along with the other ones it has and produce a summary in /actuator/health. Instead what it does it ignore all the ones but my custom one. I ended up implementing my own endpoint aggregating them.
I must be missing something. Either I am not enabling this functionality correctly (and I admit it's confusing) or I am expecting behavior that isn't what it actually should do.
My Question: Are my expectations correct? If So, how do I make it auto-aggregate? If my expectations are incorrect, what's the idiomatic way for actuator to work as I desire?
Here is my sample code:
#Component
public class HelloWorldHealthIndicator implements HealthIndicator {
#Override
public Health health() {
return Health.up().withDetail("hello","world").build();
}
}
/actuator/health
{
"status": "UP"
}
It's also not printing "hello" : "world" as I would have expected but that's another matter.
Here is my custom controller and output:
#RestController
public class CustomController {
private final Map<String,HealthIndicator> indicators;
#Autowired
public HealthController(Map<String, HealthIndicator> indicators) {
this.indicators = indicators;
}
#GetMapping("/health")
public Health getHealth(#RequestParam("deep") Boolean deep){
if(deep != null && deep)
return new CompositeHealthIndicator(new OrderedHealthAggregator(),indicators).health();
return Health.up().build();
}
}
/health?deep=true
{
"status": "UP",
"details": {
"helloWorldHealthIndicator": {
"status": "UP",
"details": {
"hello": "world"
}
},
"diskSpaceHealthIndicator": {
"status": "UP",
"details": {
"total": 74865782784,
"free": 65754009600,
"threshold": 10485760
}
},
"dbHealthIndicator": {
"status": "UP",
"details": {
"database": "MySQL",
"hello": 1
}
}
}
}

What makes you think your custom HealthIndicator is not invoked? Have you tried to run an app with the debugger to check if it was invoked?
That output doesn't tell us anything: you may simply get the summarized view of all the health indicators (no detail at all)
The doc states
Health information is collected from all HealthIndicator beans defined in your ApplicationContext. Spring Boot includes a number of auto-configured HealthIndicators and you can also write your own.
That code above should work just fine. My best guess is that the details aren't displayed (check the doc to enable that and please note that they are differences between Spring Boot 1 and Spring Boot 2).
Edit: To demonstrate that, I've created a sample project that uses the exact same code as yours and I get this when I invoke health (with security disabled so that details are shown to anonymous users):
{
"status": "UP",
"helloWorld": {
"status": "UP",
"hello": "world"
},
"diskSpace": {
"status": "UP",
"total": 499963170816,
"free": 217149034496,
"threshold": 10485760
}
}

Related

Springdocs: Specifying an explicit type for Paged responses

I'm working on a "global search" for my application.
Currently, I'm using hibernate-search to search for instances of multiple different objects and return them to the user.
The relevant code looks as follows:
Search.session(entityManager)
.search(ModelA.classs, ModelB.class)
.where(...)
.sort(...)
.fetch(skip, count);
Skip and count are calculated based on a Pageable and the result is used to create an instance of Page, which will be returned to the controller.
This works as I'd expect, however, the types generated by swagger-docs obviously doesn't know, what the type within the Page is, and therefore uses Object.
I'd like to expose the correct types, as I use them to generate the types for the frontend application.
I was able to set the type to an array, when overwriting the schema like this:
#ArraySchema(schema = #Schema(anyOf = {ModelA.class, ModelB.class}))
public Page<?> search(Pageable pageable) {
However, this just disregards the Page and also isn't correct.
The next thing I tried is extending the PageImpl, overwriting the getContent method, and specifying the same schema on this method, but this wasn't included in the output at all.
Next was implementing Page<T> myself (and later removing the implements reference to Page<T>) and specifying the same schema on getContent, iterator, and the field itself, but also to no effect.
How do I tell spring-docs, what the content of the resulting Page might be?
I stumbled upon this when trying to solve a similar problem
Inspired from this thread Springdoc with a generic return type i came up with the following solution, and it seems to apply to your case also. Code examples are in Kotlin.
I introduced a stub class that will just act as the Schema for the response:
private class PageModel(
#Schema(oneOf = [ModelA::class, ModelB::class]))
content: List<Object>
): PageImpl<Object>(content)
Then i annotated my Controller like this:
#Operation(
responses = [
ApiResponse(
responseCode = "200",
content = [Content(schema = Schema(implementation = PageModel::class))]
)
]
)
fun getPage(pageable: Pageable): Page<Object>
This generated this api response:
"PageModel": {
"properties": {
"content": {
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/ModelA"
},
{
"$ref": "#/components/schemas/ModelB"
}
],
"type": "object"
},
"type": "array"
},
... -> more page stuff from spring's PageImpl<>
And in the "responses" section for the api call:
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PageModel"
}
}
},
"description": "OK"
}
All generated openapi doc is similar to the autogenerated json when returning a Page, it just rewrites the "content" array property to have a specific type.

Can Springfox 3 generate Integer boundaries based on JSR 303 #Min/#Max in OAS3 API docs?

I created a Spring Boot application with a RestController that validates the DTO passed to a POST method based on JSR 303 validation annotations. API docs are generated using Springfox.
Validations are applied correctly and show up in the OAS2 API Docs. They are incomplete in the OAS3 API docs however - no minimum/maximum boundaries are generated for the Integer fields.
I'm using Spring Boot 2.5.2 (this is important as the latest version, 2.6.2, has issues with Springfox) and Springfox 3.0.0.
Since I found no specific hints in documentation + Springfox issue tracking and JSR303 support is working for the most part, I think this is a bug or oversight in Springfox OAS3 support. In the meantime I found a workaround which I will post as an answer - if I missed anything or there are better solutions I'd be happy to hear about that.
Details:
Controller
#Slf4j
#RestController
public class MyController {
#PostMapping
public void send(#Valid #RequestBody MyDTO dto) {
log.info("{}", dto);
}
}
DTO
#Value
public class MyDTO {
#Size(max = 200)
String text;
#Max(2)
#Min(1)
Integer number;
#Max(4)
#Min(3)
int number2;
#Max(6)
#Min(5)
BigDecimal decimal;
}
OAS2 DTO schema (extracted from http://localhost:8080/v2/api-docs)
{
"MyDTO": {
"type": "object",
"properties": {
"decimal": {
"type": "number",
"minimum": 5,
"maximum": 6,
"exclusiveMinimum": false,
"exclusiveMaximum": false
},
"number": {
"type": "integer",
"format": "int32",
"minimum": 1,
"maximum": 2,
"exclusiveMinimum": false,
"exclusiveMaximum": false
},
"number2": {
"type": "integer",
"format": "int32",
"minimum": 3,
"maximum": 4,
"exclusiveMinimum": false,
"exclusiveMaximum": false
},
"text": {
"type": "string",
"minLength": 0,
"maxLength": 200
}
},
"title": "MyDTO"
}
}
OAS3 DTO schema (extracted from http://localhost:8080/v3/api-docs)
{
"schemas": {
"MyDTO": {
"title": "MyDTO",
"type": "object",
"properties": {
"decimal": {
"maximum": 6,
"exclusiveMaximum": false,
"minimum": 5,
"exclusiveMinimum": false,
"type": "number",
"format": "bigdecimal"
},
"number": {
"type": "integer",
"format": "int32"
},
"number2": {
"type": "integer",
"format": "int32"
},
"text": {
"maxLength": 200,
"minLength": 0,
"type": "string"
}
}
}
}
}
After debugging Springfox I learned that the class springfox.documentation.oas.mappers.SchemaMapper in springfox-oas converts a "general model" to a format for OAS3.
In the "general model", field boundaries are represented by an "NumericElementFacet". A specific property that is being mapped is a subclass of "Schema".
The problem seems to happen here:
https://github.com/springfox/springfox/blob/bc9d0cad83e5dfdb30ddb487594fbc33fc1ba28c/springfox-oas/src/main/java/springfox/documentation/oas/mappers/SchemaMapper.java#L385
Properties represented by a "NumberSchema" are handled correctly (e.g. BigDecimal), their boundaries from the "NumericElementFacet" are applied. Integer fields (and further tests have shown: also Short and Long) are however represented by "IntegerSchema", which isn't handled there so the boundaries are not applied to the resulting API.
So what I did as a workaround was subclassing SchemaMapper, post-processing the results of mapProperties and registering the subclass as #Primary to override the springfox component:
import io.swagger.v3.oas.models.media.Schema;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.oas.mappers.*;
import springfox.documentation.schema.*;
import springfox.documentation.service.ModelNamesRegistry;
import java.util.*;
#Primary
#Component
#Slf4j
public class IntegerBoundarySupportingOasSchemaMapper extends SchemaMapper {
#Override
#SuppressWarnings("rawtypes")
protected Map<String, Schema> mapProperties(
Map<String, PropertySpecification> properties,
ModelNamesRegistry modelNamesRegistry) {
var result = super.mapProperties(properties, modelNamesRegistry);
result.values()
.stream()
// "integer" seems to cover at least Java Short, Integer and Long.
.filter(property -> "integer".equals(property.getType()))
.forEach(property -> properties.get(property.getName())
.getFacets()
.stream()
.filter(NumericElementFacet.class::isInstance)
.map(NumericElementFacet.class::cast)
.findFirst()
.ifPresent(f -> {
log.trace("Adding boundaries to API field {} (min={}, max={})",
property.getName(),
f.getMinimum(),
f.getMaximum());
property.setMaximum(f.getMaximum());
property.exclusiveMaximum(f.getExclusiveMaximum());
property.setMinimum(f.getMinimum());
property.exclusiveMinimum(f.getExclusiveMinimum());
}));
return result;
}
}
In my case, this is working fine, so maybe its helping someone else, too.
Side note: The SchemaMapper is called every time I retrieve http://localhost:8080/v3/api-docs, that might be something to keep in mind when considering other time-consuming modifications of the schema.

graphql-java error data (like extensions) is being stuffed into "message" field on client

In graphql-java, I'm overriding getExtensions on a GraphQLError, in order to pass a "code" extension for Apollo Client to consume.
When I added a breakpoint, I can see that GraphQLObjectMapper sees the error in the executionResult with all its fields, including extensions properly set as expected.
However, when the error arrives at apollo, it gets strangely morphed and the entire error object (including the extensions array) appears to be stuffed into the string message field like so:
{
"errors": [
{
"message": "Unexpected error value: { message: \"Exception while fetching data (/myQuery) : Didn't work\", locations: [[Object]], path: [\"myQuery\"], extensions: { code: \"MY_CODE\", classification: \"DataFetchingException\" } }",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"mailLabels"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR"
}
}
],
"data": null
}
As far as I can tell apollo is not doing anything wrong. My suspicion is that graphql-java may be the one stuffing this entire error into the "message" field" and then setting the code as "INTERNAL_SERVER_ERROR", but I'm not really sure. Is there something I can do on the graphql-java end to prevent this and make it properly pass the extensions and not stuff them into the message value? Any assistance would be appreciated.
It is not caused by graphql-java but graphql-java-servlet. In some old version , by default it does not serialize GraphQLError to the structure defined by the GraphQL specification , which should be fixed in 7.5.1 in this issue.
If you cannot upgrade to the latest version , the simplest way is customize GraphQLObjectMapper and overriding its convertSanitizedExecutionResult :
public class CustomObjectMapper extends GraphQLObjectMapper {
#Override
public Map<String, Object> convertSanitizedExecutionResult(ExecutionResult executionResult, boolean includeData) {
Map<String, Object> result = super.convertSanitizedExecutionResult(executionResult, includeData);
if(result.containsKey("errors")){
result.put("errors", executionResult.getErrors().stream().map(err->err.toSpecification()).collect(toList()));
}
}
}

Spring Integration Java DSL: How to loop the paged Rest service?

How to loop the paged Rest service with the Java DSL Http.outboundGatewaymethod?
The rest URL is for example
http://localhost:8080/people?page=3
and it returns for example
"content": [
{"name": "Mike",
"city": "MyCity"
},
{"name": "Peter",
"city": "MyCity"
},
...
]
"pageable": {
"sort": {
"sorted": false,
"unsorted": true
},
"pageSize": 20,
"pageNumber": 3,
"offset": 60,
"paged": true,
"unpaged": false
},
"last": false,
"totalElements": 250,
"totalPages": 13,
"first": false,
"sort": {
"sorted": false,
"unsorted": true
},
"number": 3,
"numberOfElements": 20,
"size": 20
}
where the variable totalPages tells the total pages amount.
So if the implementation
integrationFlowBuilder
.handle(Http
.outboundGateway("http://localhost:8080/people?page=3")
.httpMethod(HttpMethod.GET)
.expectedResponseType(String.class))
access one page, how to loop all the pages?
The easiest way to do this is like wrapping the call to this Http.outboundGateway() with the #MessagingGateway and provide a page number as an argument:
#MessagingGateway
public interface HttpPagingGateway {
#Gateway(requestChannel = "httpPagingGatewayChannel")
String getPage(int page);
}
Then you get a JSON as a result, where you can convert it into some domain model or just perform a JsonPathUtils.evaluate() (based on json-path) to get the value of the last attribute to be sure that you need to call that getPage() for the page++ or not.
The page argument is going to be a payload of the message to send and that can be used as an uriVariable:
.handle(Http
.outboundGateway("http://localhost:8080/people?page={page}")
.httpMethod(HttpMethod.GET)
.uriVariable("page", Message::getPayload)
.expectedResponseType(String.class))
Of course, we can do something similar with Spring Integration, but there are going to be involved filter, router and some other components.
UPDATE
First of all I would suggest you to create a domain model (some Java Bean), let's say PersonPageResult, to represent that JSON response and this type to the expectedResponseType(PersonPageResult.class) property of the Http.outboundGateway(). The RestTemplate together with the MappingJackson2HttpMessageConverter out-of-the-box will do the trick for you to return such an object as a reply for the downstream processing.
Then, as I said before, looping would be better done from some Java code, which you could wrap to the service activator call. For this purpose you should daclare a gateway like this:
public interface HttpPagingGateway {
PersonPageResult getPage(int page);
}
Pay attention: no annotations at all. The trick is done via IntegrationFlow:
#Bean
public IntegrationFlow httpGatewayFlow() {
return IntegrationFlows.from(HttpPagingGateway.class)
.handle(Http
.outboundGateway("http://localhost:8080/people?page={page}")
.httpMethod(HttpMethod.GET)
.uriVariable("page", Message::getPayload)
.expectedResponseType(PersonPageResult.class))
}
See IntegrationFlows.from(Class<?> aClass) JavaDocs.
Such a HttpPagingGateway can be injected into some service with hard looping logic:
int page = 1;
boolean last = false;
while(!last) {
PersonPageResult result = this.httpPagingGateway.getPage(page++);
last = result.getLast();
List<Person> persons = result.getPersons();
// Process persons
}
For processing those persons I would suggest to have separate IntegrationFlow, which may start from the gateway as well or you can just send a Message<List<Person>> to its input channel.
This way you will separate concerns about paging and processing and will have a simple loop logic in some POJO method.

How to configure Spring boot pagination starting from page 1, not 0

boot(1.4.0) "Pageable" for pagination.It works fine without any issue.But by default the page value starts from "0" but in the front-end the page value starts from "1". So is there any standard approach to increment value instead of manually increment the page number inside the code?
public Page<Device> find(DeviceFindCommand deviceFindCommand, Pageable pageable){
//page = 0 //Actual is 0, Expected increment by 1.
}
Any help should be appreciable.
After implementing Alan answers having the following issues,
1) Still i am able to access zero page which returns the first page(I don't know this is issue or not but i want to get a better clarity).
http://localhost:8180/api/v1/books/?page=3&size=2
Response
{
"content": [
{
"id": "57da9eadbee83fb037a66029",
.
.
.
}{
.
.
.
}
],
"last": false,
"totalElements": 5,
"totalPages": 3,
"size": 2,
"number": 2, //strange always getting 1 less than page number.
"sort": null,
"first": true,
"numberOfElements": 2
}
2) "number": 2, in the response always getting one less than the page number.It should return the current page index.
If you are using Spring Boot 2.X you could switch from WebMvcConfigurerAdapter to application properties. There is a set of properties to configure pageable:
# DATA WEB (SpringDataWebProperties)
spring.data.web.pageable.default-page-size=20 # Default page size.
spring.data.web.pageable.max-page-size=2000 # Maximum page size to be accepted.
spring.data.web.pageable.one-indexed-parameters=false # Whether to expose and assume 1-based page number indexes.
spring.data.web.pageable.page-parameter=page # Page index parameter name.
spring.data.web.pageable.prefix= # General prefix to be prepended to the page number and page size parameters.
spring.data.web.pageable.qualifier-delimiter=_ # Delimiter to be used between the qualifier and the actual page number and size properties.
spring.data.web.pageable.size-parameter=size # Page size parameter name.
spring.data.web.sort.sort-parameter=sort # Sort parameter name.
But please remember that even if you change the one-indexed-parameter the page response (PageImpl class) will return results with zero-based page number. It could be a little misleading.
Spring added this future as well. Just make oneIndexed parameter equals to true in your configuration file and pagination will start from page 1.By default its false and pagination starts from 0.
spring.data.web.pageable.one-indexed-parameters=true
Spring Boot will be using Spring Data under the covers.
The Spring Data class you need to configure is the following:
org.springframework.data.web.PageableHandlerMethodArgumentResolver
and in particular the following method:
http://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/web/PageableHandlerMethodArgumentResolver.html#setOneIndexedParameters-boolean-
This will allow you to use you current UI paging as is i.e. with first page = 1.
In a Boot application I think the config may look something like:
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver();
resolver.setOneIndexedParameters(true);
argumentResolvers.add(resolver);
super.addArgumentResolvers(argumentResolvers);
}
}
To get better result, you need to extend RepositoryRestMvcConfiguration, like below
#Configuration
public class RespositoryConfiguration extends RepositoryRestMvcConfiguration {
#Override
#Bean
public HateoasPageableHandlerMethodArgumentResolver pageableResolver() {
HateoasPageableHandlerMethodArgumentResolver resolver = super.pageableResolver();
resolver.setOneIndexedParameters(true);
return resolver;
}
}
Then you shall get all the links and pagination information correct except 'number', which is one less.
See the result for page=1,
"_links": {
"first": {
"href": "http://localhost:8080/user/user?page=1&size=5"
},
"self": {
"href": "http://localhost:8080/user/user"
},
"next": {
"href": "http://localhost:8080/user/user?page=2&size=5"
},
"last": {
"href": "http://localhost:8080/user/user?page=2&size=5"
}
},
"page": {
"size": 5,
"totalElements": 6,
"totalPages": 2,
"number": 0
}

Resources