This commit is contained in:
2025-12-28 23:44:40 +09:00
parent 248eb6e6e0
commit 8bcb5c7ff7
2 changed files with 309 additions and 247 deletions

506
README.md
View File

@@ -1,284 +1,336 @@
# OSOLIT Monitor # OSOLIT Monitor
시스템 헬스체크 & 모니터링 대시보드 (Nuxt 3 + SQLite + SSE) 시스템 헬스체크 & 모니터링 대시보드 (Nuxt 3 + PostgreSQL + WebSocket)
## 주요 기능 ## 주요 기능
### 📊 실시간 대시보드 (웹소켓/SSE 기반) ### 📊 실시간 대시보드
- **네트워크 점검** 포틀릿 (단일, 핵심) - **서버 상태 모니터링**: 다중 서버 헬스체크 및 상태 표시
- Server-Sent Events (SSE)로 실시간 업데이트 - **네트워크 점검**: Public(외부망) / Private(내부망) 분리 모니터링
- 점검 시간과 상태 즉시 반영 - **위치별 통계**: 서버 위치(Location)별 현황 요약
- 연결 상태 표시 (🟢 연결됨 / 🔴 연결 끊김) - **WebSocket 기반 실시간 업데이트**
- 향후 추가 포틀릿 확장 가능 - **다크/라이트 테마 지원**
### 🔄 적응형 스케줄러 ### 🌐 네트워크 모니터링
- **정상 시**: 5분 간격 점검 - **Public Network (pubnet)**: 외부망 연결 상태 점검
- **장애 시**: 1분 간격 점검 (빠른 복구 감지) - **Private Network (privnet)**: 내부망 연결 상태 점검
- 자동으로 간격 조정 - 응답 시간 측정 및 에러 메시지 상세 표시
- DB 시간 기준으로 로그 저장
### 🌐 네트워크 점검 ### 🖥️ 서버 모니터링
- World Time API를 통한 외부 네트워크 상태 확인 - 다중 서버 대상 관리
- 응답 시간 측정 - 서버별 헬스체크 이력 조회
- 에러 메시지 상세 표시 - 위치별 서버 현황 통계
- 스케줄러 제어 (시작/중지)
### 💾 데이터 저장 ### 📈 이상탐지 (Anomaly Detection)
- **SQLite 파일 DB** (`database/health_logs.db`) - **Baseline 분석**: 정상 기준선 대비 이상 탐지
- 별도 DB 서버 설치 불필요 - **Z-Score 분석**: 통계적 이상치 탐지
- 상태: UP(정상) / DOWN(장애) - **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. 의존성 설치 ### 1. 의존성 설치
```bash ```bash
npm install 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 ```bash
npm run dev npm run dev
``` ```
서버 시작 시: - 개발 서버: http://localhost:4055
- DB 자동 초기화 - WebSocket: ws://localhost:4055/_ws
- 즉시 첫 네트워크 점검 실행
- SSE 서버 시작
- 개발 서버: http://localhost:3000
### 3. 프로덕션 빌드 ### 4. 프로덕션 빌드
```bash ```bash
npm run build npm run build
npm run preview 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**
#### 네트워크 점검 포틀릿
``` ```
┌─────────────────────────────────┐ ┌─────────────────────────────────────────────────────────
🌐 네트워크 점검 ← 상태 아이콘 📊 대시보드 2024-12-25 14:30:00 │
├─────────────────────────────────┤ ├─────────────────────────────────────────────────────────
[ UP ] │ ← 상태 뱃지 ┌─────────────────────────────────┐ ┌─────────────────┐ │
│ │ │ │ 🌐 Public ✓ │
점검 시간: 12/25 14:35:20 │ ← 마지막 점검 시간 │ 서버 현황 (90%) │ │ [UP] │ │
응답 시간: 123ms ← 응답 속도 │ ├─────────────────┤ │
├─────────────────────────────────┤ │ │ 서버 목록 & 상태 │ │ 🔒 Private ✓ │ │
5분 전 ⏱️ 5분 간격 │ ← 상대시간 & 점검주기 │ │ [UP] │ │
└───────────────────────────────── │ │ │ ├─────────────────┤ │
│ │ │ │ 📍 위치별 통계 │ │
│ └─────────────────────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
🟢 연결됨
``` ```
**실시간 업데이트:** ### 서버 관리 (`/server/list`, `/server/history`)
- 점검 완료 시 즉시 화면 반영 - 모니터링 대상 서버 목록 관리
- 별도 새로고침 불필요 - 서버별 헬스체크 이력 조회
- SSE 연결 상태 표시
**향후 확장:** ### 이상탐지 (`/anomaly/*`)
- 데이터베이스 점검 포틀릿 - Baseline / Z-Score / Trend / Short-term 분석
- 서버 상태 포틀릿 - 차트 기반 시각화
- 디스크 용량 포틀릿
- 기타 모니터링 항목
### 로그 & 통계 ### 설정 (`/settings/*`)
**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
```
## API 엔드포인트 ## API 엔드포인트
### GET /api/dashboard ### 네트워크
초기 대시보드 데이터 | Method | Endpoint | 설명 |
|--------|----------|------|
| GET | `/api/network/pubnet/status` | 외부망 상태 |
| GET | `/api/network/privnet/status` | 내부망 상태 |
**Response:** ### 서버
```json | Method | Endpoint | 설명 |
{ |--------|----------|------|
"networkStatus": { | GET | `/api/server/status` | 서버 현황 |
"id": 123, | GET | `/api/server/location-stats` | 위치별 통계 |
"service_name": "Network", | GET | `/api/server/targets` | 모니터링 대상 목록 |
"status": "UP", | GET | `/api/server/history` | 헬스체크 이력 |
"check_time": "2024-12-25T12:34:56.000Z",
"response_time": 123, ### 이상탐지
"error_message": null | Method | Endpoint | 설명 |
}, |--------|----------|------|
"lastUpdate": "2024-12-25T12:34:56.789Z" | 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 | `pubnet-scheduler` | 외부망 점검 | 적응형 (정상 5분, 장애 1분) |
통계 조회 | `privnet-scheduler` | 내부망 점검 | 적응형 (정상 5분, 장애 1분) |
| `server-scheduler` | 서버 점검 | 설정 가능 |
## 데이터베이스 스키마
### 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;
```
## 브라우저 호환성 ## 브라우저 호환성
SSE 지원 브라우저:
- ✅ Chrome - ✅ Chrome
- ✅ Firefox - ✅ Firefox
- ✅ Safari - ✅ Safari
- ✅ Edge - ✅ Edge
- ❌ IE (미지원) - ❌ IE (미지원)
## 다음 단계 ## 개발 로드맵
- [ ] 데이터베이스 점검 포틀릿 추가 - [x] 서버 모니터링 포틀릿
- [ ] 서버 상태 점검 포틀릿 추가 - [x] 네트워크 모니터링 (Public/Private)
- [ ] 디스크 용량 점검 포틀릿 추가 - [x] 이상탐지 기능
- [ ] Jenkins Job 통합 - [x] Chart.js 차트
- [x] 다크/라이트 테마
- [x] Docker 지원
- [ ] 장애 알림 (이메일/Slack) - [ ] 장애 알림 (이메일/Slack)
- [ ] 차트/그래프
- [ ] 오래된 로그 자동 삭제 - [ ] 오래된 로그 자동 삭제
- [ ] Jenkins Job 통합
## 기술 스택 ## 라이선스
- **Frontend**: Nuxt 3, Vue 3 Private - TurboSoft
- **Backend**: Nitro (Node.js)
- **Database**: SQLite (better-sqlite3)
- **Real-time**: Server-Sent Events (SSE)
- **Scheduler**: 적응형 (정상 5분, 장애 1분)
- **External API**: World Time API
## 특징
**실시간 업데이트** - SSE로 즉시 반영
**적응형 스케줄링** - 장애 시 빠른 점검
**단순한 구조** - 네트워크 점검 중심
**확장 가능** - 포틀릿 추가 용이
**파일 DB** - 별도 서버 불필요

View File

@@ -1,4 +1,5 @@
import { query, queryOne, execute, getPool } from './db' import { query, queryOne, execute, getPool } from './db'
import { ofetch } from 'ofetch'
interface ServerTarget { interface ServerTarget {
target_id: number target_id: number
@@ -20,16 +21,20 @@ function timestamp(): string {
return new Date().toLocaleString('sv-SE', { timeZone: 'Asia/Seoul' }).replace('T', ' ') 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<any> { async function fetchGlancesApi(baseUrl: string, endpoint: string, version: string): Promise<any> {
try {
const url = `${baseUrl}/api/${version}/${endpoint}` const url = `${baseUrl}/api/${version}/${endpoint}`
const response = await fetch(url, { try {
signal: AbortSignal.timeout(5000) const data = await ofetch(url, {
timeout: 5000,
retry: 0
}) })
if (!response.ok) return null return data
return await response.json() } catch (err: any) {
} catch (err) { // 404, timeout 등 에러는 조용히 null 반환
if (err.statusCode !== 404) {
console.error(`[fetchGlancesApi] ${url} - Error: ${err.message}`)
}
return null return null
} }
} }
@@ -487,12 +492,12 @@ async function collectServerData(target: ServerTarget) {
quicklook?.cpu_name || null, quicklook?.cpu_name || null,
quicklook?.cpu_number || quicklook?.cpu_log_core || cpu?.cpucore || null, quicklook?.cpu_number || quicklook?.cpu_log_core || cpu?.cpucore || null,
cpu?.total ?? quicklook?.cpu ?? null, cpu?.total ?? quicklook?.cpu ?? null,
mem?.total || null, mem?.total ? Math.round(mem.total) : null, // bigint: 정수 변환
mem?.total && mem?.percent ? (mem.total * mem.percent / 100) : null, // memory_used = total × percent / 100 mem?.total && mem?.percent ? Math.round(mem.total * mem.percent / 100) : null, // bigint: 정수 변환
mem?.free || null, mem?.free ? Math.round(mem.free) : null, // bigint: 정수 변환
mem?.percent || null, mem?.percent || null,
memswap?.total || null, memswap?.total ? Math.round(memswap.total) : null, // bigint: 정수 변환
memswap?.used || null, memswap?.used ? Math.round(memswap.used) : null, // bigint: 정수 변환
memswap?.percent || null, memswap?.percent || null,
isOnline ? 1 : 0, isOnline ? 1 : 0,
apiVersion, apiVersion,
@@ -518,8 +523,8 @@ async function collectServerData(target: ServerTarget) {
disk.device_name || null, disk.device_name || null,
disk.mnt_point || null, disk.mnt_point || null,
disk.fs_type || null, disk.fs_type || null,
disk.size || null, disk.size ? Math.round(disk.size) : null, // bigint: 정수 변환
disk.used || null, disk.used ? Math.round(disk.used) : null, // bigint: 정수 변환
disk.percent || null, disk.percent || null,
now now
]) ])
@@ -563,8 +568,8 @@ async function collectServerData(target: ServerTarget) {
Array.isArray(container.image) ? container.image.join(', ') : container.image || null, Array.isArray(container.image) ? container.image.join(', ') : container.image || null,
container.status || null, container.status || null,
cpuPercent, cpuPercent,
memoryUsage, memoryUsage ? Math.round(memoryUsage) : null, // bigint: 정수 변환
memoryLimit, memoryLimit ? Math.round(memoryLimit) : null, // bigint: 정수 변환
memoryPercent, memoryPercent,
container.uptime || null, container.uptime || null,
container.network?.rx ?? container.network_rx ?? null, container.network?.rx ?? container.network_rx ?? null,
@@ -578,6 +583,11 @@ async function collectServerData(target: ServerTarget) {
if (Array.isArray(network) && network.length > 0) { if (Array.isArray(network) && network.length > 0) {
console.log(`[${now}] 🌐 [${target.server_name}] network 저장 (${network.length}개 인터페이스)`) console.log(`[${now}] 🌐 [${target.server_name}] network 저장 (${network.length}개 인터페이스)`)
for (const iface of network) { 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(` await execute(`
INSERT INTO server_networks ( INSERT INTO server_networks (
target_id, interface_name, bytes_recv, bytes_sent, target_id, interface_name, bytes_recv, bytes_sent,
@@ -587,10 +597,10 @@ async function collectServerData(target: ServerTarget) {
`, [ `, [
target.target_id, target.target_id,
iface.interface_name || null, iface.interface_name || null,
iface.bytes_recv || iface.cumulative_rx || null, bytesRecv ? Math.round(bytesRecv) : null, // bigint: 정수 변환
iface.bytes_sent || iface.cumulative_tx || null, bytesSent ? Math.round(bytesSent) : null, // bigint: 정수 변환
iface.packets_recv || null, packetsRecv ? Math.round(packetsRecv) : null, // bigint: 정수 변환
iface.packets_sent || null, packetsSent ? Math.round(packetsSent) : null, // bigint: 정수 변환
iface.bytes_recv_rate_per_sec || iface.rx || iface.bytes_recv_rate || null, 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.bytes_sent_rate_per_sec || iface.tx || iface.bytes_sent_rate || null,
iface.is_up ? 1 : 0, iface.is_up ? 1 : 0,