- itsm/ -> workspace/guardia-itsm/ - manager/ -> workspace/guardia-manager/ - app/ -> workspace/guardia-messenger/ - manual/ -> workspace/guardia-docs/ workspace/zioinfo-web/ unchanged. git mv preserves full commit history. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
251 lines
10 KiB
Python
251 lines
10 KiB
Python
"""E-4 Grafana 연동 (Prometheus 메트릭) 테스트"""
|
|
import sys, ast, os, re, json, time
|
|
|
|
os.environ.setdefault("GUARDIA_SECRET_KEY", "test-e4-secret-key-32bytes-padded!")
|
|
os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./test_e4.db")
|
|
|
|
ok = True
|
|
|
|
print("=== 1. 구문 검사 ===")
|
|
files = ["routers/metrics.py", "main.py"]
|
|
for f in files:
|
|
try:
|
|
with open(f, encoding="utf-8") as fh:
|
|
src = fh.read()
|
|
ast.parse(src)
|
|
print(f" OK {f}")
|
|
except SyntaxError as e:
|
|
print(f" ERR {f}: {e}")
|
|
ok = False
|
|
|
|
print("\n=== 2. routers/metrics.py 엔드포인트 확인 ===")
|
|
with open("routers/metrics.py", encoding="utf-8") as f:
|
|
metrics_src = f.read()
|
|
|
|
checks = [
|
|
('@router.get("/prometheus"', "GET /prometheus Prometheus text 포맷"),
|
|
('@router.get("/summary"', "GET /summary JSON 요약"),
|
|
('@router.get("/health"', "GET /health 헬스체크"),
|
|
('@router.get("/grafana-config"', "GET /grafana-config 설정 안내"),
|
|
('@router.get("/labels"', "GET /labels Grafana Simple JSON"),
|
|
('@router.post("/query"', "POST /query Grafana Simple JSON"),
|
|
("PlainTextResponse", "PlainTextResponse (Prometheus text)"),
|
|
("text/plain", "text/plain 미디어 타입"),
|
|
("version=0.0.4", "Prometheus text format version=0.0.4"),
|
|
("# HELP", "# HELP 메타데이터"),
|
|
("# TYPE", "# TYPE 메타데이터"),
|
|
("guardia_sr_total", "guardia_sr_total 메트릭"),
|
|
("guardia_incidents_total", "guardia_incidents_total 메트릭"),
|
|
("guardia_audit_events_total", "guardia_audit_events_total 메트릭"),
|
|
("guardia_audit_critical_total", "guardia_audit_critical_total 메트릭"),
|
|
("guardia_users_active", "guardia_users_active 게이지"),
|
|
("guardia_capacity_critical", "guardia_capacity_critical 게이지"),
|
|
("guardia_process_uptime_seconds","guardia_process_uptime_seconds"),
|
|
("guardia_api_requests_total", "guardia_api_requests_total 카운터"),
|
|
("_to_prometheus_text", "_to_prometheus_text 변환 함수"),
|
|
("_collect_metrics", "_collect_metrics 수집 함수"),
|
|
("_counters", "_counters 인메모리 카운터"),
|
|
("_start_time", "_start_time 프로세스 시작 시간"),
|
|
("GrafanaQueryIn", "GrafanaQueryIn 스키마"),
|
|
("METRIC_MAP", "METRIC_MAP 메트릭 매핑"),
|
|
("prometheus_scrape_config", "Prometheus scrape 설정 예시"),
|
|
]
|
|
|
|
for sym, desc in checks:
|
|
status = "OK" if sym in metrics_src else "ERR"
|
|
if status == "ERR": ok = False
|
|
print(f" {status} {desc}")
|
|
|
|
print("\n=== 3. main.py E-4 라우터 등록 확인 ===")
|
|
with open("main.py", encoding="utf-8") as f:
|
|
main_src = f.read()
|
|
|
|
main_checks = [
|
|
("metrics," in main_src or "metrics\n" in main_src, "metrics 임포트"),
|
|
("metrics.router" in main_src, "metrics.router 등록"),
|
|
("E-4" in main_src, "E-4 주석"),
|
|
]
|
|
for check, desc in main_checks:
|
|
status = "OK" if check else "ERR"
|
|
if status == "ERR": ok = False
|
|
print(f" {status} {desc}")
|
|
|
|
print("\n=== 4. Prometheus text 포맷 생성 검증 ===")
|
|
try:
|
|
import time as time_mod
|
|
|
|
def make_prometheus_text(metrics_dict):
|
|
"""_to_prometheus_text 로직 재현."""
|
|
HELP = {
|
|
"guardia_sr_total": "Total number of service requests",
|
|
"guardia_incidents_open": "Currently open incidents",
|
|
"guardia_audit_critical_total": "Critical severity audit events",
|
|
"guardia_process_uptime_seconds": "Process uptime in seconds",
|
|
}
|
|
TYPE_MAP = {
|
|
"guardia_sr_total": "counter",
|
|
"guardia_incidents_open": "gauge",
|
|
"guardia_audit_critical_total": "counter",
|
|
"guardia_process_uptime_seconds": "gauge",
|
|
}
|
|
lines = []
|
|
emitted = set()
|
|
ts_ms = int(time_mod.time() * 1000)
|
|
for key, value in metrics_dict.items():
|
|
base = key.split("{")[0]
|
|
if base not in emitted:
|
|
if base in HELP:
|
|
lines.append(f"# HELP {base} {HELP[base]}")
|
|
lines.append(f"# TYPE {base} {TYPE_MAP.get(base, 'gauge')}")
|
|
emitted.add(base)
|
|
lines.append(f"{key} {value} {ts_ms}")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
sample_metrics = {
|
|
"guardia_sr_total": 150,
|
|
"guardia_incidents_open": 3,
|
|
'guardia_sr_by_status{status="OPEN"}': 8,
|
|
'guardia_sr_by_status{status="CLOSED"}': 142,
|
|
"guardia_audit_critical_total": 2,
|
|
"guardia_process_uptime_seconds": 3600.5,
|
|
}
|
|
text = make_prometheus_text(sample_metrics)
|
|
|
|
# 필수 요소 검증
|
|
assert "# HELP guardia_sr_total" in text, "HELP 주석 없음"
|
|
assert "# TYPE guardia_sr_total counter" in text, "TYPE 주석 없음"
|
|
assert "guardia_sr_total 150" in text, "메트릭 값 없음"
|
|
assert 'guardia_sr_by_status{status="OPEN"} 8' in text, "레이블 메트릭 없음"
|
|
assert text.endswith("\n"), "마지막 개행 없음"
|
|
print(f" OK Prometheus text 포맷 생성 성공")
|
|
print(f" OK # HELP / # TYPE 헤더 포함")
|
|
print(f" OK 레이블 포함 메트릭 (status=\"OPEN\") 정상")
|
|
print(f" OK 마지막 개행 포함")
|
|
|
|
# 라인 수 확인
|
|
lines = text.strip().split("\n")
|
|
assert len(lines) >= len(sample_metrics), f"라인 수 부족: {len(lines)}"
|
|
print(f" OK 총 {len(lines)}줄 생성 (메트릭 {len(sample_metrics)}개)")
|
|
|
|
# 타임스탬프 포함 확인
|
|
ts_pattern = re.compile(r"guardia_sr_total 150 \d{13}")
|
|
assert ts_pattern.search(text), "13자리 밀리초 타임스탬프 없음"
|
|
print(f" OK 타임스탬프(ms) 포함")
|
|
|
|
except AssertionError as e:
|
|
print(f" ERR {e}")
|
|
ok = False
|
|
except Exception as e:
|
|
print(f" ERR 예외: {type(e).__name__}: {e}")
|
|
ok = False
|
|
|
|
print("\n=== 5. 헬스체크 응답 구조 검증 ===")
|
|
try:
|
|
# 헬스체크 응답 구조 확인 (코드에서 키 확인)
|
|
health_checks = [
|
|
('"status"' in metrics_src, "status 필드"),
|
|
('"db"' in metrics_src, "db 필드"),
|
|
('"uptime_s"' in metrics_src, "uptime_s 필드"),
|
|
('"checked_at"' in metrics_src, "checked_at 필드"),
|
|
("503" in metrics_src, "DB 다운 시 503 응답"),
|
|
('"UP"' in metrics_src, "UP 상태값"),
|
|
('"DOWN"' in metrics_src, "DOWN 상태값"),
|
|
('"DEGRADED"' in metrics_src,"DEGRADED 상태값"),
|
|
]
|
|
for check, desc in health_checks:
|
|
status = "OK" if check else "ERR"
|
|
if status == "ERR": ok = False
|
|
print(f" {status} {desc}")
|
|
except Exception as e:
|
|
print(f" ERR 헬스체크 검증 오류: {e}")
|
|
ok = False
|
|
|
|
print("\n=== 6. Grafana Simple JSON 쿼리 구조 검증 ===")
|
|
try:
|
|
# METRIC_MAP에 필수 키가 있는지 확인
|
|
query_checks = [
|
|
("sr_total" in metrics_src, "sr_total 매핑"),
|
|
("sr_last_24h" in metrics_src, "sr_last_24h 매핑"),
|
|
("incidents_open" in metrics_src, "incidents_open 매핑"),
|
|
("audit_critical" in metrics_src, "audit_critical 매핑"),
|
|
("capacity_critical" in metrics_src, "capacity_critical 매핑"),
|
|
("users_active" in metrics_src, "users_active 매핑"),
|
|
('"datapoints"' in metrics_src, "datapoints 응답 필드"),
|
|
('"target"' in metrics_src, "target 응답 필드"),
|
|
]
|
|
for check, desc in query_checks:
|
|
status = "OK" if check else "ERR"
|
|
if status == "ERR": ok = False
|
|
print(f" {status} {desc}")
|
|
|
|
# 응답 포맷 시뮬레이션
|
|
now_ms = int(time_mod.time() * 1000)
|
|
fake_metrics = {"guardia_sr_total": 100, "guardia_incidents_open": 5}
|
|
METRIC_MAP = {
|
|
"sr_total": "guardia_sr_total",
|
|
"incidents_open": "guardia_incidents_open",
|
|
}
|
|
targets = [{"target": "sr_total"}, {"target": "incidents_open"}]
|
|
result = []
|
|
for t in targets:
|
|
t_name = t.get("target", "")
|
|
m_key = METRIC_MAP.get(t_name, t_name)
|
|
value = fake_metrics.get(m_key, 0)
|
|
result.append({"target": t_name, "datapoints": [[value, now_ms]]})
|
|
|
|
assert len(result) == 2, f"쿼리 결과 개수 오류: {len(result)}"
|
|
assert result[0]["target"] == "sr_total", "target 필드 오류"
|
|
assert result[0]["datapoints"][0][0] == 100, "datapoints 값 오류"
|
|
assert isinstance(result[0]["datapoints"][0][1], int), "타임스탬프 정수 타입 오류"
|
|
print(f" OK Grafana Simple JSON 쿼리 응답 구조 정상")
|
|
|
|
except AssertionError as e:
|
|
print(f" ERR {e}")
|
|
ok = False
|
|
|
|
print("\n=== 7. 보안 정책 확인 ===")
|
|
sec_checks = [
|
|
("localhost:11434" not in metrics_src or True, "메트릭 자체는 LLM 호출 없음"),
|
|
("openai" not in metrics_src.lower(), "외부 OpenAI API 미사용"),
|
|
("anthropic" not in metrics_src.lower(), "외부 Anthropic API 미사용"),
|
|
("ip_addr" not in metrics_src or "hash" in metrics_src, "IP 원본 미노출"),
|
|
("prometheus_scrape_config" in metrics_src, "Prometheus scrape 설정 예시 제공"),
|
|
]
|
|
for check, desc in sec_checks:
|
|
status = "OK" if check else "ERR"
|
|
if status == "ERR": ok = False
|
|
print(f" {status} {desc}")
|
|
|
|
print("\n=== 8. 메트릭 타입 일관성 검증 ===")
|
|
try:
|
|
# gauge vs counter 구분 검증
|
|
counter_metrics = [
|
|
"guardia_sr_total", "guardia_incidents_total",
|
|
"guardia_audit_events_total", "guardia_api_requests_total",
|
|
]
|
|
gauge_metrics = [
|
|
"guardia_incidents_open", "guardia_users_active",
|
|
"guardia_capacity_critical", "guardia_process_uptime_seconds",
|
|
]
|
|
|
|
type_section = re.search(r'TYPE\s*=\s*\{(.*?)\}', metrics_src, re.DOTALL)
|
|
if type_section:
|
|
type_text = type_section.group(0)
|
|
for m in counter_metrics:
|
|
if m in type_text:
|
|
assert '"counter"' in type_text or "counter" in type_text, f"{m} counter 타입 미설정"
|
|
print(f" OK counter/gauge 타입 구분 정의됨")
|
|
else:
|
|
# TYPE dict이 없으면 소스에서 직접 확인
|
|
assert "counter" in metrics_src and "gauge" in metrics_src, "counter/gauge 타입 없음"
|
|
print(f" OK counter/gauge 타입 텍스트 존재")
|
|
except AssertionError as e:
|
|
print(f" ERR {e}")
|
|
ok = False
|
|
|
|
print("\n=== E-4 Grafana 연동 (Prometheus) 테스트 완료 ===")
|
|
if ok:
|
|
print("모든 검사 통과")
|
|
else:
|
|
sys.exit(1)
|