1. Обзор
В этом руководстве мы интегрируем базовые метрики в Spring REST API .
Мы разработаем метрическую функциональность сначала с помощью простых фильтров сервлетов, а затем с помощью Spring Boot Actuator.
2. Web.xml
Давайте начнем с регистрации фильтра - « MetricFilter » - в web.xml нашего приложения:
<filter>
<filter-name>metricFilter</filter-name>
<filter-class>org.baeldung.web.metric.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);
}
}
Поскольку фильтр не является стандартным компонентом, мы не собираемся внедрять metricService , а вместо этого извлекаем его вручную - через ServletContext .
Также обратите внимание, что мы продолжаем выполнение цепочки фильтров, вызывая здесь API doFilter .
4. Метрика - счетчик кодов состояния
Далее - давайте посмотрим на наш простой MetricService :
@Service
public class MetricService {
private ConcurrentMap<Integer, Integer> statusMetric;
public MetricService() {
statusMetric = new ConcurrentHashMap<Integer, Integer>();
}
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.
Теперь - чтобы отобразить эту базовую метрику - мы собираемся сопоставить ее с методом Controller
@RequestMapping(value = "/status-metric", method = RequestMethod.GET)
@ResponseBody
public Map getStatusMetric() {
return metricService.getStatusMetric();
}
А вот пример ответа:
{
"404":1,
"200":6,
"409":1
}
5. Метрика - коды состояния по запросу
Далее - давайте запишем показатели для Количество по запросу :
@Service
public class MetricService {
private ConcurrentMap<String, ConcurrentHashMap<Integer, Integer>> metricMap;
public void increaseCount(String request, int status) {
ConcurrentHashMap<Integer, Integer> statusMap = metricMap.get(request);
if (statusMap == null) {
statusMap = new ConcurrentHashMap<Integer, Integer>();
}
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:
@RequestMapping(value = "/metric", method = RequestMethod.GET)
@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 ConcurrentMap<String, ConcurrentHashMap<Integer, Integer>> timeMap;
private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
public void increaseCount(String request, int status) {
String time = dateFormat.format(new Date());
ConcurrentHashMap<Integer, Integer> statusMap = timeMap.get(time);
if (statusMap == null) {
statusMap = new ConcurrentHashMap<Integer, Integer>();
}
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;
ConcurrentMap<Integer, Integer> tempMap;
for (Entry<String, ConcurrentHashMap<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++;
}
return result;
}
Теперь мы собираемся отобразить это на API:
@RequestMapping(value = "/metric-graph-data", method = RequestMethod.GET)
@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 Actuator
В следующих нескольких разделах мы рассмотрим функциональность Actuator в Spring Boot, чтобы представить наши метрики.
Во-первых, нам нужно добавить зависимость привода в наш pom.xml :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
8. MetricFilter
Далее - мы можем превратить MetricFilter - в реальный бин 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);
}
}
Это, конечно, незначительное упрощение, но оно стоит того, чтобы избавиться от ранее связанного вручную соединения.
9. Использование CounterService
Давайте теперь используем 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);
}
}
}
10. Экспорт метрик с помощью MetricRepository
Далее - нам нужно экспортировать метрики - используя MetricRepository :
@Service
public class MetricService {
@Autowired
private MetricRepository repo;
private List<ArrayList<Integer>> statusMetric;
private List<String> statusList;
@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
Metric<?> metric;
ArrayList<Integer> statusCount = new ArrayList<Integer>();
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);
}
}
Обратите внимание, что мы храним количество кодов состояния в минуту .
11. Spring Boot PublicMetrics
Мы также можем использовать Spring Boot PublicMetrics для экспорта метрик вместо использования наших собственных фильтров - следующим образом:
Во-первых, у нас есть запланированное задание экспортировать показатели в минуту :
@Autowired
private MetricReaderPublicMetrics publicMetrics;
private List<ArrayList<Integer>> statusMetricsByMinute;
private List<String> statusList;
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
ArrayList<Integer> lastMinuteStatuses = initializeStatuses(statusList.size());
for (Metric<?> counterMetric : publicMetrics.metrics()) {
updateMetrics(counterMetric, lastMinuteStatuses);
}
statusMetricsByMinute.add(lastMinuteStatuses);
}
Нам, конечно, нужно инициализировать список кодов состояния HTTP:
private ArrayList<Integer> initializeStatuses(int size) {
ArrayList<Integer> counterList = new ArrayList<Integer>();
for (int i = 0; i < size; i++) {
counterList.add(0);
}
return counterList;
}
И затем мы собираемся обновить метрики с помощью кода статуса :
private void updateMetrics(Metric<?> counterMetric, ArrayList<Integer> statusCount) {
String status = "";
int index = -1;
int oldCount = 0;
if (counterMetric.getName().contains("counter.status.")) {
status = counterMetric.getName().substring(15, 18);//example 404, 200
appendStatusIfNotExist(status, statusCount);
index = statusList.indexOf(status);
oldCount = statusCount.get(index) == null ? 0 : statusCount.get(index);
statusCount.set(index, counterMetric.getValue().intValue() + oldCount);
}
}
private void appendStatusIfNotExist(String status, ArrayList<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() - (60000 ** (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;
}
12. Рисование графика с использованием метрик
Наконец, давайте представим эти метрики через двумерный массив, чтобы мы могли затем представить их график:
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() - (60000 ** (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 () :
@RequestMapping(value = "/metric-graph-data", method = RequestMethod.GET)
@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]]----
13. Заключение
В этой статье мы рассмотрели несколько простых способов встроить некоторые базовые возможности метрик в ваше веб-приложение Spring.
Обратите внимание, что счетчики не являются потокобезопасными , поэтому они могут быть неточными, если не использовать что-то вроде атомных номеров. Это было преднамеренно только потому, что дельта должна быть небольшой, и 100% -ная точность не является целью, скорее, раннее выявление трендов.
Конечно, есть более зрелые способы записи метрик HTTP в приложении, но это простой, легкий и очень полезный способ сделать это без дополнительной сложности полноценного инструмента.
-
Полная реализация ** этой статьи может быть найдена в the проект GitHub - это проект на основе Maven, поэтому он должен легко импортировать и запускать как есть.