воскресенье, 20 января 2019 г.

Кастомный Health-индикатор в Spring Boot Actuator

Для отслеживания параметров работы приложения удобно использовать Spring Boot Actuator. Он предоставляет такие возможности, как:
- создание своих (кастомных) метрик, индикаторов и счётчиков;
- экспорт метрик в различные агрегаторы и визуализаторы, включая InfluxDB и Graphite.

На одном из проектов мне необходимо было добавить возможность просмотра в заданном  формате определённых метрик микросервиса по HTTP-запросу установленного формата и выключение (полную остановку) сервиса по HTTP-запросу.

Задача интересная. И вот, как мы можем её решить:

1. Добавляем в pom.xml зависимости.

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

2. Добавляем в application.properties:

# включаем отображение в Web всех метрик
management.endpoints.web.exposure.include=*

# активируем возможность выключения сервиса по HTTP-запросу
management.endpoint.shutdown.enabled=true

# активируем показ в Health-метриках дополнительных деталей (состяние баз данных, очередей и прочего)
management.endpoint.health.show-details=always

# изменяем стандартный путь к метрикам актуатора
management.endpoints.web.base-path=/api

# изменяем стандартный путь к Health-метрикам актуатора
management.endpoints.web.path-mapping.health=status

# задаём имя сервера
server.name=defaultName-123

3. Расширяем возможности Health-индикатора актуатора.

@Service
public class StatusEndpointService implements HealthIndicator {

    private MetricsEndpoint metricsEndpoint;
    @Value("${server.name}")
    private String serverName;

    @Autowired
    public void setMetricsEndpoint(MetricsEndpoint metricsEndpoint) {
        this.metricsEndpoint = metricsEndpoint;
    }

    @Override
    public Health health() {
        Map<String, Object> statusMap = new HashMap<>();

        MetricResponse response = metricsEndpoint.metric("process.uptime", null);
        long uptimeMiliseconds = (long) (response.getMeasurements().get(0).getValue() * 1000);
        long millis = uptimeMiliseconds % 1000;
        long second = (uptimeMiliseconds / 1000) % 60;
        long minute = (uptimeMiliseconds / (1000 * 60)) % 60;
        long hour = (uptimeMiliseconds / (1000 * 60 * 60)) % 24;
        long days = uptimeMiliseconds / (1000 * 60 * 60 * 24);
        String formatedUptime = String.format("%d.%02d:%02d:%02d.%03d", days, hour, minute, second, millis);

        statusMap.put("serverName", serverName);
        statusMap.put("uptime", formatedUptime);
        return Health.up().withDetails(statusMap).build();
    }
}

В данном случае я добавил отображение в деталях Health-метрик следующих сведений:
- имя сервера, на котором запущено приложение;
- время работы сервиса с момента запуска.

Состав сведений, которые мы можем добавить для отображения в Health-метриках, окраничивается только нашей фантазией.

Запустим сервис и понаправляем на него запросы.
Посмотрим Health-статус:

$ curl http://localhost:8080/api/status | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   231    0   231    0     0  46200      0 --:--:-- --:--:-- --:--:-- 46200
{
  "status": "UP",
  "details": {
    "statusEndpointService": {
      "status": "UP",
      "details": {
        "serverName": "localhost",
        "uptime": "0.00:07:29.735"
      }
    },
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 354868289536,
        "free": 12128550912,
        "threshold": 10485760
      }
    }
  }
}

Посмотрим API метрик:

$ curl http://localhost:8080/api | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1838    0  1838    0     0   358k      0 --:--:-- --:--:-- --:--:--  358k
{
  "_links": {
    "self": {
      "href": "http://localhost:8080/api",
      "templated": false
    },
    "auditevents": {
      "href": "http://localhost:8080/api/auditevents",
      "templated": false
    },
    "beans": {
      "href": "http://localhost:8080/api/beans",
      "templated": false
    },
    "caches": {
      "href": "http://localhost:8080/api/caches",
      "templated": false
    },
    "caches-cache": {
      "href": "http://localhost:8080/api/caches/{cache}",
      "templated": true
    },
    "health": {
      "href": "http://localhost:8080/api/status",
      "templated": false
    },
    "health-component-instance": {
      "href": "http://localhost:8080/api/status/{component}/{instance}",
      "templated": true
    },
    "health-component": {
      "href": "http://localhost:8080/api/status/{component}",
      "templated": true
    },
    "conditions": {
      "href": "http://localhost:8080/api/conditions",
      "templated": false
    },
    "shutdown": {
      "href": "http://localhost:8080/api/shutdown",
      "templated": false
    },
    "configprops": {
      "href": "http://localhost:8080/api/configprops",
      "templated": false
    },
    "env-toMatch": {
      "href": "http://localhost:8080/api/env/{toMatch}",
      "templated": true
    },
    "env": {
      "href": "http://localhost:8080/api/env",
      "templated": false
    },
    "info": {
      "href": "http://localhost:8080/api/info",
      "templated": false
    },
    "loggers": {
      "href": "http://localhost:8080/api/loggers",
      "templated": false
    },
    "loggers-name": {
      "href": "http://localhost:8080/api/loggers/{name}",
      "templated": true
    },
    "heapdump": {
      "href": "http://localhost:8080/api/heapdump",
      "templated": false
    },
    "threaddump": {
      "href": "http://localhost:8080/api/threaddump",
      "templated": false
    },
    "metrics-requiredMetricName": {
      "href": "http://localhost:8080/api/metrics/{requiredMetricName}",
      "templated": true
    },
    "metrics": {
      "href": "http://localhost:8080/api/metrics",
      "templated": false
    },
    "scheduledtasks": {
      "href": "http://localhost:8080/api/scheduledtasks",
      "templated": false
    },
    "httptrace": {
      "href": "http://localhost:8080/api/httptrace",
      "templated": false
    },
    "mappings": {
      "href": "http://localhost:8080/api/mappings",
      "templated": false
    }
  }
}

Остановим сервис:

$ curl -X POST localhost:8080/api/shutdown | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    35    0    35    0     0    324      0 --:--:-- --:--:-- --:--:--   324
{
  "message": "Shutting down, bye..."
}

Весь код на GitHub