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. 교체 CounterServiceMeterRegistry을

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 프로젝트 에서 찾을 수 있습니다 .