UNPKG

@qrvey/health-checker

Version:

![install size](https://packagephobia.com/badge?p=@qrvey/health-checker) ![coverage](https://img.shields.io/badge/unit_test_coverage-87%25-brightgreen)

419 lines (410 loc) 12.8 kB
import { FetchService } from '@qrvey/fetch'; import { createClient } from 'redis'; import { format, createLogger, transports } from 'winston'; import { Client } from 'pg'; var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); // src/registry.ts var registered = /* @__PURE__ */ new Set(); function registerHealthCheck(dependency) { const deps = Array.isArray(dependency) ? dependency : [dependency]; deps.forEach((d) => registered.add(d)); } function getRegisteredHealthChecks() { return Array.from(registered); } function clearRegistry() { registered.clear(); } var SystemStatusGateway = class { static async getHealthReport(body) { const endpoint = `/api/system/v1/status`; const data = await FetchService.post(endpoint, body, { privateDomain: true, useApiKey: true }); return data; } }; var { combine, timestamp, printf, colorize } = format; var logFormat = printf(({ level, message, timestamp: timestamp2 }) => { return `${timestamp2} [${level}]: ${message}`; }); var baseLogger = createLogger({ level: "info", format: combine( timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), colorize(), logFormat ), transports: [new transports.Console()] }); function formatMessage(context, data) { if (!data) return context; let detail = ""; if (data instanceof Error) { detail = data.stack || data.message; } else if (typeof data === "object") { try { detail = JSON.stringify(data); } catch (e) { detail = "[Unserializable object]"; } } else { detail = String(data); } return `${context} ${detail}`; } var logger = { info: (msg, data) => baseLogger.info(formatMessage(msg, data)), warn: (msg, data) => baseLogger.warn(formatMessage(msg, data)), error: (msg, data) => baseLogger.error(formatMessage(msg, data)), debug: (msg, data) => baseLogger.debug(formatMessage(msg, data)) }; var logger_default = logger; // src/utils/requireEnv.ts function requireEnv(varName) { const value = process.env[varName]; if (!value) { logger_default.error(`[HealthCheck] Missing env variable: ${varName}`); throw new Error( `[HealthCheck] Missing required environment variable: ${varName}` ); } return value; } // src/utils/constants.ts var DEFAULT_HEALTH_CHECK_TIMEOUT = 5e3; var OK = "OK"; var FAILED = "FAILED"; var DEFAULT_HEALTH_STATUS = OK; var SYSTEM_STATUS_GATEWAY_CONTEXT = "system_status_gateway"; // src/services/cache/redisHealthChecker.service.ts function createRedisClient() { const redisUrl = requireEnv("REDIS_URL"); return createClient({ url: redisUrl, socket: { connectTimeout: DEFAULT_HEALTH_CHECK_TIMEOUT } }); } async function connectAndPing(client) { await client.connect(); await client.ping(); } async function closeConnection(client) { try { await client.quit(); } catch (error) { logger_default.warn( "[RedisHealthChecker] Failed to close Redis connection", error ); } } var RedisHealthChecker = { dependency: "cache", async check() { const client = createRedisClient(); try { await connectAndPing(client); if (process.env.NODE_ENV === "test") { logger_default.info("[RedisHealthCheck] check executed successfully"); } } catch (error) { logger_default.error("[RedisHealthChecker] Connection failed", error); throw error; } finally { await closeConnection(client); } } }; var PG_ENV_VAR = "MULTIPLATFORM_PG_CONNECTION_STRING"; async function connectAndQuery(client) { await client.connect(); await client.query("SELECT 1"); } async function closeConnection2(client) { try { await client.end(); } catch (error) { logger_default.warn( "[PostgreSQLHealthChecker] Failed to close connection", error ); } } var PostgreSQLHealthChecker = { dependency: "database", async check() { const connectionString = requireEnv(PG_ENV_VAR); const client = new Client({ connectionString }); try { await connectAndQuery(client); if (process.env.NODE_ENV === "test") { logger_default.info( "[PostgreSQLHealthChecker] check executed successfully" ); } } catch (error) { logger_default.error( "[PostgreSQLHealthChecker] Failed to connect or query", error ); throw error; } finally { await closeConnection2(client); } } }; function getRabbitCredentials() { const user = requireEnv("RABBITMQ_USER"); const pass = requireEnv("RABBITMQ_PASSWORD"); return { user, pass }; } function getRabbitHttpHost() { const raw = requireEnv("RABBITMQ_HTTP_HOST"); const url = new URL(raw); const basePath = url.pathname.replace(/\/$/, ""); return `${url.protocol}//${url.host}${basePath}`; } function buildRabbitHttpConfig() { const { user, pass } = getRabbitCredentials(); const basicAuth = Buffer.from(`${user}:${pass}`).toString("base64"); const authHeader = `Basic ${basicAuth}`; return { baseDomain: getRabbitHttpHost(), authHeader }; } async function ping() { const { baseDomain, authHeader } = buildRabbitHttpConfig(); const res = await fetch(`${baseDomain}/api/overview`, { headers: { Authorization: authHeader } }); if (!res.ok) { throw new Error( `[RabbitMQHealthChecker] Management API ping failed: ${res.statusText}` ); } } function listConsumers() { const { baseDomain, authHeader } = buildRabbitHttpConfig(); return FetchService.get("/api/consumers", { baseDomain, headers: { Authorization: authHeader } }); } async function getConsumersSet() { return new Set( (await listConsumers()).map( (c) => { var _a, _b, _c; return `${(_a = c == null ? void 0 : c.consumer_tag) != null ? _a : ""}::${(_c = (_b = c == null ? void 0 : c.queue) == null ? void 0 : _b.name) != null ? _c : ""}`; } ) ); } async function validateQueues(queues, consumerTag, consumersSet) { const missingQueues = queues.filter( (queue) => !consumersSet.has(`${consumerTag}::${queue}`) ); if (missingQueues.length > 0) { throw new Error( `[RabbitMQHealthChecker] Missing subscriptions for queues: ${missingQueues.join( ", " )} for consumerTag ${consumerTag}` ); } } async function handleConsumersCheck(param, metadata) { var _a, _b, _c, _d; const shouldLoadConsumers = ((_a = param == null ? void 0 : param.queues) == null ? void 0 : _a.length) || (param == null ? void 0 : param.returnConsumersSet); if (!shouldLoadConsumers) return; const consumersSet = (_b = param == null ? void 0 : param.consumersSet) != null ? _b : await getConsumersSet(); if ((_c = param == null ? void 0 : param.queues) == null ? void 0 : _c.length) { const consumerTag = (_d = param.hostName) != null ? _d : requireEnv("HOSTNAME"); await validateQueues(param.queues, consumerTag, consumersSet); } if (param == null ? void 0 : param.returnConsumersSet) { metadata.consumersSet = consumersSet; } } async function performCheck(param) { const metadata = {}; if (!(param == null ? void 0 : param.omitPing)) await ping(); await handleConsumersCheck(param, metadata); if (process.env.NODE_ENV === "test") { logger_default.info("[RabbitMQHealthChecker] check executed successfully"); } return { status: OK, metadata }; } var RabbitMQHealthChecker = { dependency: "eventBroker", async check(param) { try { return await performCheck(param); } catch (error) { logger_default.error( "[RabbitMQHealthChecker] Health check failed", error ); throw error; } } }; // src/utils/checkersMap.ts var checkersMap = { cache: RedisHealthChecker, database: PostgreSQLHealthChecker, eventBroker: RabbitMQHealthChecker }; // src/utils/withTimeout.ts function withTimeout(context, healthCheckFn, ms = DEFAULT_HEALTH_CHECK_TIMEOUT) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { const message = `[${context}] HealthChecker timeout after ${ms}ms`; logger_default.error(message); reject(new Error(message)); }, ms); healthCheckFn.then((value) => { clearTimeout(timeoutId); resolve(value); }).catch((error) => { clearTimeout(timeoutId); reject(error); }); }); } async function withTimeoutAndTiming(context, healthCheckPromise, ms = DEFAULT_HEALTH_CHECK_TIMEOUT) { const start = Date.now(); const result = await withTimeout(context, healthCheckPromise, ms); const durationMs = Date.now() - start; const maybeMetadata = result == null ? void 0 : result.metadata; return { durationMs, metadata: typeof maybeMetadata === "object" ? maybeMetadata : void 0 }; } // src/services/healthCheck.service.ts var HealthCheckService = class { static async check(dependencies, params = {}) { const selectedDependencies = resolveDependencies(dependencies); if (!params.force) { const result = await tryFetchRemoteHealthReport( selectedDependencies, params ); if (result) return result; } const results = await runDependencyChecks(selectedDependencies, params); return buildDetails(selectedDependencies, results); } }; function resolveDependencies(dependencies, skip) { const all = dependencies != null ? dependencies : getRegisteredHealthChecks(); return all; } async function runDependencyChecks(dependencies, params = {}) { return Promise.allSettled( dependencies.map(async (dep) => { var _a; const checker = checkersMap[dep]; if (!checker) { const msg = `[HealthCheckService] No checker found for dependency "${dep}"`; logger_default.error(msg); return Promise.reject(new Error(msg)); } const depParams = params; const dependencyParam = (_a = depParams[dep]) != null ? _a : {}; const mergedParam = __spreadProps(__spreadValues({}, dependencyParam), { hostName: params.hostName }); try { return await withTimeoutAndTiming( dep, checker.check(mergedParam) ); } catch (err) { logger_default.error(`[${dep}] HealthChecker threw an error`, err); return Promise.reject(err); } }) ); } async function tryFetchRemoteHealthReport(dependencies, params) { try { const { skip = [], force = false, eventBroker = {}, hostName } = params; const body = { dependencies, service: hostName != null ? hostName : process.env.HOSTNAME || "unknown", skip, force, params: { eventBroker } }; const result = await withTimeout( SYSTEM_STATUS_GATEWAY_CONTEXT, SystemStatusGateway.getHealthReport(body) ); return result; } catch (err) { const message = err instanceof Error ? err.message : "Unknown error during remote check"; logger_default.warn( `[HealthCheckService] Remote health check fallback triggered: ${message}` ); return null; } } function buildDetails(dependencies, results) { var _a, _b, _c; const details = {}; let hasFailure = false; for (let i = 0; i < dependencies.length; i++) { const dep = dependencies[i]; const result = results[i]; if (result.status === "fulfilled") { details[dep] = { status: OK, durationMs: result.value.durationMs, metadata: (_a = result.value.metadata) != null ? _a : {} }; } else { details[dep] = { status: FAILED, durationMs: (_c = (_b = result.reason) == null ? void 0 : _b.durationMs) != null ? _c : 0 }; hasFailure = true; } } return { status: hasFailure ? FAILED : OK, details }; } export { RedisHealthChecker as CacheHealthChecker, DEFAULT_HEALTH_STATUS, PostgreSQLHealthChecker as DatabaseHealthChecker, RabbitMQHealthChecker as EventBrokerHealthChecker, FAILED, HealthCheckService, OK, clearRegistry, registerHealthCheck }; //# sourceMappingURL=out.js.map //# sourceMappingURL=index.mjs.map