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
This commit is contained in:
Claude
2026-01-24 20:04:53 +00:00
parent 22eda03eb2
commit d132e1cfd8
5 changed files with 243 additions and 15 deletions
+2 -2
View File
@@ -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) {
+20 -9
View File
@@ -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
}
}
+209
View File
@@ -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'
}
}
+3
View File
@@ -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'
+9 -4
View File
@@ -86,11 +86,16 @@ async function readCacheFile(filePath: string): Promise<VersionCacheEntry | null
/**
* Writes the cache to file
*/
async function writeCacheFile(filePath: string, entry: VersionCacheEntry): Promise<void> {
async function writeCacheFile(
filePath: string,
entry: VersionCacheEntry,
logger?: VersionCacheLogger
): Promise<void> {
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 },