7

I have tried the new way of customizing the health Actuator in Spring Boot 2.0.0.M5, as described here: https://spring.io/blog/2017/08/22/introducing-actuator-endpoints-in-spring-boot-2-0:

@Endpoint(id = "health")
public class HealthEndpoint {
    @ReadOperation
    public Health health() {
        return new Health.Builder()
            .up()
            .withDetail("MyStatus", "is happy")
            .build();
    }
}

However, when I run HTTP GET to localhost:port/application/health, I still get the standard default health info. My code is completely ignored.

When I use the "traditional way" of customizing the health info via implementation of HealthIndicator, it works as expected, the health information is decorated with the given details:

@Component
public class MyHealthIndicator implements HealthIndicator {
    @Override
    public Health health() {
        return new Health.Builder()
            .up()
            .withDetail("MyStatus 1.1", "is happy")
            .withDetail("MyStatus 1.2", "is also happy")
            .build();
    }
}

QUESTION: What more shall I configure and/or implement to make the @Endpoint(id = "health") solution working?

My intention is not to create a custom actuator myhealth, but to customize the existing health actuator. Based on the documentation I expect to reach the same result as by implementing HealthIndicator. Am I wrong in that assumption?


The Maven configuration pom.xml contains:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.M5</version>
    <relativePath/>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>

The Spring Boot configuration application.properties contains:

endpoints.health.enabled=true
endpoints.autoconfig.enabled=true
endpoints.autoconfig.web.enabled=true
Honza Zidek
  • 9,204
  • 4
  • 72
  • 118
  • Have you configured your `HealthEndpoint` as a bean? To do so, you'd typically declare a `@Bean` method for it or, if you're using component scanning, annotate it with `@Component` – Andy Wilkinson Oct 17 '17 at 19:12
  • 1
    @AndyWilkinson Well, I would expect that `@Endpoint`-annotated classes are scanned automagically in Spring Boot. However, now I tried to add also `@Component` before the `@Endpoint` and there is no difference. – Honza Zidek Oct 17 '17 at 19:17
  • @AndyWilkinson: Indra Basak already answered to me in his comment below, that my failure was based on a wrong assumption. So probably you would not be able to make it working either :) I edited my question so it should be more clear. Indra: "The documentation is trying to explain the new endpoint infrastructure with the existing health endpoint as an example. A new endpoint ID has to be unique and shouldn't be same as an existing actuator endpoint." – Honza Zidek Oct 18 '17 at 19:38

2 Answers2

7

Update

  • The documentation on the new Spring Actuator Endpoints is not very lucid. It's trying to explain the new endpoint infrastructure with the existing health endpoint as an example.

  • A new endpoint ID has to be unique and shouldn't be same as an existing actuator endpoint. If one tries to the change the ID of the example shown below to health, one will get the following exception:

     java.lang.IllegalStateException: Found two endpoints with the id 'health'
    
  • The above comment about declaring the endpoint classes with @Bean annotation is correct.

  • Customizing the health endpoint hasn't changed in Spring Boot 2.0. You still have to implement HealthIndicator to add custom values.

Custom Actuator Endpoint

Here are the changes needed to create a custom Actuator endpoint in Spring Boot 2.0.

Model

The domain containing your custom information.

@Data
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class MyHealth {

    private Map<String, Object> details;

    @JsonAnyGetter
    public Map<String, Object> getDetails() {
        return this.details;
    }
}

My Health Endpoint

Declaring myhealth endpoint,

@Endpoint(id = "myhealth")
public class MyHealthEndpoint {

    @ReadOperation
    public MyHealth health() {
        Map<String, Object> details = new LinkedHashMap<>();
        details.put("MyStatus", "is happy");
        MyHealth health = new MyHealth();
        health.setDetails(details);

        return health;
    }
}

My Health Extension

Extension for myhealth endpoint,

@WebEndpointExtension(endpoint = MyHealthEndpoint.class)
public class MyHealthWebEndpointExtension {

    private final MyHealthEndpoint delegate;

    public MyHealthWebEndpointExtension(MyHealthEndpoint delegate) {
        this.delegate = delegate;
    }

    @ReadOperation
    public WebEndpointResponse<MyHealth> getHealth() {
        MyHealth health = delegate.health();
        return new WebEndpointResponse<>(health, 200);
    }
}

Actuator Configuration

Configuration to expose the two newly created actuator classes as beans,

@Configuration
public class ActuatorConfiguration {

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnEnabledEndpoint
    public MyHealthEndpoint myHealthEndpoint() {
        return new MyHealthEndpoint();
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnEnabledEndpoint
    @ConditionalOnBean({MyHealthEndpoint.class})
    public MyHealthWebEndpointExtension myHealthWebEndpointExtension(
            MyHealthEndpoint delegate) {
        return new MyHealthWebEndpointExtension(delegate);
    }
}

Application Properties

Changes to application.yml,

endpoints:
  myhealth:
    enabled: true

Once you start your application, you should be able to access the newly actuator endpoint at http://<host>:<port>/application/myhealth.

You should expect a response similar to one shown below,

{
  "MyStatus": "is happy"
}

A complete working example can be found here.

Community
  • 1
  • 1
Indra Basak
  • 7,124
  • 1
  • 26
  • 45
  • Thanks, it - almost - works :) Somehow it duplicates the information, the output is { "details": { "MyStatus": "is happy" }, "MyStatus": "is happy" } – Honza Zidek Dec 09 '17 at 00:55
-1

Provide your own @WebEndpoint like

@Component
@WebEndpoint(id = "acmehealth")
public class AcmeHealthEndpoint {

    @ReadOperation
    public String hello() {
      return "hello health";
    }
}

and

  1. include it
  2. map the original /health to, say, /internal/health
  3. map your custom endpoint to /health

via application.properties:

management.endpoints.web.exposure.include=acmehealth
management.endpoints.web.path-mapping.health=internal/health
management.endpoints.web.path-mapping.acmehealth=/health

This will override /health completely, not just add the information to the existing /health, as a custom HealthIndicator would. Question is, what you want, because @Endpoint(id = "health") and "My intention is not to create a custom actuator myhealth, but to customize the existing health actuator" kind of collide. But you can use the existing HealthEndpoint in your AcmeHealthEndpoint and accomplish both:

@Component
@WebEndpoint(id = "prettyhealth")
public class PrettyHealthEndpoint {

    private final HealthEndpoint healthEndpoint;
    private final ObjectMapper objectMapper;

    @Autowired
    public PrettyHealthEndpoint(HealthEndpoint healthEndpoint, ObjectMapper objectMapper) {
        this.healthEndpoint = healthEndpoint;
        this.objectMapper = objectMapper;
    }

    @ReadOperation(produces = "application/json")
    public String getHealthJson() throws JsonProcessingException {
        Health health = healthEndpoint.health();
        ObjectWriter writer = objectMapper.writerWithDefaultPrettyPrinter();
        return writer.writeValueAsString(health);
    }

    @ReadOperation
    public String prettyHealth() throws JsonProcessingException {
        return "<html><body><pre>" + getHealthJson() + "</pre></body></html>";
    }
}
crusy
  • 1,424
  • 2
  • 25
  • 54