From d3c275451572431f5c00c066683b897da8229e7a Mon Sep 17 00:00:00 2001 From: "DESKTOP-TKLFCPR\\ython" Date: Fri, 29 May 2026 19:45:52 +0900 Subject: [PATCH] =?UTF-8?q?feat(monitoring):=20Scouter=20APM=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EB=AA=A8=EB=8B=88=ED=84=B0=EB=A7=81=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Scouter APM 연동] - core/scouter.py: Scouter HTTP API 클라이언트 - get_summary(): 전체 WAS 모니터링 현황 (CPU/TPS/응답시간/위험서버) - get_server_metrics(): 특정 서버 실시간 메트릭 - get_alert_list(): Scouter 경보 목록 - get_xlog_recent(): 최근 트랜잭션 X-Log - routers/scouter.py: REST API 엔드포인트 (6개) - GET /api/scouter/status, /servers, /servers/{hash}/metrics - GET /api/scouter/servers/{hash}/services, /xlog, /alerts - POST /api/scouter/agent/deploy: SSH로 scouter-agent.jar 자동 배포 [스케줄러] - scheduler.py: Scouter 경보 수집 (5분마다) - CPU > 80% 또는 에러율 > 5% 서버 자동 감지 → GUARDiA 알림 [Docker Compose] - docker-compose.yml: scouteross/scouter-server:2.20.0 서비스 추가 - 포트 6100 (UDP/TCP 에이전트 수집) + 6180 (HTTP API) [설치 스크립트] - setup/scouter/download_scouter.sh: 에이전트/서버 다운로드 - scouter-agent.jar + agent.conf.template 생성 - setup_ubuntu.sh: Scouter 서버 설치 단계 추가 (14단계로 확장) - --test 검증: Scouter API + Gitea HTTP 검사 추가 환경변수: SCOUTER_HOST, SCOUTER_HTTP_PORT=6180, SCOUTER_USER, SCOUTER_PASSWORD Co-Authored-By: Claude Sonnet 4.6 --- docker-compose.yml | 25 ++++ itsm/core/scheduler.py | 29 ++++ itsm/core/scouter.py | 190 +++++++++++++++++++++++++ itsm/main.py | 4 + itsm/routers/scouter.py | 222 ++++++++++++++++++++++++++++++ setup/scouter/download_scouter.sh | 103 ++++++++++++++ setup/setup_ubuntu.sh | 118 +++++++++++++--- 7 files changed, 673 insertions(+), 18 deletions(-) create mode 100644 itsm/core/scouter.py create mode 100644 itsm/routers/scouter.py create mode 100644 setup/scouter/download_scouter.sh diff --git a/docker-compose.yml b/docker-compose.yml index 4b2ee681..d3c88754 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -179,6 +179,30 @@ services: retries: 3 start_period: 30s + # ── Scouter Server (Java WAS APM 모니터링 서버) ───────────── + # Scouter: Tomcat/JBoss/JEUs 성능 모니터링 (CPU/Heap/TPS/응답시간) + # Agent는 대상 WAS 서버에 setup/scouter/scouter-agent.jar 로 배포 + scouter-server: + image: scouteross/scouter-server:2.20.0 + container_name: guardia-scouter + ports: + - "6100:6100/udp" # 에이전트 데이터 수신 (UDP) + - "6100:6100/tcp" # 에이전트 TCP + - "6180:6180" # HTTP API + volumes: + - guardia-scouter-data:/home/scouter-server/database + environment: + SCOUTER_HTTP_PORT: "6180" + networks: + - guardia-net + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:6180/scouter/v1/info/version"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + # ── Qdrant (전용 벡터 DB — 고성능 시맨틱 검색) ─────────── # pgvector보다 빠른 ANN 검색이 필요할 때 사용 # QDRANT_ENABLED=true 시 guardia에서 QDRANT_URL=http://qdrant:6333 설정 @@ -249,6 +273,7 @@ volumes: guardia-qdrant: # Qdrant 벡터 데이터 guardia-gitea-data: # Gitea 저장소 + DB guardia-gitea-config: # Gitea 설정 + guardia-scouter-data: # Scouter 성능 데이터 # ── 네트워크 ────────────────────────────────────────────── networks: diff --git a/itsm/core/scheduler.py b/itsm/core/scheduler.py index ed1ac594..dccdeed3 100644 --- a/itsm/core/scheduler.py +++ b/itsm/core/scheduler.py @@ -622,6 +622,35 @@ def start_scheduler() -> None: except Exception as exc: logger.warning("SLA 스케줄 등록 실패 (무시): %s", exc) + # ── Scouter APM 알람 수집 (5분마다) ───────────────────────── + try: + async def _scouter_alert_check(): + """Scouter 경보 목록 조회 → GUARDiA 이상 탐지 연동.""" + from core.scouter import get_summary, get_alert_list + summary = await get_summary() + if not summary.get("enabled"): + return + # CPU > 80% 또는 에러율 위험 서버 알림 + critical = summary.get("critical_servers", []) + if critical: + await _push_ops_notify( + f"[Scouter 경보] {len(critical)}개 서버 위험", + f"CPU 80% 초과 또는 에러율 5% 초과:\n" + "\n".join(f" - {s}" for s in critical[:5]) + ) + + _scheduler.add_job( + _scouter_alert_check, + "interval", + minutes=5, + id="scouter_alert_check", + name="Scouter APM 경보 수집 (5분)", + replace_existing=True, + misfire_grace_time=60, + ) + logger.info("Scouter APM 경보 수집 스케줄 등록 완료") + except Exception as exc: + logger.warning("Scouter 스케줄 등록 실패 (무시): %s", exc) + # ── G-3: 라이선스 만료 알림 (매일 09:00 KST) ───────────────────────────── try: _scheduler.add_job( diff --git a/itsm/core/scouter.py b/itsm/core/scouter.py new file mode 100644 index 00000000..0e2b8d30 --- /dev/null +++ b/itsm/core/scouter.py @@ -0,0 +1,190 @@ +""" +Scouter APM 연동 — GUARDiA ITSM + +Scouter는 Java WAS(Tomcat/JBoss/JEUs) 전문 APM 도구입니다. +Scouter HTTP API를 통해 실시간 메트릭을 수집하여 GUARDiA 대시보드에 표시합니다. + +환경변수: + SCOUTER_HOST : Scouter 서버 호스트 (기본: localhost) + SCOUTER_HTTP_PORT : Scouter HTTP API 포트 (기본: 6180) + SCOUTER_USER : Scouter 사용자 (기본: admin) + SCOUTER_PASSWORD : Scouter 비밀번호 (기본: admin) + +Scouter HTTP API 문서: https://github.com/scouter-project/scouter/wiki/Scouter-HTTP-API +""" +from __future__ import annotations + +import logging +import os +from typing import Optional + +import httpx + +logger = logging.getLogger(__name__) + +SCOUTER_HOST = os.getenv("SCOUTER_HOST", "localhost") +SCOUTER_HTTP_PORT = int(os.getenv("SCOUTER_HTTP_PORT", "6180")) +SCOUTER_USER = os.getenv("SCOUTER_USER", "admin") +SCOUTER_PASSWORD = os.getenv("SCOUTER_PASSWORD", "admin") + +_BASE = f"http://{SCOUTER_HOST}:{SCOUTER_HTTP_PORT}/scouter/v1" +_ENABLED = bool(os.getenv("SCOUTER_HOST")) # 환경변수 없으면 비활성화 + + +async def _get(path: str, params: dict = None) -> Optional[dict]: + """Scouter HTTP API GET 요청.""" + if not _ENABLED: + return None + try: + async with httpx.AsyncClient(timeout=5.0) as client: + r = await client.get( + f"{_BASE}{path}", + params=params or {}, + auth=(SCOUTER_USER, SCOUTER_PASSWORD), + ) + if r.status_code == 200: + return r.json() + except Exception as e: + logger.debug("Scouter API 오류 (%s): %s", path, str(e)[:80]) + return None + + +async def is_available() -> bool: + """Scouter 서버 응답 여부 확인.""" + result = await _get("/info/version") + return result is not None + + +async def get_object_list() -> list: + """모니터링 대상 객체(서버/서비스) 목록 조회.""" + result = await _get("/object") + if not result: + return [] + return result.get("result", []) + + +async def get_server_metrics(obj_hash: int) -> dict: + """ + 특정 서버의 실시간 메트릭 조회. + + Returns: + { + "cpu": float, # CPU 사용률 (%) + "heap_used": int, # Heap 사용량 (MB) + "heap_max": int, # Heap 최대 (MB) + "tps": float, # 초당 트랜잭션 + "active_service": int, # 활성 서비스 수 + "response_time": float, # 평균 응답시간 (ms) + "error_rate": float, # 에러율 (%) + } + """ + result = await _get(f"/object/{obj_hash}/realtime/summary") + if not result: + return {} + + data = result.get("result", {}) + return { + "cpu": data.get("cpuPct", 0.0), + "heap_used": data.get("heapUsed", 0) // (1024 * 1024), + "heap_max": data.get("heapMax", 0) // (1024 * 1024), + "tps": data.get("tps", 0.0), + "active_service": data.get("activeService", 0), + "response_time": data.get("elapsedTime", 0.0), + "error_rate": data.get("errorRate", 0.0), + } + + +async def get_all_metrics() -> list: + """ + 모든 모니터링 대상 서버의 실시간 메트릭 수집. + + Returns: [{"name": str, "type": str, "metrics": {...}}, ...] + """ + objects = await get_object_list() + if not objects: + return [] + + results = [] + for obj in objects: + obj_hash = obj.get("objHash") + if not obj_hash: + continue + + metrics = await get_server_metrics(obj_hash) + results.append({ + "name": obj.get("objName", "unknown"), + "type": obj.get("objType", "unknown"), + "host": obj.get("address", ""), + "metrics": metrics, + }) + + return results + + +async def get_active_services(obj_hash: int) -> list: + """활성 서비스(현재 처리 중인 요청) 목록 조회.""" + result = await _get(f"/object/{obj_hash}/activeService") + if not result: + return [] + return result.get("result", []) + + +async def get_xlog_recent(obj_hash: int, limit: int = 20) -> list: + """최근 트랜잭션 X-Log 조회.""" + result = await _get(f"/xlog/realtime/{obj_hash}", params={"limit": limit}) + if not result: + return [] + return result.get("result", []) + + +async def get_alert_list(obj_hash: int = None) -> list: + """Scouter 경보 목록 조회.""" + path = f"/object/{obj_hash}/alert" if obj_hash else "/alert" + result = await _get(path) + if not result: + return [] + return result.get("result", []) + + +async def get_summary() -> dict: + """ + 전체 모니터링 현황 요약 (GUARDiA 대시보드용). + + Returns: + { + "enabled": bool, + "total_servers": int, + "avg_cpu": float, + "avg_tps": float, + "avg_response_ms": float, + "critical_servers": [str], # CPU > 80% or 에러율 > 5% + "servers": [...] + } + """ + if not _ENABLED: + return {"enabled": False, "total_servers": 0, "avg_cpu": 0, "avg_tps": 0} + + all_metrics = await get_all_metrics() + if not all_metrics: + return {"enabled": True, "total_servers": 0, "avg_cpu": 0, "avg_tps": 0, "servers": []} + + total = len(all_metrics) + avg_cpu = sum(m["metrics"].get("cpu", 0) for m in all_metrics) / total + avg_tps = sum(m["metrics"].get("tps", 0) for m in all_metrics) / total + avg_resp = sum(m["metrics"].get("response_time", 0) for m in all_metrics) / total + + critical = [ + m["name"] for m in all_metrics + if m["metrics"].get("cpu", 0) > 80 + or m["metrics"].get("error_rate", 0) > 5 + ] + + return { + "enabled": True, + "total_servers": total, + "avg_cpu": round(avg_cpu, 1), + "avg_tps": round(avg_tps, 2), + "avg_response_ms": round(avg_resp, 1), + "critical_servers": critical, + "servers": all_metrics, + } diff --git a/itsm/main.py b/itsm/main.py index e351ab8c..d195139a 100644 --- a/itsm/main.py +++ b/itsm/main.py @@ -39,6 +39,7 @@ from routers import ( license as license_router, learning, push as push_router, + scouter as scouter_router, ) @@ -226,6 +227,9 @@ app.include_router(license_router.router) # ── G-10: PWA Push 알림 ────────────────────────────────────────────────── app.include_router(push_router.router) +# Scouter APM +app.include_router(scouter_router.router) + app.mount("/static", StaticFiles(directory="static"), name="static") diff --git a/itsm/routers/scouter.py b/itsm/routers/scouter.py new file mode 100644 index 00000000..12517bf9 --- /dev/null +++ b/itsm/routers/scouter.py @@ -0,0 +1,222 @@ +""" +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 "" diff --git a/setup/scouter/download_scouter.sh b/setup/scouter/download_scouter.sh new file mode 100644 index 00000000..12f586aa --- /dev/null +++ b/setup/scouter/download_scouter.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# ============================================================== +# Scouter 에이전트/서버 다운로드 스크립트 +# ============================================================== +# 사용법: +# bash setup/scouter/download_scouter.sh +# SCOUTER_VER=2.20.0 bash download_scouter.sh +# ============================================================== + +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +SCOUTER_VER="${SCOUTER_VER:-2.20.0}" +SCOUTER_MIRROR="${SCOUTER_MIRROR:-https://github.com/scouter-project/scouter/releases/download}" + +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' +ok() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +info() { echo -e " $*"; } + +echo "==================================================" +echo " Scouter $SCOUTER_VER 다운로드" +echo " 저장 위치: $SCRIPT_DIR" +echo "==================================================" + +# ── scouter-agent.jar (Tomcat WAS에 배포) ──────────────────── +AGENT_URL="${SCOUTER_MIRROR}/v${SCOUTER_VER}/scouter-agent.tar.gz" +AGENT_TAR="$SCRIPT_DIR/scouter-agent.tar.gz" + +if [[ ! -f "$SCRIPT_DIR/scouter-agent.jar" ]]; then + echo "" + echo "[1/3] Scouter 에이전트 다운로드..." + wget -q "$AGENT_URL" -O "$AGENT_TAR" \ + || { warn "에이전트 다운로드 실패 — SCOUTER_MIRROR 환경변수 설정"; exit 1; } + tar -xzf "$AGENT_TAR" -C "$SCRIPT_DIR" --strip-components=1 \ + "scouter-agent/scouter-agent.jar" 2>/dev/null \ + || tar -xzf "$AGENT_TAR" -C "$SCRIPT_DIR" 2>/dev/null + rm -f "$AGENT_TAR" + ok "scouter-agent.jar 다운로드 완료" +else + info "scouter-agent.jar 이미 존재" +fi + +# ── scouter-server (Paper) ─────────────────────────────────── +SERVER_URL="${SCOUTER_MIRROR}/v${SCOUTER_VER}/scouter-all-${SCOUTER_VER}.tar.gz" + +if [[ ! -d "$SCRIPT_DIR/scouter-server" ]]; then + echo "" + echo "[2/3] Scouter 서버(Paper) 다운로드..." + wget -q "$SERVER_URL" -O "$SCRIPT_DIR/scouter-all.tar.gz" \ + && tar -xzf "$SCRIPT_DIR/scouter-all.tar.gz" -C "$SCRIPT_DIR" \ + && rm -f "$SCRIPT_DIR/scouter-all.tar.gz" \ + && ok "Scouter 서버 다운로드 완료" \ + || warn "서버 패키지 다운로드 실패 — Docker 이미지 사용 권장" +else + info "scouter-server 이미 존재" +fi + +# ── 에이전트 설정 템플릿 생성 ───────────────────────────────── +echo "" +echo "[3/3] 에이전트 설정 템플릿 생성..." +cat > "$SCRIPT_DIR/agent.conf.template" << 'CONFEOF' +# Scouter 에이전트 설정 템플릿 +# GUARDiA가 자동으로 서버명과 Scouter 호스트를 치환합니다 + +# Scouter 서버 연결 +net.collector.ip=${SCOUTER_HOST} +net.collector.udp_port=${SCOUTER_PORT} +net.collector.tcp_port=${SCOUTER_PORT} + +# 서버 식별 +obj_name=${SERVER_NAME} +obj_type=tomcat + +# 성능 수집 설정 +trace_interservice_enabled=true +trace_user_enabled=true +trace_user_cookie_limit=100 +counter_mem_enabled=true + +# 느린 쿼리 추적 (임계값: 1초) +hook_jdbc_pstmt_enabled=true +profile_jdbc_fetch_max=100 +sqltrace_limit_elapsed_ms=1000 + +# 트랜잭션 추적 +trace_active_transaction_enabled=true +trace_active_transaction_yellow_time=3000 +trace_active_transaction_red_time=8000 +CONFEOF + +ok "agent.conf.template 생성 완료" + +echo "" +echo "==================================================" +ok "Scouter 다운로드 완료!" +echo "" +info "파일 목록:" +ls -lh "$SCRIPT_DIR/"*.jar "$SCRIPT_DIR/"*.template 2>/dev/null || true +echo "" +info "에이전트 배포: GUARDiA ITSM → 서버 관리 → Scouter 에이전트 배포" +info " 또는 API: POST /api/scouter/agent/deploy" +echo "==================================================" diff --git a/setup/setup_ubuntu.sh b/setup/setup_ubuntu.sh index 18010735..0d89515e 100644 --- a/setup/setup_ubuntu.sh +++ b/setup/setup_ubuntu.sh @@ -35,6 +35,16 @@ echo "==================================================" [[ $EUID -eq 0 ]] || fail "root 권한으로 실행하세요: sudo bash $0" +# 공통 라이브러리 로드 +LIB_DIR="$SCRIPT_DIR/lib" +# shellcheck source=setup/lib/db_select.sh +source "$LIB_DIR/db_select.sh" +# shellcheck source=setup/lib/gitea_setup.sh +source "$LIB_DIR/gitea_setup.sh" + +# ── DB 선택 (설치 전 먼저 물어봄) ──────────────────────────── +select_database + # ── 테스트 모드 ────────────────────────────────────────────── if [[ "$TEST_MODE" == "--test" ]]; then echo "=== 설치 검증 모드 ===" @@ -64,6 +74,9 @@ if [[ "$TEST_MODE" == "--test" ]]; then -H "Content-Type: application/json" -d "{\"username\":\"admin\",\"password\":\"1111\"}" -o /dev/null' check "Fail2ban 실행" systemctl is-active fail2ban check "Chrony NTP" chronyc tracking + check "Scouter APM API" bash -c 'curl -sf http://localhost:6180/scouter/v1/info/version -o /dev/null' + check "Gitea 서비스" systemctl is-active gitea + check "Gitea HTTP" bash -c 'curl -sf http://localhost:3000/api/v1/version -o /dev/null' check "Nginx 설정" nginx -t check "Python UTF-8 인코딩" bash -c 'PYTHONIOENCODING=utf-8 python3.11 -c "print(\"OK\")" > /dev/null' @@ -193,25 +206,24 @@ pip install --upgrade pip -q pip install -r "$GUARDIA_ROOT/itsm/requirements.txt" -q ok "Python 패키지 설치 완료" -# ── 4. PostgreSQL 설정 ───────────────────────────────────── +# ── 4. PostgreSQL 설정 (DB 선택에 따라 조건부 실행) ───────── echo "" -echo "[4/10] PostgreSQL 설정..." -systemctl start postgresql -systemctl enable postgresql - -sudo -u postgres psql -tc "SELECT 1 FROM pg_user WHERE usename='guardia'" | grep -q 1 || \ - sudo -u postgres psql -c "CREATE USER guardia WITH PASSWORD 'guardia_secure_pw';" -sudo -u postgres psql -tc "SELECT 1 FROM pg_database WHERE datname='guardia'" | grep -q 1 || \ - sudo -u postgres psql -c "CREATE DATABASE guardia OWNER guardia;" -ok "PostgreSQL 설정 완료" +echo "[4/10] 데이터베이스 설정..." +if [[ "$INSTALL_POSTGRES" == "true" ]]; then + systemctl start postgresql + systemctl enable postgresql + setup_postgres +else + ok "SQLite 사용 — PostgreSQL 설치 건너뜀" +fi # ── 5. 환경 설정 파일 ───────────────────────────────────── echo "" echo "[5/10] 환경 설정 파일 생성..." ENV_FILE="$GUARDIA_ROOT/itsm/.env" if [[ ! -f "$ENV_FILE" ]]; then - cat > "$ENV_FILE" << 'ENVEOF' -DATABASE_URL=postgresql+asyncpg://guardia:guardia_secure_pw@localhost:5432/guardia + cat > "$ENV_FILE" << ENVEOF +DATABASE_URL=${DATABASE_URL} SECRET_KEY=change_this_secret_key_in_production_min_32chars ALGORITHM=HS256 ACCESS_TOKEN_EXPIRE_MINUTES=480 @@ -221,10 +233,12 @@ GUARDIA_LLM_MODEL=llama3.1:8b MESSENGER_BASE_URL=http://localhost:8002 MESSENGER_OPS_ROOM=ops CATALINA_HOME=/app/tomcat +ENABLE_VECTOR=${INSTALL_PGVECTOR:-false} ENVEOF warn ".env 생성됨 — SECRET_KEY를 변경하세요: $ENV_FILE" else - info ".env 파일 이미 존재 — 건너뜀" + # 기존 .env에 DATABASE_URL 업데이트 + write_db_env "$ENV_FILE" fi # ── 6. DB 초기화 (스키마 불일치 자동 감지·복구) ───────────────────────── @@ -313,7 +327,61 @@ ok "Nginx 설정 완료" # ── 9. 방화벽 ──────────────────────────────────────────── echo "" -echo "[9/13] Ollama (온프레미스 sLLM 서버) 설치..." +echo "[9/14] Scouter APM 서버 설치..." +INSTALL_SCOUTER="${INSTALL_SCOUTER:-true}" +if [[ "$INSTALL_SCOUTER" == "true" ]]; then + SCOUTER_VER="${SCOUTER_VER:-2.20.0}" + SCOUTER_HOME="/opt/scouter-server" + SCOUTER_PORT="${SCOUTER_PORT:-6100}" + + # Scouter 서버 다운로드 + if [[ ! -d "$SCOUTER_HOME" ]]; then + SCOUTER_URL="${SCOUTER_MIRROR:-https://github.com/scouter-project/scouter/releases/download/v${SCOUTER_VER}/scouter-all-${SCOUTER_VER}.tar.gz}" + wget -q "$SCOUTER_URL" -O /tmp/scouter-all.tar.gz 2>/dev/null \ + || warn "Scouter 다운로드 실패 — Docker 이미지 사용 권장: docker compose up -d scouter-server" + if [[ -f /tmp/scouter-all.tar.gz ]]; then + mkdir -p "$SCOUTER_HOME" + tar -xzf /tmp/scouter-all.tar.gz -C "$SCOUTER_HOME" --strip-components=1 2>/dev/null || true + rm -f /tmp/scouter-all.tar.gz + fi + fi + + # Scouter 서버 systemd 등록 + if [[ -d "$SCOUTER_HOME" ]]; then + cat > /etc/systemd/system/scouter-server.service << SCEOF +[Unit] +Description=Scouter APM Server +After=network.target + +[Service] +Type=simple +WorkingDirectory=$SCOUTER_HOME +ExecStart=/bin/bash $SCOUTER_HOME/startup.sh +Restart=on-failure +RestartSec=10 + +[Install] +WantedBy=multi-user.target +SCEOF + systemctl daemon-reload + systemctl enable scouter-server 2>/dev/null + systemctl start scouter-server 2>/dev/null || true + ok "Scouter APM 서버 등록 완료 (HTTP API: 포트 6180)" + else + warn "Scouter 미설치 — docker compose up -d scouter-server 사용" + fi + + # 에이전트 JAR 다운로드 + SCOUTER_AGENT_DIR="$SCRIPT_DIR/scouter" + mkdir -p "$SCOUTER_AGENT_DIR" + bash "$SCRIPT_DIR/scouter/download_scouter.sh" 2>/dev/null \ + && ok "Scouter 에이전트 다운로드 완료" \ + || warn "Scouter 에이전트 다운로드 실패 — 수동 다운로드 필요" +else + warn "Scouter 설치 건너뜀 (INSTALL_SCOUTER=false)" +fi + +echo "[10/14] Ollama (온프레미스 sLLM 서버) 설치..." # Ollama: GUARDiA AI 기능의 핵심 — 없으면 챗봇/RCA/티켓분류 모두 비작동 OLLAMA_INSTALL="${OLLAMA_INSTALL:-online}" # online | offline | skip OLLAMA_MODELS="${OLLAMA_MODELS:-llama3.1:8b}" @@ -352,7 +420,7 @@ fi # ── 10. 보안·운영 도구 설치 ────────────────────────────── echo "" -echo "[10/13] 보안·운영 도구 (Fail2ban / NTP / JDK 다중 버전 / Logrotate)..." +echo "[11/14] 보안·운영 도구 (Fail2ban / NTP / JDK 다중 버전 / Logrotate)..." # Fail2ban — SSH 무차별 대입 방지 apt-get install -y -qq fail2ban @@ -405,7 +473,7 @@ ok "Logrotate 설정 완료 (catalina.out 14일 보관)" # ── 11. 방화벽 설정 ────────────────────────────────────── echo "" -echo "[11/13] 방화벽 설정..." +echo "[12/14] 방화벽 설정..." if command -v ufw &>/dev/null; then ufw allow 22/tcp 2>/dev/null || true ufw allow 80/tcp 2>/dev/null || true @@ -417,8 +485,22 @@ fi # ── 12. 최종 상태 확인 ─────────────────────────────────── echo "" -echo "[12/13] 서비스 상태 확인..." -for svc in tomcat9 ollama guardia-itsm nginx postgresql redis-server fail2ban chrony; do +echo "[13/14] Gitea 설치 및 초기화..." +INSTALL_GITEA="${INSTALL_GITEA:-true}" +if [[ "$INSTALL_GITEA" == "true" ]]; then + install_gitea + # 개발자 계정 생성 (환경변수로 지정 가능: GITEA_DEV_USERS="user1 user2") + for devuser in ${GITEA_DEV_USERS:-engineer1 engineer2 pm1}; do + create_dev_user "$devuser" "Dev@guardia!" "${devuser}@guardia.local" + done + init_gitea_repos + ok "Gitea 설치 + 초기화 완료" +else + warn "Gitea 설치 건너뜀 (INSTALL_GITEA=false)" +fi + +echo "[14/14] 서비스 상태 확인..." +for svc in tomcat9 ollama guardia-itsm nginx postgresql redis-server fail2ban chrony gitea; do if systemctl is-active "$svc" &>/dev/null; then ok "$svc 실행 중" else