15.3.4 Creating custom endpoints
At first glance, you might think that Actuator’s endpoints are implemented as nothing more than Spring MVC controllers. But as you’ll see in chapter 17, the endpoints are also exposed as JMX MBeans as well as through HTTP requests. Therefore, there must be something more to these endpoints than just a controller class.
In fact, Actuator endpoints are defined quite differently from controllers. Instead of a class that’s annotated with @Controller or @RestController, Actuator endpoints are defined with classes that are annotated with @Endpoint.
What’s more, instead of using HTTP-named annotations such as @GetMapping, @PostMapping, or @DeleteMapping, Actuator endpoint operations are defined by methods annotated with @ReadOperation, @WriteOperation, and @DeleteOperation. These annotations don’t imply any specific communication mechanism and, in fact, allow Actuator to communicate by any variety of communication mechanisms, HTTP, and JMX out of the box. To demonstrate how to write a custom Actuator endpoint, consider NotesEndpoint in the next listing.
Listing 15.7 A custom endpoint for taking notes
package tacos.ingredients;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.stereotype.Component;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Component
@Endpoint(id="notes", enableByDefault=true)
public class NotesEndpoint {
private List<Note> notes = new ArrayList<>();
@ReadOperation
public List<Note> notes() {
return notes;
}
@WriteOperation
public List<Note> addNote(String text) {
notes.add(new Note(text));
return notes;
}
@DeleteOperation
public List<Note> deleteNote(int index) {
if (index < notes.size()) {
notes.remove(index);
}
return notes;
}
@RequiredArgsConstructor
private class Note {
@Getter
private Date time = new Date();
@Getter
private final String text;
}
}This endpoint is a simple note-taking endpoint, wherein one can submit a note with a write operation, read the list of notes with a read operation, and remove a note with the delete operation. Admittedly, this endpoint isn’t very useful as far as Actuator endpoints go. But when you consider that the out-of-the-box Actuator endpoints cover so much ground, it’s difficult to come up with a practical example of a custom Actuator endpoint.
At any rate, the NotesEndpoint class is annotated with @Component so that it will be picked up by Spring’s component scanning and instantiated as a bean in the Spring application context. But more relevant to this discussion, it’s also annotated with @Endpoint, making it an Actuator endpoint with an ID of notes. And it’s enabled by default so that you won’t need to explicitly enable it by including it in the management.web.endpoints.web.exposure.include configuration property.
As you can see, NotesEndpoint offers one of each kind of operation:
The
notes()method is annotated with@ReadOperation. When invoked, it will return a list of available notes. In HTTP terms, this means it will handle an HTTP GET request for /actuator/notes and respond with a JSON list of notes.The
addNote()method is annotated with@WriteOperation. When invoked, it will create a new note from the given text and add it to the list. In HTTP terms, it handles aPOSTrequest where the body of the request is a JSON object with a text property. It finishes by responding with the current state of the notes list.The
deleteNote()method is annotated with@DeleteOperation. When invoked, it will delete the note at the given index. In HTTP terms, this endpoint handlesDELETErequests where the index is given as a request parameter.
To see this in action, you can use curl to poke about with this new endpoint. First, add a couple of notes, using two separate POST requests, as shown here:
$ curl localhost:8080/actuator/notes \
-d'{"text":"Bring home milk"}' \
-H"Content-type: application/json"
[{"time":"2020-06-08T13:50:45.085+0000","text":"Bring home milk"}]
$ curl localhost:8080/actuator/notes \
-d'{"text":"Take dry cleaning"}' \
-H"Content-type: application/json"
[{"time":"2021-07-03T12:39:13.058+0000","text":"Bring home milk"},
{"time":"2021-07-03T12:39:16.012+0000","text":"Take dry cleaning"}]As you can see, each time a new note is posted, the endpoint responds with the newly appended list of notes. But if later you want to view the list of notes, you can do a simple GET request like so:
$ curl localhost:8080/actuator/notes
[{"time":"2021-07-03T12:39:13.058+0000","text":"Bring home milk"},
{"time":"2021-07-03T12:39:16.012+0000","text":"Take dry cleaning"}]
`If you decide to remove one of the notes, a DELETE request with an index request parameter, shown next, should do the trick:
$ curl localhost:8080/actuator/notes?index=1 -X DELETE
[{"time":"2021-07-03T12:39:13.058+0000","text":"Bring home milk"}]It’s important to note that although I’ve shown only how to interact with the endpoint using HTTP, it will also be exposed as an MBean that can be accessed using whatever JMX client you choose. But if you want to limit it to only exposing an HTTP endpoint, you can annotate the endpoint class with @WebEndpoint instead of @Endpoint as follows:
@Component
@WebEndpoint(id="notes", enableByDefault=true)
public class NotesEndpoint {
...
}Likewise, if you prefer an MBean-only endpoint, annotate the class with @JmxEndpoint.
