refactor(logger): remove private CircuitBreaker from structured-logger

The structured-logger had its OWN private CircuitBreaker class for
external-hook protection — gate hook calls when the user's hook keeps
throwing, to prevent log explosion.

Replaced by simple try/catch + hookFailures metric increment:
- If the user's hook throws synchronously → catch + increment metric.
- If the hook returns a rejected promise → .catch + increment metric.
- We do NOT gate further calls. If the hook is broken, the metric
  goes up rapidly and operators see it. Consistent with the rest of
  the codebase after the socket-layer circuit-breaker removal:
  fast-fail, no state machines, no cross-call gating.

Removed:
- The internal CircuitBreaker class (lines 244-298, 55 lines)
- circuitBreakerThreshold + circuitBreakerResetMs config options
- circuitBreakerTrips metric field (LoggerMetrics)
- circuitBreakerState statistics field (LoggerStatistics)
- circuitBreaker init in constructor + null assignment in destroy()

Build passes; 19 structured-logger tests pass; 35/35 suites pass.
This commit is contained in:
Renato Alcara
2026-04-26 23:15:47 -03:00
parent c557714e72
commit 5017393277
+16 -96
View File
@@ -73,10 +73,6 @@ export interface StructuredLoggerConfig {
maxLogsPerSecond?: number
/** Enable async logging queue (default: false) */
enableAsyncQueue?: boolean
/** Circuit breaker failure threshold for external hooks (default: 5) */
circuitBreakerThreshold?: number
/** Circuit breaker reset timeout in ms (default: 30000) */
circuitBreakerResetMs?: number
/** Enable Prometheus metrics integration (default: false) */
enableMetrics?: boolean
}
@@ -155,8 +151,6 @@ export interface LoggerMetrics {
bufferFlushes: number
/** External hook failures */
hookFailures: number
/** Circuit breaker trips */
circuitBreakerTrips: number
/** Average log processing time in ms */
avgProcessingTimeMs: number
}
@@ -169,8 +163,6 @@ export interface LoggerStatistics extends LoggerMetrics {
bufferSize: number
/** Rate limiter tokens available */
rateLimiterTokens: number
/** Circuit breaker state */
circuitBreakerState: 'closed' | 'open' | 'half-open'
/** Queue size (if async enabled) */
queueSize: number
/** Created timestamp */
@@ -242,69 +234,6 @@ class RateLimiter {
}
}
// ============================================================================
// CIRCUIT BREAKER
// ============================================================================
/**
* Circuit breaker for external hook protection
*/
class CircuitBreaker {
private failures = 0
private lastFailure = 0
private state: 'closed' | 'open' | 'half-open' = 'closed'
constructor(
private readonly threshold: number,
private readonly resetTimeoutMs: number
) {}
async execute<T>(operation: () => Promise<T>): Promise<T | null> {
if (this.state === 'open') {
if (Date.now() - this.lastFailure > this.resetTimeoutMs) {
this.state = 'half-open'
} else {
return null // Circuit is open, skip
}
}
try {
const result = await operation()
this.onSuccess()
return result
} catch (error) {
this.onFailure()
throw error
}
}
private onSuccess(): void {
this.failures = 0
this.state = 'closed'
}
private onFailure(): void {
this.failures++
this.lastFailure = Date.now()
if (this.failures >= this.threshold) {
this.state = 'open'
}
}
getState(): 'closed' | 'open' | 'half-open' {
// Check if should transition from open to half-open
if (this.state === 'open' && Date.now() - this.lastFailure > this.resetTimeoutMs) {
this.state = 'half-open'
}
return this.state
}
getFailures(): number {
return this.failures
}
}
// ============================================================================
// ASYNC LOG QUEUE
// ============================================================================
@@ -403,8 +332,6 @@ export class StructuredLogger implements ILogger {
private rateLimiter: RateLimiter | null = null
// Circuit breaker for external hook
private circuitBreaker: CircuitBreaker | null = null
// Async queue
private asyncQueue: AsyncLogQueue | null = null
@@ -435,8 +362,6 @@ export class StructuredLogger implements ILogger {
enableRateLimiting: config.enableRateLimiting ?? envConfig.enableRateLimiting ?? false,
maxLogsPerSecond: config.maxLogsPerSecond ?? envConfig.maxLogsPerSecond ?? 1000,
enableAsyncQueue: config.enableAsyncQueue ?? envConfig.enableAsyncQueue ?? false,
circuitBreakerThreshold: config.circuitBreakerThreshold ?? 5,
circuitBreakerResetMs: config.circuitBreakerResetMs ?? 30000,
enableMetrics: config.enableMetrics ?? envConfig.enableMetrics ?? false
}
@@ -455,7 +380,6 @@ export class StructuredLogger implements ILogger {
droppedLogs: 0,
bufferFlushes: 0,
hookFailures: 0,
circuitBreakerTrips: 0,
avgProcessingTimeMs: 0
}
@@ -464,11 +388,6 @@ export class StructuredLogger implements ILogger {
this.rateLimiter = new RateLimiter(this.config.maxLogsPerSecond)
}
// Initialize circuit breaker for external hook
if (this.config.externalHook) {
this.circuitBreaker = new CircuitBreaker(this.config.circuitBreakerThreshold, this.config.circuitBreakerResetMs)
}
// Initialize async queue
if (this.config.enableAsyncQueue) {
this.asyncQueue = new AsyncLogQueue()
@@ -569,19 +488,23 @@ export class StructuredLogger implements ILogger {
this.output(entry)
}
// External hook with circuit breaker
// External hook — fire-and-forget. Failures are counted in
// hookFailures metric so chronic breakage is visible to operators
// who can fix their hook. We do not gate further calls — that is
// the consumer's responsibility (consistent with the rest of the
// codebase after the circuit-breaker removal).
const externalHook = this.config.externalHook
if (externalHook && this.circuitBreaker) {
this.circuitBreaker
.execute(async () => {
await Promise.resolve(externalHook(entry))
})
.catch(() => {
this.metrics.hookFailures++
if (this.circuitBreaker?.getState() === 'open') {
this.metrics.circuitBreakerTrips++
}
})
if (externalHook) {
try {
const hookResult = externalHook(entry) as unknown
if (hookResult && typeof (hookResult as Promise<unknown>).then === 'function') {
;(hookResult as Promise<unknown>).catch(() => {
this.metrics.hookFailures++
})
}
} catch {
this.metrics.hookFailures++
}
}
// Track processing time
@@ -834,7 +757,6 @@ export class StructuredLogger implements ILogger {
...this.metrics,
bufferSize: this.buffer.length,
rateLimiterTokens: this.rateLimiter?.getTokens() ?? 0,
circuitBreakerState: this.circuitBreaker?.getState() ?? 'closed',
queueSize: this.asyncQueue?.getSize() ?? 0,
createdAt: this.createdAt,
uptimeMs: Date.now() - this.createdAt
@@ -860,7 +782,6 @@ export class StructuredLogger implements ILogger {
droppedLogs: 0,
bufferFlushes: 0,
hookFailures: 0,
circuitBreakerTrips: 0,
avgProcessingTimeMs: 0
}
this.totalProcessingTime = 0
@@ -893,7 +814,6 @@ export class StructuredLogger implements ILogger {
// Clear references
this.rateLimiter = null
this.circuitBreaker = null
this.metricsModule = null
}
}