Spring Boot Actuator offers you a set of endpoints to monitor your application, but sometimes you may need to create a custom endpoint to get specific metrics.
There are 2 way to do custom endpoint:
- custom endpoint with @Endpoint,
- controller endpoint with @RestControllerEndpoint, we will see only the second one.
In our example we don't create a custom endpoint for our application but to monitor our Jira server hosted externally. Indeed we need to know if it is up or down and how many time it spend to return a response.
You will need the following dependecies :
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-security"
implementation 'org.springframework.boot:spring-boot-starter-tomcat'
implementation group: 'com.konghq', name: 'unirest-java', version: '3.11.06'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.11.3'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.5.0'
implementation group: 'org.springdoc', name: 'springdoc-openapi-data-rest', version: '1.5.0'
}
Here, our application.yml file with actuator and jira server connection properties
management:
endpoints:
web:
exposure:
include: '*'
base-path: "/management"
security:
enabled: false
endpoint:
health:
show-details: always
app:
config:
jira:
host: "https://myjiraserver"
user: "jiraUser"
password: "jirapwd"
api-path: "/rest/api/2"
We mapped the jira properties with a java configuration class.
@Component
@ConfigurationProperties("app.config.jira")
@Validated
public class JiraConfig {
@NotEmpty
protected String host;
@NotEmpty
protected String user;
@NotEmpty
protected String password;
@NotEmpty
protected String apiPath;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getApiPath() {
return apiPath;
}
public void setApiPath(String apiPath) {
this.apiPath = apiPath;
}
}
Then, here is the service that call jira endpoint and give the response time.
@Service
public class JiraConnectorService {
private static final Logger logger = LoggerFactory.getLogger(JiraConnectorService.class);
public static final String HEADER_ACCEPT = "accept";
public static final String HEADER_APP_JSON = "application/json";
public static final String JIRA_MYSELF_ENDPOINT = "/myself";
private JiraConfig jiraConfig;
@Autowired
public void setJiraConfig(JiraConfig jiraConfig) {
this.jiraConfig = jiraConfig;
}
public ResponseTimeData getResponseTime() throws UnirestException {
logger.info("Get responseTime info");
String mySelfEndPointUrl = jiraConfig.getHost() + jiraConfig.getApiPath() + JIRA_MYSELF_ENDPOINT;
logger.info("Call {}", mySelfEndPointUrl);
ResponseTimeData data = new ResponseTimeData();
long start = System.currentTimeMillis();
HttpResponse<JsonNode> jsonResponse = Unirest.get(mySelfEndPointUrl)
.basicAuth(jiraConfig.getUser(), jiraConfig.getPassword())
.header(HEADER_ACCEPT, HEADER_APP_JSON)
.asJson();
data.setTime(System.currentTimeMillis() - start);
data.setHttpStatusCode(jsonResponse.getStatus());
data.setMessage(jsonResponse.getStatusText());
logger.info("Call {} successfull", mySelfEndPointUrl);
return data;
}
}
Data model
public class ResponseTimeData {
private long time;
private int httpStatusCode;
private String message;
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public int getHttpStatusCode() {
return httpStatusCode;
}
public void setHttpStatusCode(int httpStatusCode) {
this.httpStatusCode = httpStatusCode;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
public class HealthDtl {
protected String status;
private int httpStatusCode;
private String message;
private Long responseTimeMs;
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public int getHttpStatusCode() {
return httpStatusCode;
}
public void setHttpStatusCode(int httpStatusCode) {
this.httpStatusCode = httpStatusCode;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Long getResponseTimeMs() {
return responseTimeMs;
}
public void setResponseTimeMs(Long responseTimeMs) {
this.responseTimeMs = responseTimeMs;
}
}
And now, the custom rest endpoint !
@Component
@RestControllerEndpoint(id = "jira")
public class RestJiraEndPoint {
private static final Logger logger = LoggerFactory.getLogger(RestJiraEndPoint.class);
private final JiraConnectorService jiraConnectorService;
public RestJiraEndPoint(JiraConnectorService jiraConnectorService, MessageSource messageSource){
this.jiraConnectorService = jiraConnectorService;
}
@GetMapping("/healthDtl")
@ResponseBody
public ResponseEntity<HealthDtl> healthDtl() {
logger.info("/jira/healthDtl endpoint called");
HealthDtl health = new HealthDtl();
ResponseTimeData data = new ResponseTimeData();
try {
data = jiraConnectorService.getResponseTime();
if(data.getHttpStatusCode() == HttpStatus.OK.value())
health.setStatus("UP");
else
health.setStatus("DOWN");
health.setMessage(data.getMessage());
health.setResponseTimeMs(data.getTime());
} catch (UnirestException e) {
logger.error(e.getLocalizedMessage(), e);
health.setStatus("DOWN");
health.setMessage(e.getMessage());
}
health.setHttpStatusCode(data.getHttpStatusCode());
return new ResponseEntity<>(health, HttpStatus.OK);
}
}
You have now a new actuator endpoint /management/jira/healthDtl that replies :
{
"status":"UP",
"httpStatusCode":200,
"message":"",
"responseTimeMs":369
}
I'll add a new post on how to unit test this custom actuator endpoint.