spring boot/actuator in spring boot

spring boot 3.x + actuator 파헤치기. 8. about metrics endpoint ( Counter )

Hello World Study 2023. 4. 8. 16:40

https://youtu.be/px_eSKOAQMk

Counter 는 이름 그대로 횟수를 세어 metric 으로 제공합니다. 횟수이므로 1, 2, 100, 3000 처럼 자연수만 가능하지 1.3 처럼 소수나 -100 처럼 음수는 불가능합니다.

일반적으로 cache hit 에 대한 누적 counter, http request 누적 횟수 counter 와 같이 지금까지 특정 이벤트가 몇번 발생했는지를 누적값으로 제공할때 Counter 를 사용하면 됩니다.

 

공식가이드( https://micrometer.io/docs/concepts#_counters )에는 아래처럼 Counter builder 를 이용해서 값을 세팅한 후 MeterRegistry 에 등록하면 Counter 가 만들어진다고 적혀있습니다.

Counter counter = Counter
    .builder("counter")
    .baseUnit("beans") // optional
    .description("a description of what this counter does") // optional
    .tags("region", "test") // optional
    .register(registry);

 

Rest controller 를 하나 만들고 http request 가 올때마다 count 를 하나씩 증가시키는 Counter 를 만들어보겠습니다. 

 

Counter.builder() 사용

counter 관리하는 manager 를 bean 으로 등록시키고 , 다른 bean 에서 counter 증가시키는 메서드를 호출하도록 구현했습니다.

MeterRegistry는 spring boot 에서 기본 생성해주기에 생성자 주입으로 bean을 주입했습니다.

counter 의 필수필드만으로 httpRequestCounter 를 생성하고 meterRegistry에 등록했습니다. ( 아래 init 메서드 참고 )

@Service
@RequiredArgsConstructor
public class MyHttpRequestManager {

    private final MeterRegistry meterRegistry;  // 생성자 주입

    private Counter httpRequestCounter;  //  아래 init() 메서드에서 객체 생성 후 대입해줌

    /**
     * registry 에 등록
     */
    @PostConstruct
    void init() {
        httpRequestCounter = Counter.builder("myHttpRequest")
                .register(meterRegistry);
    }

    /**
     * counter 증가시킬 필요가 있을때 외부에서 이 메서드를 호출
     */
    public void increase() {
        httpRequestCounter.increment();
    }
}

이제 위의 bean을 호출해주는 부분이 필요합니다. 

 

아래처럼 rest controller 를 하나 만들고 실행메서드에서 위에서 만든 MyHttpRequestManager 의 increase 메서드를 호출해서 counter 가 증가되도록 해줍니다.

@RestController
@RequestMapping("/api")
@Slf4j
@RequiredArgsConstructor
public class MetricsController {

    private final MyHttpRequestManager myHttpRequestManager;  // 생성자 주입

    @GetMapping("/req")
    public String request() {
        myHttpRequestManager.increase();  // counter 를 증가시킴
        return "ok";
    }

}

이후 아래처럼 rest api 가 호출된횟수만큼 counter 값이 증가되어 표시되는걸 알 수 있습니다. 

위 코드의 문제점은 Counter 값을 증가시키기 위해 application 내의 비지니스 로직 사이에 counter 증가시키는 메서드를 명시적으로 호출해야 하는 부분입니다.

또 하나의 문제는 actuator / micrometer 와 무관하게 application 내부적으로 특정 이벤트나 상태에 대한 횟수를 관리하는 클래스를 이미 만들어두었는데, 이걸 actuator 를 통해 외부로 노출만 시키고 싶을때 입니다. 지금까지 알아본 방법으로는 또 다시 Counter 를 만들고 이 counter 에서 횟수를 관리하도록 구현해야 하는데, 이는 기존에 만든 횟수 관리 클래스를 재활용하지 못하기 때문입니다.

마지막으로 모니터링 시스템에 따라 어떤 경우는 전체 누적값을 전달하면 되고, 어떤 경우는 시간당 누적값을 전달해야 할수도 있습니다. 후자의 경우 단순 누적값이 아니라서 시간당 누적값을 계산해야 하는데, 위의 구현방법으로는 불가능합니다.

이런 단점만 있는건 아닙니다. counter 값을 라이브러리내에서 관리해주기에 우린 단순히 counter 를 등록한후 increment() 라는 메서드만 호출해주면, 라이브러리가 값이 +1 증가시켜서 보관해주기에 사용이 쉬운 장점이 있습니다.

이런 점을 고려해서 적절한 방법을 선택하면 됩니다. 

 

FunctionCounter.builder() 사용

micrometer 에서는 위 문제를 해결하기 위해 FunctionCounter 라는 걸 제공합니다. ( https://micrometer.io/docs/concepts#_function_tracking_counters )

MyCounterState state = ...;   // 횟수관리하는 클래스라고 가정.

FunctionCounter counter = FunctionCounter
    .builder("counter", state, state -> state.count())  // count 값을 특정 함수 호출하고 리턴값을 통해 받아오도록 함
    .register(registry);

위 코드에서 state 는 actuator / micrometer 와 무관하게 만든 횟수관리 객체라고 보면 됩니다.

builder 메서드의 2번째값으로 위 객체를 전달하고, 3번째 값으로 해당 객체의 어떤 메서드를 호출하면 되는지를 람다식으로 넣어주면 됩니다. 이를 통해 횟수 관리는 app 자체적으로 하고, metrics 노출만 micrometer 와 연동되도록 할 수 있습니다.

 

위 예제는 너무 간결해서 좀 더 구체적인 예제를 만들어봤습니다.

아래처럼 micrometer와 아무 연관성이 없는 횟수 관리 bean을 생성합니다. http request 횟수를 관리하는 bean 이므로 실제 count 증가는 filter 나 interceptor 에서 아래 bean의 increase() 메서드를 호출하도록 하면 됩니다. 예제를 단순하게 하기 위해 filter, interceptor 연동은 생략하고, 횟수 조회하는 메서드에서는 임의로  횟수값을 리턴하도록 했습니다.

@Service
public class MyHttpRequestManagerWithoutMicrometer {

    private AtomicLong count = new AtomicLong(0);

    public long getCount() {
        return count.get() + System.currentTimeMillis(); // 값이 변경되는걸 보기 위해 현재시간을 추가함. 원칙적으로는 filter 등을 통해 count값이 변경되어야 함.
    }

    // 아래 메서드는 filter 나 interceptor 등을 통해 http 요청시마다 호출되도록 구현했다고 가정.
    public void increase() {
        count.incrementAndGet();
    }
}

위 bean 은 micrometer 와 아무런 연관이 없는 상태이므로, FunctionCounter 를 이용해서 counter 를 등록하면서 위 bean을 파라미터로 넣어줍니다. 이제 micrometer 는 counter metric이 등록되었고, counter 값이 필요할때마다 파라미터로 넘긴 bean의 메서드를 호출해서 count를 가져오게 됩니다. 

@Configuration
@RequiredArgsConstructor
public class MyFunctionCounterConfig {

    private final MyHttpRequestManagerWithoutMicrometer myManager;

    private final MeterRegistry meterRegistry;

    @PostConstruct
    void init() {
        FunctionCounter.builder("myHttpRequestWithoutMicrometer", myManager, myManager -> {
            return myManager.getCount();
        })
        .register(meterRegistry);
    }
}

spring boot 소스에서도 위와 같이 FunctionCounter 를 이용한 예제를 쉽게 찾을 수 있습니다.

아래처럼 Counter.builder 라고 검색을 하면 spring boot 자체 소스 혹은 사용하는 라이브러리중 해당 코드가 있는걸 쉽게 찾을 수 있으며, 위 예제처럼 bean과 bean을 파라미터로 넘겨주는 람다식을 builder() 메서드의 파라미터로 넘겨주고 있습니다.

동작 확인을 위해 spring boot 를 재구동하고 FunctionCounter 사용시 입력한 이름을 복사한 후 /actuator/metrics 에서 검색을 해보면 아래처럼 찾을 수 있으며, 실제 값도 잘 나오는걸 알 수 있습니다. 

 

MeterBinder 사용

FunctionCounter 를 meterRegistry에 등록하는 방법으로 MeterBinder 를 이용할수도 있습니다.

MeterBinder 가 인터페이스 타입이라서 bindTo() 메서드안에 FunctionCounter 를 사용하면 됩니다. counter 등록 방법이 여러가지가 있으므로 자주 쓸만한 기능을 모두 알아두어야 다른 사람코드를 분석하는데 도움이 될 수있습니다. 

@Configuration
public class CounterConfigWithMeterBinder {

    @Bean
    public MeterBinder myTimerWithMeterBinder(MyHttpRequestManagerWithoutMicrometer manager) {
        return new MeterBinder() {
            @Override
            public void bindTo(MeterRegistry registry) {
                FunctionCounter.builder("myHttpRequestWithMeterBinder", manager, m -> {
                            return m.getCount();
                        })
                        .register(registry);
            }
        };
    }
}

 

@Counted 사용

counter 를 가만히 생각해보면 아래처럼 특정 메서드 호출될때마다 counter 값을 하나씩 증가시켜 주는 식으로 구현해도 됩니다.

 

void doSomething() {   
  Counter.builder(...).register(registry).increment();  <--- 이 코드가 메서드 앞부분에 들어가면 됨
  
  // some codes
}

위 메서드가 호출될때마다 동일한 카운터를 계속 register 하는거 아닌가? 라고 오해할 수 있습니다. 

그러나 아래 문서처럼 register()는 새로운걸 등록하거나, 이미 존재하는걸 리턴해줍니다. 즉 동일한 이름의 counter 로 register()를 호출하면, 두번째부터는 등록이 아니라 등록된걸 리턴해줍니다. 따라서 중복 등록에 대해 걱정할 필요 없습니다. 이 부분은 추후 다룰 Gauge, Timer 도 동일하게 적용됩니다.

다시 위 코드로 돌아와서, 원래 메서드의 시작부분에 Counter 관련 코드 한줄만 넣어주면 됩니다. 매번 수작업으로 count 하고 싶은 메서드 앞부분에 넣는건 너무 무식하지요?

Counter 관련 코드는 공용 코드라고 볼수 있으며, 각각의 메서드는 개별 코드라고 볼수 있습니다.

AOP에 대해 알고 있다면 이를 적용하면 되겠다 생각할 수 있습니다.

 

spring에 @Transactional 이나 @Cacheable 과 같은 어노테이션을 적어주면 공통 코드가 각 메서드에 들어가게 되듯이 micrometer 에서도 이런 어노테이션을 제공해주고 있습니다. 그게 바로 @Counted 입니다.

 

우선 spring aop에 대해 기억이 가물가물한 분을 위해 잠깐 소개를 하겠습니다.

 

일반적으로 spring aop 라고 한다면 아래처럼 @Around 어노테이션에 target 이 되는 패키지,클래스,메서드 등을 지정하는 방식으로 사용합니다.

@Aspect
@Configuration
public class MyAspect {

    @Around("execution(public * dev.developery.aaa.bbb..*(..))")
    public Object advice(ProceedingJoinPoint proceedingJoinPoint) {
       // 이곳에 AOP 코드를 넣어줍니다. 생략
    }
 }

또한 아래처럼 직접 만든 어노테이션에 대해 AOP 적용이 가능한 방법도 있습니다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRetryable {  // aop에 사용할 어노테이션을 직접 생성
   int value() default 3;
}

///////////////////////////


@Aspect
@Configuration
public class MyRetryableAspect {

    @Around("@annotation(myRetry)") 
    public Object doRetry(ProceedingJoinPoint joinPoint, MyRetryable myRetry) {  // MyRetryable 는 위에서 만든 어노테이션클래스명
      이곳에 aop 적용할 코드를 넣어줌
    }
}

/////////////////////////////////



@Component
public class SomeCode

    @MyRetryable            // aop 적용하고 싶은 곳에 어노테이션을 지정
    public void someCodes() {
        생략
    }
}

좀 더 자세한 건 아래 링크를 참고해주세요

https://blog.naver.com/semtul79/222710277218

 

spring boot + proxy 기술 ( AOP 활용기술 + 주의사항#1 )

#spring #boot #proxy 기술 시리즈 마지막 편입니다. #AOP 를 활용해서 사용되고 있는 실제 기술들, 그...

blog.naver.com

 

micrometer 의 @Counted 어노테이션도 위 방법을 이용해서 AOP를 제공해줍니다. 

 

spring AOP 를 이용하므로 아래처럼 aop 의존성을 넣어줍니다. 정확히는 aspectj 의존성이 필요한데, spring aop 의존성을 넣으면 함께 포함되기에 아래 의존성만으로 충분합니다.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

이후 메서드 호출 횟수를 count 하고 싶은 메서드위에 @Counted 를 아래처럼 적어주면 됩니다. 아래예는 테스트용도이니 간단한 예시를 위해 rest controller 메서드에 적용을 바로 했습니다.

import io.micrometer.core.annotation.Counted;

@Counted("myCountedAnnotationCount")   <-- count하고 싶은 메서드위에 @Counted를 사용하고, metric 명을 함께 적어줍니다.
@GetMapping("/counted")
public String counted() {
    return "ok";
}

 

AOP를 이용하려면 기본적으로 @Aspect 라는 어노테이션도 사용해야 AOP가 적용됩니다. 그런데, 위 예제에는 그게 없습니다. 따라서 CountedAspect 라는 클래스를 bean 으로 등록해줘야 한다고 가이드에 나와 있습니다. 해당 가이드는 아래처럼 Counted 어노테이션의 주석을 따라가면 확인할 수 있습니다.

 

위 가이드대로 아래처럼 CountedAspect 클래스를 bean 으로 등록해줍니다. 

@Configuration
public class CountConfig {

    @Bean
    public CountedAspect countedAspect(MeterRegistry meterRegistry) {
        return new CountedAspect(meterRegistry);
    }
}

CountedAspect 클래스를 살펴보면 아래처럼 micrometer 에서 제공하는 클래스이며, 클래스에 @Aspect 가 붙어있고, @Around("@annotation...  과 같이 annotation을 이용한 AOP 관련 코드가 있는걸 알 수 있습니다. 

package io.micrometer.core.aop;

@Aspect        <--- 
public class CountedAspect {
    @Around("@annotation(counted)")   <----
    public Object interceptAndRecord(ProceedingJoinPoint pjp, Counted counted) {
        생략
    }
}

spring aop 에 익숙한 분이라면 "아! 별거 없구나. 나도 할수 있겠네" 라는 생각이 들겁니다. :)

 

이제 동작 확인을 위해 spring boot 를 재구동해줍니다.

http://127.0.0.1:8080/actuator/metrics 에 들어간 후 

myCountedAnnotationCount 를 찾아보면 보이지 않습니다. myCountedAnnotationCount  는 아래처럼 @Counted 에 넣은 값입니다. 

import io.micrometer.core.annotation.Counted;

@Counted("myCountedAnnotationCount")   <-- count하고 싶은 메서드위에 @Counted를 사용하고, metric 명을 함께 적어줍니다.
@GetMapping("/counted")
public String counted() {
    return "ok";
}

@Counted 는 최소 1번은 실행되어야 meterRegistry 에 등록되기에 위 rest api를 1번 이상 호출 한 후 다시 http://127.0.0.1:8080/actuator/metrics 에서 찾아보면 값이 보입니다.

 

이후 아래처럼 count 값이 잘 보이는걸 볼 수 있습니다.

 

 

최소 1번 이상 @Counted 가 붙은 메서드가 실행되어야 하는 이유는 아래처럼 AOP 로직 내부에 meterRegistry에 등록 및 값 증가하는 코드가 있기 때문입니다. 즉 AOP 코드가 1번은 호출되어야 meterRegistry에 등록되는 제약? 이 있습니다.

public class CountedAspect {
    (중략)
    private void record(ProceedingJoinPoint pjp, Counted counted, String exception, String result) {
        counter(pjp, counted).tag(EXCEPTION_TAG, exception).tag(RESULT_TAG, result).tags(counted.extraTags())
                .register(registry).increment();   <--- meterRegistry에 등록 및 값증가
    }

    private Counter.Builder counter(ProceedingJoinPoint pjp, Counted counted) {
        Counter.Builder builder = Counter.builder(counted.value()).tags(tagsBasedOnJoinPoint.apply(pjp));  <-- Counter builder 생성        String description = counted.description();
        if (!description.isEmpty()) {
            builder.description(description);
        }
        return builder;
    }
}

 

Counter 는 이정도로 마무리 하겠습니다.

 

여기서 사용된 코드는 아래 링크에 있습니다.

https://github.com/ChunGeun-Yu/spring-actuator-study/tree/metricsEndpoint

 

다음 포스팅에서는 metric에서 없어서는 안될 존재인 tag 에 대해 알아보겠습니다. 

 


지금껏 잘 달려오신 여러분 대단하십니다.