147 lines
4.7 KiB
Python
147 lines
4.7 KiB
Python
"""자동 RCA 분석 — Ollama LLM을 활용한 근본 원인 초안 생성."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
RCA_PROMPT = """당신은 IT 인프라 전문가입니다. 다음 장애 정보를 분석하여 RCA(근본 원인 분석)를 JSON으로 제공하세요.
|
|
|
|
장애 정보:
|
|
{incident_info}
|
|
|
|
변경 이력:
|
|
{change_history}
|
|
|
|
다음 필드를 포함하는 JSON만 출력하세요 (한국어):
|
|
{{
|
|
"root_cause": "추정 근본 원인",
|
|
"contributing_factors": ["기여 요인 1", "기여 요인 2"],
|
|
"timeline": "발생부터 발견까지 타임라인",
|
|
"prevention": ["재발 방지 조치 1", "재발 방지 조치 2"],
|
|
"confidence": 0.8
|
|
}}"""
|
|
|
|
|
|
async def analyze_rca(incident_id: int, db) -> dict:
|
|
"""장애 ID로 RCA 초안 자동 생성."""
|
|
from models import Incident
|
|
from sqlalchemy import select, text
|
|
|
|
incident = await db.get(Incident, incident_id)
|
|
if not incident:
|
|
raise ValueError(f"장애 ID {incident_id}를 찾을 수 없습니다.")
|
|
|
|
incident_info = (
|
|
f"장애번호: {incident.incident_id}\n"
|
|
f"제목: {incident.title}\n"
|
|
f"등급: {incident.grade}\n"
|
|
f"설명: {incident.description or '없음'}\n"
|
|
f"영향 서비스: {incident.affected_service or '없음'}\n"
|
|
f"발생시각: {incident.occurred_at}\n"
|
|
f"복구시각: {incident.resolved_at or '미복구'}"
|
|
)
|
|
|
|
# 최근 변경 이력 조회 (CI 변경 로그)
|
|
try:
|
|
from models import CIChangeLog
|
|
logs = (await db.execute(
|
|
select(CIChangeLog)
|
|
.order_by(CIChangeLog.changed_at.desc())
|
|
.limit(5)
|
|
)).scalars().all()
|
|
change_history = "\n".join(
|
|
f"- [{l.changed_at}] {l.change_type}: {l.summary or ''}"
|
|
for l in logs
|
|
) or "최근 변경 이력 없음"
|
|
except Exception:
|
|
change_history = "변경 이력 조회 불가"
|
|
|
|
prompt = RCA_PROMPT.format(
|
|
incident_info=incident_info,
|
|
change_history=change_history,
|
|
)
|
|
|
|
try:
|
|
from core.llm_client import get_llm_client
|
|
client = get_llm_client()
|
|
resp = await client.chat(prompt)
|
|
raw = resp.content.strip()
|
|
# JSON 블록 추출
|
|
if "```" in raw:
|
|
raw = raw.split("```")[1]
|
|
if raw.startswith("json"):
|
|
raw = raw[4:]
|
|
rca = json.loads(raw)
|
|
except Exception as e:
|
|
logger.warning("LLM RCA 분석 실패 — 기본 템플릿 사용: %s", e)
|
|
rca = {
|
|
"root_cause": f"자동 분석 실패 — 수동 분석 필요. ({str(e)[:100]})",
|
|
"contributing_factors": ["분석 데이터 부족"],
|
|
"timeline": incident_info,
|
|
"prevention": ["장애 재발 방지 조치를 수동으로 기록하세요."],
|
|
"confidence": 0.0,
|
|
}
|
|
|
|
return {
|
|
"incident_id": incident.incident_id,
|
|
"rca": rca,
|
|
"auto_generated": True,
|
|
"generated_at": datetime.utcnow().isoformat(),
|
|
}
|
|
|
|
|
|
async def analyze_problem_rca(problem_id: int, db) -> dict:
|
|
"""Problem 레코드 ID로 RCA 초안 자동 생성."""
|
|
from models import ProblemRecord
|
|
from sqlalchemy import select
|
|
|
|
prb = await db.get(ProblemRecord, problem_id)
|
|
if not prb:
|
|
raise ValueError(f"Problem ID {problem_id}를 찾을 수 없습니다.")
|
|
|
|
incident_info = (
|
|
f"문제번호: {prb.problem_id}\n"
|
|
f"제목: {prb.title}\n"
|
|
f"설명: {prb.description or '없음'}\n"
|
|
f"카테고리: {prb.category}\n"
|
|
f"영향 사용자: {prb.affected_users}명\n"
|
|
f"관련 인시던트 수: {prb.incident_count}건\n"
|
|
f"누적 다운타임: {prb.total_downtime_min}분"
|
|
)
|
|
|
|
prompt = RCA_PROMPT.format(
|
|
incident_info=incident_info,
|
|
change_history="문제 레코드 기반 분석",
|
|
)
|
|
|
|
try:
|
|
from core.llm_client import get_llm_client
|
|
client = get_llm_client()
|
|
resp = await client.chat(prompt)
|
|
raw = resp.content.strip()
|
|
if "```" in raw:
|
|
raw = raw.split("```")[1]
|
|
if raw.startswith("json"):
|
|
raw = raw[4:]
|
|
rca = json.loads(raw)
|
|
except Exception as e:
|
|
logger.warning("LLM Problem RCA 분석 실패: %s", e)
|
|
rca = {
|
|
"root_cause": "자동 분석 실패 — 수동 분석 필요.",
|
|
"contributing_factors": [],
|
|
"timeline": incident_info,
|
|
"prevention": [],
|
|
"confidence": 0.0,
|
|
}
|
|
|
|
return {
|
|
"problem_id": prb.problem_id,
|
|
"rca": rca,
|
|
"auto_generated": True,
|
|
"generated_at": datetime.utcnow().isoformat(),
|
|
}
|