From d132e1cfd8eb5cf9dd672b2be2adc99fe03b33e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 20:04:53 +0000 Subject: [PATCH] fix: address code review feedback (batch 2) 1. Add logger to writeCacheFile (version-cache.ts) - Now logs warnings when file write fails - Helps debugging in production environments 2. Sanitize logging in parseGroupResult (communities.ts) - Changed from info to debug level - Removed full node/groupNode dumps (sensitive data) - Now only logs nodeTag and groupId 3. Add input validation in parseNewsletterCreateResponse (newsletter.ts) - Validates response structure before destructuring - Adds fallback values for parseInt (prevents NaN) - Adds null checks for optional fields 4. Add health-status.ts module - getHealthStatus(): Full health check with circuit breakers, cache, memory - isHealthy(): Simple boolean for liveness probes - getSimpleHealthStatus(): Returns 'ok', 'degraded', or 'error' - Useful for k8s probes and monitoring dashboards --- src/Socket/communities.ts | 4 +- src/Socket/newsletter.ts | 29 +++-- src/Utils/health-status.ts | 209 +++++++++++++++++++++++++++++++++++++ src/Utils/index.ts | 3 + src/Utils/version-cache.ts | 13 ++- 5 files changed, 243 insertions(+), 15 deletions(-) create mode 100644 src/Utils/health-status.ts diff --git a/src/Socket/communities.ts b/src/Socket/communities.ts index 7cfabfd2..0d99e9ea 100644 --- a/src/Socket/communities.ts +++ b/src/Socket/communities.ts @@ -78,11 +78,11 @@ export const makeCommunitiesSocket = (config: SocketConfig) => { } async function parseGroupResult(node: BinaryNode) { - logger.info({ node }, 'parseGroupResult') + logger.debug({ nodeTag: node.tag }, 'parseGroupResult') const groupNode = getBinaryNodeChild(node, 'group') if (groupNode) { try { - logger.info({ groupNode }, 'groupNode') + logger.debug({ groupId: groupNode.attrs?.id }, 'parsing group metadata') const metadata = await sock.groupMetadata(`${groupNode.attrs.id}@g.us`) return metadata ? metadata : Optional.empty() } catch (error) { diff --git a/src/Socket/newsletter.ts b/src/Socket/newsletter.ts index 00c8555c..dac16139 100644 --- a/src/Socket/newsletter.ts +++ b/src/Socket/newsletter.ts @@ -7,21 +7,32 @@ import { makeGroupsSocket } from './groups' import { executeWMexQuery as genericExecuteWMexQuery } from './mex' const parseNewsletterCreateResponse = (response: NewsletterCreateResponse): NewsletterMetadata => { + // Validate response structure before destructuring + if (!response?.id || !response?.thread_metadata) { + throw new Error('Invalid newsletter response: missing id or thread_metadata') + } + const { id, thread_metadata: thread, viewer_metadata: viewer } = response + + // Validate required thread metadata fields + if (!thread.name?.text) { + throw new Error('Invalid newsletter response: missing thread name') + } + return { id: id, owner: undefined, name: thread.name.text, - creation_time: parseInt(thread.creation_time, 10), - description: thread.description.text, - invite: thread.invite, - subscribers: parseInt(thread.subscribers_count, 10), + creation_time: parseInt(thread.creation_time, 10) || 0, + description: thread.description?.text || '', + invite: thread.invite || '', + subscribers: parseInt(thread.subscribers_count, 10) || 0, verification: thread.verification, - picture: { - id: thread.picture.id, - directPath: thread.picture.direct_path - }, - mute_state: viewer.mute + picture: thread.picture ? { + id: thread.picture.id || '', + directPath: thread.picture.direct_path || '' + } : { id: '', directPath: '' }, + mute_state: viewer?.mute } } diff --git a/src/Utils/health-status.ts b/src/Utils/health-status.ts new file mode 100644 index 00000000..25a92033 --- /dev/null +++ b/src/Utils/health-status.ts @@ -0,0 +1,209 @@ +/** + * Health Status Utilities + * + * Provides health check and status information for monitoring and k8s probes. + * + * @module Utils/health-status + */ + +import { getVersionCacheStatus } from './version-cache.js' +import { globalCircuitRegistry } from './circuit-breaker.js' + +/** + * Circuit breaker health information + */ +export interface CircuitBreakerHealth { + name: string + state: 'closed' | 'open' | 'half-open' + failures: number + successes: number + totalCalls: number +} + +/** + * Cache health information + */ +export interface CacheHealth { + versionCache: { + hasCache: boolean + isExpired: boolean + ageMs: number | null + source: string | null + } +} + +/** + * Overall health status + */ +export interface HealthStatus { + status: 'healthy' | 'degraded' | 'unhealthy' + timestamp: number + uptime: number + version: string + circuitBreakers: CircuitBreakerHealth[] + cache: CacheHealth + checks: { + name: string + status: 'pass' | 'warn' | 'fail' + message?: string + }[] +} + +/** + * Get the current health status of the Baileys instance. + * + * Useful for: + * - Kubernetes liveness/readiness probes + * - Load balancer health checks + * - Monitoring dashboards + * + * @returns HealthStatus object with detailed status information + * + * @example + * ```typescript + * import { getHealthStatus } from '@whiskeysockets/baileys' + * + * // Simple health check endpoint + * app.get('/health', (req, res) => { + * const health = getHealthStatus() + * const statusCode = health.status === 'healthy' ? 200 : + * health.status === 'degraded' ? 200 : 503 + * res.status(statusCode).json(health) + * }) + * + * // Kubernetes probe + * app.get('/healthz', (req, res) => { + * const health = getHealthStatus() + * res.status(health.status !== 'unhealthy' ? 200 : 503).send(health.status) + * }) + * ``` + */ +export function getHealthStatus(): HealthStatus { + const checks: HealthStatus['checks'] = [] + let overallStatus: HealthStatus['status'] = 'healthy' + + // 1. Check version cache + const versionCacheStatus = getVersionCacheStatus() + if (!versionCacheStatus.hasCache) { + checks.push({ + name: 'version_cache', + status: 'warn', + message: 'No version cache available' + }) + if (overallStatus === 'healthy') overallStatus = 'degraded' + } else if (versionCacheStatus.isExpired) { + checks.push({ + name: 'version_cache', + status: 'warn', + message: 'Version cache is expired' + }) + if (overallStatus === 'healthy') overallStatus = 'degraded' + } else { + checks.push({ + name: 'version_cache', + status: 'pass' + }) + } + + // 2. Check circuit breakers + const circuitBreakers: CircuitBreakerHealth[] = [] + let openCircuits = 0 + + for (const [name, breaker] of globalCircuitRegistry.getAll()) { + const stats = breaker.getStats() + const state = breaker.getState() + + circuitBreakers.push({ + name, + state, + failures: stats.totalFailures, + successes: stats.totalSuccesses, + totalCalls: stats.totalCalls + }) + + if (state === 'open') { + openCircuits++ + } + } + + if (openCircuits > 0) { + checks.push({ + name: 'circuit_breakers', + status: openCircuits > 2 ? 'fail' : 'warn', + message: `${openCircuits} circuit breaker(s) are open` + }) + if (openCircuits > 2) { + overallStatus = 'unhealthy' + } else if (overallStatus === 'healthy') { + overallStatus = 'degraded' + } + } else { + checks.push({ + name: 'circuit_breakers', + status: 'pass' + }) + } + + // 3. Check memory usage (warn if > 90%) + const memUsage = process.memoryUsage() + const heapUsedPercent = (memUsage.heapUsed / memUsage.heapTotal) * 100 + + if (heapUsedPercent > 90) { + checks.push({ + name: 'memory', + status: 'warn', + message: `Heap usage is ${heapUsedPercent.toFixed(1)}%` + }) + if (overallStatus === 'healthy') overallStatus = 'degraded' + } else { + checks.push({ + name: 'memory', + status: 'pass' + }) + } + + return { + status: overallStatus, + timestamp: Date.now(), + uptime: process.uptime(), + version: process.env.npm_package_version || 'unknown', + circuitBreakers, + cache: { + versionCache: { + hasCache: versionCacheStatus.hasCache, + isExpired: versionCacheStatus.isExpired, + ageMs: versionCacheStatus.age, + source: versionCacheStatus.source + } + }, + checks + } +} + +/** + * Simple health check - returns true if system is healthy or degraded. + * Use this for basic liveness probes. + * + * @returns true if healthy or degraded, false if unhealthy + */ +export function isHealthy(): boolean { + const status = getHealthStatus() + return status.status !== 'unhealthy' +} + +/** + * Get a simple status string for minimal health endpoints. + * + * @returns 'ok', 'degraded', or 'error' + */ +export function getSimpleHealthStatus(): 'ok' | 'degraded' | 'error' { + const status = getHealthStatus() + switch (status.status) { + case 'healthy': + return 'ok' + case 'degraded': + return 'degraded' + case 'unhealthy': + return 'error' + } +} diff --git a/src/Utils/index.ts b/src/Utils/index.ts index 4d4a493a..0b376f3d 100644 --- a/src/Utils/index.ts +++ b/src/Utils/index.ts @@ -42,5 +42,8 @@ export * from './unified-session' // Version management export * from './version-cache' +// Health monitoring +export * from './health-status' + // Event streaming export * from './baileys-event-stream' diff --git a/src/Utils/version-cache.ts b/src/Utils/version-cache.ts index 9941d890..a5cd9679 100644 --- a/src/Utils/version-cache.ts +++ b/src/Utils/version-cache.ts @@ -86,11 +86,16 @@ async function readCacheFile(filePath: string): Promise { +async function writeCacheFile( + filePath: string, + entry: VersionCacheEntry, + logger?: VersionCacheLogger +): Promise { try { await fs.writeFile(filePath, JSON.stringify(entry, null, 2), 'utf-8') - } catch { - // Ignore write errors - cache is optional + } catch (error) { + // Log write errors for debugging - cache is optional but failures should be visible + logger?.warn({ error, filePath }, 'Failed to write version cache file') } } @@ -124,7 +129,7 @@ async function fetchVersionOnce( memoryCache = entry // Persist to file (async, don't wait) - writeCacheFile(cacheFilePath, entry).catch(() => {}) + writeCacheFile(cacheFilePath, entry, logger).catch(() => {}) logger?.info( { version: entry.version, source: entry.source },