๐ง ๋ค์ด๊ฐ๊ธฐ์
"์ ๋ง ํ์ํ" ์๋ฌ๋ก๊ทธ์ ์๋ฆผ๋ง ๋ฐ๋ก ์ฌ๋์ผ๋ก ๋ฐ์ผ๋ฉด ์ผ๋ง๋ ์ข์๊น?
Datadog์ ์ฌ์ฉํ๋ ์์ ์๋ ์๋ ๊ทธ๋ฃนํ, ํจํด ์ธ์, ์๋ฆผ ์ค์ ๊น์ง ์ ๊ฐ์ถฐ์ ธ ์์๊ธฐ ๋๋ฌธ์ ์๋ฌ ๋ก๊ทธ ๋ชจ๋ํฐ๋ง์ด ๋น๊ต์ ์์ํ๋ค๊ณ ํ๋ค (ํ์์ Datadog์ ์ฌ์ฉํ๋ ๋น์์ ๋๋ ์์๊ธฐ์ ๊ทธ ์ด์ ์ ์ ๋ชจ๋ฅธ๋ค). ๊ทธ๋ฌ๋ Datadog์ ๋น์ฉ ๋ถ๋ด์ด ์ปค์ง๋ฉด์ ์ง์ํด์ ์ฌ์ฉํ๊ธฐ ์ด๋ ค์ ๊ณ , ๋์ฒด์ฌ๋ก ์คํ์์ค ๊ธฐ๋ฐ์ Prometheus + Loki + Grafana + Tempo ์คํ์ผ๋ก ์ ํํ๊ฒ ๋์๋ค.
ํ์ฌ๋ฅผ ์ฒ์์ ๋ค์ด์ค๋ ๋น์์๋ ํด๋น ์คํ์ ๋ํ ๋ถํธํจ์ ์ ํ ๋๋ผ์ง ๋ชปํ์๋ค. ํ์ง๋ง ์ด์ ์๋น์ค๋ฅผ ๊ฐ๋ฐํ๊ธฐ ์์ํ๋ฉด์ ์ ๋ง ํ์ํ ์๋ฌ ๋ก๊ทธ ์๋ฆผ๋ง ๋ฐ๋ก ๋ฐ์๋ณด๊ณ ์ถ๋ค๋ ์๊ฐ์ด ๊ฐํ๊ฒ ๋ค๊ธฐ ์์ํ์๋ค. ๊ทธ๋์ ๋ก๊น ์๋ฆผ ์์คํ ์ ์ง์ ๊ตฌ์ถํ๊ณ ์ ๊ณ ๋ฏผ์ ํ๊ธฐ ์์ํ์๋ค. ๋ฉํธ๋ฆญ ์๋ฆผ์ ์ด์ ๊ธ์์ ๋ค๋ค์ง๋ง, ์๋ฌ ๋ก๊ทธ ์๋ฆผ์ ์ฐจ์์ด ๋ค๋ฅธ ๋ฌธ์ ์๋ค.
์ฐ์ ์๋ฌ ๋ก๊ทธ์ ์๋ฆผ ์์คํ ์ ๊ตฌ์ถํ๊ธฐ ์ ์ ํ์ฌ์ Grafana์์ ์๋ฌ ๋ก๊ทธ๋ฅผ ์ง์ ํ์ธํ๋ ๋ฐฉ์์ ๋ํ ๋ถํธํ ์ ์ ์ ๋ฆฌํด๋ณด์๋ค.
- ๋ถํ์ํ ์๋ฌ์ ์ค์ํ ์๋ฌ๋ฅผ ํ๋์ ๊ตฌ๋ถํ๊ธฐ ์ด๋ ต๋ค. (NoResourceException๊ณผ OutOfMemoryError๊ฐ ๊ฐ์ ๋ฌด๊ฒ๋ก ๋์ด๋๋ค)
- ์๋ฌ์ ์์ธ์ ํ์ ํ๋ ๋ฐ ๋ฆฌ์์ค๊ฐ ๋ ๋ค. ์์คํ ๋ฌธ์ ์ธ์ง, ์ธ๋ถ API ๋ฌธ์ ์ธ์ง, ํด๋ผ์ด์ธํธ ์์ฒญ ๋ฌธ์ ์ธ์ง ๋งค๋ฒ ์คํํธ๋ ์ด์ค๋ฅผ ๋ถ์ํด์ผ ํ๋ค.
- ์ค์๊ฐ ๋์์ด ๋ถ๊ฐ๋ฅํ๋ค. ๋ก๊ทธ๋ฅผ ๊ณ์ ์ณ๋ค๋ณด๊ณ ์์ง ์๋ ์ด์, ์๋ฌ ๋ฐ์ ์์ ์ ๋์น๊ธฐ ์ฝ๋ค.
- ์ค๋ณต ์๋ฌ๊ฐ ์์์ง๋ฉด ์ ์ ์ค์ํ ์๋ฌ๋ฅผ ๋์น๋ค. ๊ฐ์ NullPointerException์ด 500๋ฒ ๋ฐ์ํ๋ฉด ๊ทธ ์ฌ์ด์ ์์ธ ์๋ก์ด ์๋ฌ๋ฅผ ๋ฐ๊ฒฌํ๊ธฐ ์ด๋ ต๋ค.
ํ์ง๋ง ๊ทธ๋ ๋ค๊ณ ๋จ์ํ ์๋ฌ ๋ก๊ทธ๋ฅผ ๊ณง๋ฐ๋ก Slack์ผ๋ก ์ ์กํ๋ฉด? ์ด๋ ์๋ฆผ ํผ๋ก๋๊ฐ ๊ธ๊ฒฉํ ์ฌ๋ผ๊ฐ๋ค. ๋ ๊ฑฐ์ ์ฝ๋์์ ์์ธ ์ฒ๋ฆฌ๊ฐ ๋ฏธํกํ ๋ถ๋ถ์ด ๋ง์์ ์กฐ์น๊ฐ ํ์ ์๋ ์๋ฆผ์ด ์์์ง ๋ฟ๋ง ์๋๋ผ ๋จ์ ๋ด๋ค์ ํฌ๋กค๋ง์ผ๋ก ์ธํ NoResourceException๊ณผ ๊ฐ์ ๋ณ๋ค๋ฅธ ๋์์ด ํ์ํ์ง ์์ ์๋ฆผ๋ค์ด ๋ฌด์ํ ์์ด๊ฒ ๋๋ค. ์ด๋ ์์นซํ๋ฉด "์ ๋ง ํ์ํด์ ๋ฐ๋ก ๋ด์ผํ๋ ์กด์ฌ"๊ฐ ์๋ "๊ท์ฐฎ์ ์กด์ฌ, ์คํธ"์ผ๋ก ์ธ์๋ ์ ์๊ธฐ์ "์ ๋ง ํ์ํ ์๋ฌ ์๋ฆผ๋ง ์ ๋ํฌํ๊ฒ ๋ณด๋ด๋ ๊ฒ"์ ์ค์ฌ์ผ๋ก ์ค๊ณํ๊ณ ์ ํ์๋ค.
๊ฒฐ๋ก ๋ถํฐ ๋งํ์๋ฉด ํด๋น ์์คํ ์ ์๋ง์ ์ํ์ฐฉ์ค ๋์ ๋ค์๊ณผ ๊ฐ์ ๊ตฌ์กฐ๋ก ์ฑ๊ณต์ ์ผ๋ก ์ด์๋ ์ ์์๋ค.
์์คํ ์ํคํ ์ฒ

