1. Обзор
В этом руководстве мы интегрируем базовые метрики в Spring REST API .
Мы создадим метрическую функциональность сначала с помощью простых фильтров сервлетов, а затем с помощью модуля Spring Boot Actuator.
2. Веб- файл.xml
Начнем с регистрации фильтра — « MetricFilter
» — в файле web.xml
нашего приложения:
<filter>
<filter-name>metricFilter</filter-name>
<filter-class>org.foreach.metrics.filter.MetricFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>metricFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Обратите внимание, как мы сопоставляем фильтр с охватом всех входящих запросов — «/*»
— который, конечно же, полностью настраивается.
3. Фильтр сервлетов
Теперь давайте создадим наш собственный фильтр:
public class MetricFilter implements Filter {
private MetricService metricService;
@Override
public void init(FilterConfig config) throws ServletException {
metricService = (MetricService) WebApplicationContextUtils
.getRequiredWebApplicationContext(config.getServletContext())
.getBean("metricService");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws java.io.IOException, ServletException {
HttpServletRequest httpRequest = ((HttpServletRequest) request);
String req = httpRequest.getMethod() + " " + httpRequest.getRequestURI();
chain.doFilter(request, response);
int status = ((HttpServletResponse) response).getStatus();
metricService.increaseCount(req, status);
}
}
Поскольку фильтр не является стандартным bean-компонентом, мы не собираемся внедрять metricService,
а вместо этого извлекать его вручную — через ServletContext
.
Также обратите внимание, что мы продолжаем выполнение цепочки фильтров, вызывая здесь doFilter
API.
4. Метрика — количество кодов состояния
Далее — давайте взглянем на наш простой InMemoryMetricService
:
@Service
public class MetricService {
private Map<Integer, Integer> statusMetric;
public MetricService() {
statusMetric = new ConcurrentHashMap<>();
}
public void increaseCount(String request, int status) {
Integer statusCount = statusMetric.get(status);
if (statusCount == null) {
statusMetric.put(status, 1);
} else {
statusMetric.put(status, statusCount + 1);
}
}
public Map getStatusMetric() {
return statusMetric;
}
}
Мы используем ConcurrentMap
в памяти для хранения счетчиков для каждого типа кода состояния HTTP.
Теперь, чтобы отобразить эту базовую метрику, мы собираемся сопоставить ее с методом контроллера :
@GetMapping(value = "/status-metric")
@ResponseBody
public Map getStatusMetric() {
return metricService.getStatusMetric();
}
А вот и образец ответа:
{
"404":1,
"200":6,
"409":1
}
5. Метрика — коды состояния по запросу
Далее — записываем метрики для Counts by Request :
@Service
public class MetricService {
private Map<String, Map<Integer, Integer>> metricMap;
public void increaseCount(String request, int status) {
Map<Integer, Integer> statusMap = metricMap.get(request);
if (statusMap == null) {
statusMap = new ConcurrentHashMap<>();
}
Integer count = statusMap.get(status);
if (count == null) {
count = 1;
} else {
count++;
}
statusMap.put(status, count);
metricMap.put(request, statusMap);
}
public Map getFullMetric() {
return metricMap;
}
}
Мы будем отображать результаты метрик через API:
@GetMapping(value = "/metric")
@ResponseBody
public Map getMetric() {
return metricService.getFullMetric();
}
Вот как выглядят эти показатели:
{
"GET /users":
{
"200":6,
"409":1
},
"GET /users/1":
{
"404":1
}
}
Согласно приведенному выше примеру, API выполнял следующие действия:
- «7» запросов к «GET
/users
» - «6» из них привели к ответам с кодом состояния «200» и только один — к «409».
6. Метрика — данные временных рядов
Общие подсчеты несколько полезны в приложении, но если система работала в течение значительного времени , трудно сказать, что на самом деле означают эти показатели .
Вам нужен контекст времени, чтобы данные имели смысл и были легко интерпретированы.
Давайте теперь создадим простую метрику, основанную на времени; мы будем вести учет количества кодов состояния в минуту — следующим образом:
@Service
public class MetricService {
private static final SimpleDateFormat DATE_FORMAT =
new SimpleDateFormat("yyyy-MM-dd HH:mm");
private Map<String, Map<Integer, Integer>> timeMap;
public void increaseCount(String request, int status) {
String time = DATE_FORMAT.format(new Date());
Map<Integer, Integer> statusMap = timeMap.get(time);
if (statusMap == null) {
statusMap = new ConcurrentHashMap<>();
}
Integer count = statusMap.get(status);
if (count == null) {
count = 1;
} else {
count++;
}
statusMap.put(status, count);
timeMap.put(time, statusMap);
}
}
И getGraphData()
:
public Object[][] getGraphData() {
int colCount = statusMetric.keySet().size() + 1;
Set<Integer> allStatus = statusMetric.keySet();
int rowCount = timeMap.keySet().size() + 1;
Object[][] result = new Object[rowCount][colCount];
result[0][0] = "Time";
int j = 1;
for (int status : allStatus) {
result[0][j] = status;
j++;
}
int i = 1;
Map<Integer, Integer> tempMap;
for (Entry<String, Map<Integer, Integer>> entry : timeMap.entrySet()) {
result[i][0] = entry.getKey();
tempMap = entry.getValue();
for (j = 1; j < colCount; j++) {
result[i][j] = tempMap.get(result[0][j]);
if (result[i][j] == null) {
result[i][j] = 0;
}
}
i++;
}
for (int k = 1; k < result[0].length; k++) { результат[0][k] = результат[0][k].toString(); }
return result;
}
Теперь мы собираемся сопоставить это с API:
@GetMapping(value = "/metric-graph-data")
@ResponseBody
public Object[][] getMetricData() {
return metricService.getGraphData();
}
И, наконец, мы собираемся отобразить это с помощью Google Charts:
<html>
<head>
<title>Metric Graph</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("visualization", "1", {packages : [ "corechart" ]});
function drawChart() {
$.get("/metric-graph-data",function(mydata) {
var data = google.visualization.arrayToDataTable(mydata);
var options = {title : 'Website Metric',
hAxis : {title : 'Time',titleTextStyle : {color : '#333'}},
vAxis : {minValue : 0}};
var chart = new google.visualization.AreaChart(document.getElementById('chart_div'));
chart.draw(data, options);
});
}
</script>
</head>
<body onload="drawChart()">
<div id="chart_div" style="width: 900px; height: 500px;"></div>
</body>
</html>
7. Использование Spring Boot 1.x Actuator
В следующих нескольких разделах мы собираемся подключиться к функциям Actuator в Spring Boot, чтобы представить наши показатели.
Во-первых, нам нужно добавить зависимость привода в наш pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
7.1. МетрикаФильтр
_ ``
Далее — мы можем превратить MetricFilter
— в реальный bean-компонент Spring:
@Component
public class MetricFilter implements Filter {
@Autowired
private MetricService metricService;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws java.io.IOException, ServletException {
chain.doFilter(request, response);
int status = ((HttpServletResponse) response).getStatus();
metricService.increaseCount(status);
}
}
Это, конечно, незначительное упрощение, но его стоит сделать, чтобы избавиться от ранее ручного связывания зависимостей.
7.2. Использование службы счетчика
Давайте теперь воспользуемся CounterService
для подсчета вхождений для каждого кода состояния:
@Service
public class MetricService {
@Autowired
private CounterService counter;
private List<String> statusList;
public void increaseCount(int status) {
counter.increment("status." + status);
if (!statusList.contains("counter.status." + status)) {
statusList.add("counter.status." + status);
}
}
}
7.3. Экспорт метрик с помощью MetricRepository
Далее — нам нужно экспортировать метрики — используя MetricRepository
:
@Service
public class MetricService {
@Autowired
private MetricRepository repo;
private List<List<Integer>> statusMetric;
private List<String> statusList;
@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
Metric<?> metric;
List<Integer> statusCount = new ArrayList<>();
for (String status : statusList) {
metric = repo.findOne(status);
if (metric != null) {
statusCount.add(metric.getValue().intValue());
repo.reset(status);
} else {
statusCount.add(0);
}
}
statusMetric.add(statusCount);
}
}
Обратите внимание, что мы сохраняем количество кодов состояния в минуту .
7.4. Spring Boot PublicMetrics
Мы также можем использовать Spring Boot PublicMetrics
для экспорта метрик вместо использования наших собственных фильтров — следующим образом:
Во- первых, у нас есть запланированная задача по экспорту метрик в минуту :
@Autowired
private MetricReaderPublicMetrics publicMetrics;
private List<List<Integer>> statusMetricsByMinute;
private List<String> statusList;
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
List<Integer> lastMinuteStatuses = initializeStatuses(statusList.size());
for (Metric<?> counterMetric : publicMetrics.metrics()) {
updateMetrics(counterMetric, lastMinuteStatuses);
}
statusMetricsByMinute.add(lastMinuteStatuses);
}
Нам, конечно же, нужно инициализировать список кодов состояния HTTP:
private List<Integer> initializeStatuses(int size) {
List<Integer> counterList = new ArrayList<>();
for (int i = 0; i < size; i++) {
counterList.add(0);
}
return counterList;
}
И затем мы собираемся обновить метрики с помощью счетчика кодов состояния :
private void updateMetrics(Metric<?> counterMetric, List<Integer> statusCount) {
if (counterMetric.getName().contains("counter.status.")) {
String status = counterMetric.getName().substring(15, 18); // example 404, 200
appendStatusIfNotExist(status, statusCount);
int index = statusList.indexOf(status);
int oldCount = statusCount.get(index) == null ? 0 : statusCount.get(index);
statusCount.set(index, counterMetric.getValue().intValue() + oldCount);
}
}
private void appendStatusIfNotExist(String status, List<Integer> statusCount) {
if (!statusList.contains(status)) {
statusList.add(status);
statusCount.add(0);
}
}
Обратите внимание, что:
Имя счетчика состояния PublicMetics
начинается с «counter.status
», например «counter.status.200.root
» .- Мы ведем учет количества статусов в минуту в нашем списке
statusMetricsByMinute.
Мы можем экспортировать собранные данные, чтобы нарисовать их на графике , следующим образом:
public Object[][] getGraphData() {
Date current = new Date();
int colCount = statusList.size() + 1;
int rowCount = statusMetricsByMinute.size() + 1;
Object[][] result = new Object[rowCount][colCount];
result[0][0] = "Time";
int j = 1;
for (String status : statusList) {
result[0][j] = status;
j++;
}
for (int i = 1; i < rowCount; i++) {
result[i][0] = dateFormat.format(
new Date(current.getTime() - (60000L * (rowCount - i))));
}
List<Integer> minuteOfStatuses;
List<Integer> last = new ArrayList<Integer>();
for (int i = 1; i < rowCount; i++) {
minuteOfStatuses = statusMetricsByMinute.get(i - 1);
for (j = 1; j <= minuteOfStatuses.size(); j++) {
result[i][j] =
minuteOfStatuses.get(j - 1) - (last.size() >= j ? last.get(j - 1) : 0);
}
while (j < colCount) {
result[i][j] = 0;
j++;
}
last = minuteOfStatuses;
}
return result;
}
7.5. Нарисуйте график, используя метрики
Наконец, давайте представим эти показатели в виде двумерного массива, чтобы мы могли затем изобразить их в виде графика:
public Object[][] getGraphData() {
Date current = new Date();
int colCount = statusList.size() + 1;
int rowCount = statusMetric.size() + 1;
Object[][] result = new Object[rowCount][colCount];
result[0][0] = "Time";
int j = 1;
for (String status : statusList) {
result[0][j] = status;
j++;
}
ArrayList<Integer> temp;
for (int i = 1; i < rowCount; i++) {
temp = statusMetric.get(i - 1);
result[i][0] = dateFormat.format
(new Date(current.getTime() - (60000L * (rowCount - i))));
for (j = 1; j <= temp.size(); j++) {
result[i][j] = temp.get(j - 1);
}
while (j < colCount) {
result[i][j] = 0;
j++;
}
}
return result;
}
А вот наш метод контроллера getMetricData()
:
@GetMapping(value = "/metric-graph-data")
@ResponseBody
public Object[][] getMetricData() {
return metricService.getGraphData();
}
А вот и образец ответа:
[
["Time","counter.status.302","counter.status.200","counter.status.304"],
["2015-03-26 19:59",3,12,7],
["2015-03-26 20:00",0,4,1]
]
8. Использование Spring Boot 2.x Actuator
В Spring Boot 2 API-интерфейсы Spring Actuator претерпели серьезные изменения. Собственные метрики Spring заменены на Micrometer
. Итак, давайте напишем тот же пример метрик выше с Micrometer
.
8.1. Замена CounterService
на
MeterRegistry ``
Поскольку наше приложение Spring Boot уже зависит от стартера Actuator, Micrometer уже настроен автоматически. Мы можем внедрить MeterRegistry
вместо CounterService
. Мы можем использовать различные типы счетчиков
для сбора показателей. Счетчик является одним из счетчиков :
@Autowired
private MeterRegistry registry;
private List<String> statusList;
@Override
public void increaseCount(int status) {
String counterName = "counter.status." + status;
registry.counter(counterName).increment(1);
if (!statusList.contains(counterName)) {
statusList.add(counterName);
}
}
8.2. Просмотр пользовательских показателей
Поскольку наши метрики теперь зарегистрированы в Micrometer, сначала давайте включим их в конфигурации приложения . Теперь мы можем просмотреть их, перейдя к конечной точке Actuator в /actuator/metrics
:
{
"names": [
"application.ready.time",
"application.started.time",
"counter.status.200",
"disk.free",
"disk.total",
.....
]
}
Здесь мы видим, что наша метрика counter.status.200
указана среди стандартных метрик Actuator. Кроме того, мы также можем получить последнее значение этой метрики, предоставив селектор в URI как /actuator/metrics/counter.status.200
:
{
"name": "counter.status.200",
"description": null,
"baseUnit": null,
"measurements": [
{
"statistic": "COUNT",
"value": 2
}
],
"availableTags": []
}
8.3. Экспорт счетчиков с помощью MeterRegistry
В Micrometer мы можем экспортировать значения счетчика
с помощью MeterRegistry:
@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
List<Integer> statusCount = new ArrayList<>();
for (String status : statusList) {
Search search = registry.find(status);
Counter counter = search.counter();
if (counter == null) {
statusCount.add(0);
} else {
statusCount.add(counter != null ? ((int) counter.count()) : 0);
registry.remove(counter);
}
}
statusMetricsByMinute.add(statusCount);
}
8.3. Публикация показателей с использованием счетчиков
Теперь мы также можем публиковать метрики, используя счетчики MeterRegistry:
@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
List<Integer> lastMinuteStatuses = initializeStatuses(statusList.size());
for (Meter counterMetric : publicMetrics.getMeters()) {
updateMetrics(counterMetric, lastMinuteStatuses);
}
statusMetricsByMinute.add(lastMinuteStatuses);
}
private void updateMetrics(Meter counterMetric, List<Integer> statusCount) {
String metricName = counterMetric.getId().getName();
if (metricName.contains("counter.status.")) {
String status = metricName.substring(15, 18); // example 404, 200
appendStatusIfNotExist(status, statusCount);
int index = statusList.indexOf(status);
int oldCount = statusCount.get(index) == null ? 0 : statusCount.get(index);
statusCount.set(index, (int)((Counter) counterMetric).count() + oldCount);
}
}
9. Заключение
В этой статье мы рассмотрели несколько простых способов встроить некоторые базовые возможности метрик в веб-приложение Spring.
Обратите внимание, что счетчики не потокобезопасны, поэтому они могут быть неточными без использования чего-то вроде атомных чисел. Это было преднамеренно только потому, что дельта должна быть небольшой, а 100% точность не является целью — скорее, раннее выявление трендов.
Конечно, есть более зрелые способы записи метрик HTTP в приложении, но это простой, легкий и очень полезный способ сделать это без дополнительной сложности полноценного инструмента.
Полную реализацию этой статьи можно найти в проекте GitHub .