feat(utils): add observability and resilience utilities

Implement comprehensive utility modules for InfiniteAPI:

Logging:
- structured-logger: JSON logging with levels, sanitization, metrics
- logger-adapter: Pino/Console/Structured adapter pattern
- baileys-logger: WhatsApp-specific logger with categories

Observability:
- trace-context: Distributed tracing with spans, correlation IDs
- prometheus-metrics: Counter, Gauge, Histogram, Summary metrics

Resilience:
- cache-utils: LRU cache with TTL, multi-level support
- circuit-breaker: 3-state circuit breaker with fallbacks
- retry-utils: Exponential backoff with jitter, predicates

Event Streaming:
- baileys-event-stream: Priority queues, backpressure, DLQ

Includes unit tests for all new modules.
This commit is contained in:
Claude
2026-01-20 01:26:06 +00:00
parent 5b5ac85443
commit 882a1f57cf
16 changed files with 7695 additions and 0 deletions
+635
View File
@@ -0,0 +1,635 @@
/**
* @fileoverview Lógica de retry inteligente
* @module Utils/retry-utils
*
* Fornece:
* - Exponential backoff
* - Jitter para evitar thundering herd
* - Max attempts configurável
* - Predicates de retry customizáveis
* - Integração com circuit breaker
* - Hooks de eventos
* - Cancelamento
*/
import { EventEmitter } from 'events'
import { metrics } from './prometheus-metrics.js'
import type { CircuitBreaker } from './circuit-breaker.js'
/**
* Estratégias de backoff
*/
export type BackoffStrategy = 'exponential' | 'linear' | 'constant' | 'fibonacci'
/**
* Opções de configuração de retry
*/
export interface RetryOptions {
/** Número máximo de tentativas (default: 3) */
maxAttempts?: number
/** Delay base em ms (default: 1000) */
baseDelay?: number
/** Delay máximo em ms (default: 30000) */
maxDelay?: number
/** Estratégia de backoff (default: exponential) */
backoffStrategy?: BackoffStrategy
/** Multiplicador para exponential backoff (default: 2) */
backoffMultiplier?: number
/** Percentual de jitter (0-1, default: 0.1) */
jitter?: number
/** Função para determinar se deve retry */
shouldRetry?: (error: Error, attempt: number) => boolean | Promise<boolean>
/** Timeout por tentativa em ms */
timeout?: number
/** Nome da operação para métricas */
operationName?: string
/** Coletar métricas */
collectMetrics?: boolean
/** Circuit breaker para integração */
circuitBreaker?: CircuitBreaker
/** Callback antes de cada retry */
onRetry?: (error: Error, attempt: number, delay: number) => void | Promise<void>
/** Callback em sucesso */
onSuccess?: (result: unknown, attempt: number) => void
/** Callback em falha final */
onFailure?: (error: Error, attempts: number) => void
/** Signal para cancelamento */
abortSignal?: AbortSignal
}
/**
* Resultado de operação com retry
*/
export interface RetryResult<T> {
success: boolean
result?: T
error?: Error
attempts: number
totalDuration: number
lastAttemptDuration: number
}
/**
* Contexto de retry
*/
export interface RetryContext {
attempt: number
maxAttempts: number
lastError?: Error
startTime: number
aborted: boolean
}
/**
* Erro de retry esgotado
*/
export class RetryExhaustedError extends Error {
constructor(
public readonly originalError: Error,
public readonly attempts: number,
public readonly operationName?: string
) {
super(
`Retry exhausted after ${attempts} attempts${operationName ? ` for "${operationName}"` : ''}: ${originalError.message}`
)
this.name = 'RetryExhaustedError'
}
}
/**
* Erro de abort
*/
export class RetryAbortedError extends Error {
constructor(public readonly attempt: number) {
super(`Retry aborted at attempt ${attempt}`)
this.name = 'RetryAbortedError'
}
}
/**
* Calcula delay com base na estratégia
*/
export function calculateDelay(
attempt: number,
baseDelay: number,
maxDelay: number,
strategy: BackoffStrategy,
multiplier: number,
jitter: number
): number {
let delay: number
switch (strategy) {
case 'exponential':
delay = baseDelay * Math.pow(multiplier, attempt - 1)
break
case 'linear':
delay = baseDelay * attempt
break
case 'constant':
delay = baseDelay
break
case 'fibonacci': {
const fib = fibonacciNumber(attempt)
delay = baseDelay * fib
break
}
default:
delay = baseDelay
}
// Aplicar cap de delay máximo
delay = Math.min(delay, maxDelay)
// Aplicar jitter
if (jitter > 0) {
const jitterAmount = delay * jitter
delay = delay + (Math.random() * 2 - 1) * jitterAmount
}
return Math.max(0, Math.round(delay))
}
/**
* Calcula número de Fibonacci
*/
function fibonacciNumber(n: number): number {
if (n <= 1) return 1
let a = 1,
b = 1
for (let i = 2; i < n; i++) {
const c = a + b
a = b
b = c
}
return b
}
/**
* Sleep com suporte a abort
*/
async function sleep(ms: number, signal?: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, ms)
if (signal) {
if (signal.aborted) {
clearTimeout(timer)
reject(new RetryAbortedError(0))
return
}
const abortHandler = () => {
clearTimeout(timer)
reject(new RetryAbortedError(0))
}
signal.addEventListener('abort', abortHandler, { once: true })
}
})
}
/**
* Executa operação com timeout
*/
async function executeWithTimeout<T>(
operation: () => Promise<T>,
timeout: number,
signal?: AbortSignal
): Promise<T> {
return new Promise<T>((resolve, reject) => {
let completed = false
const timer = setTimeout(() => {
if (!completed) {
completed = true
reject(new Error(`Operation timed out after ${timeout}ms`))
}
}, timeout)
if (signal?.aborted) {
clearTimeout(timer)
reject(new RetryAbortedError(0))
return
}
operation()
.then((result) => {
if (!completed) {
completed = true
clearTimeout(timer)
resolve(result)
}
})
.catch((error) => {
if (!completed) {
completed = true
clearTimeout(timer)
reject(error)
}
})
})
}
/**
* Função principal de retry
*/
export async function retry<T>(
operation: (context: RetryContext) => T | Promise<T>,
options: RetryOptions = {}
): Promise<T> {
const config = {
maxAttempts: options.maxAttempts ?? 3,
baseDelay: options.baseDelay ?? 1000,
maxDelay: options.maxDelay ?? 30000,
backoffStrategy: options.backoffStrategy ?? 'exponential',
backoffMultiplier: options.backoffMultiplier ?? 2,
jitter: options.jitter ?? 0.1,
shouldRetry: options.shouldRetry ?? (() => true),
timeout: options.timeout,
operationName: options.operationName ?? 'operation',
collectMetrics: options.collectMetrics ?? true,
circuitBreaker: options.circuitBreaker,
onRetry: options.onRetry ?? (() => {}),
onSuccess: options.onSuccess ?? (() => {}),
onFailure: options.onFailure ?? (() => {}),
abortSignal: options.abortSignal,
}
const context: RetryContext = {
attempt: 0,
maxAttempts: config.maxAttempts,
startTime: Date.now(),
aborted: false,
}
let lastError: Error | undefined
// Verificar abort inicial
if (config.abortSignal?.aborted) {
throw new RetryAbortedError(0)
}
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
context.attempt = attempt
// Verificar abort
if (config.abortSignal?.aborted) {
context.aborted = true
throw new RetryAbortedError(attempt)
}
// Verificar circuit breaker
if (config.circuitBreaker?.isOpen()) {
throw new Error(`Circuit breaker "${config.circuitBreaker.getName()}" is open`)
}
try {
// Executar operação
let result: T
if (config.timeout) {
result = await executeWithTimeout(
() => Promise.resolve(operation(context)),
config.timeout,
config.abortSignal
)
} else {
result = await operation(context)
}
// Sucesso
if (config.collectMetrics) {
metrics.retries.inc({ operation: config.operationName })
}
config.onSuccess(result, attempt)
return result
} catch (error) {
lastError = error as Error
context.lastError = lastError
// Verificar se deve retry
const shouldRetry = await config.shouldRetry(lastError, attempt)
if (!shouldRetry || attempt >= config.maxAttempts) {
// Falha final
if (config.collectMetrics) {
metrics.errors.inc({ category: 'retry', code: 'exhausted' })
}
config.onFailure(lastError, attempt)
throw new RetryExhaustedError(lastError, attempt, config.operationName)
}
// Calcular delay
const delay = calculateDelay(
attempt,
config.baseDelay,
config.maxDelay,
config.backoffStrategy,
config.backoffMultiplier,
config.jitter
)
// Callback de retry
await config.onRetry(lastError, attempt, delay)
if (config.collectMetrics) {
metrics.retryLatency.observe({ operation: config.operationName }, delay)
}
// Aguardar delay
await sleep(delay, config.abortSignal)
}
}
// Nunca deve chegar aqui, mas TypeScript precisa
throw new RetryExhaustedError(lastError || new Error('Unknown error'), config.maxAttempts, config.operationName)
}
/**
* Retry com resultado detalhado
*/
export async function retryWithResult<T>(
operation: (context: RetryContext) => T | Promise<T>,
options: RetryOptions = {}
): Promise<RetryResult<T>> {
const startTime = Date.now()
let attempts = 0
let lastAttemptStart = startTime
try {
const result = await retry(
(context) => {
attempts = context.attempt
lastAttemptStart = Date.now()
return operation(context)
},
options
)
return {
success: true,
result,
attempts,
totalDuration: Date.now() - startTime,
lastAttemptDuration: Date.now() - lastAttemptStart,
}
} catch (error) {
return {
success: false,
error: error as Error,
attempts,
totalDuration: Date.now() - startTime,
lastAttemptDuration: Date.now() - lastAttemptStart,
}
}
}
/**
* Factory para criar função de retry configurada
*/
export function createRetrier(defaultOptions: RetryOptions = {}) {
return <T>(
operation: (context: RetryContext) => T | Promise<T>,
options?: RetryOptions
): Promise<T> => {
return retry(operation, { ...defaultOptions, ...options })
}
}
/**
* Decorator para adicionar retry a método
*/
export function withRetry(options: RetryOptions = {}) {
return function (
_target: unknown,
propertyKey: string,
descriptor: TypedPropertyDescriptor<(...args: unknown[]) => unknown>
) {
const originalMethod = descriptor.value
if (!originalMethod) return descriptor
descriptor.value = async function (...args: unknown[]): Promise<unknown> {
return retry(
() => originalMethod.apply(this, args),
{ ...options, operationName: options.operationName || propertyKey }
)
}
return descriptor
}
}
/**
* Wrapper para função com retry
*/
export function retryable<T extends (...args: unknown[]) => unknown>(
fn: T,
options: RetryOptions = {}
): (...args: Parameters<T>) => Promise<ReturnType<T>> {
return async (...args: Parameters<T>): Promise<ReturnType<T>> => {
return retry(() => fn(...args), options) as Promise<ReturnType<T>>
}
}
/**
* Classe para gerenciar retries com estado
*/
export class RetryManager extends EventEmitter {
private activeRetries: Map<string, { cancel: () => void; context: RetryContext }> = new Map()
private defaultOptions: RetryOptions
constructor(defaultOptions: RetryOptions = {}) {
super()
this.defaultOptions = defaultOptions
}
/**
* Executa operação com retry
*/
async execute<T>(
id: string,
operation: (context: RetryContext) => T | Promise<T>,
options?: RetryOptions
): Promise<T> {
// Cancelar retry anterior com mesmo ID
this.cancel(id)
const abortController = new AbortController()
const mergedOptions = { ...this.defaultOptions, ...options, abortSignal: abortController.signal }
const retryPromise = retry((context) => {
this.activeRetries.set(id, {
cancel: () => abortController.abort(),
context,
})
this.emit('attempt', { id, attempt: context.attempt })
return operation(context)
}, mergedOptions)
try {
const result = await retryPromise
this.emit('success', { id })
return result
} catch (error) {
this.emit('failure', { id, error })
throw error
} finally {
this.activeRetries.delete(id)
}
}
/**
* Cancela retry em andamento
*/
cancel(id: string): boolean {
const active = this.activeRetries.get(id)
if (active) {
active.cancel()
this.activeRetries.delete(id)
this.emit('cancelled', { id })
return true
}
return false
}
/**
* Cancela todos os retries
*/
cancelAll(): void {
for (const [id, active] of this.activeRetries) {
active.cancel()
this.emit('cancelled', { id })
}
this.activeRetries.clear()
}
/**
* Verifica se há retry ativo
*/
isActive(id: string): boolean {
return this.activeRetries.has(id)
}
/**
* Retorna contexto de retry ativo
*/
getContext(id: string): RetryContext | undefined {
return this.activeRetries.get(id)?.context
}
/**
* Retorna IDs de retries ativos
*/
getActiveIds(): string[] {
return Array.from(this.activeRetries.keys())
}
}
/**
* Predicates comuns para shouldRetry
*/
export const retryPredicates = {
/** Sempre retry (até max attempts) */
always: () => true,
/** Nunca retry */
never: () => false,
/** Retry apenas em erros de rede */
onNetworkError: (error: Error) => {
const networkErrors = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN']
return networkErrors.some((code) => error.message.includes(code) || (error as NodeJS.ErrnoException).code === code)
},
/** Retry apenas em erros específicos */
onErrorCodes:
(codes: string[]) =>
(error: Error): boolean => {
return codes.some((code) => error.message.includes(code) || (error as NodeJS.ErrnoException).code === code)
},
/** Retry exceto em erros específicos */
exceptErrorCodes:
(codes: string[]) =>
(error: Error): boolean => {
return !codes.some((code) => error.message.includes(code) || (error as NodeJS.ErrnoException).code === code)
},
/** Retry em erros HTTP 5xx ou timeout */
onServerError: (error: Error) => {
const message = error.message.toLowerCase()
return (
message.includes('500') ||
message.includes('502') ||
message.includes('503') ||
message.includes('504') ||
message.includes('timeout')
)
},
/** Combina múltiplos predicates com OR */
or:
(...predicates: Array<(error: Error, attempt: number) => boolean>) =>
(error: Error, attempt: number): boolean => {
return predicates.some((p) => p(error, attempt))
},
/** Combina múltiplos predicates com AND */
and:
(...predicates: Array<(error: Error, attempt: number) => boolean>) =>
(error: Error, attempt: number): boolean => {
return predicates.every((p) => p(error, attempt))
},
}
/**
* Configurações pré-definidas de retry
*/
export const retryConfigs = {
/** Retry agressivo (muitas tentativas, delays curtos) */
aggressive: {
maxAttempts: 10,
baseDelay: 100,
maxDelay: 5000,
backoffStrategy: 'exponential' as const,
jitter: 0.2,
},
/** Retry conservador (poucas tentativas, delays longos) */
conservative: {
maxAttempts: 3,
baseDelay: 2000,
maxDelay: 60000,
backoffStrategy: 'exponential' as const,
jitter: 0.1,
},
/** Retry rápido (para operações curtas) */
fast: {
maxAttempts: 5,
baseDelay: 50,
maxDelay: 1000,
backoffStrategy: 'linear' as const,
jitter: 0.05,
},
/** Retry para operações de rede */
network: {
maxAttempts: 5,
baseDelay: 1000,
maxDelay: 30000,
backoffStrategy: 'exponential' as const,
jitter: 0.1,
shouldRetry: retryPredicates.onNetworkError,
},
}
export default retry