- FastAPI + WebSocket 백엔드 (ws_relay, webhook, messages 라우터) - GUARDiA-Bot: @bot 명령 응답 + 선제적 맥락 분석 (DB 지연, 디스크, 장애 감지) - 승인 워크플로우: 재기동/배포 SR → 승인/반려 인터랙티브 버튼 - 다크 테마 Slack형 프론트엔드 (5개 채널, 실시간 메시지) - 채널: 일반/배포/운영/PM관리/알림 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
179 lines
6.6 KiB
Python
179 lines
6.6 KiB
Python
from datetime import datetime
|
|
from uuid import uuid4
|
|
from typing import Optional
|
|
|
|
# In-memory message store (room_id -> list of message dicts)
|
|
message_store: dict[str, list] = {}
|
|
|
|
ROOMS: dict[str, str] = {
|
|
"general": "일반",
|
|
"deploy": "배포",
|
|
"ops": "운영",
|
|
"pm": "PM관리",
|
|
"alerts": "알림",
|
|
}
|
|
|
|
CONTEXT_RULES = {
|
|
"db_delay": {
|
|
"keywords": ["DB", "디비", "쿼리", "느리", "타임아웃", "락", "lock", "slow"],
|
|
"action": "FETCH_DB_LOCKS",
|
|
"message": "DB 지연 징후가 감지되었습니다. 대기 쿼리를 조회할까요?",
|
|
},
|
|
"disk_full": {
|
|
"keywords": ["용량", "디스크", "로그", "쌓였", "full", "disk", "no space"],
|
|
"action": "CHECK_DISK_SPACE",
|
|
"message": "디스크 공간 관련 문제가 감지되었습니다. 용량을 확인할까요?",
|
|
},
|
|
"service_down": {
|
|
"keywords": ["다운", "죽었", "응답없", "timeout", "503", "502", "500", "오류", "장애"],
|
|
"action": "CHECK_SERVICE_STATUS",
|
|
"message": "서비스 장애 징후가 감지되었습니다. 서비스 상태를 확인할까요?",
|
|
},
|
|
"ssl_expire": {
|
|
"keywords": ["ssl", "인증서", "만료", "expire", "https"],
|
|
"action": "CHECK_SSL_CERTS",
|
|
"message": "SSL 인증서 관련 문의가 감지되었습니다. 만료일을 확인할까요?",
|
|
},
|
|
}
|
|
|
|
|
|
def _new_sr() -> str:
|
|
return f"SR-{datetime.now().strftime('%Y%m%d')}-{str(uuid4())[:6].upper()}"
|
|
|
|
|
|
def get_bot_response(text: str) -> Optional[dict]:
|
|
"""@bot 멘션 또는 guardia 키워드 → 명령 응답 생성."""
|
|
t = text.lower()
|
|
if "@bot" not in t and "guardia" not in t:
|
|
return None
|
|
|
|
if "도움" in text or "help" in t or "명령" in text:
|
|
return {
|
|
"content": (
|
|
"**GUARDiA Bot** 사용 가능한 명령어:\n"
|
|
"• `@bot 배포` — 배포 SR 생성\n"
|
|
"• `@bot 재기동` — WAS 재기동 요청 (PM 승인 필요)\n"
|
|
"• `@bot 로그 확인` — 에러 로그 분석\n"
|
|
"• `@bot 서버 상태` — 인프라 현황 조회\n"
|
|
"• `@bot SSL 확인` — 인증서 만료일 조회\n"
|
|
"• `@bot 정지` — 진행 중 작업 즉시 중단"
|
|
),
|
|
"is_widget": False,
|
|
}
|
|
|
|
if "배포" in text or "deploy" in t:
|
|
sr_id = _new_sr()
|
|
return {
|
|
"content": (
|
|
f"배포 SR이 생성되었습니다: `{sr_id}`\n"
|
|
"배포 파일을 첨부하고 대상 서버(기관명/시스템명)를 지정해주세요."
|
|
),
|
|
"is_widget": True,
|
|
"interactive_action": {
|
|
"type": "BUTTON",
|
|
"label": "배포 취소",
|
|
"command_code": f"CANCEL_SR:{sr_id}",
|
|
},
|
|
}
|
|
|
|
if "재기동" in text or "restart" in t or "리스타트" in text:
|
|
sr_id = _new_sr()
|
|
return {
|
|
"content": (
|
|
f"WAS 재기동 요청 `{sr_id}`이 접수되었습니다.\n"
|
|
"PM 승인 후 롤링 방식으로 실행됩니다."
|
|
),
|
|
"is_widget": True,
|
|
"interactive_action": {
|
|
"type": "APPROVAL_BUTTONS",
|
|
"approve_url": f"/api/sr/approve/{sr_id}",
|
|
"reject_url": f"/api/sr/reject/{sr_id}",
|
|
},
|
|
}
|
|
|
|
if "로그" in text or "log" in t:
|
|
return {
|
|
"content": (
|
|
"로그 분석 결과 (모의 데이터):\n"
|
|
"```\n"
|
|
"[ERROR] 2026-05-24 17:32:11 - ORA-01555: snapshot too old\n"
|
|
"[WARN] 2026-05-24 17:33:05 - Connection pool exhausted (200/200)\n"
|
|
"[ERROR] 2026-05-24 17:34:22 - java.lang.OutOfMemoryError: GC overhead\n"
|
|
"```\n"
|
|
"OutOfMemoryError 감지 — WAS 재기동을 권장합니다."
|
|
),
|
|
"is_widget": True,
|
|
"interactive_action": {
|
|
"type": "BUTTON",
|
|
"label": "WAS 재기동 요청",
|
|
"command_code": "REQUEST_RESTART",
|
|
},
|
|
}
|
|
|
|
if "서버 상태" in text or "상태" in text or "status" in t:
|
|
return {
|
|
"content": (
|
|
"**인프라 상태 요약** (모의 데이터)\n"
|
|
"• WEB-01 ✅ 정상 CPU 12% MEM 45%\n"
|
|
"• WAS-01 ✅ 정상 CPU 34% MEM 62%\n"
|
|
"• WAS-02 ⚠️ 경고 CPU 87% MEM 78%\n"
|
|
"• DB-01 ✅ 정상 연결 42/200\n"
|
|
"• DB-02 ✅ 정상 연결 11/200"
|
|
),
|
|
"is_widget": False,
|
|
}
|
|
|
|
if "ssl" in t or "인증서" in text:
|
|
return {
|
|
"content": (
|
|
"**SSL 인증서 만료일 조회** (모의 데이터)\n"
|
|
"• portal.mof.go.kr — 잔여 **D-14** ⚠️\n"
|
|
"• api.molit.go.kr — 잔여 D-87 ✅\n"
|
|
"• www.nts.go.kr — 잔여 D-203 ✅"
|
|
),
|
|
"is_widget": True,
|
|
"interactive_action": {
|
|
"type": "BUTTON",
|
|
"label": "갱신 SR 생성",
|
|
"command_code": "RENEW_SSL:portal.mof.go.kr",
|
|
},
|
|
}
|
|
|
|
if "정지" in text or "stop" in t or "중단" in text:
|
|
return {
|
|
"content": "⛔ Kill-Switch 실행 — 진행 중인 모든 작업을 중단합니다.",
|
|
"is_widget": True,
|
|
"interactive_action": {
|
|
"type": "APPROVAL_BUTTONS",
|
|
"approve_url": "/api/killswitch/confirm",
|
|
"reject_url": "/api/killswitch/cancel",
|
|
},
|
|
}
|
|
|
|
return {
|
|
"content": f"명령을 처리하고 있습니다: \"{text}\"\n자세한 명령은 `@bot 도움`을 입력하세요.",
|
|
"is_widget": False,
|
|
}
|
|
|
|
|
|
def check_proactive_context(text: str) -> Optional[dict]:
|
|
"""명시적 봇 멘션 없이도 대화 맥락에서 선제적 제안 생성."""
|
|
for rule in CONTEXT_RULES.values():
|
|
if any(kw in text for kw in rule["keywords"]):
|
|
return {
|
|
"content": f"{rule['message']}",
|
|
"is_widget": True,
|
|
"interactive_action": {
|
|
"type": "BUTTON",
|
|
"label": "즉시 확인",
|
|
"command_code": rule["action"],
|
|
},
|
|
}
|
|
return None
|
|
|
|
|
|
def store_message(room_id: str, msg: dict) -> None:
|
|
message_store.setdefault(room_id, []).append(msg)
|
|
if len(message_store[room_id]) > 200:
|
|
message_store[room_id] = message_store[room_id][-200:]
|