마찬가지로, 여기서 이번에 Micrometer 타입 기준으로 Counter, Gauge, Timer 를 사용하려고한다
그리고 위의 경우 어노테이션 지원되는게 있고 안되는게 있다.
간단한건 어노테이션으로 해도되지만, 중간 로직에서 예외가발생할수있는경우에는 프로그래밍 방식이 더 낫다.
우선 간단한 특정 Countroller 생성한뒤 여기서 하나씩 맛보자
1. 항상 증가만하는 호출 횟수는 Counter 타입의 매트릭으로 정의
2. 동적으로 변하는 수는 Gauge 타입의 매트릭으로 정의
3. 메서드 실행 시간은 Timer 타입의 매트릭으로 정의
참고로 해보니. 카운터는 조회했을때만 매트릭이 생성되어서
/actuator/prometheus 에서 확인할 수 있고. 게이지의 경우는 미리 생성된다.
코드를보자 정말 간단하다.
package com.example.sj_shop.web.orders.controller;
import com.example.sj_shop.web.orders.service.OrderService;
import io.micrometer.core.annotation.Counted;
import io.micrometer.core.annotation.Timed;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@RestController
@RequiredArgsConstructor
public class ApiOrderController {
private final OrderService orderService;
private final MeterRegistry meterRegistry;
private final AtomicInteger activeRequests = new AtomicInteger();
@PostConstruct
public void initGauge() {
Gauge.builder(
"order.controller.active", // 메트릭 이름
activeRequests, // 관찰할 객체
value -> value.get() // 이 객체에서 숫자를 어떻게 꺼낼지
)
.register(meterRegistry);
}
// 프로메테우스에서는 . 구분자가 _ 로 바끼고 마지막에 _total 로 suffix 가 붙는다
@Counted("order.controller.response")
@GetMapping("/order")
public ResponseEntity<?> order() {
meterRegistry.counter("order.controller.request").increment();
log.info("[log.info level] /order 호출");
log.warn("[log.warn level] /order 호출");
log.error("[log.error level] /order 호출", new RuntimeException("런타임에러 발생"));
return ResponseEntity.status(HttpStatus.OK).build();
}
@GetMapping("/order/gauge/up")
public ResponseEntity<?> gaugeUp() {
activeRequests.incrementAndGet();
return ResponseEntity.ok().build();
}
@GetMapping("/order/gauge/down")
public ResponseEntity<?> gaugeDown() {
activeRequests.decrementAndGet();
return ResponseEntity.ok().build();
}
@Timed("order.controller.timer")
@GetMapping("/order/timer")
public ResponseEntity<?> timer() throws InterruptedException {
// 처리 시간이 보이도록 일부러 1초 대기
Thread.sleep(1000);
return ResponseEntity.ok().build();
}
}
1. Counted
우선 어노테이션기반으로 완전히 메서드가 종료가되면 order.controller.response 의 매트릭값을 증가시키게했다
왜냐면 AOP 기반으로 동작한다. 따라서 response 로 지정했다. 완전히 호출이 종료됨을 보장했을때 카운터를 증가시키려고.
그리고 메서드 시작할때는 프로그래밍 방식으로 증가 시켯다.
// 프로메테우스에서는 . 구분자가 _ 로 바끼고 마지막에 _total 로 suffix 가 붙는다
@Counted("order.controller.response")
@GetMapping("/order")
public ResponseEntity<?> order() {
meterRegistry.counter("order.controller.request").increment();
log.info("[log.info level] /order 호출");
log.warn("[log.warn level] /order 호출");
log.error("[log.error level] /order 호출", new RuntimeException("런타임에러 발생"));
return ResponseEntity.status(HttpStatus.OK).build();
}
자 호출하고 결과를 보자
카운터를 사용할 경우 마지막에 _total 이 붙는다.
한번더 요청해보자
번외로, 메서드 내에서 logback 을 활용하여 로그를 info, warn, error 레벨로 남겼는데 이또한 수집된다.
마이크로 미터가 등록되면서 자동으로 모아준거다.
2. Gauge
이것도 보면 단순하자 원자성 보장되는 AtomicInter 만 사용했을뿐이다.
Gauge 는 프로그래밍 방식만 지원되므로 다음처럼 만들어야한다
이름과, 관찰할 객체와 여기서 어떻게 값을 꺼낼지 정의한다. value 가 AtomicInter 변수를 가르킨다
private final AtomicInteger activeRequests = new AtomicInteger();
@PostConstruct
public void initGauge() {
Gauge.builder(
"order.controller.active", // 메트릭 이름
activeRequests, // 관찰할 객체
value -> value.get() // 이 객체에서 숫자를 어떻게 꺼낼지
)
.register(meterRegistry);
}
올리거나 내리거나하는 api 이다.
@GetMapping("/order/gauge/up")
public ResponseEntity<?> gaugeUp() {
activeRequests.incrementAndGet();
return ResponseEntity.ok().build();
}
@GetMapping("/order/gauge/down")
public ResponseEntity<?> gaugeDown() {
activeRequests.decrementAndGet();
return ResponseEntity.ok().build();
}
호출전에 메트릭을 보자 게이지는 미리 생성된다
up api 를 호출하고 메트릭 값을 보자
# HELP order_controller_active
# TYPE order_controller_active gauge
order_controller_active{application="sj-shop",hostname="DESKTOP-UOTDLQQ"} 1.0
down api 를 호출하고 메트릭 값을 보자
# HELP order_controller_active
# TYPE order_controller_active gauge
order_controller_active{application="sj-shop",hostname="DESKTOP-UOTDLQQ"} 0.0
3. Timer
이제 타이머를 보자 이는 프로그래밍 방식, 어노테이션 방식 둘다 지원하는데
안전하게측정하려면 프로그래밍 방식을 추천한다. 예외가 터지면 종료를해야하니까..
우선 전자의 방식은 좀따 알아보고, 우선 어노테이션 방식먼저 보자
Timer 역시 호출되어서 매트릭이 생성되어야 볼 수 있다.
참쉽다.
@Timed("order.controller.timer")
@GetMapping("/order/timer")
public ResponseEntity<?> timer() throws InterruptedException {
// 처리 시간이 보이도록 일부러 1초 대기
Thread.sleep(1000);
return ResponseEntity.ok().build();
}
api 를 호출 하고 매트릭을봐보자. 두개의 지표가 나오는데.
_count : 실행 횟수
_sum : 실행 시간 누적합
_max: 제일 오래걸린 시간
이라는것을 알 수 있다.
# HELP order_controller_timer_seconds
# TYPE order_controller_timer_seconds summary
order_controller_timer_seconds_count{application="sj-shop",class="com.example.sj_shop.web.orders.controller.ApiOrderController",exception="none",hostname="DESKTOP-UOTDLQQ",method="timer"} 1
order_controller_timer_seconds_sum{application="sj-shop",class="com.example.sj_shop.web.orders.controller.ApiOrderController",exception="none",hostname="DESKTOP-UOTDLQQ",method="timer"} 1.0010387
# HELP order_controller_timer_seconds_max
# TYPE order_controller_timer_seconds_max gauge
order_controller_timer_seconds_max{application="sj-shop",class="com.example.sj_shop.web.orders.controller.ApiOrderController",exception="none",hostname="DESKTOP-UOTDLQQ",method="timer"} 1.0010387
한번더 요청해보자
# HELP order_controller_timer_seconds
# TYPE order_controller_timer_seconds summary
order_controller_timer_seconds_count{application="sj-shop",class="com.example.sj_shop.web.orders.controller.ApiOrderController",exception="none",hostname="DESKTOP-UOTDLQQ",method="timer"} 2
order_controller_timer_seconds_sum{application="sj-shop",class="com.example.sj_shop.web.orders.controller.ApiOrderController",exception="none",hostname="DESKTOP-UOTDLQQ",method="timer"} 2.0024442
# HELP order_controller_timer_seconds_max
# TYPE order_controller_timer_seconds_max gauge
order_controller_timer_seconds_max{application="sj-shop",class="com.example.sj_shop.web.orders.controller.ApiOrderController",exception="none",hostname="DESKTOP-UOTDLQQ",method="timer"} 1.0014055
management:
# Actuator 엔드포인트를 HTTP로 외부에 공개할 범위를 설정한다.
endpoints:
web:
exposure:
# health : 애플리케이션 상태 확인
# info : 애플리케이션 기본 정보 확인
# prometheus : Prometheus가 수집할 메트릭 제공
include: health,info,prometheus
# 개별 Actuator 엔드포인트 설정이다.
endpoint:
# /actuator/health 응답 설정이다.
health:
# 단순히 UP/DOWN만 보여주지 않고
# DB, Redis, 디스크 공간 등 상세 상태도 함께 보여준다.
show-details: always
서버 재실행 후 다음 엔드포인트로 접속해보면 , 별도의 설명대신 보자마자 알 수 있을 것이다
개별 엔드포인트를 다시 클릭해보자
헬스 엔드포인트로 접속하니 mysql, redis 부터 다양한 헬스체크를 할 수 있다. show-details 가 always 라 많이 노출되었다.
이제 프로메테우스 엔드포인트 보자
기본적으로 마이크로 미터가 모아다 준 정보를 볼 수 있다. 경우에 따라 개발자가 특정 매트릭도 생성할 수 있다.
# HELP .... 는 매트릭 설명이고
# TYPE ... 은 매트릭 타입이다
그리고 그 아래줄에 써져있는게 값이다
이 정보들이 프로메테우스에게 전달된 매트릭 정보이다.
그리고 프로메테우스 컨테이너는 다음과같은 설정정보가 있어야
저 데이터를 가져올 수 있다.
그리고 프로메테우스 서버를 실행해서, 주어진 포트로 접속해보자
9090 포트에서 /targets 에 해당하는 페이지를보면 현재 로컬 스프링부트가 추가되어있는것을 확인할 수 있다.
이제Query 탭 으로 가서 조회를 해보자
아까 위에서 빨간색 네모로 표시되었던 application_ready_time_seconds 이 매트릭을 조회하면 자동완성도 될 뿐만아니라 잘 조회 된다
참고로 자바에서는 application.ready.time.seconds 이런 점 표기법이라면
프로메테우스는 _ 표기법을 쓴다
간소하게 그래프로도 볼 수 있다. (추후에는 Grafana 에서 더 이쁘게 볼 수 있다.)
다음으로 ...
다음으로는 그라파나까지 사용하여 데이터 시각화를 진행해보자.
사실 지금 Micrometer 에 대해서는 의존성만 추가했지 별다른것을 진행하지 않았는데
1. Micrometer 을 이용하여 커스텀 매트릭을 생성
2. 이를 Prometheus 에서 조회하고 (또는 /actuator/prometheus )