아무거나

[Spring Actuator] Aspect 를 활용한 특정 metric 의 tag 의 여러 value 값 별로 시간단위로 실시간 카운트 저장 본문

Java & Kotlin/Spring

[Spring Actuator] Aspect 를 활용한 특정 metric 의 tag 의 여러 value 값 별로 시간단위로 실시간 카운트 저장

전봉근 2023. 12. 4. 18:49
반응형

Aspect 를 활용한 특정 metric 의 tag 의 여러 value 값 별로 시간단위로 실시간 카운트 저장

예시로 호출시 특정 타입번호기준으로 페이지를 이동하는 기능에 타입번호_시간단위로 카운트를 저장하는 기능을 생성 (참고: https://github.com/bkjeon1614/java-example-code/tree/develop/bkjeon-mybatis-codebase)

  1. Custom Annotation 생성
    [CustomCounter.java]
package com.example.bkjeon.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomCounter {

}  
  1. Counter Metric 에 지정할 Metric Name, Tag Name 관련 Enum 생성
    [MetricType.java]
package com.example.bkjeon.enums.actuator;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
* Metric 관련 Enum
* metricName: 메트릭명
* tagName: 호출된 메소드명
*/
@Getter
@AllArgsConstructor
public enum MetricType {

    CUSTOM_METRIC_INFO("custom.metric.count", "isCustomCounterReq");

    private final String metricName;
    private final String tagName;

}  
  1. Aspect 생성
    [CounterAspect.java]
package com.example.bkjeon.base.aspect;

import com.example.bkjeon.base.services.api.v1.actuator.service.counter.CustomCounterService;
import com.example.bkjeon.enums.actuator.MetricType;
import javax.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.joda.time.LocalDateTime;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
* 실시간 카운트 집계
*/
@Slf4j
@Component
@Aspect
@RequiredArgsConstructor
public class CounterAspect {

    private final CustomCounterService customCounterService;

    @Pointcut("@annotation(com.example.bkjeon.annotation.CustomCounter)")
    private void isCustomCounter(){}

    @Before("isCustomCounter()")
    public void isCustomIncrCount() {
        try {
            LocalDateTime nowDateTime = new LocalDateTime();
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
                .currentRequestAttributes()).getRequest();

            String valueName = request.getServletPath().split("/")[4]
                + "_"
                + nowDateTime.toString("yyMMddHHmmss");
            customCounterService.increment(
                MetricType.CUSTOM_METRIC_INFO.getMetricName(),
                MetricType.CUSTOM_METRIC_INFO.getTagName(),
                valueName,
                2,
                10
            );

            log.debug("Value Size: {}", customCounterService.getSize(
                MetricType.CUSTOM_METRIC_INFO.getMetricName(),
                MetricType.CUSTOM_METRIC_INFO.getTagName(), valueName));

            log.debug("Value Sum Size: {}", customCounterService.getAllValueSumSize(
                MetricType.CUSTOM_METRIC_INFO.getMetricName(),
                MetricType.CUSTOM_METRIC_INFO.getTagName(), valueName, 2));
        } catch (Throwable throwable) {
            log.error("isDshopSetCount(Aspect) ERROR !! {}", (Object) throwable.getStackTrace());
        }
    }

}  
  1. Counter Service 생성
    [CustomCounterService.java]
package com.example.bkjeon.base.services.api.v1.actuator.service.counter;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CustomCounterService {

    private final MeterRegistry meterRegistry;

    /**
    * 카운트 저장
    * @param metricName  metric 명
    * @param tagName   tag 명
    * @param valueName tag value 값 저장 (Ex: {매장/기획전 번호}_{yyMMddHHmmss})
    * @param delLimit 최근 시간 오름 차순 으로 이전에 먼저 등록된 데이터 를 제거할 값의 개수
    */
    public void increment(String metricName, String tagName, String valueName, int delLimit,
        int maintainLimit) {
        List<Meter> meterList = meterRegistry.getMeters().stream()
            .filter(meter -> metricName.equals(meter.getId().getName()))
            .collect(Collectors.toList());
        List<Tag> tagList = meterList.stream()
            .flatMap(meter -> meter.getId().getTags().stream())
            .filter(tag -> valueName.split("_")[0].equals(tag.getValue().split("_")[0]))
            .sorted(Comparator.comparing(Tag::getValue))
            .collect(Collectors.toList());
        List<Tag> removeTagList = tagList.stream()
            .limit(delLimit)
            .collect(Collectors.toList());

        if (tagList.size() > maintainLimit) {
            for (Tag tag: removeTagList) {
                remove(metricName, tag.getKey(), tag.getValue());
            }
        }

        Counter.builder(metricName)
            .tag(tagName, valueName)
            .register(meterRegistry).increment();
    }

    /**
    * 특정 tag value 의 카운트 조회
    * @param metricName  metric 명
    * @param tagName   tag 명
    * @param valueName tag value 값 저장 (Ex: {매장/기획전 번호}_{yyMMddHHmmss})
    */
    public Double getSize(String metricName, String tagName, String valueName) {
        return Counter.builder(metricName)
            .tag(tagName, valueName)
            .register(meterRegistry).count();
    }

    /**
    * 특정 tag value 의 최근 시간 내림 차순 으로 총 n 개의 카운트 값 을 sum 하여 가져옴
    * @param metricName  metric 명
    * @param tagName   tag 명
    * @param valueName tag value 값 저장 (Ex: {매장/기획전 번호}_{yyMMddHHmmss})
    * @param limit 카운트 를 집계할 최근 시간 내림 차순 으로 sum 할 개수
    */
    public Double getAllValueSumSize(String metricName, String tagName, String valueName, int limit) {
        List<Meter> meterList = meterRegistry.getMeters().stream()
            .filter(meter -> metricName.equals(meter.getId().getName()))
            .collect(Collectors.toList());

        return meterList.stream()
            .flatMap(meter -> meter.getId().getTags().stream())
            .filter(tag -> valueName.split("_")[0].equals(tag.getValue().split("_")[0]))
            .sorted(Comparator.comparing(Tag::getValue).reversed())
            .limit(limit)
            .mapToDouble(tag -> Counter.builder(metricName).tag(tagName, tag.getValue())
                .register(meterRegistry).count())
            .sum();
    }

    /**
    * 특정 tag value 를 삭제
    * @param metricName  metric 명
    * @param tagName   tag 명
    * @param valueName tag value 값 저장 (Ex: {매장/기획전 번호}_{yyMMddHHmmss})
    */
    private void remove(String metricName, String tagName, String valueName) {
        meterRegistry.remove(Counter.builder(metricName)
            .tag(tagName, valueName)
            .register(meterRegistry));
    }

}  
  1. Counter Controller 기능 추가
    [CounterController.java]
package com.example.bkjeon.base.services.api.v1.actuator.controller;

import com.example.bkjeon.annotation.CustomCounter;
import com.example.bkjeon.base.services.api.v1.actuator.service.ApplicationRequestManager;
import com.example.bkjeon.base.services.api.v1.actuator.service.ApplicationRequestWithoutMicrometer;
import io.micrometer.core.annotation.Counted;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("v1/counter")
@RequiredArgsConstructor
public class CounterController {

    ...

    /**
    * 실시간 카운트 집계
    */
    @CustomCounter
    @GetMapping("customCounterReq/{typeNo}")
    public String isCustomCounterReq(@PathVariable Long typeNo) {
        return HttpStatus.OK.getReasonPhrase();
    }

}
반응형
Comments