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:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user