Loki 로그 분석과 Grafana Slack 장애 알림 개선

yonghyun lee·2026년 8월 19일

1. 오늘 학습 키워드

  • Observability
  • Loki
  • Promtail
  • Grafana Explore
  • LogQL
  • 로그 레벨
  • Trace ID와 Span ID
  • 구조화 로그
  • Grafana Alert Rule
  • Labels와 Annotations
  • Slack Contact Point
  • Notification Policy
  • Notification Template
  • MSA
  • 분산 트랜잭션
  • 회복성

2. 오늘 학습한 내용을 나만의 언어로 정리하기

Loki를 이용한 애플리케이션 로그 모니터링

오늘은 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를 검토해야 한다.

LogQL을 이용한 로그 검색

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 timeout
  • External payment API not responding
  • Payment failed
  • Order failed
  • HTTP 500 Internal Server Error

로그를 분석한 결과 장애 흐름은 다음과 같았다.

  1. Order Service가 Payment Service에 결제를 요청한다.
  2. Payment Service가 외부 PG를 호출한다.
  3. 외부 PG가 응답하지 않아 타임아웃이 발생한다.
  4. Payment Service가 HTTP 500을 반환한다.
  5. Order Service가 결제 실패 응답을 받는다.
  6. 최종적으로 주문 API도 HTTP 500을 반환한다.

로그는 단순히 위에서 아래로 읽는 것이 아니라 다음 순서로 확인해야 한다.

  1. 언제 발생했는가?
  2. 어느 서비스에서 발생했는가?
  3. 로그 레벨은 무엇인가?
  4. 어떤 요청에서 발생했는가?
  5. 어느 클래스에서 발생했는가?
  6. 직접적인 오류 메시지는 무엇인가?
  7. 다른 서비스에 어떤 영향을 주었는가?

Trace ID와 Span ID

로그에는 다음과 같은 값이 포함되어 있었다.

