비즈니스에 특화된 부분을 모니터링 하고 싶으면 어떻게 해야할까?
비즈니스 메트릭은 직접 등록하고 확인해야 한다.
예제
OrderService
package hello.order;
import java.util.concurrent.atomic.AtomicInteger;
public interface OrderService {
void order();
void cancel();
AtomicInteger getStock();
}
OrderServiceV0
package hello.order.v0;
import hello.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class OrderServiceV0 implements OrderService {
private AtomicInteger stock = new AtomicInteger(100);
@Override
public void order() {
log.info("주문");
stock.decrementAndGet();
}
@Override
public void cancel() {
log.info("취소");
stock.incrementAndGet();
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
new AtomicInteger(100)
: 초기값을 100으로 설정OrderConfigV0
package hello.order.v0;
import hello.order.OrderService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OrderConfigV0 {
@Bean
OrderService orderService() {
return new OrderServiceV0();
}
}
OrderService
빈을 직접 등록OrderController
package hello.controller;
import hello.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class OrderController {
public final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/order")
public String order() {
log.info("order");
orderService.order();
return "order";
}
@GetMapping("/cancel")
public String cancel() {
log.info("cancel");
orderService.cancel();
return "cancel";
}
@GetMapping("/stock")
public int stock() {
log.info("stock");
return orderService.getStock().get();
}
}
ActuatorApplication
에 OrderConfigV0.class
Import 후 실행
마이크로미터를 사용해서 메트릭을 직접 등록하는 방법을 알아보자. 먼저 주문수, 취소수를 대상으로 카운터 메트릭을 등록해보자.
Counter(카운터)
_total
을 붙여서 my_order_total
과 같이 표현함OrderServiceV1
package hello.order.v1;
import hello.order.OrderService;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class OrderServiceV1 implements OrderService {
private final MeterRegistry registry;
private AtomicInteger stock = new AtomicInteger(100);
public OrderServiceV1(MeterRegistry registry) {
this.registry = registry;
}
@Override
public void order() {
log.info("주문");
stock.decrementAndGet();
Counter.builder("my.order")
.tag("class", this.getClass().getName())
.tag("method", "order")
.description("order")
.register(registry).increment();
}
@Override
public void cancel() {
log.info("취소");
stock.incrementAndGet();
Counter.builder("my.order")
.tag("class", this.getClass().getName())
.tag("method", "cancel")
.description("order")
.register(registry).increment();
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
Counter.builder(name)
를 통해서 카운터를 생성tag
를 사용했는데, 프로메테우스에서 필터할 수 있는 레이블로 사용tag
를 통해서 구분register(registry)
: 만든 카운터를 MeterRegistry
에 등록increment()
: 카운터의 값을 하나 증가OrderConfigV1
package hello.order.v1;
import hello.order.OrderService;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OrderConfigV1 {
@Bean
OrderService orderService(MeterRegistry registry) {
return new OrderServiceV1(registry);
}
}
액츄에이터 메트릭 확인
프로메테우스 포멧 메트릭 확인
order
과 cancel
이 한번 씩 호출 된 것을 확인 할 수 있다.그라파나 등록 - 주문수, 취소수
확인
OrderServiceV1
의 가장 큰 단점은 메트릭을 관리하는 로직이 핵심 비즈니스 개발 로직에 침투했다는 점이다. 해결하기 위해선 스프링 AOP를 활용하면 된다.
OrderServiceV2
package hello.order.v2;
import hello.order.OrderService;
import io.micrometer.core.annotation.Counted;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class OrderServiceV2 implements OrderService {
private AtomicInteger stock = new AtomicInteger(100);
@Counted("my.order")
@Override
public void order() {
log.info("주문");
stock.decrementAndGet();
}
@Counted("my.order")
@Override
public void cancel() {
log.info("취소");
stock.incrementAndGet();
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
@Counted
애노테이션을 측정을 원하는 메서드에 적용한다. 주문과 취소 메서드에 적용했다.my.order
를 적용했다.OrderConfigV2
package hello.order.v2;
import hello.order.OrderService;
import io.micrometer.core.aop.CountedAspect;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OrderConfigV2 {
@Bean
public OrderService orderService() {
return new OrderServiceV2();
}
@Bean
public CountedAspect countedAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}
}
CountedAspect
를 등록하면 @Counted 를 인지해서 Counter 를 사용하는 AOP를 적용한다.그라파나 대시보드 확인
Timer
Timer는 좀 특별한 메트릭 측정 도구인데, 시간을 측정하는데 사용된다.
Timer
는 다음과 같은 내용을 한번에 측정해준다.seconds_count
: 누적 실행 수 - 카운터
seconds_sum
: 실행 시간의 합 - sum
seconds_max
: 최대 실행 시간(가장 오래걸린 실행 시간) - 게이지
OrderServiceV3
package hello.order.v3;
import hello.order.OrderService;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class OrderServiceV3 implements OrderService {
private final MeterRegistry registry;
private AtomicInteger stock = new AtomicInteger(100);
public OrderServiceV3(MeterRegistry registry) {
this.registry = registry;
}
@Override
public void order() {
Timer timer = Timer.builder("my.order")
.tag("class", this.getClass().getName())
.tag("method", "order")
.description("order")
.register(registry);
timer.record(() -> {
log.info("주문");
stock.decrementAndGet();
sleep(500);
});
}
@Override
public void cancel() {
Timer timer = Timer.builder("my.order")
.tag("class", this.getClass().getName())
.tag("method", "cancel")
.description("order")
.register(registry);
timer.record(() -> {
log.info("취소");
stock.incrementAndGet();
sleep(200);
});
}
private static void sleep(int l) {
try {
Thread.sleep(l + new Random().nextInt(200));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
register(registry)
: 만든 타이머를 MeterRegistry
에 등록한다. 이렇게 등록해야 실제 동작 한다.OrderConfigV3
package hello.order.v3;
import hello.order.OrderService;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OrderConfigV3 {
@Bean
OrderService orderService(MeterRegistry registry) {
return new OrderServiceV3(registry);
}
}
액츄에이터 메트릭 확인
COUNT
: 누적 실행 수(카운터와 같다)TOTAL_TIME
: 실행 시간의 합(각각의 실행 시간의 누적 합이다)MAX
: 최대 실행 시간(가장 오래 걸린 실행시간이다)프로메테우스 포멧 메트릭 확인
seconds_count
: 누적 실행 수seconds_sum
: 실행 시간의 합seconds_max
: 최대 실행 시간(가장 오래걸린 실행 시간)그라파나 패널 추가
increase(my_order_seconds_count{method="order"}[1m])
increase(my_order_seconds_count{method="cancel"}[1m])
my_order_seconds_max
increase(my_order_seconds_sum[1m]) / increase(my_order_seconds_count[1m])
타이머는 @Timed
라는 애노테이션을 통해 AOP를 적용할 수 있다.
OrderServiceV4
package hello.order.v4;
import hello.order.OrderService;
import io.micrometer.core.annotation.Timed;
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
@Timed("my.order")
@Slf4j
public class OrderServiceV4 implements OrderService {
private AtomicInteger stock = new AtomicInteger(100);
@Override
public void order() {
log.info("주문");
stock.decrementAndGet();
sleep(500);
}
@Override
public void cancel() {
log.info("취소");
stock.incrementAndGet();
sleep(200);
}
private static void sleep(int l) {
try {
Thread.sleep(l + new Random().nextInt(200));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
@Timed("my.order")
타입이나 메서드 중에 적용할 수 있다.OrderConfigV4
package hello.order.v4;
import hello.order.OrderService;
import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OrderConfigV4 {
@Bean
OrderService orderService() {
return new OrderServiceV4();
}
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
TimedAspect
를 적용해야 @Timed 에 AOP가 적용된다.그라파나 대시보드 확인
재고 수량을 통해 게이지를 등록하는 간단 예제를 해보겠습니다.
StockConfigV1
package hello.order.gauge;
import hello.order.OrderService;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class StockConfigV1 {
@Bean
public MyStockMetric myStockMetric(OrderService orderService, MeterRegistry registry) {
return new MyStockMetric(orderService, registry);
}
@Slf4j
static class MyStockMetric {
private OrderService orderService;
private MeterRegistry registry;
public MyStockMetric(OrderService orderService, MeterRegistry registry) {
this.orderService = orderService;
this.registry = registry;
}
@PostConstruct
public void init() {
Gauge.builder("my.stock", orderService, service -> {
log.info("stock gauge call");
return service.getStock().get();
}).register(registry);
}
}
}
액츄에이터 메트릭 확인
게이지 단순 등록
StockConfigV2
package hello.order.gauge;
import hello.order.OrderService;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.binder.MeterBinder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class StockConfigV2 {
@Bean
public MeterBinder stockSize(OrderService orderService) {
return registry -> Gauge.builder("my.stock", orderService, service -> {log.info("stock gauge call");
return service.getStock().get();
}).register(registry);
}
}
액츄에이터 메트릭 확인
그라파나 등록 - 재고