telemetry-error.tsβ’6.09 kB
/**
* Telemetry Error Classes
* Custom error types for telemetry system with enhanced tracking
*/
import { TelemetryErrorType, TelemetryErrorContext } from './telemetry-types';
import { logger } from '../utils/logger';
// Re-export types for convenience
export { TelemetryErrorType, TelemetryErrorContext } from './telemetry-types';
export class TelemetryError extends Error {
public readonly type: TelemetryErrorType;
public readonly context?: Record<string, any>;
public readonly timestamp: number;
public readonly retryable: boolean;
constructor(
type: TelemetryErrorType,
message: string,
context?: Record<string, any>,
retryable: boolean = false
) {
super(message);
this.name = 'TelemetryError';
this.type = type;
this.context = context;
this.timestamp = Date.now();
this.retryable = retryable;
// Ensure proper prototype chain
Object.setPrototypeOf(this, TelemetryError.prototype);
}
/**
* Convert error to context object
*/
toContext(): TelemetryErrorContext {
return {
type: this.type,
message: this.message,
context: this.context,
timestamp: this.timestamp,
retryable: this.retryable
};
}
/**
* Log the error with appropriate level
*/
log(): void {
const logContext = {
type: this.type,
message: this.message,
...this.context
};
if (this.retryable) {
logger.debug('Retryable telemetry error:', logContext);
} else {
logger.debug('Non-retryable telemetry error:', logContext);
}
}
}
/**
* Circuit Breaker for handling repeated failures
*/
export class TelemetryCircuitBreaker {
private failureCount: number = 0;
private lastFailureTime: number = 0;
private state: 'closed' | 'open' | 'half-open' = 'closed';
private readonly failureThreshold: number;
private readonly resetTimeout: number;
private readonly halfOpenRequests: number;
private halfOpenCount: number = 0;
constructor(
failureThreshold: number = 5,
resetTimeout: number = 60000, // 1 minute
halfOpenRequests: number = 3
) {
this.failureThreshold = failureThreshold;
this.resetTimeout = resetTimeout;
this.halfOpenRequests = halfOpenRequests;
}
/**
* Check if requests should be allowed
*/
shouldAllow(): boolean {
const now = Date.now();
switch (this.state) {
case 'closed':
return true;
case 'open':
// Check if enough time has passed to try half-open
if (now - this.lastFailureTime > this.resetTimeout) {
this.state = 'half-open';
this.halfOpenCount = 0;
logger.debug('Circuit breaker transitioning to half-open');
return true;
}
return false;
case 'half-open':
// Allow limited requests in half-open state
if (this.halfOpenCount < this.halfOpenRequests) {
this.halfOpenCount++;
return true;
}
return false;
default:
return false;
}
}
/**
* Record a success
*/
recordSuccess(): void {
if (this.state === 'half-open') {
// If we've had enough successful requests, close the circuit
if (this.halfOpenCount >= this.halfOpenRequests) {
this.state = 'closed';
this.failureCount = 0;
logger.debug('Circuit breaker closed after successful recovery');
}
} else if (this.state === 'closed') {
// Reset failure count on success
this.failureCount = 0;
}
}
/**
* Record a failure
*/
recordFailure(error?: Error): void {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.state === 'half-open') {
// Immediately open on failure in half-open state
this.state = 'open';
logger.debug('Circuit breaker opened from half-open state', { error: error?.message });
} else if (this.state === 'closed' && this.failureCount >= this.failureThreshold) {
// Open circuit after threshold reached
this.state = 'open';
logger.debug(
`Circuit breaker opened after ${this.failureCount} failures`,
{ error: error?.message }
);
}
}
/**
* Get current state
*/
getState(): { state: string; failureCount: number; canRetry: boolean } {
return {
state: this.state,
failureCount: this.failureCount,
canRetry: this.shouldAllow()
};
}
/**
* Force reset the circuit breaker
*/
reset(): void {
this.state = 'closed';
this.failureCount = 0;
this.lastFailureTime = 0;
this.halfOpenCount = 0;
}
}
/**
* Error aggregator for tracking error patterns
*/
export class TelemetryErrorAggregator {
private errors: Map<TelemetryErrorType, number> = new Map();
private errorDetails: TelemetryErrorContext[] = [];
private readonly maxDetails: number = 100;
/**
* Record an error
*/
record(error: TelemetryError): void {
// Increment counter for this error type
const count = this.errors.get(error.type) || 0;
this.errors.set(error.type, count + 1);
// Store error details (limited)
this.errorDetails.push(error.toContext());
if (this.errorDetails.length > this.maxDetails) {
this.errorDetails.shift();
}
}
/**
* Get error statistics
*/
getStats(): {
totalErrors: number;
errorsByType: Record<string, number>;
mostCommonError?: string;
recentErrors: TelemetryErrorContext[];
} {
const errorsByType: Record<string, number> = {};
let totalErrors = 0;
let mostCommonError: string | undefined;
let maxCount = 0;
for (const [type, count] of this.errors.entries()) {
errorsByType[type] = count;
totalErrors += count;
if (count > maxCount) {
maxCount = count;
mostCommonError = type;
}
}
return {
totalErrors,
errorsByType,
mostCommonError,
recentErrors: this.errorDetails.slice(-10) // Last 10 errors
};
}
/**
* Clear error history
*/
reset(): void {
this.errors.clear();
this.errorDetails = [];
}
}