
이번에 모니터링을 평소 하던 AWS가 아닌 Railway와 연결해서 하면서, 어떤 모니터링 방식으로 메트릭 수집을 하면 좋을지 생각해보게 되었다.
메트릭은 매 이벤트를 전송하는 게 아니라, 애플리케이션 메모리 안에서 숫자를 계속 누적하거나 갱신하다가 일정 주기마다 스냅샷을 보내는 구조였다.
이번 글에서는 Spring Boot Actuator, Micrometer, OTLP, Grafana까지 메트릭이 흘러가는 과정을 한 번에 정리한다.
Spring Boot 애플리케이션에서 JVM 메트릭이 Grafana 같은 백엔드까지 가는 흐름은 대략 이렇다.
JVM 내부 상태
-> Micrometer 계측/집계
-> OTLP registry가 주기적으로 전송
-> Grafana 같은 백엔드에서 저장/조회
중요한 점은 요청마다 외부로 메트릭을 보내지 않는다는 것이다.
애플리케이션 안의 MeterRegistry가 숫자를 관리하고, OtlpMeterRegistry 같은 registry가 설정된 주기마다 현재 상태를 묶어서 전송한다. 예를 들어 step이 30초라면 30초마다 한 번씩 스냅샷을 보낸다.
그래서 트래픽이 많아져도 메트릭 전송량이 요청 수만큼 폭증하지 않는다. 전송량은 주로 메트릭 개수, 라벨 조합 수, 전송 주기에 영향을 받는다.
JVM은 내부 상태를 MXBean, 즉 JMX 기반 인터페이스로 노출한다.
대표적으로 이런 값들이 있다.
MemoryMXBean: 힙/논힙 메모리 사용량GarbageCollectorMXBean: GC 횟수, 누적 시간OperatingSystemMXBean: CPU 사용률ThreadMXBean: 스레드 수Spring Boot Actuator를 붙이면 기동 시 여러 MeterBinder가 자동으로 등록된다.
JvmMemoryMetricsJvmGcMetricsProcessorMetricsJvmThreadMetrics이 Binder들이 JVM의 MXBean 값을 읽어서 Micrometer의 Meter로 연결한다. 그래서 애플리케이션에서 jvm.memory.used, process.cpu.usage, jvm.threads.live 같은 메트릭을 볼 수 있게 된다.
Micrometer는 계측 추상화 계층이다. 중심에는 MeterRegistry가 있고, 그 안에 여러 Meter가 등록된다.
대표적인 Meter는 세 가지다.
| Meter | 의미 | 예시 |
|---|---|---|
| Gauge | 현재 순간 값 | 힙 사용량, 스레드 수 |
| Counter | 단조 증가 누적값 | 이벤트 횟수, 누적 바이트 |
| Timer | 호출 수, 총 시간, 최대값, 히스토그램 | HTTP 지연 시간, GC pause |
여기서 헷갈리기 쉬운 점은 계측과 전송이 분리되어 있다는 것이다.
Micrometer는 앱 안에서 값을 관리한다.
외부로 보내는 일은 Prometheus registry, OTLP registry 같은 구현체가 맡는다.
OTLP 방식에서 OtlpMeterRegistry는 push 방식으로 동작한다.
앱이 시작되면 백그라운드 스레드가 돌고, 설정된 step마다 publish 과정이 실행된다.
1. Registry의 모든 Meter를 확인한다.
2. Gauge는 지금 값을 읽는다.
3. Counter, Timer는 누적값과 히스토그램 상태를 읽는다.
4. OTLP protobuf 포맷으로 직렬화한다.
5. 설정된 endpoint로 HTTP POST를 보낸다.
설정에서는 보통 이런 값이 중요하다.
management:
otlp:
metrics:
export:
url: https://example-otlp-endpoint
step: 30s
headers:
Authorization: Bearer ${OTLP_TOKEN}
실제 속성 이름은 Spring Boot와 Micrometer 버전에 따라 확인이 필요하지만, 개념적으로는 "어디로", "얼마나 자주", "어떤 인증으로" 보낼지가 핵심이다.
메트릭을 수집하는 방식은 크게 pull과 push로 나눠서 볼 수 있다.
Prometheus 방식은 수집기가 앱으로 들어와서 값을 가져간다.
Prometheus -> /actuator/prometheus -> Spring Boot App
앱은 /actuator/prometheus 같은 엔드포인트에 현재 값을 노출하고, Prometheus가 주기적으로 scrape한다.
Docker Compose나 Kubernetes처럼 내부 네트워크와 service discovery가 잘 잡힌 환경에서는 자연스럽다. 하지만 Railway처럼 외부 수집기가 앱 주소로 접근하거나 target을 관리하기 애매한 환경에서는 설정이 번거로울 수 있다.
OTLP 방식은 앱이 외부 endpoint로 직접 보낸다.
Spring Boot App -> OTLP endpoint -> Grafana Cloud
수집기가 앱으로 들어올 필요가 없다. 앱이 인증 정보를 들고 외부 endpoint로 POST를 보내면 된다.
그래서 PaaS 환경이나 외부 접근이 까다로운 환경에서는 push 방식이 더 단순할 수 있다.
Grafana 같은 백엔드는 받은 숫자를 시계열로 저장한다.
시계열은 보통 다음 조합으로 구분된다.
메트릭 이름 + 라벨 조합
예를 들어 같은 jvm.memory.used라도 라벨이 다르면 다른 시계열이다.
area=heaparea=nonheapid=G1 Eden Spaceapplication=gerdmanagement.metrics.tags.application=gerd처럼 공통 라벨을 붙여두면 나중에 Grafana에서 특정 앱만 필터링하기 좋다.
PromQL에서는 이런 식으로 조회할 수 있다.
process_cpu_usage
rate(jvm_gc_pause_seconds_sum[5m])
미리 만들어진 JVM 대시보드를 가져오면 heap, GC pause, CPU, thread 같은 기본 패널을 빠르게 구성할 수 있다.
Better Stack은 로그 수집, 업타임 모니터링, 온콜까지 한 번에 묶기 좋다. 별도로 Loki나 알림 시스템을 붙이지 않아도 시작하기 쉽다는 장점이 있다.
대신 메트릭 대시보드는 Grafana의 PromQL 대시보드를 그대로 쓰기보다 Better Stack의 SQL 방언에 맞춰 직접 패널을 구성해야 할 수 있다.
예를 들어 진단용으로는 이런 패널을 만들 수 있다.
Better Stack 쿼리에서는 게이지 값에 avgMerge(value_avg)를 쓰거나, 라벨을 label('level')처럼 꺼내는 식의 차이가 있다. 히스토그램 quantile도 저장소 방언에 맞춰 확인해야 한다.
여기서 중요한 건 쿼리를 외우는 게 아니라, 실제 저장된 metric name과 label을 먼저 확인하는 것이다. Micrometer에서 보던 이름, Prometheus 이름, OTLP 저장소에서 보이는 이름이 조금씩 다를 수 있다.
Better Stack은 그라파냐처럼 spring boot 전용 세팅되어있는게 없다.
그래서 확인할 항목을 골라서 커스텀으로 쿼리를 입력해서 직접 만들어야 한다.
다음과 같은 순서로 제작했다.

