How to create a custom actuator endpoint to monitor Jira

Michèle - Jan 4 '21 - - Dev Community

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'
    }
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode
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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

You have now a new actuator endpoint /management/jira/healthDtl that replies :

{
"status":"UP",
"httpStatusCode":200,
"message":"",
"responseTimeMs":369
}
Enter fullscreen mode Exit fullscreen mode

I'll add a new post on how to unit test this custom actuator endpoint.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .