diff --git a/docker-compose.base.yml b/docker-compose.base.yml index 4e9dc82..3c0ece6 100644 --- a/docker-compose.base.yml +++ b/docker-compose.base.yml @@ -58,6 +58,11 @@ services: depends_on: - postgres # - redis + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3001/health"] + interval: 30s + timeout: 10s + retries: 3 postgres: image: postgres:16-alpine diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 19f5ebe..7b53a70 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -12,8 +12,8 @@ services: # 개발 환경에서는 healthcheck 비활성화 volumes: - ../../wacefems/uploads:/app/uploads - healthcheck: - disable: true + # healthcheck: + # disable: true fems-app: ports: diff --git a/fems-api/src/controllers/app/health/health.controller.js b/fems-api/src/controllers/app/health/health.controller.js new file mode 100644 index 0000000..d991d53 --- /dev/null +++ b/fems-api/src/controllers/app/health/health.controller.js @@ -0,0 +1,80 @@ +// src/controllers/app/health/health.controller.js +const express = require("express"); +const router = express.Router(); +const logger = require("../../../config/logger"); +const { getSystemStatus } = require("../../../services/system.service"); + +// 기본 health check +router.get("/", async (req, res) => { + try { + // 시스템 기본 상태 반환 + res.json({ + status: "healthy", + timestamp: new Date().toISOString(), + serverTime: new Date().toISOString(), + }); + } catch (error) { + logger.error("Health check failed:", error); + res.status(500).json({ + status: "unhealthy", + timestamp: new Date().toISOString(), + error: error.message, + }); + } +}); + +// 상세 health check +router.get("/detail", async (req, res) => { + try { + // 시스템 상세 상태 조회 + const systemStatus = await getSystemStatus(); + + res.json({ + status: "healthy", + timestamp: new Date().toISOString(), + serverTime: new Date().toISOString(), + details: { + database: systemStatus.database, + redis: systemStatus.redis, + services: systemStatus.services, + uptime: process.uptime(), + memory: process.memoryUsage(), + }, + }); + } catch (error) { + logger.error("Detailed health check failed:", error); + res.status(500).json({ + status: "unhealthy", + timestamp: new Date().toISOString(), + error: error.message, + }); + } +}); + +// Edge 서버 상태 확인용 특별 엔드포인트 +router.get("/edge", async (req, res) => { + try { + const edgeId = req.header("X-Edge-ID"); + logger.info(`Health check from Edge server: ${edgeId}`); + + res.json({ + status: "healthy", + timestamp: new Date().toISOString(), + serverTime: new Date().toISOString(), + edgeServer: { + id: edgeId, + lastCheck: new Date().toISOString(), + connectionStatus: "connected", + }, + }); + } catch (error) { + logger.error("Edge health check failed:", error); + res.status(500).json({ + status: "unhealthy", + timestamp: new Date().toISOString(), + error: error.message, + }); + } +}); + +module.exports = router; diff --git a/fems-api/src/routes/app.js b/fems-api/src/routes/app.js index d091184..aeab15e 100644 --- a/fems-api/src/routes/app.js +++ b/fems-api/src/routes/app.js @@ -13,7 +13,9 @@ const personnelController = require("../controllers/app/personnel/personnel.cont const partsController = require("../controllers/app/parts/parts.controller"); const equipmentPartsController = require("../controllers/app/equipmentParts/equipmentParts.controller"); // 추가 const departmentController = require("../controllers/app/department/department.controller"); +const healthController = require("../controllers/app/health/health.controller"); +router.use("/health", healthController); router.use("/auth", authController); router.use("/users", usersController); router.use("/dashboard", dashboardController); diff --git a/fems-api/src/services/system.service.js b/fems-api/src/services/system.service.js new file mode 100644 index 0000000..a49c000 --- /dev/null +++ b/fems-api/src/services/system.service.js @@ -0,0 +1,123 @@ +// src/services/system.service.js +const Redis = require("ioredis"); +const mongoose = require("mongoose"); +const logger = require("../config/logger"); +const config = require("../config/config"); + +class SystemService { + constructor() { + this.redis = new Redis(config.redis); + } + + async getSystemStatus() { + try { + // Redis 상태 체크 + // const redisStatus = await this.checkRedisStatus(); + + // 데이터베이스 상태 체크 + // const dbStatus = await this.checkDatabaseStatus(); + + // 서비스 상태 체크 + const servicesStatus = await this.checkServicesStatus(); + + return { + // database: dbStatus, + // redis: redisStatus, + services: servicesStatus, + }; + } catch (error) { + logger.error("Failed to get system status:", error); + throw error; + } + } + +// async checkRedisStatus() { +// try { +// await this.redis.ping(); +// return { +// status: "healthy", +// latency: await this.measureRedisLatency(), +// }; +// } catch (error) { +// logger.error("Redis health check failed:", error); +// return { +// status: "unhealthy", +// error: error.message, +// }; +// } +// } + +// async checkDatabaseStatus() { +// try { +// const status = mongoose.connection.readyState; +// const statusMap = { +// 0: "disconnected", +// 1: "connected", +// 2: "connecting", +// 3: "disconnecting", +// }; + +// return { +// status: status === 1 ? "healthy" : "unhealthy", +// state: statusMap[status], +// latency: await this.measureDatabaseLatency(), +// }; +// } catch (error) { +// logger.error("Database health check failed:", error); +// return { +// status: "unhealthy", +// error: error.message, +// }; +// } +// } + + async checkServicesStatus() { + // 필요한 서비스들의 상태 체크 + const services = { + api: await this.checkAPIStatus(), + scheduler: await this.checkSchedulerStatus(), + worker: await this.checkWorkerStatus(), + }; + + return services; + } + + async measureRedisLatency() { + const start = process.hrtime(); + await this.redis.ping(); + const [seconds, nanoseconds] = process.hrtime(start); + return (seconds * 1000 + nanoseconds / 1000000).toFixed(2); + } + + async measureDatabaseLatency() { + const start = process.hrtime(); + await mongoose.connection.db.admin().ping(); + const [seconds, nanoseconds] = process.hrtime(start); + return (seconds * 1000 + nanoseconds / 1000000).toFixed(2); + } + + async checkAPIStatus() { + return { + status: "healthy", + uptime: process.uptime(), + }; + } + + async checkSchedulerStatus() { + // 스케줄러 상태 체크 로직 + return { + status: "healthy", + lastRun: new Date().toISOString(), + }; + } + + async checkWorkerStatus() { + // 워커 상태 체크 로직 + return { + status: "healthy", + activeWorkers: 0, + }; + } +} + +module.exports = new SystemService();