์๋ฌ ์ฒ๋ฆฌ ํ๋ก์ฐ

์ค์ ์ด์ ์ค์ธ ๋ชจ์ต



์ด์ ์ด ๊ณผ์ ์ ๋ํด ๊ธ์ ์์ฑํด๋ณด๊ณ ์ ํ๋ค.
๐ธ 1. Loki Alert Rule ์ค๊ณ
ํ์ฌ ์๋ฌ ๋ก๊ทธ๋ฅผ ๊ด๋ฆฌํ๋ ์ฃผ์ฒด๋ Loki์ด๋ค. ์ฐ๋ฆฌ๊ฐ ์ํ๋ "์ ๋ง ํ์ํ ์๋ฌ ์๋ฆผ"๋ง ๋ณด๋ด๋ ค๋ฉด, ๊ฒฐ๊ตญ Prometheus Alertmanager์์ ์ปค์คํ ํ๊ฒ ๊ตฌํํ Webhook ์๋ฒ๋ก ์๋ฆผ์ ์ ๋ฌํด์ผ ํ๋ค. ๊ทธ๋์ผ ์๋ฆผ์ ์ฐ๋ฆฌ ๋ง์๋๋ก ์ปจํธ๋กคํ ์ ์๋ค.
๊ทธ ์ ์ ์๋ฌ ๋ก๊ทธ๊ฐ Alertmanager๊น์ง ๋๋ฌํ ์ ์๋๋ก ์ค๊ณํด์ผ ํ๋ค. ์ด ์ญํ ์ ํ๋ ๊ฒ์ด ๋ฐ๋ก Loki Alert Rule์ด๋ค. Loki Ruler๊ฐ ์ ์๋ ๊ท์น์ ๋ฐ๋ผ ๋ก๊ทธ๋ฅผ ํ๊ฐํ๊ณ , ์กฐ๊ฑด์ ๋ง์กฑํ๋ฉด Alertmanager๋ก ์๋ฆผ์ ๋ฐ์กํ๋ค.
์ฌ๊ธฐ์ Loki Ruler์ Prometheus Alertmanager์ ๊ด๊ณ๋ฅผ ์ดํดํ ํ์๊ฐ ์๋ค. Loki๋ ๋ก๊ทธ ์์ง ๋ฐ ์ฟผ๋ฆฌ ์์คํ ์ด๊ณ , Loki Ruler๋ Loki์ ๋ด์ฅ๋ ์๋ฆผ ํ๊ฐ ์์ง์ด๋ค. Prometheus Alertmanager๋ ์๋ฆผ์ ๋ผ์ฐํ , ๊ทธ๋ฃนํ, ์ค๋ณต ์ ๊ฑฐ, ๋ฐ์ก์ ๋ด๋นํ๋ ๋ ๋ฆฝ์ ์ธ ์ปดํฌ๋ํธ์ด๋ค. Loki Ruler๊ฐ "์ด ์กฐ๊ฑด์ ํด๋นํ๋ ๋ก๊ทธ๊ฐ ๋ฐ์ํ๋ค"๊ณ ํ๋จํ๋ฉด, ๊ทธ ์๋ฆผ์ Alertmanager๋ก ์ ์กํ๋ค. Alertmanager๋ ์์ ํ ์๋ฆผ์ ์ค์ ๋ ๊ท์น์ ๋ฐ๋ผ Slack, Webhook, ์ด๋ฉ์ผ ๋ฑ์ผ๋ก ๋ผ์ฐํ ํ๋ค.
์ฆ, Loki Ruler๋ "์ธ์ ์๋ฆผ์ ๋ฐ์์ํฌ์ง"๋ฅผ ๊ฒฐ์ ํ๊ณ , Alertmanager๋ "๋ฐ์ํ ์๋ฆผ์ ์ด๋๋ก ๋ณด๋ผ์ง"๋ฅผ ๊ฒฐ์ ํ๋ค.
1.1 ์๋ฌ ๋ก๊ทธ ๊ฐ์ง ํ์ดํ๋ผ์ธ
์ ์ฒด ํ๋ฆ์ ๋ค์๊ณผ ๊ฐ์ด ์ค๊ณํ์๋ค.

