diff --git a/README.md b/README.md index d0a48b7..ed69bb8 100644 --- a/README.md +++ b/README.md @@ -1,284 +1,336 @@ # OSOLIT Monitor -시스템 헬스체크 & 모니터링 대시보드 (Nuxt 3 + SQLite + SSE) +시스템 헬스체크 & 모니터링 대시보드 (Nuxt 3 + PostgreSQL + WebSocket) ## 주요 기능 -### 📊 실시간 대시보드 (웹소켓/SSE 기반) -- **네트워크 점검** 포틀릿 (단일, 핵심) -- Server-Sent Events (SSE)로 실시간 업데이트 -- 점검 시간과 상태 즉시 반영 -- 연결 상태 표시 (🟢 연결됨 / 🔴 연결 끊김) -- 향후 추가 포틀릿 확장 가능 +### 📊 실시간 대시보드 +- **서버 상태 모니터링**: 다중 서버 헬스체크 및 상태 표시 +- **네트워크 점검**: Public(외부망) / Private(내부망) 분리 모니터링 +- **위치별 통계**: 서버 위치(Location)별 현황 요약 +- **WebSocket 기반 실시간 업데이트** +- **다크/라이트 테마 지원** -### 🔄 적응형 스케줄러 -- **정상 시**: 5분 간격 점검 -- **장애 시**: 1분 간격 점검 (빠른 복구 감지) -- 자동으로 간격 조정 -- DB 시간 기준으로 로그 저장 +### 🌐 네트워크 모니터링 +- **Public Network (pubnet)**: 외부망 연결 상태 점검 +- **Private Network (privnet)**: 내부망 연결 상태 점검 +- 응답 시간 측정 및 에러 메시지 상세 표시 -### 🌐 네트워크 점검 -- World Time API를 통한 외부 네트워크 상태 확인 -- 응답 시간 측정 -- 에러 메시지 상세 표시 +### 🖥️ 서버 모니터링 +- 다중 서버 대상 관리 +- 서버별 헬스체크 이력 조회 +- 위치별 서버 현황 통계 +- 스케줄러 제어 (시작/중지) -### 💾 데이터 저장 -- **SQLite 파일 DB** (`database/health_logs.db`) -- 별도 DB 서버 설치 불필요 -- 상태: UP(정상) / DOWN(장애) +### 📈 이상탐지 (Anomaly Detection) +- **Baseline 분석**: 정상 기준선 대비 이상 탐지 +- **Z-Score 분석**: 통계적 이상치 탐지 +- **Trend 분석**: 장기 추세 분석 +- **Short-term 분석**: 단기 변동 탐지 +- Chart.js 기반 시각화 + +### ⚙️ 설정 관리 +- 임계값(Threshold) 설정 +- 스케줄러 간격 조정 + +## 기술 스택 + +| 구분 | 기술 | +|------|------| +| **Frontend** | Nuxt 3, Vue 3, Chart.js | +| **Backend** | Nitro (Node.js) | +| **Database** | PostgreSQL | +| **Real-time** | WebSocket | +| **Container** | Docker | +| **Timezone** | Asia/Seoul | + +## 프로젝트 구조 + +``` +osolit-monitor/ +├── frontend/ # 프론트엔드 (Pages & Components) +│ ├── index.vue # 📊 메인 대시보드 +│ ├── components/ +│ │ ├── DashboardControl.vue +│ │ ├── ServerPortlet.vue +│ │ ├── NetworkPortlet.vue +│ │ ├── LocationStatsPortlet.vue +│ │ ├── SidebarNav.vue +│ │ └── ThemeToggle.vue +│ ├── network/ # 네트워크 모니터링 +│ ├── server/ # 서버 모니터링 +│ │ ├── list.vue +│ │ └── history.vue +│ ├── anomaly/ # 이상탐지 +│ │ ├── baseline.vue +│ │ ├── zscore.vue +│ │ ├── trend.vue +│ │ └── short-term.vue +│ ├── settings/ # 설정 +│ ├── assets/css/ +│ └── composables/ +│ +├── backend/ # 백엔드 (API & Plugins) +│ ├── api/ +│ │ ├── network/ +│ │ │ ├── pubnet/ # 외부망 API +│ │ │ └── privnet/ # 내부망 API +│ │ ├── server/ # 서버 모니터링 API +│ │ │ ├── status.get.ts +│ │ │ ├── location-stats.get.ts +│ │ │ ├── targets/ +│ │ │ ├── history/ +│ │ │ └── scheduler/ +│ │ ├── anomaly/ # 이상탐지 API +│ │ │ ├── baseline.get.ts +│ │ │ ├── zscore.get.ts +│ │ │ ├── trend.get.ts +│ │ │ ├── short-term.get.ts +│ │ │ ├── chart.get.ts +│ │ │ └── logs.get.ts +│ │ └── settings/ # 설정 API +│ │ ├── thresholds.get.ts +│ │ └── thresholds.put.ts +│ ├── plugins/ +│ │ ├── pubnet-init.ts # 외부망 스케줄러 초기화 +│ │ ├── privnet-init.ts # 내부망 스케줄러 초기화 +│ │ └── server-init.ts # 서버 스케줄러 초기화 +│ ├── routes/ # WebSocket 라우트 +│ └── utils/ +│ ├── db.ts # PostgreSQL 연결 +│ ├── pubnet-scheduler.ts +│ ├── privnet-scheduler.ts +│ └── server-scheduler.ts +│ +├── database/ # DB 관련 (마이그레이션 등) +├── .env # 개발 환경변수 +├── .env.prod # 운영 환경변수 +├── Dockerfile # 프로덕션 Docker 이미지 +├── nuxt.config.ts +└── package.json +``` ## 설치 및 실행 +### 환경 요구사항 +- Node.js 20+ +- PostgreSQL 14+ + ### 1. 의존성 설치 ```bash npm install ``` -### 2. 개발 서버 실행 +### 2. 환경변수 설정 +```bash +# .env 파일 생성 +cp .env.example .env +``` + +```env +# 환경 설정 +NODE_ENV=development + +# PostgreSQL 연결 정보 +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=osolit_monitor +DB_USER=postgres +DB_PASSWORD=your_password + +# 스케줄러 자동시작 (development: false 권장) +AUTO_START_SCHEDULER=false +``` + +### 3. 개발 서버 실행 ```bash npm run dev ``` -서버 시작 시: -- DB 자동 초기화 -- 즉시 첫 네트워크 점검 실행 -- SSE 서버 시작 -- 개발 서버: http://localhost:3000 +- 개발 서버: http://localhost:4055 +- WebSocket: ws://localhost:4055/_ws -### 3. 프로덕션 빌드 +### 4. 프로덕션 빌드 ```bash npm run build npm run preview ``` +## Docker 배포 + +### 빌드 및 실행 +```bash +# 이미지 빌드 +docker build -t osolit-monitor . + +# 컨테이너 실행 +docker run -d \ + --name osolit-monitor \ + -p 4055:4055 \ + -e DB_HOST=your-db-host \ + -e DB_PORT=5432 \ + -e DB_NAME=osolit_monitor \ + -e DB_USER=osolit \ + -e DB_PASSWORD=your_password \ + osolit-monitor +``` + +### Docker 특징 +- Multi-stage 빌드 (최적화된 이미지 크기) +- Asia/Seoul 타임존 설정 +- 비root 사용자 실행 (보안) +- 환경변수 기반 설정 + ## 화면 구성 -### 홈 (실시간 대시보드) -**http://localhost:3000** - -#### 네트워크 점검 포틀릿 +### 메인 대시보드 (`/`) ``` -┌─────────────────────────────────┐ -│ 🌐 네트워크 점검 ✓ │ ← 상태 아이콘 -├─────────────────────────────────┤ -│ [ UP ] │ ← 상태 뱃지 -│ │ -│ 점검 시간: 12/25 14:35:20 │ ← 마지막 점검 시간 -│ 응답 시간: 123ms │ ← 응답 속도 -├─────────────────────────────────┤ -│ 5분 전 ⏱️ 5분 간격 │ ← 상대시간 & 점검주기 -└─────────────────────────────────┘ +┌─────────────────────────────────────────────────────────┐ +│ 📊 대시보드 2024-12-25 14:30:00 │ +├─────────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────┐ ┌─────────────────┐ │ +│ │ │ │ 🌐 Public ✓ │ │ +│ │ 서버 현황 (90%) │ │ [UP] │ │ +│ │ │ ├─────────────────┤ │ +│ │ 서버 목록 & 상태 │ │ 🔒 Private ✓ │ │ +│ │ │ │ [UP] │ │ +│ │ │ ├─────────────────┤ │ +│ │ │ │ 📍 위치별 통계 │ │ +│ └─────────────────────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────┘ + 🟢 연결됨 ``` -**실시간 업데이트:** -- 점검 완료 시 즉시 화면 반영 -- 별도 새로고침 불필요 -- SSE 연결 상태 표시 +### 서버 관리 (`/server/list`, `/server/history`) +- 모니터링 대상 서버 목록 관리 +- 서버별 헬스체크 이력 조회 -**향후 확장:** -- 데이터베이스 점검 포틀릿 -- 서버 상태 포틀릿 -- 디스크 용량 포틀릿 -- 기타 모니터링 항목 +### 이상탐지 (`/anomaly/*`) +- Baseline / Z-Score / Trend / Short-term 분석 +- 차트 기반 시각화 -### 로그 & 통계 -**http://localhost:3000/logs** -- 전체 점검 이력 -- 통계 정보 - -### 시스템 헬스체크 -**http://localhost:3000/health** -- 실시간 상태 조회 - -### 글로벌 표준시 -**http://localhost:3000/time** -- Time API 직접 테스트 - -## 프로젝트 구조 - -``` -osolit-monitor/ -├── frontend/ -│ ├── index.vue # 🏠 실시간 대시보드 -│ ├── logs.vue # 📊 로그 & 통계 -│ ├── health.vue # 🏥 헬스체크 -│ └── time.vue # 🌍 시간 API -├── backend/ -│ ├── api/ -│ │ ├── dashboard.get.ts # 대시보드 초기 데이터 -│ │ ├── stream.get.ts # SSE 스트림 (실시간) -│ │ ├── logs.get.ts -│ │ └── stats.get.ts -│ ├── plugins/ -│ │ └── scheduler.ts # 적응형 스케줄러 -│ └── utils/ -│ ├── database.ts # SQLite -│ └── timecheck.ts # 네트워크 체크 -├── database/ -│ └── health_logs.db # SQLite (자동 생성) -└── nuxt.config.ts -``` - -## 실시간 업데이트 (SSE) - -### Server-Sent Events -```javascript -// 프론트엔드 -const eventSource = new EventSource('/api/stream') - -eventSource.onmessage = (event) => { - const message = JSON.parse(event.data) - - if (message.type === 'health_update') { - // 화면 즉시 업데이트 - networkStatus.value = message.data - } -} -``` - -### 메시지 타입 -- `health_update`: 헬스체크 결과 -- `ping`: 연결 유지 (30초마다) - -### 자동 재연결 -연결 끊김 시 5초 후 자동 재연결 시도 - -## 적응형 스케줄링 - -### 동작 방식 -1. **서버 시작**: 즉시 첫 점검 -2. **정상(UP)**: 다음 점검 5분 후 -3. **장애(DOWN)**: 다음 점검 1분 후 (빠른 복구 감지) -4. **상태 변경 시**: 자동으로 간격 조정 - -### 콘솔 로그 -``` -🚀 Initializing Health Check Scheduler... -✅ Database initialized -⏰ Running network health check... - Result: UP (123ms) -✅ Health Check Scheduler started - -[5분 후] -⏰ Running network health check... - Result: DOWN (5001ms) -🔄 Interval changed: 1 minutes - -[1분 후] -⏰ Running network health check... - Result: UP (125ms) -🔄 Interval changed: 5 minutes -``` +### 설정 (`/settings/*`) +- 임계값 설정 관리 ## API 엔드포인트 -### GET /api/dashboard -초기 대시보드 데이터 +### 네트워크 +| Method | Endpoint | 설명 | +|--------|----------|------| +| GET | `/api/network/pubnet/status` | 외부망 상태 | +| GET | `/api/network/privnet/status` | 내부망 상태 | -**Response:** -```json -{ - "networkStatus": { - "id": 123, - "service_name": "Network", - "status": "UP", - "check_time": "2024-12-25T12:34:56.000Z", - "response_time": 123, - "error_message": null - }, - "lastUpdate": "2024-12-25T12:34:56.789Z" +### 서버 +| Method | Endpoint | 설명 | +|--------|----------|------| +| GET | `/api/server/status` | 서버 현황 | +| GET | `/api/server/location-stats` | 위치별 통계 | +| GET | `/api/server/targets` | 모니터링 대상 목록 | +| GET | `/api/server/history` | 헬스체크 이력 | + +### 이상탐지 +| Method | Endpoint | 설명 | +|--------|----------|------| +| GET | `/api/anomaly/baseline` | Baseline 분석 | +| GET | `/api/anomaly/zscore` | Z-Score 분석 | +| GET | `/api/anomaly/trend` | Trend 분석 | +| GET | `/api/anomaly/short-term` | 단기 분석 | +| GET | `/api/anomaly/chart` | 차트 데이터 | + +### 설정 +| Method | Endpoint | 설명 | +|--------|----------|------| +| GET | `/api/settings/thresholds` | 임계값 조회 | +| PUT | `/api/settings/thresholds` | 임계값 수정 | + +## WebSocket 통신 + +### 연결 +```javascript +const ws = new WebSocket('ws://localhost:4055/_ws') +``` + +### 메시지 타입 + +**클라이언트 → 서버:** +```javascript +// 새로고침 간격 설정 +{ type: 'set_interval', interval: 1 } + +// 자동 새로고침 토글 +{ type: 'set_auto_refresh', enabled: true } + +// 즉시 새로고침 +{ type: 'refresh' } + +// 특정 시점 데이터 조회 +{ type: 'fetch_at', datetime: '2024-12-25T14:30:00' } +``` + +**서버 → 클라이언트:** +```javascript +// 실시간 상태 +{ type: 'status', data: { pubnet: {...}, privnet: {...} } } + +// 서버 현황 +{ type: 'server', data: { servers: [...], summary: {...} } } + +// 과거 데이터 +{ type: 'historical', data: {...} } +``` + +## 데이터베이스 + +### PostgreSQL 연결 +```typescript +// backend/utils/db.ts +const config = { + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, } ``` -### GET /api/stream (SSE) -실시간 업데이트 스트림 +### 주요 테이블 +- `health_check_log`: 헬스체크 로그 +- `server_targets`: 모니터링 대상 서버 +- `thresholds`: 임계값 설정 -**Event Stream:** -``` -data: {"type":"health_update","data":{...}} +## 스케줄러 -data: {"type":"ping","timestamp":"2024-12-25T12:34:56.789Z"} -``` +### 동작 방식 +- **개발 환경**: 수동 시작 (`AUTO_START_SCHEDULER=false`) +- **운영 환경**: 자동 시작 (`AUTO_START_SCHEDULER=true`) -### GET /api/logs -헬스체크 로그 조회 - -### GET /api/stats -통계 조회 - -## 데이터베이스 스키마 - -### health_check_log 테이블 -```sql -CREATE TABLE health_check_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - check_time DATETIME DEFAULT CURRENT_TIMESTAMP, - service_name TEXT NOT NULL, -- 'Network' - status TEXT NOT NULL, -- 'UP' or 'DOWN' - response_time INTEGER, -- 밀리초 - error_message TEXT, - details TEXT -) -``` - -## 데이터 관리 - -### DB 백업 -```bash -cp database/health_logs.db database/backup_$(date +%Y%m%d).db -``` - -### DB 조회 -```bash -sqlite3 database/health_logs.db - -# 최근 로그 -SELECT * FROM health_check_log -WHERE service_name = 'Network' -ORDER BY check_time DESC LIMIT 20; - -# 통계 -SELECT - status, - COUNT(*) as count, - AVG(response_time) as avg_ms -FROM health_check_log -WHERE service_name = 'Network' -GROUP BY status; -``` +### 스케줄러 종류 +| 스케줄러 | 설명 | 간격 | +|----------|------|------| +| `pubnet-scheduler` | 외부망 점검 | 적응형 (정상 5분, 장애 1분) | +| `privnet-scheduler` | 내부망 점검 | 적응형 (정상 5분, 장애 1분) | +| `server-scheduler` | 서버 점검 | 설정 가능 | ## 브라우저 호환성 -SSE 지원 브라우저: - ✅ Chrome - ✅ Firefox - ✅ Safari - ✅ Edge - ❌ IE (미지원) -## 다음 단계 +## 개발 로드맵 -- [ ] 데이터베이스 점검 포틀릿 추가 -- [ ] 서버 상태 점검 포틀릿 추가 -- [ ] 디스크 용량 점검 포틀릿 추가 -- [ ] Jenkins Job 통합 +- [x] 서버 모니터링 포틀릿 +- [x] 네트워크 모니터링 (Public/Private) +- [x] 이상탐지 기능 +- [x] Chart.js 차트 +- [x] 다크/라이트 테마 +- [x] Docker 지원 - [ ] 장애 알림 (이메일/Slack) -- [ ] 차트/그래프 - [ ] 오래된 로그 자동 삭제 +- [ ] Jenkins Job 통합 -## 기술 스택 +## 라이선스 -- **Frontend**: Nuxt 3, Vue 3 -- **Backend**: Nitro (Node.js) -- **Database**: SQLite (better-sqlite3) -- **Real-time**: Server-Sent Events (SSE) -- **Scheduler**: 적응형 (정상 5분, 장애 1분) -- **External API**: World Time API - -## 특징 - -✅ **실시간 업데이트** - SSE로 즉시 반영 -✅ **적응형 스케줄링** - 장애 시 빠른 점검 -✅ **단순한 구조** - 네트워크 점검 중심 -✅ **확장 가능** - 포틀릿 추가 용이 -✅ **파일 DB** - 별도 서버 불필요 +Private - TurboSoft diff --git a/backend/utils/server-scheduler.ts b/backend/utils/server-scheduler.ts index dacbaee..db15c21 100644 --- a/backend/utils/server-scheduler.ts +++ b/backend/utils/server-scheduler.ts @@ -1,4 +1,5 @@ import { query, queryOne, execute, getPool } from './db' +import { ofetch } from 'ofetch' interface ServerTarget { target_id: number @@ -20,16 +21,20 @@ function timestamp(): string { return new Date().toLocaleString('sv-SE', { timeZone: 'Asia/Seoul' }).replace('T', ' ') } -// Glances API 호출 (버전 지정) +// Glances API 호출 (버전 지정) - ofetch 사용 async function fetchGlancesApi(baseUrl: string, endpoint: string, version: string): Promise { + const url = `${baseUrl}/api/${version}/${endpoint}` try { - const url = `${baseUrl}/api/${version}/${endpoint}` - const response = await fetch(url, { - signal: AbortSignal.timeout(5000) + const data = await ofetch(url, { + timeout: 5000, + retry: 0 }) - if (!response.ok) return null - return await response.json() - } catch (err) { + return data + } catch (err: any) { + // 404, timeout 등 에러는 조용히 null 반환 + if (err.statusCode !== 404) { + console.error(`[fetchGlancesApi] ${url} - Error: ${err.message}`) + } return null } } @@ -487,12 +492,12 @@ async function collectServerData(target: ServerTarget) { quicklook?.cpu_name || null, quicklook?.cpu_number || quicklook?.cpu_log_core || cpu?.cpucore || null, cpu?.total ?? quicklook?.cpu ?? null, - mem?.total || null, - mem?.total && mem?.percent ? (mem.total * mem.percent / 100) : null, // memory_used = total × percent / 100 - mem?.free || null, + mem?.total ? Math.round(mem.total) : null, // bigint: 정수 변환 + mem?.total && mem?.percent ? Math.round(mem.total * mem.percent / 100) : null, // bigint: 정수 변환 + mem?.free ? Math.round(mem.free) : null, // bigint: 정수 변환 mem?.percent || null, - memswap?.total || null, - memswap?.used || null, + memswap?.total ? Math.round(memswap.total) : null, // bigint: 정수 변환 + memswap?.used ? Math.round(memswap.used) : null, // bigint: 정수 변환 memswap?.percent || null, isOnline ? 1 : 0, apiVersion, @@ -518,8 +523,8 @@ async function collectServerData(target: ServerTarget) { disk.device_name || null, disk.mnt_point || null, disk.fs_type || null, - disk.size || null, - disk.used || null, + disk.size ? Math.round(disk.size) : null, // bigint: 정수 변환 + disk.used ? Math.round(disk.used) : null, // bigint: 정수 변환 disk.percent || null, now ]) @@ -563,8 +568,8 @@ async function collectServerData(target: ServerTarget) { Array.isArray(container.image) ? container.image.join(', ') : container.image || null, container.status || null, cpuPercent, - memoryUsage, - memoryLimit, + memoryUsage ? Math.round(memoryUsage) : null, // bigint: 정수 변환 + memoryLimit ? Math.round(memoryLimit) : null, // bigint: 정수 변환 memoryPercent, container.uptime || null, container.network?.rx ?? container.network_rx ?? null, @@ -578,6 +583,11 @@ async function collectServerData(target: ServerTarget) { if (Array.isArray(network) && network.length > 0) { console.log(`[${now}] 🌐 [${target.server_name}] network 저장 (${network.length}개 인터페이스)`) for (const iface of network) { + const bytesRecv = iface.bytes_recv || iface.cumulative_rx || null + const bytesSent = iface.bytes_sent || iface.cumulative_tx || null + const packetsRecv = iface.packets_recv || null + const packetsSent = iface.packets_sent || null + await execute(` INSERT INTO server_networks ( target_id, interface_name, bytes_recv, bytes_sent, @@ -587,10 +597,10 @@ async function collectServerData(target: ServerTarget) { `, [ target.target_id, iface.interface_name || null, - iface.bytes_recv || iface.cumulative_rx || null, - iface.bytes_sent || iface.cumulative_tx || null, - iface.packets_recv || null, - iface.packets_sent || null, + bytesRecv ? Math.round(bytesRecv) : null, // bigint: 정수 변환 + bytesSent ? Math.round(bytesSent) : null, // bigint: 정수 변환 + packetsRecv ? Math.round(packetsRecv) : null, // bigint: 정수 변환 + packetsSent ? Math.round(packetsSent) : null, // bigint: 정수 변환 iface.bytes_recv_rate_per_sec || iface.rx || iface.bytes_recv_rate || null, iface.bytes_sent_rate_per_sec || iface.tx || iface.bytes_sent_rate || null, iface.is_up ? 1 : 0,