Telemetry -> Sources 탭에 들어가서 Connect Source를 누른다.
Source를 만들면 Source token, url 이렇게 두개가 나온다.
curl -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer [token]' \
-d '{"dt":"'"$(date -u +'%Y-%m-%d %T UTC')"'","message":"Hello from Better Stack!"}' \
--insecure \
https://[url]
management:
endpoints:
web:
exposure:
include: health,info,env,configprops
endpoint:
health:
show-details: never
probes:
enabled: true
health:
diskspace:
enabled: true
ping:
enabled: true
livenessstate:
enabled: true
readinessstate:
enabled: true
metrics:
tags:
application: ${spring.application.name}
otlp:
metrics:
export:
enabled: true
url: ${BETTERSTACK_OTLP_URL}/v1/metrics
headers:
Authorization: Bearer ${BETTERSTACK_SOURCE_TOKEN}
step: 30s
각각 OTLP, SOURCE_TOKEN으로 넣어준다!

해당 탭에서 Create dashboard로 들어간다. 그러면 처음에는 아예 대시보드가 비어있다.

맨 오른쪽의 플러스 버튼을 누르면 차트를 만들 수 있다.

source를 아까 만들었던것으로 절정하고, Query를 입력하고 Run query를 하면 설정한 차트 타입에 따라 볼 수 있다.
나는 다음과 같은 항목을 추가해줬다.
| 패널 | 확인하려는 것 |
|---|---|
| Heap used vs max | 메모리 기준선과 누수 의심 |
| GC pause p95/p99 | 사용자 지연으로 이어질 수 있는 멈춤 |
| CPU usage | 앱 CPU와 시스템 CPU 추세 |
| Live threads | 스레드 증가나 고착 여부 |
| Logback events | error/warn 로그 급증 |
| Disk free | 디스크 고갈 위험 |
이렇게 항목을 달아주면...

완성이된다~!
메트릭 수집의 핵심은 이벤트 전송이 아니라 숫자 상태의 주기적 스냅샷이다.
Prometheus pull이 좋은 환경도 있고, OTLP push가 더 단순한 환경도 있다. 지금 나같은 경우는 Railway를 사용하고 있기에 Better Stack을 사용했다.