- Backend ์๋น์ค์์ ๋ฐ์ํ ๋ก๊ทธ๋ Grafana Alloy(OTEL Collector)๋ฅผ ํตํด Loki๋ก ์์ง๋๋ค.
- Loki Ruler๊ฐ ์ ์๋ ๊ท์น์ ๋ฐ๋ผ ERROR ๋ ๋ฒจ ๋ก๊ทธ๋ฅผ ๊ฐ์งํ๋ค
- ์กฐ๊ฑด์ ๋ง์กฑํ๋ฉด Alertmanager๋ก ์๋ฆผ์ ์ ์กํ๋ค.
- Alertmanager๋ ์ค์ ๋ ๋ผ์ฐํ ๊ท์น์ ๋ฐ๋ผ Webhook ์๋ฒ๋ก ์๋ฆผ์ ์ ๋ฌํ๋ค.
1.2 Alert Rule ๊ตฌ์ฑ: service_name + exception_type ๊ทธ๋ฃนํ
Loki Alert Rule์ ์ค๊ณํ ๋ ๊ฐ์ฅ ์ค์ํ ๊ฒฐ์ ์ "์ด๋ค ๊ธฐ์ค์ผ๋ก ์๋ฆผ์ ๊ทธ๋ฃนํํ ๊ฒ์ธ๊ฐ"์๋ค.
apiVersion: v1
kind: ConfigMap
metadata:
name: loki-ruler-rules
namespace: devops
data:
rules.yaml: |
groups:
- name: application-errors
interval: 1m
rules:
- alert: ApplicationError
expr: |
sum by (service_name, exception_type) (
count_over_time(
{service_name=~"accounts|booking|catalog|payments|..."}
| detected_level="ERROR"
| json
| label_format exception_type=attributes_exception_type
| exception_type != ""
[2m]
)
) > 0
for: 0m
labels:
alert_type: log
severity: error
annotations:
summary: "์ ํ๋ฆฌ์ผ์ด์
์๋ฌ ๋ฐ์"
description: "{{ $labels.service_name }}์์ {{ $labels.exception_type }} ์๋ฌ ๊ฐ์ง"
์ด ๊ท์น์ด ์ด๋ป๊ฒ ๋์ํ๋์ง ๋จ๊ณ๋ณ๋ก ์ดํด๋ณด์.
- interval: 1m
์ด ๊ท์น์ 1๋ถ๋ง๋ค ํ๊ฐํ๋ค๋ ์๋ฏธ์ด๋ค. Loki Ruler๊ฐ 1๋ถ ๊ฐ๊ฒฉ์ผ๋ก ์๋ expr ์ฟผ๋ฆฌ๋ฅผ ์คํํ์ฌ ์กฐ๊ฑด์ ๋ง์กฑํ๋์ง ํ์ธํ๋ค. - {service_name=~"accounts|booking|..."}
๋ชจ๋ํฐ๋ง ๋์ ์๋น์ค๋ฅผ ํํฐ๋งํ๋ค. ์ ๊ทํํ์์ผ๋ก ์ฌ๋ฌ ์๋น์ค๋ฅผ ํ ๋ฒ์ ์ง์ ํ ์ ์๋ค. ์ฌ๊ธฐ์ ํฌํจ๋์ง ์์ ์๋น์ค์ ์๋ฌ ๋ก๊ทธ๋ ์ด ๊ท์น์ ํ๊ฐ ๋์์์ ์ ์ธ๋๋ค. - detected_level="ERROR"
ERROR ๋ ๋ฒจ ๋ก๊ทธ๋ง ์ ๋ณํ๋ค. Loki๊ฐ ๋ก๊ทธ ๋ ๋ฒจ์ ์๋์ผ๋ก ๊ฐ์งํ ๊ฒฐ๊ณผ๋ฅผ ํ์ฉํ๋ค. DEBUG, INFO, WARN ๋ ๋ฒจ์ ๋ก๊ทธ๋ ์ด ๋จ๊ณ์์ ํํฐ๋ง๋๋ค. - json | label_format exception_type=attributes_exception_type
SON ํ์์ ๋ก๊ทธ๋ฅผ ํ์ฑํ์ฌ exception_type ํ๋๋ฅผ ๋ผ๋ฒจ๋ก ์ถ์ถํ๋ค. ์๋ฅผ ๋ค์ด {"attributes": {"exception_type": "NullPointerException"}} ํํ์ ๋ก๊ทธ์์ NullPointerException์ ์ถ์ถํ์ฌ ๋ผ๋ฒจ๋ก ์ฌ์ฉํ ์ ์๊ฒ ๋ง๋ ๋ค. - exception_type != ""
๋ถ๋ถ์ exception_type์ด ์ถ์ถ๋์ง ์์ ๋ก๊ทธ๋ฅผ ์ ์ธํ๋ค. ์คํํธ๋ ์ด์ค ์์ด ๋จ์ ์๋ฌ ๋ฉ์์ง๋ง ์๋ ๋ก๊ทธ๋ ์ด ๊ท์น์ ๋์์์ ์ ์ธ๋๋ค. - count_over_time(...[2m]) > 0
์ต๊ทผ 2๋ถ ๋์ ์ ์กฐ๊ฑด์ ๋ง์กฑํ๋ ๋ก๊ทธ๊ฐ 1๊ฑด ์ด์ ์์ผ๋ฉด ์๋ฆผ์ ๋ฐ์์ํจ๋ค. - sum by (service_name, exception_type)
์ด ๋ถ๋ถ์ด ํต์ฌ์ด๋ค. ์ด ๊ทธ๋ฃนํ ๊ธฐ์ค์ ๋ฐ๋ผ ์๋ฆผ์ด ์ด๋ป๊ฒ ๋ฌถ์ด๋์ง ๊ฒฐ์ ๋๋ค. - for: 0m
์กฐ๊ฑด์ ๋ง์กฑํ๋ฉด ์ฆ์ ์๋ฆผ์ ๋ฐ์์ํจ๋ค๋ ์๋ฏธ์ด๋ค. ๋ง์ฝ for: 5m์ผ๋ก ์ค์ ํ๋ฉด ์กฐ๊ฑด์ด 5๋ถ ๋์ ์ง์๋์ด์ผ ์๋ฆผ์ด ๋ฐ์ํ๋ค. - labels์ ์ ์๋ alert_type: log
์ดํ Alertmanager์์ ๋ผ์ฐํ ๊ท์น์ ์ ์ฉํ ๋ ์ฌ์ฉ๋๋ค. ๋ฉํธ๋ฆญ ๊ธฐ๋ฐ ์๋ฆผ๊ณผ ๋ก๊ทธ ๊ธฐ๋ฐ ์๋ฆผ์ ๊ตฌ๋ถํ์ฌ ์๋ก ๋ค๋ฅธ Webhook ์๋ฒ๋ก ์ ์กํ ์ ์๋ค.
์ฌ๊ธฐ์ ์๋ฌธ์ด ์๊ธธ ๊ฒ์ด๋ค. ์ service_name๊ณผ exception_type ๋ ๊ฐ์ง๋ก ๊ทธ๋ฃนํํ๋๊ฐ?
๋ค์ ์ํฉ์ ๊ฐ์ ํด๋ณด์. 2๋ถ ๋์ ์๋์ ๊ฐ์ ์๋ฌ ๋ก๊ทธ๊ฐ ๋ฐ์ํ๋ค.
์๊ฐ ์๋น์ค ์์ธ ํ์ ๋ฉ์์ง
| 10:01 | payments | NullPointerException | user is null |
| 10:01 | payments | NullPointerException | order is null |
| 10:02 | payments | IllegalStateException | invalid state |
| 10:02 | accounts | NullPointerException | account is null |
sum by (service_name, exception_type) ๊ทธ๋ฃนํ๋ฅผ ์ ์ฉํ๋ฉด Alertmanager๋ 3๊ฐ์ ๋ ๋ฆฝ์ ์ธ ์๋ฆผ์ ์์ ํ๋ค.
- payments + NullPointerException ๊ทธ๋ฃน์๋ 2๊ฑด์ด ์ง๊ณ๋๋ค. user is null๊ณผ order is null์ ๊ฐ์ ๊ทธ๋ฃน์ผ๋ก ๋ฌถ์ธ๋ค.
- payments + IllegalStateException ๊ทธ๋ฃน์๋ 1๊ฑด์ด ์ง๊ณ๋๋ค.
- accounts + NullPointerException ๊ทธ๋ฃน์๋ 1๊ฑด์ด ์ง๊ณ๋๋ค.
๋ง์ฝ service_name๋ง์ผ๋ก ๊ทธ๋ฃนํํ๋ค๋ฉด payments ์๋น์ค์ ๋ชจ๋ ์๋ฌ๊ฐ ํ๋๋ก ๋ฌถ์ฌ์, NullPointerException๊ณผ IllegalStateException์ ๊ตฌ๋ถํ ์ ์์์ ๊ฒ์ด๋ค. ๋ฐ๋๋ก ๊ทธ๋ฃนํ ์์ด ๋ชจ๋ ์๋ฌ๋ฅผ ๊ฐ๋ณ ์๋ฆผ์ผ๋ก ๋ณด๋๋ค๋ฉด, ๊ฐ์ NullPointerException์ด 100๋ฒ ๋ฐ์ํ ๋ 100๊ฐ์ ์๋ฆผ์ด ์์์ก์ ๊ฒ์ด๋ค.
1.3 Alertmanager ๋ผ์ฐํ ์ค์
Loki Ruler์์ ๋ฐ์ํ ์๋ฆผ์ Alertmanager๋ฅผ ๊ฑฐ์ณ Webhook ์๋ฒ๋ก ์ ๋ฌ๋๋ค. Alertmanager๋ ์์ ํ ์๋ฆผ์ ์ด๋๋ก ๋ณด๋ผ์ง, ์ด๋ป๊ฒ ๊ทธ๋ฃนํํ ์ง, ์ผ๋ง๋ ์์ฃผ ๋ฐ๋ณตํ ์ง๋ฅผ ๊ฒฐ์ ํ๋ค.
alertmanager:
config:
route:
routes:
- receiver: 'log-alert'
matchers:
- alert_type = log
group_by: ['alertname', 'service_name', 'exception_type']
group_wait: 1s
group_interval: 30s
repeat_interval: 30m
receivers:
- name: 'log-alert'
webhook_configs:
- url: 'http://claude-log-alert-manager.devops.svc.cluster.local/webhook'
send_resolved: false
- matchers: alert_type = log
์กฐ๊ฑด์ผ๋ก ๋ก๊ทธ ๊ธฐ๋ฐ ์๋ฆผ๋ง ๋ณ๋ ๋ผ์ฐํ ํ๋ค. Loki Alert Rule์์ labels: alert_type: log๋ก ์ค์ ํ ์๋ฆผ๋ง ์ด ๋ผ์ฐํ ๊ท์น์ ํ๊ฒ ๋๋ค. ๋ฉํธ๋ฆญ ๊ธฐ๋ฐ ์๋ฆผ์ ๋ค๋ฅธ ๋ผ์ฐํ ๊ท์น์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋ค. - group_by: ['alertname', 'service_name', 'exception_type']
Alertmanager๊ฐ ์๋ฆผ์ ๊ทธ๋ฃนํํ๋ ๊ธฐ์ค์ด๋ค. ์ด ์ธ ๊ฐ์ง ๋ผ๋ฒจ์ด ๋ชจ๋ ๋์ผํ ์๋ฆผ๋ค์ ํ๋์ ๊ทธ๋ฃน์ผ๋ก ๋ฌถ์ฌ์ Webhook ์๋ฒ๋ก ์ ์ก๋๋ค. - group_wait: 1s
์๋ก์ด ๊ทธ๋ฃน์ ์๋ฆผ์ด ๋ฐ์ํ์ ๋ 1์ด ๋์ ๋๊ธฐํ๋ค๊ฐ ์ ์กํ๋ค๋ ์๋ฏธ์ด๋ค. ์ด ์งง์ ๋๊ธฐ ์๊ฐ ๋์ ๊ฐ์ ๊ทธ๋ฃน์ ์ํ๋ ๋ค๋ฅธ ์๋ฆผ์ด ์ถ๊ฐ๋ก ๋ค์ด์ค๋ฉด ํจ๊ป ๋ฌถ์ด์ ์ ์กํ๋ค. Loki๊ฐ HA ๊ตฌ์ฑ์ผ๋ก ์ด์๋ ๊ฒฝ์ฐ ๋์ผ ์๋ฆผ์ด ์ฌ๋ฌ Ruler์์ ์ค๋ณต ๋ฐ์ํ ์ ์๋๋ฐ, ์ด๋ฅผ 1์ด ๋ด์ ๋ณํฉํ๋ ํจ๊ณผ๊ฐ ์๋ค. - group_interval: 30s
์ด๋ฏธ ์๋ฆผ์ด ์ ์ก๋ ๊ทธ๋ฃน์ ์๋ก์ด ์๋ฆผ์ด ์ถ๊ฐ๋๋ฉด 30์ด ํ์ ์ ๋ฐ์ดํธ๋ฅผ ์ ์กํ๋ค๋ ์๋ฏธ์ด๋ค. - repeat_interval: 30m
ํด๊ฒฐ๋์ง ์์ ์๋ฌ๊ฐ ๊ณ์ ๋ฐ์ํ๋ฉด 30๋ถ๋ง๋ค ์๋ฆผ์ ๋ค์ ์ ์กํ๋ค๋ ์๋ฏธ์ด๋ค. ์ด ๊ฐ์ด ๋๋ฌด ์งง์ผ๋ฉด ๊ฐ์ ์๋ฆผ์ด ๋ฐ๋ณต์ ์ผ๋ก ์์์ง๊ณ , ๋๋ฌด ๊ธธ๋ฉด ์ค์ํ ์๋ฌ๋ฅผ ๋์น ์ ์๋ค. - send_resolved: false
์๋ฌ๊ฐ ํด๊ฒฐ๋์๋ค๋ ์๋ฆผ์ ์ ์กํ์ง ์๋๋ค๋ ์๋ฏธ์ด๋ค. ๋ฉํธ๋ฆญ ๊ธฐ๋ฐ ์๋ฆผ์์๋ "CPU ์ฌ์ฉ๋ฅ ์ด ์ ์์ผ๋ก ๋์์๋ค"๋ resolved ์๋ฆผ์ด ์๋ฏธ๊ฐ ์์ง๋ง, ๋ก๊ทธ ๊ธฐ๋ฐ ์๋ฆผ์์๋ "์๋ฌ ๋ก๊ทธ๊ฐ ๋ ์ด์ ๋ฐ์ํ์ง ์๋๋ค"๋ ๊ฒ์ ๊ตณ์ด ์๋ฆด ํ์๊ฐ ์๋ค.
์ด๋ ๊ฒ Loki Alert Rule๊ณผ Alertmanager๋ฅผ ๊ตฌ์ฑํจ์ผ๋ก์จ, ์๋จ์์ 1์ฐจ์ ์ธ ํํฐ๋ง๊ณผ ๊ทธ๋ฃนํ๊ฐ ์ด๋ฃจ์ด์ง๋ค. service_name + exception_type ๊ธฐ์ค์ผ๋ก ์๋ฆผ์ด ๋ฌถ์ด๊ธฐ ๋๋ฌธ์, ๋์ผํ ์์ธ๊ฐ 100๋ฒ ๋ฐ์ํด๋ Alertmanager๋ ์ด๋ฅผ ํ๋์ ๊ทธ๋ฃน์ผ๋ก ์ธ์ํ์ฌ Webhook ์๋ฒ์ ๋จ์ผ ์๋ฆผ์ผ๋ก ์ ๋ฌํ๋ค. repeat_interval ์ค์ ๋๋ถ์ ํด๊ฒฐ๋์ง ์์ ์๋ฌ๋ผ ํ๋๋ผ๋ 30๋ถ์ ํ ๋ฒ๋ง ์๋ฆผ์ด ๋ฐ๋ณต๋๋ค.
ํ์ง๋ง ์ด๊ฒ๋ง์ผ๋ก๋ ์ถฉ๋ถํ์ง ์๋ค. Alertmanager ์์ค์ ๊ทธ๋ฃนํ๋ service_name๊ณผ exception_type์ด๋ผ๋ ๋น๊ต์ ๋์ ๋ฒ์๋ก ๋ฌถ๋๋ค. ๊ฐ์ NullPointerException์ด๋ผ๋ ๋ฐ์ ์์น์ ์์ธ์ด ๋ค๋ฅผ ์ ์๊ณ , ์ด๋ฏธ ์๊ณ ์๋ ์๋ฌ์ ์๋ก์ด ์๋ฌ๋ฅผ ๊ตฌ๋ถํด์ผ ํ๋ค.
๐ธ 2. Fingerprint: ์๋ฌ ์๋ณ์ ์ฒซ ๋ฒ์งธ ์๋
Alertmanager๋ฅผ ํตํด ์๋ฌ ์๋ฆผ์ ์์ ํ๋ ๊ฒ๊น์ง๋ ํด๊ฒฐ๋์๋ค. ํ์ง๋ง ๊ฐ์ฅ ์ค์ํ ํ๋์ ๊ณผ์ ๊ฐ ๋จ์์๋ค.
๋ ๊ฑฐ์ ์ฝ๋์์ ์์ธ ์ฒ๋ฆฌ๊ฐ ๋ฏธํกํ ๋ถ๋ถ์ด ๋ง์์ ์กฐ์น๊ฐ ํ์ ์๋ ์๋ฆผ์ด ์์์ง ๋ฟ๋ง ์๋๋ผ ๋จ์ ๋ด๋ค์ ํฌ๋กค๋ง์ผ๋ก ์ธํ NoResourceException๊ณผ ๊ฐ์ ๋ณ๋ค๋ฅธ ๋์์ด ํ์ํ์ง ์์ ์๋ฆผ๋ค์ด ๋ฌด์ํ ์์ด๊ฒ ๋๋ค
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์๋ ๋ ๊ฐ์ง์ ๋ฐฉ์์ด ์ ์๋์๋ค.
- ์ฝ๋ ๋ ๋ฒจ์์ ์ปค์คํ Exception ํด๋์ค๋ฅผ ์ ์ํ๊ณ , ์๋ฌ ๋ก๊ทธ ์๋ฆผ์ด ํ์ํ ๋ถ๋ถ๋ง ํด๋น ํด๋์ค๋ก ๊ฐ์ธ๋ ๊ฒ์ด๋ค. ์๋ฆผ์ด ํ์ํ ์์ธ์ ๊ทธ๋ ์ง ์์ ์์ธ๋ฅผ ์ฝ๋์์ ๋ช ์์ ์ผ๋ก ๊ตฌ๋ถํ ์ ์๋ค.
- "๋ฌด์" ๋๋ "๋ฌด์" ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ ๊ฒ์ด๋ค. Slack์์ ํน์ ์๋ฌ ์๋ฆผ์ "๋ฌด์" ๋ฒํผ์ ๋๋ฅด๋ฉด, ์ดํ ๊ฐ์ ์๋ฌ์ ๋ํด์๋ ์๋ฆผ์ด ๋ฐ์ก๋์ง ์๋๋ก ํ๋ค.
์ฒซ ๋ฒ์งธ ๋ฐฉ์์ ํ์คํ์ง๋ง, ํ์ฌ ๋ฐฉ๋ํ๊ฒ ๊ตฌ์ถ๋ ๋ชจ๋ ํ๋ก์ ํธ์ ๋์ผํ๊ฒ ์ ์ฉํด์ผ ํ๋ค๋ ์ ์์ ๋ง์ ๋ฆฌ์์ค๊ฐ ํ์ํ๋ค. ๊ธฐ์กด ์ฝ๋๋ฅผ ๋ชจ๋ ์์ ํด์ผ ํ๊ณ , ์๋ก์ด ์ปจ๋ฒค์ ์ ๊ฐ๋ฐ์๋ค์๊ฒ ์ ํํด์ผ ํ๋ ์ปค๋ฎค๋์ผ์ด์ ๋น์ฉ๋ ์๋นํ๋ค.
๋ฐ๋ฉด ๋ ๋ฒ์งธ ๋ฐฉ์์ Slack Bolt๋ฅผ ์ด์ฉํ์ฌ ๋ฒํผ ์ธํฐ๋์ ๋ง ๊ตฌํํ๋ฉด ๋๋ค. "๋ฌด์" ๋ฒํผ์ ํตํด ์ ์ง์ ์ผ๋ก ๋ถํ์ํ ์๋ฌ ์๋ฆผ์ ์ค์ฌ๋๊ฐ ์ ์๊ธฐ ๋๋ฌธ์ ์ด๊ธฐ ๊ตฌ์ถ ๋น์ฉ์ด ํฌ์ง ์๋ค. ๋ฌผ๋ก ๊ฐ๋ฐ์๋ค์ด ์ ๊ทน์ ์ผ๋ก "๋ฌด์" ๋ฒํผ์ ๋๋ฌ์ค์ผ ์๊ฐ์ด ์ง๋จ์ ๋ฐ๋ผ ๋ถํ์ํ ์๋ฆผ์ด ์ค์ด๋๋ ๊ตฌ์กฐ์ด์ง๋ง, ์ด์ํ๋ฉด์ ์์ฐ์ค๋ฝ๊ฒ ํํฐ๋ง์ด ๊ฐํ๋๋ ์ฅ์ ์ด ์๋ค.
๋ ๋ฒ์งธ ๋ฐฉ์์ ์ฑํํ๊ธฐ๋ก ํ๋ค. ๊ทธ๋ฐ๋ฐ "๋ฌด์" ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ค๋ฉด ํ ๊ฐ์ง ์ ์ ์กฐ๊ฑด์ด ํ์ํ๋ค. ์๋ฌ๋ฅผ ๊ณ ์ ํ๊ฒ ์๋ณํ ์ ์๋ Uniqueํ ์๋ณ์๊ฐ ์์ด์ผ ํ๋ค. "์ด ์๋ฌ"๋ฅผ ๋ฌด์ํ๊ฒ ๋ค๊ณ ํ์ ๋, "์ด ์๋ฌ"๊ฐ ๋ฌด์์ธ์ง ์ ์ํ ์ ์์ด์ผ ํ๋ค. ๊ฐ์ ์๋ฌ๊ฐ ๋ค์ ๋ฐ์ํ์ ๋ ์ด์ ์ ๋ฌด์ ์ฒ๋ฆฌํ ์๋ฌ์ ๋์ผํ์ง ํ๋จํ ์ ์์ด์ผ ํ๋ค.
์ด๋ฅผ ์ํด ๊ฐ ์๋ฌ๋ฅผ ๊ณ ์ ํ๊ฒ ์๋ณํ ์ ์๋ Fingerprint ๊ฐ๋ ์ ๋์ ํ๋ค.
2.1 Fingerprint ๊ตฌ์ฑ ์์
Fingerprint๋ ๋ค์ 4๊ฐ์ง ์์๋ฅผ ์กฐํฉํ์ฌ SHA-256 ํด์๋ก ์์ฑํ๋ค.
๊ตฌ์ฑ์์ : service_name + exception_type + message_hash + frames
def generate_fingerprint(service_name: str, exception_type: str,
error_message: str, stacktrace: str) -> str:
frames = extract_top_frames(stacktrace, count=3)
message_hash = hashlib.sha256((error_message or "").encode()).hexdigest()[:16]
fingerprint_source = f"{service_name}:{exception_type}:{message_hash}:{':'.join(frames)}"
fingerprint = hashlib.sha256(fingerprint_source.encode()).hexdigest()[:32]
return fingerprint
- service_name
์๋ฌ๊ฐ ๋ฐ์ํ ์๋น์ค๋ฅผ ์๋ณํ๋ค. payments ์๋น์ค์์ ๋ฐ์ํ ์๋ฌ์ accounts ์๋น์ค์์ ๋ฐ์ํ ์๋ฌ๋ ์ค๋ น ๊ฐ์ ์์ธ ํ์ ์ด๋ผ๋ ๋ค๋ฅธ Fingerprint๋ฅผ ๊ฐ๋๋ค. - exception_type
NullPointerException, IllegalStateException ๋ฑ ์์ธ ํด๋์ค๋ฅผ ์๋ณํ๋ค. ๊ฐ์ ์๋น์ค์์ ๋ฐ์ํ ์๋ฌ๋ผ๋ ์์ธ ํ์ ์ด ๋ค๋ฅด๋ฉด ๋ค๋ฅธ Fingerprint๋ฅผ ๊ฐ๋๋ค. - message_hash
์๋ณธ ์๋ฌ ๋ฉ์์ง์ ํด์๊ฐ์ด๋ค. ์๋ฌ ๋ฉ์์ง ์ ์ฒด๋ฅผ ์ ์ฅํ๋ฉด ๋๋ฌด ๊ธธ์ด์ง๋ฏ๋ก SHA-256 ํด์์ ์ 16์๋ฆฌ๋ง ์ฌ์ฉํ๋ค. - frames
์คํํธ๋ ์ด์ค์ ์์ 3๊ฐ ํ๋ ์์ด๋ค. ์๋ฌ๊ฐ ๋ฐ์ํ ์ฝ๋ ์์น๋ฅผ ์๋ณํ๋ค. ๊ฐ์ ์์ธ ํ์ ์ด๋ผ๋ ๋ฐ์ ์์น๊ฐ ๋ค๋ฅด๋ฉด ๋ค๋ฅธ Fingerprint๋ฅผ ๊ฐ๋๋ค.
์ด ๋ค ๊ฐ์ง ์์๋ฅผ ์กฐํฉํ๋ฉด "์ด๋ค ์๋น์ค์์, ์ด๋ค ์์ธ๊ฐ, ์ด๋ค ๋ฉ์์ง์ ํจ๊ป, ์ด๋ค ์ฝ๋ ์์น์์ ๋ฐ์ํ๋๊ฐ"๋ฅผ ๊ณ ์ ํ๊ฒ ์๋ณํ ์ ์๋ค.
2.2 ์คํํธ๋ ์ด์ค์์ ๋ผ์ธ๋ฒํธ๋ฅผ ์ ์ธํ ์ด์
์คํํธ๋ ์ด์ค์์ ํ๋ ์์ ์ถ์ถํ ๋, ๋ผ์ธ๋ฒํธ๋ ์๋์ ์ผ๋ก ์ ์ธํ๋ค.
def extract_top_frames(stacktrace: str, count: int = 3) -> list:
"""Extract top N frames from stacktrace (without line numbers)"""
if not stacktrace:
return []
frame_pattern = r'at\s+([\w.$]+)\(([\w]+\.java)' # ๋ผ์ธ๋ฒํธ ์ ์ธ
matches = re.findall(frame_pattern, stacktrace)
frames = []
for match in matches[:count]:
method, file = match
frames.append(f"{method}:{file}")
return frames
์ด๊ธฐ์๋ ๋ผ์ธ๋ฒํธ๋ฅผ ํฌํจํ์๋ค. ๊ทธ๋ฐ๋ฐ ์ด์ํ๋ค ๋ณด๋ ๋ฌธ์ ๊ฐ ์๊ฒผ๋ค. ์ฝ๋์ ์ฃผ์ ํ ์ค๋ง ์ถ๊ฐํด๋, ๋ฆฌํฉํ ๋ง์ผ๋ก ๋ฉ์๋ ์์๋ฅผ ๋ฐ๊ฟ๋ ๋ผ์ธ๋ฒํธ๊ฐ ๋ณ๊ฒฝ๋๋ค. ๊ทธ๋ฌ๋ฉด ์์ ํ ๋์ผํ ์๋ฌ์ธ๋ฐ๋ ๋ค๋ฅธ Fingerprint๋ก ์ธ์๋์ด ์๋ก์ด ์๋ฆผ์ด ๋ฐ์ก๋์๋ค.
๋ผ์ธ๋ฒํธ๋ฅผ ์ ์ธํ๊ณ ํด๋์ค๋ช + ๋ฉ์๋๋ช + ํ์ผ๋ช ์กฐํฉ์ผ๋ก ํ๋ ์์ ๊ตฌ์ฑํ๋, ์ฝ๋ ๋ณ๊ฒฝ์ ์ํฅ๋ฐ์ง ์์ผ๋ฉด์๋ ์๋ฌ ์์น๋ฅผ ์ถฉ๋ถํ ์๋ณํ ์ ์์๋ค.
2.3 Fingerprint ๊ธฐ๋ฐ ์ค๋ณต ์๋ฆผ ๋ฐฉ์ง
Fingerprint๋ฅผ ๋์ ํ๋ฉด์ ๋ค์๊ณผ ๊ฐ์ ๋ก์ง์ ๊ตฌํํ ์ ์์๋ค.

