guardia-itsm/test_e4_metrics.py
DESKTOP-TKLFCPRython 64c27c3509 feat(itsm): G-1~G-12 확장 기능 + 하네스/봇/설치스크립트 구현
G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현
G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건)
G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST)
G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직
G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트
G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트
G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API
G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트
G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트
G-10: core/push_notify.py + routers/push.py + PushSubscription 모델
G-11: approvals 다중승인 (위임/서명/기한초과/마감연장)
G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh

하네스: guardia-orchestrator 확장기능 Phase 반영
봇명령어: /sr /status /license /bulk 슬래시 명령어 추가
설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:18:52 +09:00

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)