오늘은 Spring Boot 애플리케이션에서 발생한 로그가 어떤 과정을 거쳐 Grafana에 표시되는지 확인했다.
현재 실습 프로젝트의 로그 흐름은 다음과 같다.
Spring Boot 애플리케이션에서 로그가 발생하면 Docker가 해당 로그를 저장한다. Promtail은 Docker 로그를 수집해 Loki로 전달하고, Grafana는 Loki를 데이터 소스로 사용하여 로그를 조회한다.
정리하면 다음과 같다.
Spring Boot → Docker 로그 → Promtail → Loki → Grafana
강의 자료에서는 loki-logback-appender를 사용해 애플리케이션이 Loki로 로그를 직접 전송했지만, 현재 프로젝트에서는 별도의 로그 수집기를 사용하고 있었다.
다만 Promtail은 2026년 3월 2일부로 지원이 종료되었기 때문에 새로운 운영 환경에서는 Grafana Alloy를 사용하는 것이 권장된다는 점도 알게 되었다. 기존 프로젝트의 Promtail은 실습을 위해 유지하되, 새 프로젝트에서는 Alloy를 검토해야 한다.
Grafana Explore에서 Loki를 선택하고 다음 LogQL을 사용해 세 서비스의 로그를 조회했다.
{job=~"order-service|payment-service|inventory-service"}
여기서 job은 로그가 어느 서비스에서 발생했는지 나타내는 라벨이다. =~는 정규식을 이용해 여러 서비스 중 하나와 일치하는 로그를 검색한다는 의미다.
ERROR 로그만 조회할 때는 다음과 같이 로그 본문 필터를 추가했다.
{job=~"order-service|payment-service|inventory-service"} |= "ERROR"
특정 결제 타임아웃만 보고 싶을 때는 검색 범위를 더 좁힐 수 있었다.
{job="payment-service"} |= "Payment gateway timeout"
Loki는 로그 전체를 모두 색인하는 것이 아니라 서비스나 환경처럼 값의 종류가 제한적인 라벨을 중심으로 로그 스트림을 구성한다.
traceId, orderId, userId처럼 계속 새로운 값이 생기는 정보는 라벨로 사용하면 카디널리티가 지나치게 높아질 수 있다. 이러한 값은 로그 본문이나 구조화된 메타데이터에서 검색하는 것이 더 적절하다.
주문 API를 여러 번 호출하여 결제 서비스에서 의도적으로 발생하는 외부 PG 타임아웃 로그를 확인했다.
대표적인 오류 메시지는 다음과 같았다.
Payment gateway timeoutExternal payment API not respondingPayment failedOrder failed로그를 분석한 결과 장애 흐름은 다음과 같았다.
로그는 단순히 위에서 아래로 읽는 것이 아니라 다음 순서로 확인해야 한다.
로그에는 다음과 같은 값이 포함되어 있었다.
[6a8503ff344fc4d4c025b11c1ab8839c/498ea535493e0286]
앞의 값은 Trace ID이고 뒤의 값은 Span ID다.
Trace ID는 사용자의 요청 전체를 식별한다. 여러 서비스의 로그에 동일한 Trace ID가 있다면 같은 사용자 요청에서 발생한 로그라는 의미다.
Span ID는 요청 내부의 개별 작업을 나타낸다. Order Controller, HTTP 클라이언트 호출, Payment Controller는 같은 요청에 포함되어 있어 Trace ID는 같지만 서로 다른 작업이므로 Span ID는 다르게 나타난다.
MSA 환경에서는 하나의 요청이 여러 서비스를 거치기 때문에 시간만으로 로그를 연결하기 어렵다. 따라서 Trace ID가 있어야 동일한 요청의 전체 흐름을 정확하게 추적할 수 있다.
결제 실패처럼 결과만 기록한 로그는 실제 장애 분석에 큰 도움이 되지 않는다.
좋은 로그는 다음 질문에 답할 수 있어야 한다.
requestId, traceIduserIdorderIdserviceerrorCode, messageelapsedMstraceId다만 비밀번호, 전체 카드번호, CVC, JWT, Refresh Token, Slack Webhook URL과 같은 민감정보는 로그에 남기면 안 된다. 꼭 필요한 정보만 선택하고 민감한 값은 마스킹해야 한다.
이전에 생성한 Order Service Down 규칙을 개선했다.
Prometheus 쿼리는 다음과 같다.
up{job="order-service"}
up 값은 서비스 상태를 나타낸다.
up=1이면 Prometheus가 해당 서비스를 정상적으로 수집하고 있다.up=0이면 Prometheus가 해당 서비스에 접근하지 못하고 있다.Grafana에서는 다음과 같이 조건을 설정했다.
up{job="order-service"}LastIS BELOW 1따라서 up 값이 1보다 작아지면 장애 상태로 판단한다.
Grafana Alert Rule에 다음 Labels를 추가했다.
service=order-serviceseverity=criticalteam=backendenvironment=localLabels는 알림을 식별하고 분류하며 Notification Policy에서 알림을 전달할 대상을 결정할 때 사용한다.
Annotations에는 장애 대응에 필요한 설명을 추가했다.
summary: 어떤 현상이 발생했는지description: 어떤 조건으로 장애가 감지됐는지impact: 사용자와 업무에 어떤 영향이 있는지possible_cause: 예상 가능한 원인이 무엇인지action: 담당자가 무엇을 확인해야 하는지Labels가 “이 알림을 어떻게 분류할 것인가”에 대한 정보라면, Annotations는 “담당자가 이 장애를 어떻게 이해하고 대응할 것인가”에 대한 정보다.
Grafana의 기본 Slack 메시지에는 다음과 같은 내부 정보가 그대로 출력됐다.
A=0, B=0, C=1디버깅에는 도움이 되지만 운영자가 빠르게 장애를 파악하기에는 불편했다.
그래서 Slack Contact Point의 Title과 Text Body를 직접 수정했다.
수정된 메시지에는 다음 정보가 표시되도록 구성했다.
기본 메시지는 Grafana의 내부 데이터를 그대로 보여주는 데 가깝고, 수정한 메시지는 담당자가 장애를 발견한 뒤 바로 행동할 수 있도록 구성되어 있었다.
좋은 Slack 장애 알림은 다음 질문에 답할 수 있어야 한다.
docker stop order-service를 실행해 Order Service를 중단했다.
Prometheus의 up 값이 0으로 변경됐고, Grafana 상태가 Normal → Pending → Firing 순서로 변경됐다. 이후 직접 구성한 형식으로 Slack 장애 메시지가 도착했다.
그다음 docker start order-service를 실행해 서비스를 복구했다.
Prometheus의 up 값이 다시 1로 변경됐고, Grafana Alert Rule도 Normal 상태로 돌아왔다.
이 과정을 통해 단순히 Alert Rule을 만드는 것에서 끝나는 것이 아니라 실제 장애 발생과 복구 알림까지 검증해야 한다는 것을 알게 되었다.
추가 과제로 MSA와 관련된 개념도 정리했다.
MSA는 서비스를 여러 개로 나누는 것으로 끝나는 것이 아니다. 서비스를 분리한 뒤 발생하는 데이터 일관성, 네트워크 장애, 서비스 검색, 인증과 인가, 장애 전파 문제까지 함께 해결해야 한다.
Loki와 Promtail의 상태를 확인하기 위해 docker compose ps를 실행했지만 다음 오류가 발생했다.
failed to connect to the docker API
Loki의 /ready 엔드포인트에도 연결할 수 없었다.
/ready 주소로 직접 요청했다.dockerDesktopLinuxEngine을 확인했다.Docker Desktop이 실행되지 않은 것이 원인이었다. Docker Desktop을 실행하고 엔진이 시작될 때까지 기다린 뒤 다시 확인했다.
그 결과 Loki와 Promtail이 Up 상태로 표시됐고 Loki에서 ready 응답을 받을 수 있었다.
컨테이너나 애플리케이션의 문제를 확인하기 전에 Docker 엔진 자체가 실행 중인지 먼저 확인해야 한다.
다음 순서로 확인할 것이다.
docker info로 Docker Engine 확인docker compose ps로 컨테이너 상태 확인Alert Rule에 Custom annotation을 추가한 뒤 저장했지만 다음 메시지가 발생했다.
There are errors in the form. Please correct them and try again!
Annotations와 Labels에서 빈 항목이나 중복된 Key가 있는지 확인했다.
처음에는 큰 입력란에 다음과 같이 Key와 Value를 함께 작성했다.
Key: impact
Value: 신규 주문 생성과 후속 결제 처리가 실패할 수 있습니다.
하지만 실제로는 Custom annotation의 이름과 내용을 서로 다른 입력란에 작성해야 했다.
작은 입력란에는 Annotation 이름만 작성했다.
impactpossible_causeaction큰 입력란에는 해당 Annotation의 내용만 작성했다.
Key와 Value를 올바르게 분리한 후 Alert Rule이 정상적으로 저장됐다.
Grafana에서 Custom annotation은 단순한 설명란이 아니라 Key-Value 구조다. 이름 입력란을 비워두면 본문에 Key:를 작성하더라도 정상적인 Annotation으로 인식하지 않는다.
Slack 장애 알림은 정상적으로 도착했지만 Grafana의 기본 메시지는 내부 값과 라벨이 그대로 표시되어 장애의 영향과 대응 방법을 빠르게 파악하기 어려웠다.
Alert Rule에는 장애 정보와 대응 방법을 저장하고, Slack Contact Point에서는 해당 정보를 사람이 읽기 좋은 형식으로 출력하도록 역할을 분리했다.
Slack 메시지가 다음 순서로 보이도록 구성했다.
상태 → 서비스 → 영향 → 예상 원인 → 상세 설명 → 권장 조치 → 바로가기
알림이 도착하는 것만으로 모니터링이 완성되는 것은 아니다. 담당자가 알림을 읽고 바로 대응할 수 있어야 좋은 알림이라고 할 수 있다.
또한 서비스 전체의 상태를 감시하는 up 알림에는 개별 요청의 traceId나 orderId가 존재하지 않는다. 알림 종류에 따라 포함할 수 있는 정보가 다르므로 존재하지 않는 값을 억지로 추가하면 안 된다.
먼저 알림 메시지의 표준 구조를 정하고 다음 항목을 점검할 것이다.
count_over_time을 이용해 서비스별 ERROR 로그 수 계산하기