""" Scouter APM 연동 API 라우터 엔드포인트: GET /api/scouter/status — Scouter 연결 상태 + 전체 요약 GET /api/scouter/servers — 모니터링 대상 서버 목록 GET /api/scouter/servers/{hash}/metrics — 특정 서버 실시간 메트릭 GET /api/scouter/servers/{hash}/services — 활성 서비스 목록 GET /api/scouter/servers/{hash}/xlog — 최근 트랜잭션 X-Log GET /api/scouter/alerts — 경보 목록 POST /api/scouter/agent/deploy — Tomcat 에이전트 SSH 배포 (ADMIN/ENGINEER) """ from __future__ import annotations import logging import os from typing import Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user from database import get_db from models import User, UserRole logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/scouter", tags=["scouter"]) # ── 스키마 ─────────────────────────────────────────────────────────────────── class AgentDeployRequest(BaseModel): server_id: int # CMDB 서버 ID scouter_host: Optional[str] = None # Scouter 서버 호스트 (기본: 환경변수) scouter_port: int = 6100 # Scouter 수집 포트 (기본: 6100) # ── 엔드포인트 ─────────────────────────────────────────────────────────────── @router.get("/status") async def scouter_status( current_user: User = Depends(get_current_user), ): """Scouter 연결 상태 및 전체 모니터링 현황 요약.""" from core.scouter import get_summary, is_available available = await is_available() summary = await get_summary() return { "connected": available, "host": os.getenv("SCOUTER_HOST", ""), "port": os.getenv("SCOUTER_HTTP_PORT", "6180"), **summary, } @router.get("/servers") async def list_servers( current_user: User = Depends(get_current_user), ): """모니터링 대상 객체(서버/서비스) 목록.""" from core.scouter import get_object_list return {"servers": await get_object_list()} @router.get("/servers/{obj_hash}/metrics") async def server_metrics( obj_hash: int, current_user: User = Depends(get_current_user), ): """특정 서버 실시간 메트릭 (CPU, Heap, TPS, 응답시간).""" from core.scouter import get_server_metrics metrics = await get_server_metrics(obj_hash) if not metrics: raise HTTPException(404, "서버 메트릭을 가져올 수 없습니다.") return {"obj_hash": obj_hash, "metrics": metrics} @router.get("/servers/{obj_hash}/services") async def active_services( obj_hash: int, current_user: User = Depends(get_current_user), ): """활성 서비스 목록 (현재 처리 중인 요청).""" from core.scouter import get_active_services return {"services": await get_active_services(obj_hash)} @router.get("/servers/{obj_hash}/xlog") async def xlog( obj_hash: int, limit: int = 20, current_user: User = Depends(get_current_user), ): """최근 트랜잭션 X-Log 조회.""" from core.scouter import get_xlog_recent return {"xlogs": await get_xlog_recent(obj_hash, limit)} @router.get("/alerts") async def alerts( obj_hash: Optional[int] = None, current_user: User = Depends(get_current_user), ): """Scouter 경보 목록.""" from core.scouter import get_alert_list return {"alerts": await get_alert_list(obj_hash)} @router.post("/agent/deploy") async def deploy_agent( body: AgentDeployRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Tomcat 서버에 Scouter 에이전트 SSH 배포 (ADMIN/ENGINEER 전용). 배포 절차: 1. CMDB에서 서버 SSH 접속 정보 조회 (AES-256-GCM 복호화) 2. scouter-agent.jar 및 설정 파일 SFTP 전송 3. Tomcat JAVA_OPTS에 -javaagent 옵션 추가 4. Tomcat 재기동 (선택) """ if current_user.role not in (UserRole.ADMIN, UserRole.ENGINEER): raise HTTPException(403, "ADMIN 또는 ENGINEER만 에이전트를 배포할 수 있습니다.") from models import Server from sqlalchemy import select srv = await db.get(Server, body.server_id) if not srv: raise HTTPException(404, f"서버 ID {body.server_id}를 찾을 수 없습니다.") scouter_host = body.scouter_host or os.getenv("SCOUTER_HOST", "localhost") scouter_port = body.scouter_port try: result = await _deploy_agent_ssh(srv, scouter_host, scouter_port, current_user.username) return { "message": f"Scouter 에이전트 배포 완료: {srv.server_name}", "server": srv.server_name, "scouter": f"{scouter_host}:{scouter_port}", "deployed_at": result.get("deployed_at"), "restart_required": True, } except Exception as e: raise HTTPException(500, f"에이전트 배포 실패: {str(e)[:200]}") async def _deploy_agent_ssh(srv, scouter_host: str, scouter_port: int, actor: str) -> dict: """SSH/SFTP로 Scouter 에이전트 파일 배포.""" from datetime import datetime from core.ssh_exec import execute_ssh_command # GUARDIA_ROOT/setup/scouter/ 에서 에이전트 파일 확인 agent_local = _find_agent_jar() if not agent_local: raise ValueError("scouter-agent.jar를 찾을 수 없습니다. setup/scouter/에 파일을 배치하세요.") # Scouter 에이전트 설정 파일 생성 (임시) conf_content = f""" net.collector.ip={scouter_host} net.collector.udp_port={scouter_port} net.collector.tcp_port={scouter_port} obj_name={srv.server_name} trace_interservice_enabled=true hook_method_patterns=io.guardia.*,com.agency.* """ # 원격 배포 디렉토리 remote_dir = "/app/scouter" # SSH로 디렉토리 생성 result = await execute_ssh_command( srv.id, f"mkdir -p {remote_dir} && echo OK", timeout=30, ) if not result.success: raise ValueError(f"원격 디렉토리 생성 실패: {result.error}") # 설정 파일 생성 await execute_ssh_command( srv.id, f"cat > {remote_dir}/agent.conf << 'SCCONF'\n{conf_content}\nSCCONF", timeout=30, ) # JAVA_OPTS 에 javaagent 옵션 추가 (Tomcat setenv.sh) javaagent_opts = ( f'-javaagent:{remote_dir}/scouter-agent.jar ' f'-Dscouter.config={remote_dir}/agent.conf' ) setenv_cmd = f""" SETENV=/app/tomcat/bin/setenv.sh if [ -f "$SETENV" ]; then grep -q 'scouter' "$SETENV" || echo 'export JAVA_OPTS="$JAVA_OPTS {javaagent_opts}"' >> "$SETENV" else echo '#!/bin/bash' > "$SETENV" echo 'export JAVA_OPTS="$JAVA_OPTS {javaagent_opts}"' >> "$SETENV" chmod +x "$SETENV" fi echo "setenv.sh 업데이트 완료" """ await execute_ssh_command(srv.id, setenv_cmd, timeout=30) return {"deployed_at": datetime.utcnow().isoformat()} def _find_agent_jar() -> str: """Scouter 에이전트 JAR 파일 탐색.""" import pathlib candidates = [ pathlib.Path(__file__).parent.parent.parent / "setup" / "scouter" / "scouter-agent.jar", pathlib.Path("/opt/scouter/agent/scouter-agent.jar"), pathlib.Path("/app/scouter/scouter-agent.jar"), ] for p in candidates: if p.exists(): return str(p) return ""