๋์ผ Fingerprint๋ฅผ ๊ฐ์ง ์๋ฌ๊ฐ 1์๊ฐ ๋ด์ ๋ค์ ๋ฐ์ํ๋ฉด ์๋ฆผ์ ์คํตํ๊ณ ๋ฐ์ ํ์๋ง ์ฆ๊ฐ์ํจ๋ค. ์ฌ์ฉ์๊ฐ ํน์ ์๋ฌ๋ฅผ "๋ฌด์"๋ก ์ค์ ํ๋ฉด ํด๋น Fingerprint์ ์๋ฌ๋ ๋ ์ด์ ์๋ฆผ์ ๋ณด๋ด์ง ์๋๋ค. "๋ฌด์"์ผ๋ก ์ค์ ํ๋ฉด ์ง์ ๋ ์๊ฐ ๋์ ์๋ฆผ์ ์ค๋จํ๋ค.
2.4 ํ๊ณ: ๋์ผ ์์ธ์ธ๋ฐ ๋ฉ์์ง๋ง ๋ค๋ฅธ ๊ฒฝ์ฐ
ํ์ง๋ง ์์ ๊ฐ์ด Fingerprint ์์คํ ์ ์ด์ํ๋ค๋ณด๋ ๋ค์๊ณผ ๊ฐ์ ์์์น ๋ชปํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์๋ค.
payments ์๋น์ค์์ UserNotFoundException์ด ๋ฐ์ํ๋๋ฐ, ๋ฉ์์ง๊ฐ ๋งค๋ฒ ๋ฌ๋๋ค.
UserNotFoundException: User not found: userId=12345
UserNotFoundException: User not found: userId=67890
UserNotFoundException: User not found: userId=11111
์ธ ์๋ฌ ๋ชจ๋ ๊ฐ์ ์ฝ๋ ์์น์์, ๊ฐ์ ์ด์ ๋ก ๋ฐ์ํ ๋์ผํ ์๋ฌ์ด๋ค. ํ์ง๋ง userId ๊ฐ์ด ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ message_hash๊ฐ ๋ฌ๋ผ์ง๊ณ , ๊ฒฐ๊ณผ์ ์ผ๋ก ์๋ก ๋ค๋ฅธ Fingerprint๊ฐ ์์ฑ๋์๋ค.
ํ๋์ ์๋ฌ๋ฅผ "๋ฌด์"๋ก ์ค์ ํด๋ ๋ค์ ์์ฒญ์์ ๋ค๋ฅธ userId๋ก ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ์๋ก์ด ์๋ฆผ์ด ์ธ๋ ธ๋ค. AI ๋ถ์ ๊ฒฐ๊ณผ๋ ์บ์๋์ง ์์์ ๋งค๋ฒ ์๋ก ๋ถ์์ ์์ฒญํ๊ณ , ํ ํฐ ์๋น๊ฐ ๊ธ๊ฒฉํ ๋์ด๋ฌ๋ค. (AI ๋ถ์์ ๋ํด์๋ 2ํธ์์ ๋ค๋ฃฐ ์์ ์ด๋ค.)
๐ธ 3. Chunk ID: ์ ์ฌ ์๋ฌ ๊ทธ๋ฃนํ
3.1 ๋ฌธ์ ์ํฉ ๋ถ์
Fingerprint๋ง์ผ๋ก๋ ํด๊ฒฐํ ์ ์๋ ๋ฌธ์ ๋ฅผ ์ ๋ฆฌํด๋ณด์.
[Fingerprint ์์ธ ํ์ ๋ฉ์์ง ๋ฐ์ ์์น]
| abc123 | UserNotFoundException | userId=12345 | UserService.getUser |
| def456 | UserNotFoundException | userId=67890 | UserService.getUser |
| ghi789 | UserNotFoundException | userId=11111 | UserService.getUser |
์ธ ์๋ฌ๋ ๋ ผ๋ฆฌ์ ์ผ๋ก ์์ ํ ๋์ผํ ์๋ฌ์ด๋ค. ๊ฐ์ ์๋น์ค, ๊ฐ์ ์์ธ ํ์ , ๊ฐ์ ์ฝ๋ ์์น์์ ๋ฐ์ํ๋ค. ๋จ์ง ์์ฒญ ํ๋ผ๋ฏธํฐ์ธ userId๋ง ๋ค๋ฅผ ๋ฟ์ด๋ค. ์ด๋ฐ "์ ์ฌ ์๋ฌ"๋ฅผ ํ๋์ ๊ทธ๋ฃน์ผ๋ก ๋ฌถ์ด์ ๊ด๋ฆฌํ ์ ์๋ ์์ ๊ฐ๋ ์ด ํ์ํ๋ค. ๊ทธ๋์ Chunk ID๋ฅผ ๋์ ํ๋ค.
3.2 Chunk ID ์ค๊ณ: ์ฒซ ๋ฒ์งธ ์๋
์ฒ์์๋ ๋ฉ์์ง๋ฅผ ์ ์ธํ๊ณ ๋๋จธ์ง ์์๋ง์ผ๋ก Chunk ID๋ฅผ ๊ตฌ์ฑํ๋ค.
chunk_id = hash(service_name + exception_type + frame1 + frame2 + frame3)
์ด๋ ๊ฒ ํ๋ฉด userId=12345์ userId=67890 ์๋ฌ๊ฐ ๊ฐ์ Chunk๋ก ๋ฌถ์ธ๋ค. "๋ฌด์" ์ค์ ์ Chunk ๋จ์๋ก ์ ์ฉํ๋ฉด ํ ๋ฒ์ ์ก์ ์ผ๋ก ๋ชจ๋ ์ ์ฌ ์๋ฌ๋ฅผ ๋ฌด์ํ ์ ์๋ค. AI ๋ถ์ ๊ฒฐ๊ณผ๋ Chunk ๋จ์๋ก ์บ์ํ๋ฉด ํ ํฐ์ ์ ์ฝํ ์ ์๋ค.
๊ทธ๋ฐ๋ฐ ์ด ์ค๊ณ์๋ ๋ฌธ์ ๊ฐ ์์๋ค.
3.3 ๋ ๋ค๋ฅธ ๋ฌธ์ : ๊ฐ์ ์์น์์ ๋ค๋ฅธ ์๋ฌ
๊ฐ์ ๋ฉ์๋์์ ์ฌ๋ฌ ์ข ๋ฅ์ ์๋ฌ๊ฐ ๋ฐ์ํ ์ ์๋ค.
fun getUser(userId: String?): User {
requireNotNull(userId) { "userId is required" }
val user = userRepository.findById(userId)
?: throw UserNotFoundException("User not found: $userId")
if (user.isDeleted) {
throw UserNotFoundException("User is deleted: $userId")
}
return user
}
์ธ ๊ฐ์ง ๋ค๋ฅธ ์๋ฌ๊ฐ ๋ชจ๋ ๊ฐ์ UserService.getUser()์์ ๋ฐ์ํ๋ค. ๋ฉ์์ง๋ฅผ ์์ ํ ์ ์ธํ๋ฉด ์ด ์ธ ์๋ฌ๊ฐ ๊ฐ์ Chunk๋ก ๋ฌถ์ฌ๋ฒ๋ฆฐ๋ค.
"User not found"๋ฅผ ๋ฌด์ํ๋ฉด "User is deleted"๊น์ง ํจ๊ป ๋ฌด์๋๋ ์ํฉ์ด ๋ฐ์ํ๋ค. ์ด๊ฑด ์๋ํ ๋์์ด ์๋๋ค.
3.4 ๋ฉ์์ง ์ ๊ทํ: g-z๋ง ์ ์งํ๋ ์ด์
๋ฉ์์ง๋ฅผ ์์ ํ ์ ์ธํ๋ฉด ๋ค๋ฅธ ์๋ฌ๊ฐ ๊ฐ์ด ๋ฌถ์ด๊ณ , ๋ฉ์์ง๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ๋ฉด ID ๊ฐ ๋๋ฌธ์ ๊ฐ์ ์๋ฌ๊ฐ ๋ถ๋ฆฌ๋๋ค. ํด๊ฒฐ์ฑ ์ ๋ฉ์์ง์์ "๋ณ๋๋๋ ๋ถ๋ถ"๋ง ์ ๊ฑฐํ๋ ๊ฒ์ด์๋ค. ๋ณ๋๋๋ ๋ถ๋ถ์ ๋๋ถ๋ถ ID ๊ฐ์ด๋ค. ๊ทธ๋ฆฌ๊ณ ID ๊ฐ์ ๋ณดํต ๋ค์ ํํ ์ค ํ๋์ด๋ค.
- ์ซ์: userId=12345, orderId=67890
- UUID: 550e8400-e29b-41d4-a716-446655440000
- 16์ง์(hex): 0x1A2B3C, transactionId=a1b2c3d4
์ซ์๋ 0-9, UUID์ hex๋ 0-9์ a-f๋ฅผ ์ฌ์ฉํ๋ค. ๊ทธ๋ ๋ค๋ฉด ์ด๊ฒ๋ค์ ์ ๊ฑฐํ๊ณ ๋๋จธ์ง ๋ฌธ์๋ง ๋จ๊ธฐ๋ฉด ๋๋ค.
def normalize_message_for_chunk(message: str) -> str:
"""Normalize message for chunk ID (keep only g-z letters)"""
if not message:
return ""
# g-z๋ง ์ ์ง (์ซ์ 0-9, hex ๋ฌธ์ a-f ์ ๊ฑฐ)
msg = re.sub(r'[^g-zG-Z\s]', '', message)
msg = re.sub(r'\s+', ' ', msg)
return msg.strip()
์ํ๋ฒณ ์ค a-f๋ hex ๊ฐ์ ํฌํจ๋๋ฏ๋ก ์ ๊ฑฐํ๊ณ , g-z๋ง ๋จ๊ธด๋ค. ๊ฒฐ๊ณผ์ ์ผ๋ก "์๋ฏธ ์๋ ์์ด ๋จ์ด์ ํต์ฌ ๊ธ์"๋ง ๋จ๊ฒ ๋๋ค.
์ค์ ์ ๊ทํ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์.
"User not found: userId=12345" → "Usr not oun"
"User not found: userId=67890" → "Usr not oun" (๊ฐ์ ๊ฒฐ๊ณผ!)
"User is deleted: userId=12345" → "Usr is lt" (๋ค๋ฅธ ๊ฒฐ๊ณผ!)
๊ฐ์ ์๋ฌ ๋ฉ์์ง๋ ๊ฐ์ ์ ๊ทํ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ๊ณ , ๋ค๋ฅธ ์๋ฌ ๋ฉ์์ง๋ ๋ค๋ฅธ ์ ๊ทํ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ๋๋ค. UUID๊ฐ ํฌํจ๋ ๊ฒฝ์ฐ๋ ์ ์ฒ๋ฆฌ๋๋ค.
"Order 550e8400-e29b-41d4-a716-446655440000 failed" → "Orr il"
"Order a1b2c3d4-e5f6-7890-abcd-ef1234567890 failed" → "Orr il" (๊ฐ์ ๊ฒฐ๊ณผ!)
์ ๊ทํ๋ ๊ฒฐ๊ณผ๋ ์ฌ๋์ด ์ฝ๊ธฐ ์ด๋ ต์ง๋ง, ํด์ ํค๋ก๋ง ์ฌ์ฉํ๋ฏ๋ก ๊ฐ๋ ์ฑ์ ์ค์ํ์ง ์๋ค.
3.5 ์ต์ข ์ค๊ณ: Fingerprint vs Chunk ID
def generate_fingerprint(service_name: str, exception_type: str,
error_message: str, stacktrace: str) -> tuple:
frames = extract_top_frames(stacktrace, count=3)
# Chunk ID: ์ ๊ทํ๋ ๋ฉ์์ง ์ฌ์ฉ
normalized_message = normalize_message_for_chunk(error_message or "")
normalized_hash = hashlib.sha256(normalized_message.encode()).hexdigest()[:16]
chunk_source = f"{service_name}:{exception_type}:{normalized_hash}:{':'.join(frames)}"
chunk_id = hashlib.sha256(chunk_source.encode()).hexdigest()[:32]
# Fingerprint: ์๋ณธ ๋ฉ์์ง ์ฌ์ฉ
message_hash = hashlib.sha256((error_message or "").encode()).hexdigest()[:16]
fingerprint_source = f"{service_name}:{exception_type}:{message_hash}:{':'.join(frames)}"
fingerprint = hashlib.sha256(fingerprint_source.encode()).hexdigest()[:32]
return (fingerprint, chunk_id)
| Fingerprint | ์๋น์ค + ์์ธํ์ + ์๋ณธ ๋ฉ์์ง ํด์ + ํ๋ ์ | ๊ฐ๋ณ ์๋ฌ ์ผ์ด์ค ์๋ณ, 1์๊ฐ ๋ด ์ค๋ณต ์๋ฆผ ๋ฐฉ์ง |
| Chunk ID | ์๋น์ค + ์์ธํ์ + ์ ๊ทํ ๋ฉ์์ง ํด์ + ํ๋ ์ | ์ ์ฌ ์๋ฌ ๊ทธ๋ฃนํ, ์ผ๊ด ๋ฌด์/๋ฌด์, AI ์บ์ ๊ณต์ |
๋ ID์ ๊ด๊ณ๋ฅผ ๊ทธ๋ฆผ์ผ๋ก ํํํ๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.

