1. 개요
이 사용방법(예제)에서는 기본 메트릭을 Spring REST API에 통합 합니다 .
먼저 간단한 서블릿 필터를 사용한 다음 Spring Boot Actuator를 사용하여 메트릭 기능을 구축 할 것입니다.
2. web.xml
앱 의 web.xml 에 " MetricFilter " 필터를 등록하는 것으로 시작하겠습니다 .
<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 를 통해 수동으로 검색합니다 .
또한 여기 에서 doFilter API 를 호출하여 필터 체인의 실행을 계속하고 있습니다.
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;
}
}
우리는 각 HTTP 상태 코드 유형에 대한 개수를 저장하기 위해 메모리 내 ConcurrentMap 을 사용하고 있습니다.
이제이 기본 메트릭을 표시하기 위해 컨트롤러 메서드 에 매핑합니다 .
@RequestMapping(value = "/status-metric", method = RequestMethod.GET)
@ResponseBody
public Map getStatusMetric() {
return metricService.getStatusMetric();
}
다음은 샘플 응답입니다.
{
"404":1,
"200":6,
"409":1
}
5. 메트릭 – 요청 별 상태 코드
다음 – Counts by Request에 대한 메트릭을 기록해 보겠습니다 .
@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에는 다음과 같은 활동이 있습니다.
- “GET / users ”에 대한“7”요청
- 그 중 "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 차트를 사용하여 렌더링합니다.
<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. 스프링 부트 1.x 액추에이터 사용
다음 몇 섹션에서는 Spring Boot의 Actuator 기능에 연결하여 메트릭을 제시 할 것입니다.
먼저 pom.xml에 액추에이터 의존성을 추가해야합니다 .
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
7.1. MetricFilter
다음으로 MetricFilter 를 실제 Spring Bean으로 바꿀 수 있습니다 .
@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 사용
이제 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<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);
}
}
분당 상태 코드 수를 저장하고 있습니다.
7.4. 스프링 부트 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 상태 코드 List을 초기화해야합니다.
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 ").
- list 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;
}
7.5. 메트릭을 사용하여 그래프 그리기
마지막으로 2 차원 배열을 통해 이러한 측정 항목을 표시하여 그래프로 표시 할 수 있습니다.
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]
]
8. Spring Boot 2.x Actuator 사용
Spring Boot 2에서 Spring Actuator의 API는 큰 변화를 목격했습니다. Spring의 자체 메트릭이 Micrometer 로 대체되었습니다 . 따라서 Micrometer를 사용 하여 위의 동일한 메트릭 예제를 작성해 보겠습니다 .
8.1. 교체 CounterService 와 MeterRegistry을
Spring Boot 애플리케이션은 이미 Actuator 스타터에 의존하므로 Micrometer는 이미 자동 구성되어 있습니다. CounterService 대신 MeterRegistry 를 삽입 할 수 있습니다 . 다양한 유형의 미터 를 사용하여 메트릭을 캡처 할 수 있습니다 . 카운터 미터 중 하나입니다 :
@Autowired
private MeterRegistry registry;
private List<String> statusList;
@Override
public void increaseCount(final int status) {
String counterName = "counter.status." + status;
registry.counter(counterName).increment(1);
if (!statusList.contains(counterName)) {
statusList.add(counterName);
}
}
8.2. MeterRegistry를 사용하여 카운트 내보내기
Micrometer에서는 MeterRegistry를 사용하여 카운터 값을 내보낼 수 있습니다 .
@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
ArrayList<Integer> statusCount = new ArrayList<Integer>();
for (String status : statusList) {
Search search = registry.find(status);
if (search != null) {
Counter counter = search.counter();
statusCount.add(counter != null ? ((int) counter.count()) : 0);
registry.remove(counter);
} else {
statusCount.add(0);
}
}
statusMetricsByMinute.add(statusCount);
}
8.3. 미터를 사용하여 지표 게시
이제 MeterRegistry의 미터를 사용하여 메트릭을 게시 할 수도 있습니다 .
@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
ArrayList<Integer> lastMinuteStatuses = initializeStatuses(statusList.size());
for (Meter counterMetric : publicMetrics.getMeters()) {
updateMetrics(counterMetric, lastMinuteStatuses);
}
statusMetricsByMinute.add(lastMinuteStatuses);
}
private void updateMetrics(final Meter counterMetric, final ArrayList<Integer> statusCount) {
String status = "";
int index = -1;
int oldCount = 0;
if (counterMetric.getId().getName().contains("counter.status.")) {
status = counterMetric.getId().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, (int)((Counter) counterMetric).count() + oldCount);
}
}
9. 결론
이 기사에서는 몇 가지 기본적인 메트릭 기능을 Spring 웹 애플리케이션에 구축하는 몇 가지 간단한 방법을 살펴 보았습니다.
카운터 는 스레드로부터 안전 하지 않으므로 원자 번호와 같은 것을 사용하지 않으면 정확하지 않을 수 있습니다. 이는 델타가 작아야하고 100 % 정확도가 목표가 아니기 때문에 의도적 인 것입니다. 오히려 트렌드를 조기에 파악하는 것이 좋습니다.
물론 애플리케이션에서 HTTP 메트릭을 기록하는 더 성숙한 방법이 있지만, 이는 본격적인 도구의 추가 복잡성없이이를 수행하는 간단하고 가볍고 매우 유용한 방법입니다.
이 기사의 전체 구현은 GitHub 프로젝트 에서 찾을 수 있습니다 .