[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, traceId
  • 어떤 사용자인가? → userId
  • 어떤 주문인가? → orderId
  • 어느 서비스인가? → service
  • 어떤 오류인가? → errorCode, message
  • 얼마나 걸렸는가? → elapsedMs
  • 다른 서비스의 같은 요청인가? → traceId

다만 비밀번호, 전체 카드번호, CVC, JWT, Refresh Token, Slack Webhook URL과 같은 민감정보는 로그에 남기면 안 된다. 꼭 필요한 정보만 선택하고 민감한 값은 마스킹해야 한다.

Grafana와 Slack 장애 알림 구성

이전에 생성한 Order Service Down 규칙을 개선했다.

Prometheus 쿼리는 다음과 같다.

up{job="order-service"}

up 값은 서비스 상태를 나타낸다.

  • up=1이면 Prometheus가 해당 서비스를 정상적으로 수집하고 있다.
  • up=0이면 Prometheus가 해당 서비스에 접근하지 못하고 있다.

Grafana에서는 다음과 같이 조건을 설정했다.

  • Query A: up{job="order-service"}
  • Reduce B: Last
  • Threshold C: IS BELOW 1

따라서 up 값이 1보다 작아지면 장애 상태로 판단한다.

Labels와 Annotations의 차이

Grafana Alert Rule에 다음 Labels를 추가했다.

  • service=order-service
  • severity=critical
  • team=backend
  • environment=local

Labels는 알림을 식별하고 분류하며 Notification Policy에서 알림을 전달할 대상을 결정할 때 사용한다.

Annotations에는 장애 대응에 필요한 설명을 추가했다.

  • summary: 어떤 현상이 발생했는지
  • description: 어떤 조건으로 장애가 감지됐는지
  • impact: 사용자와 업무에 어떤 영향이 있는지
  • possible_cause: 예상 가능한 원인이 무엇인지
  • action: 담당자가 무엇을 확인해야 하는지

Labels가 “이 알림을 어떻게 분류할 것인가”에 대한 정보라면, Annotations는 “담당자가 이 장애를 어떻게 이해하고 대응할 것인가”에 대한 정보다.

Slack 메시지 개선

Grafana의 기본 Slack 메시지에는 다음과 같은 내부 정보가 그대로 출력됐다.

  • A=0, B=0, C=1
  • 전체 Labels
  • 긴 Silence URL
  • Grafana 내부 상태값

디버깅에는 도움이 되지만 운영자가 빠르게 장애를 파악하기에는 불편했다.

그래서 Slack Contact Point의 Title과 Text Body를 직접 수정했다.

수정된 메시지에는 다음 정보가 표시되도록 구성했다.

  • 장애 발생 또는 복구 상태
  • 서비스
  • 실행 환경
  • 심각도
  • 담당 팀
  • 장애 요약
  • 사용자 영향
  • 예상 원인
  • 상세 설명
  • 권장 조치
  • 대상 인스턴스
  • 발생 시각
  • Grafana 바로가기

기본 메시지는 Grafana의 내부 데이터를 그대로 보여주는 데 가깝고, 수정한 메시지는 담당자가 장애를 발견한 뒤 바로 행동할 수 있도록 구성되어 있었다.

좋은 Slack 장애 알림은 다음 질문에 답할 수 있어야 한다.

  1. 무슨 장애인가?
  2. 어느 서비스인가?
  3. 얼마나 심각한가?
  4. 사용자에게 어떤 영향이 있는가?
  5. 예상 원인은 무엇인가?
  6. 담당자가 가장 먼저 무엇을 해야 하는가?
  7. 어디에서 상세 정보를 확인할 수 있는가?

장애 발생과 복구 검증

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와 관련된 개념도 정리했다.

  • 모놀리식 아키텍처
  • 마이크로서비스 아키텍처
  • 서비스 디스커버리
  • 회복성
  • SPOF
  • MSA 환경에서의 인증 및 인가
  • 분산 트랜잭션과 데이터 일관성
  • 서비스 간 통신 방법

MSA는 서비스를 여러 개로 나누는 것으로 끝나는 것이 아니다. 서비스를 분리한 뒤 발생하는 데이터 일관성, 네트워크 장애, 서비스 검색, 인증과 인가, 장애 전파 문제까지 함께 해결해야 한다.


3. 학습하며 겪었던 문제점 & 에러

문제 1. Docker API에 연결할 수 없었던 문제

문제&에러에 대한 정의

Loki와 Promtail의 상태를 확인하기 위해 docker compose ps를 실행했지만 다음 오류가 발생했다.

failed to connect to the docker API

Loki의 /ready 엔드포인트에도 연결할 수 없었다.

내가 한 시도

  • Docker Compose 명령을 다시 실행했다.
  • Loki의 /ready 주소로 직접 요청했다.
  • 오류 메시지에서 dockerDesktopLinuxEngine을 확인했다.

해결 방법

Docker Desktop이 실행되지 않은 것이 원인이었다. Docker Desktop을 실행하고 엔진이 시작될 때까지 기다린 뒤 다시 확인했다.

그 결과 Loki와 Promtail이 Up 상태로 표시됐고 Loki에서 ready 응답을 받을 수 있었다.

새롭게 알게 된 점

컨테이너나 애플리케이션의 문제를 확인하기 전에 Docker 엔진 자체가 실행 중인지 먼저 확인해야 한다.

이 문제&에러를 다시 만나게 되었다면?

다음 순서로 확인할 것이다.

  1. Docker Desktop 실행 여부 확인
  2. docker info로 Docker Engine 확인
  3. docker compose ps로 컨테이너 상태 확인
  4. 각 서비스의 Health 또는 Ready 엔드포인트 확인

문제 2. Grafana Alert Rule 저장 오류

문제&에러에 대한 정의

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 이름만 작성했다.

  • impact
  • possible_cause
  • action

큰 입력란에는 해당 Annotation의 내용만 작성했다.

Key와 Value를 올바르게 분리한 후 Alert Rule이 정상적으로 저장됐다.

새롭게 알게 된 점

Grafana에서 Custom annotation은 단순한 설명란이 아니라 Key-Value 구조다. 이름 입력란을 비워두면 본문에 Key:를 작성하더라도 정상적인 Annotation으로 인식하지 않는다.

이 문제&에러를 다시 만나게 되었다면?

  • 비어 있는 Annotation 행이 있는지 확인한다.
  • Key와 Value가 올바른 입력란에 작성됐는지 확인한다.
  • 중복된 Key가 있는지 확인한다.
  • Key에 공백이나 한글이 포함됐는지 확인한다.
  • Runbook URL이 잘못 작성되지 않았는지 확인한다.

문제 3. Slack 기본 메시지의 가독성 문제

문제&에러에 대한 정의

Slack 장애 알림은 정상적으로 도착했지만 Grafana의 기본 메시지는 내부 값과 라벨이 그대로 표시되어 장애의 영향과 대응 방법을 빠르게 파악하기 어려웠다.

내가 한 시도

  • Alert Rule에 Labels와 Annotations를 추가했다.
  • Slack Contact Point의 Optional Slack settings를 확인했다.
  • Title과 Text Body의 기본 템플릿을 직접 수정했다.
  • 실제로 Order Service를 중단해 새로운 메시지 형태를 검증했다.

해결 방법

Alert Rule에는 장애 정보와 대응 방법을 저장하고, Slack Contact Point에서는 해당 정보를 사람이 읽기 좋은 형식으로 출력하도록 역할을 분리했다.

Slack 메시지가 다음 순서로 보이도록 구성했다.

상태 → 서비스 → 영향 → 예상 원인 → 상세 설명 → 권장 조치 → 바로가기

새롭게 알게 된 점

알림이 도착하는 것만으로 모니터링이 완성되는 것은 아니다. 담당자가 알림을 읽고 바로 대응할 수 있어야 좋은 알림이라고 할 수 있다.

또한 서비스 전체의 상태를 감시하는 up 알림에는 개별 요청의 traceId나 orderId가 존재하지 않는다. 알림 종류에 따라 포함할 수 있는 정보가 다르므로 존재하지 않는 값을 억지로 추가하면 안 된다.

이 문제&에러를 다시 만나게 되었다면?

먼저 알림 메시지의 표준 구조를 정하고 다음 항목을 점검할 것이다.

  • 장애 제목
  • 서비스와 환경
  • 심각도와 담당 팀
  • 사용자 영향
  • 예상 원인
  • 권장 조치
  • 상세 확인 링크
  • 장애 발생과 복구 메시지 모두 전송되는지 여부

4. 내일 학습할 것은 무엇인지

  • LogQL의 count_over_time을 이용해 서비스별 ERROR 로그 수 계산하기
  • 단순 로그 검색과 로그 기반 메트릭의 차이 이해하기
  • 중복 ERROR 로그와 실제 실패 요청 수의 차이 분석하기
  • JSON 구조화 로그 적용 방법 확인하기
  • 로그에 남기면 안 되는 개인정보와 인증정보 정리하기
  • Promtail과 Grafana Alloy의 차이 알아보기
  • 애플리케이션 로그 모니터링 실습 마무리하기
  • 다음 강의인 CORS 개념과 동작 원리 학습하기
profile
개발이 하고 싶은 남자

0개의 댓글