ํ๋์ Chunk ID ์๋์ ์ฌ๋ฌ Fingerprint๊ฐ ์ํ๋ค. AI ๋ถ์ ๊ฒฐ๊ณผ๋ Chunk ๋จ์๋ก ์บ์๋์ด ์ ์ฌ ์๋ฌ ๊ฐ์ ๊ณต์ ๋๋ค. ๋ฌด์/๋ฌด์ ์ค์ ๋ Chunk ๋จ์๋ก ์ ์ฉ๋์ด ํ ๋ฒ์ ์ก์ ์ผ๋ก ๋ชจ๋ ์ ์ฌ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ ์ ์๋ค.
3.6 ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง
์ด ์ค๊ณ๋ฅผ ๋ฐ์ํ ํ ์ด๋ธ ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ๋ค.

error_patterns ํ ์ด๋ธ์ ๊ฐ๋ณ ์๋ฌ ์ผ์ด์ค๋ฅผ ์ ์ฅํ๋ค. fingerprint๊ฐ ๊ณ ์ ํค์ด๋ค.
chunk_status ํ ์ด๋ธ์ Chunk ๋จ์์ ๋ฌด์/๋ฌด์/ํด๊ฒฐ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ค. ์ด ํ ์ด๋ธ์ ๋ ์ฝ๋๊ฐ ์์ผ๋ฉด ํด๋น Chunk์ ๋ชจ๋ ์๋ฌ์ ์ํ๊ฐ ์ ์ฉ๋๋ค.
chunk_ai_cache ํ ์ด๋ธ์ Chunk ๋จ์๋ก AI ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ์บ์ํ๋ค. ๊ฐ์ Chunk์ ์ํ ์๋ฌ๋ค์ ์ด ์บ์๋ฅผ ๊ณต์ ํ๋ค.
๐ช ์ ๋ฆฌ
Loki Alert Rule์์ service_name + exception_type์ผ๋ก ๊ทธ๋ฃนํํ์ฌ ์๋ฌ ๋ก๊ทธ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ๊ฐ์งํ๋ค. ์ด ์กฐํฉ์ด Alertmanager๋ก ์ ๋ฌ๋๋ ์๋ฆผ์ ๋จ์๋ฅผ ๊ฒฐ์ ํ๋ค.
Fingerprint๋ ๊ฐ๋ณ ์๋ฌ ์ผ์ด์ค๋ฅผ ๊ณ ์ ํ๊ฒ ์๋ณํ๋ค. ์๋น์ค๋ช , ์์ธ ํ์ , ์๋ณธ ๋ฉ์์ง ํด์, ์คํํธ๋ ์ด์ค ํ๋ ์์ผ๋ก ๊ตฌ์ฑ๋๋ค. 1์๊ฐ ๋ด ๋์ผ Fingerprint์ ์๋ฌ๋ ์๋ฆผ์ ์คํตํ๋ค.
Chunk ID๋ ์ ์ฌ ์๋ฌ๋ฅผ ํ๋์ ๊ทธ๋ฃน์ผ๋ก ๋ฌถ๋๋ค. ๋ฉ์์ง์์ ID ๊ฐ(์ซ์, UUID, hex)์ ์ ๊ฑฐํ ์ ๊ทํ๋ ํด์๋ฅผ ์ฌ์ฉํ๋ค. g-z ๋ฌธ์๋ง ๋จ๊ธฐ๋ ์ ๊ทํ ์ ๋ต์ผ๋ก ๋ณ๋ ๊ฐ์ ํจ๊ณผ์ ์ผ๋ก ํํฐ๋งํ๋ค.
Chunk ๋จ์๋ก ๋ฌด์/๋ฌด์ ์ค์ ๊ณผ AI ์บ์๋ฅผ ์ ์ฉํจ์ผ๋ก์จ, ์๋ฆผ ํผ๋ก๋๋ฅผ ์ค์ด๊ณ AI ํ ํฐ ์๋น๋ฅผ 90% ์ด์ ์ ๊ฐํ ์ ์์๋ค.
2ํธ์์๋ 4-Tier ์๋ฌ ๋ถ๋ฅ ์ฒด๊ณ, Claude Headless๋ฅผ ํ์ฉํ AI ๋ถ์ ํ์ดํ๋ผ์ธ, ๊ทธ๋ฆฌ๊ณ Slack ์ธํฐ๋์ ์ค๊ณ๋ฅผ ๋ค๋ฃฌ๋ค.
๐ ์ถ์ฒ
- Loki Ruler ๊ณต์ ๋ฌธ์: https://grafana.com/docs/loki/latest/alert/
- Alertmanager Configuration: https://prometheus.io/docs/alerting/latest/configuration/
- LogQL ์ฟผ๋ฆฌ ๋ฌธ๋ฒ: https://grafana.com/docs/loki/latest/query/