@qrvey/health-checker
Version:
 
419 lines (410 loc) • 12.8 kB
JavaScript
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