digital-samba-mcp-server
Version:
Digital Samba MCP Server - Model Context Protocol server for Digital Samba's video conferencing API
802 lines • 35.6 kB
JavaScript
/**
* Graceful Degradation Module
*
* This module provides mechanisms for handling partial API outages and ensuring
* continued functionality with degraded service levels. It works alongside the
* circuit breaker pattern to provide a comprehensive approach to API resilience.
*
* Key features:
* - Multiple degradation levels based on service health
* - Fallback strategies for critical functionality
* - Cached content serving during outages
* - Intelligent retry mechanisms with exponential backoff
* - Health monitoring and automatic recovery
* - Metrics integration for observability
*
* @module graceful-degradation
* @author Digital Samba Team
* @version 0.1.0
*/
// Node.js modules
import { EventEmitter } from 'events';
import logger from './logger.js';
import { DegradedServiceError } from './errors.js';
import circuitBreakerRegistry, { CircuitState } from './circuit-breaker.js';
/**
* Service health status levels
*/
export var ServiceHealthStatus;
(function (ServiceHealthStatus) {
ServiceHealthStatus["HEALTHY"] = "HEALTHY";
ServiceHealthStatus["PARTIALLY_DEGRADED"] = "PARTIALLY_DEGRADED";
ServiceHealthStatus["SEVERELY_DEGRADED"] = "SEVERELY_DEGRADED";
ServiceHealthStatus["UNAVAILABLE"] = "UNAVAILABLE"; // Service completely unavailable
})(ServiceHealthStatus || (ServiceHealthStatus = {}));
/**
* Graceful degradation service for handling API outages
*
* This class provides mechanisms for handling partial API outages and ensuring
* the application can continue functioning with degraded service levels.
*
* @class GracefulDegradation
* @example
* // Create a graceful degradation service
* const degradationService = new GracefulDegradation({
* cache: new MemoryCache(),
* maxRetryAttempts: 3,
* initialRetryDelay: 1000
* });
*
* // Register a fallback for the listRooms operation
* degradationService.registerFallback('listRooms', {
* fallbackFn: async () => ({ data: [], total_count: 0, length: 0, map: () => [] }),
* isCritical: true
* });
*
* // Execute an operation with graceful degradation
* const result = await degradationService.executeWithFallback(
* 'listRooms',
* () => apiClient.listRooms(),
* { cacheKey: 'rooms' }
* );
*/
export class GracefulDegradation extends EventEmitter {
/**
* Creates a new GracefulDegradation instance
*
* @constructor
* @param {GracefulDegradationOptions} options - Configuration options
*/
constructor(options = {}) {
super();
this.fallbacks = new Map();
this.componentHealth = new Map();
this.overallHealth = ServiceHealthStatus.HEALTHY;
this.cache = options.cache;
this.maxRetryAttempts = options.maxRetryAttempts ?? 3;
this.initialRetryDelay = options.initialRetryDelay ?? 1000;
this.retryBackoffFactor = options.retryBackoffFactor ?? 2;
this.maxRetryDelay = options.maxRetryDelay ?? 30000;
this.healthCheckInterval = options.healthCheckInterval ?? 60000;
this.componentFailureThreshold = options.componentFailureThreshold ?? 3;
this.componentRecoveryThreshold = options.componentRecoveryThreshold ?? 2;
// Log the service creation
logger.debug('Graceful degradation service created', {
maxRetryAttempts: this.maxRetryAttempts,
initialRetryDelay: this.initialRetryDelay,
retryBackoffFactor: this.retryBackoffFactor,
maxRetryDelay: this.maxRetryDelay,
healthCheckInterval: this.healthCheckInterval,
componentFailureThreshold: this.componentFailureThreshold,
componentRecoveryThreshold: this.componentRecoveryThreshold
});
// Monitor circuit breakers to automatically detect component health
this.monitorCircuitBreakers();
// Start health check timer
this.startHealthCheck();
// Track metrics
this.updateMetrics('created');
}
/**
* Start periodic health checks
*
* @private
*/
startHealthCheck() {
if (this.healthCheckTimer) {
clearInterval(this.healthCheckTimer);
}
this.healthCheckTimer = setInterval(() => {
this.checkHealth();
}, this.healthCheckInterval);
// Initial health check
this.checkHealth();
}
/**
* Stop periodic health checks
*
* @private
*/
stopHealthCheck() {
if (this.healthCheckTimer) {
clearInterval(this.healthCheckTimer);
this.healthCheckTimer = undefined;
}
}
/**
* Cleanup resources - call when shutting down
*/
dispose() {
this.stopHealthCheck();
}
/**
* Monitor circuit breakers to detect component health
*
* @private
*/
monitorCircuitBreakers() {
const circuits = circuitBreakerRegistry.getAll();
for (const circuit of circuits) {
this.monitorCircuitBreaker(circuit);
}
// Listen for new circuit breakers
try {
if (typeof circuitBreakerRegistry.on === 'function') {
circuitBreakerRegistry.on('created', (circuit) => {
this.monitorCircuitBreaker(circuit);
});
}
else {
logger.debug('Circuit breaker registry does not support events, will not monitor new circuits');
}
}
catch (error) {
logger.debug(`Error setting up circuit breaker registry listener: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Monitor a specific circuit breaker
*
* @private
* @param {CircuitBreaker} circuit - The circuit breaker to monitor
*/
monitorCircuitBreaker(circuit) {
const circuitName = circuit.getName();
// Add to component health if not already present
if (!this.componentHealth.has(circuitName)) {
this.componentHealth.set(circuitName, {
name: circuitName,
status: ServiceHealthStatus.HEALTHY,
lastCheck: new Date(),
errorCount: 0
});
}
// Monitor state changes
circuit.on('open', () => {
this.updateComponentHealth(circuitName, ServiceHealthStatus.SEVERELY_DEGRADED);
logger.warn(`Circuit ${circuitName} opened, component marked as severely degraded`);
});
circuit.on('half-open', () => {
this.updateComponentHealth(circuitName, ServiceHealthStatus.PARTIALLY_DEGRADED);
logger.info(`Circuit ${circuitName} half-opened, component marked as partially degraded`);
});
circuit.on('close', () => {
this.updateComponentHealth(circuitName, ServiceHealthStatus.HEALTHY);
logger.info(`Circuit ${circuitName} closed, component marked as healthy`);
});
}
/**
* Register a fallback strategy for an operation
*
* @param {string} operationName - The name of the operation
* @param {FallbackConfig<T>} config - The fallback configuration
* @example
* // Register a fallback for the listRooms operation
* degradationService.registerFallback('listRooms', {
* fallbackFn: async () => ({ data: [], total_count: 0, length: 0, map: () => [] }),
* isCritical: true,
* cacheTTL: 600000 // 10 minutes
* });
*/
registerFallback(operationName, config) {
// Ensure the fallback has the isActive property
const fullConfig = {
...config,
isActive: false
};
this.fallbacks.set(operationName, fullConfig);
logger.debug(`Registered fallback for operation: ${operationName}`, {
operation: operationName,
isCritical: fullConfig.isCritical,
cacheTTL: fullConfig.cacheTTL
});
this.updateMetrics('fallback_registered', { operation: operationName });
}
/**
* Execute an operation with graceful degradation and fallback support
*
* This method attempts to execute the primary operation, and if it fails,
* it follows a degradation strategy:
* 1. Try to fetch from cache if available
* 2. Retry with exponential backoff if appropriate
* 3. Use the registered fallback if available
* 4. Throw an error if all strategies fail
*
* @template T - The type of the operation result
* @param {string} operationName - The name of the operation (should match the registered fallback)
* @param {() => Promise<T>} primaryFn - The primary operation function
* @param {Object} options - Additional options
* @param {string} [options.cacheKey] - Key to use for caching results
* @param {number} [options.cacheTTL] - TTL for cache in milliseconds
* @param {boolean} [options.skipCache] - Skip cache check
* @param {boolean} [options.skipRetry] - Skip retry attempts
* @returns {Promise<DegradedResult<T>>} The operation result with degradation information
* @throws {DegradedServiceError} If all strategies fail and no fallback is available
* @example
* // Execute a function with graceful degradation
* const result = await degradationService.executeWithFallback(
* 'listRooms',
* () => apiClient.listRooms(),
* { cacheKey: 'rooms', cacheTTL: 300000 }
* );
*
* // Use the result, checking if it's degraded
* if (result.isDegraded) {
* console.log(`Using ${result.source} data due to ${result.degradationLevel} service`);
* }
* const rooms = result.data;
*/
async executeWithFallback(operationName, primaryFn, options = {}) {
const startTime = Date.now();
const { cacheKey, cacheTTL, skipCache = false, skipRetry = false } = options;
const fallbackConfig = this.fallbacks.get(operationName);
const cacheNamespace = 'degradation';
const componentHealth = this.componentHealth.get(operationName) || {
name: operationName,
status: ServiceHealthStatus.HEALTHY,
lastCheck: new Date(),
errorCount: 0
};
let source = 'primary';
let retryAttempt = 0;
// Check component health - if severely degraded or unavailable, use fallback immediately
if (componentHealth.status === ServiceHealthStatus.SEVERELY_DEGRADED ||
componentHealth.status === ServiceHealthStatus.UNAVAILABLE) {
logger.debug(`Skipping primary execution for ${operationName} due to ${componentHealth.status} status`);
// Try cache first
if (!skipCache && this.cache && cacheKey) {
const cachedResult = this.cache.get(cacheNamespace, cacheKey);
if (cachedResult) {
logger.debug(`Cache hit for ${operationName} during degraded service`);
source = 'cache';
// Track metrics
this.updateMetrics('cache_hit', { operation: operationName });
return {
data: cachedResult.value,
isDegraded: true,
degradationLevel: componentHealth.status,
source,
message: `Using cached data due to ${componentHealth.status} service`
};
}
}
// If cache miss, use fallback
if (fallbackConfig) {
// Activate fallback if not already active
if (!fallbackConfig.isActive) {
fallbackConfig.isActive = true;
this.emit('fallback_activated', { operationName });
this.updateMetrics('fallback_activated', { operation: operationName });
}
logger.debug(`Using fallback for ${operationName} due to ${componentHealth.status} status`);
source = 'fallback';
try {
const fallbackResult = await fallbackConfig.fallbackFn();
// Track metrics
this.updateMetrics('fallback_success', { operation: operationName });
return {
data: fallbackResult,
isDegraded: true,
degradationLevel: componentHealth.status,
source,
message: `Using fallback data due to ${componentHealth.status} service`
};
}
catch (error) {
// Fallback also failed
logger.error(`Fallback for ${operationName} failed: ${error instanceof Error ? error.message : String(error)}`);
// Track metrics
this.updateMetrics('fallback_failure', { operation: operationName });
throw new DegradedServiceError(`All strategies failed for ${operationName}: ${error instanceof Error ? error.message : String(error)}`, {
operationName,
componentStatus: componentHealth.status,
attemptedStrategies: ['cache', 'fallback']
});
}
}
}
// Try primary function first
try {
const result = await primaryFn();
// Success - reset error count
if (this.componentHealth.has(operationName)) {
const health = this.componentHealth.get(operationName);
health.errorCount = 0;
// If it was degraded, update status
if (health.status !== ServiceHealthStatus.HEALTHY) {
this.updateComponentHealth(operationName, ServiceHealthStatus.HEALTHY);
}
}
// Deactivate fallback if active
if (fallbackConfig?.isActive) {
fallbackConfig.isActive = false;
this.emit('fallback_deactivated', { operationName });
this.updateMetrics('fallback_deactivated', { operation: operationName });
}
// Cache the successful result if caching is enabled
if (this.cache && cacheKey && cacheTTL) {
this.cache.set(cacheNamespace, cacheKey, result, cacheTTL);
logger.debug(`Cached result for ${operationName}`, { cacheKey, cacheTTL });
}
// Track execution time in metrics
const duration = (Date.now() - startTime) / 1000; // Convert to seconds
this.updateMetrics('operation_duration', { operation: operationName, source }, duration);
return {
data: result,
isDegraded: false,
source: 'primary'
};
}
catch (error) {
// Primary operation failed
const errorMessage = error instanceof Error ? error.message : String(error);
logger.warn(`Primary operation ${operationName} failed: ${errorMessage}`);
// Track failure in metrics
this.updateMetrics('operation_failure', { operation: operationName });
// Increment error count and update component health
if (this.componentHealth.has(operationName)) {
const health = this.componentHealth.get(operationName);
health.errorCount++;
if (health.errorCount >= this.componentFailureThreshold) {
this.updateComponentHealth(operationName, ServiceHealthStatus.PARTIALLY_DEGRADED);
}
}
else {
// Create new component health entry
this.componentHealth.set(operationName, {
name: operationName,
status: ServiceHealthStatus.HEALTHY, // Start as healthy
lastCheck: new Date(),
errorCount: 1 // First error
});
}
// Try cache first (if available)
if (!skipCache && this.cache && cacheKey) {
const cachedResult = this.cache.get(cacheNamespace, cacheKey);
if (cachedResult) {
logger.debug(`Cache hit for ${operationName} after primary failure`);
source = 'cache';
// Track metrics
this.updateMetrics('cache_hit', { operation: operationName });
return {
data: cachedResult.value,
isDegraded: true,
degradationLevel: ServiceHealthStatus.PARTIALLY_DEGRADED,
source,
message: `Using cached data due to temporary service disruption`
};
}
}
// Try retry with exponential backoff (if not skipped)
if (!skipRetry && retryAttempt < this.maxRetryAttempts) {
logger.debug(`Retry attempt ${retryAttempt + 1}/${this.maxRetryAttempts} for ${operationName}`);
// Calculate the delay with exponential backoff
const delay = Math.min(this.initialRetryDelay * Math.pow(this.retryBackoffFactor, retryAttempt), this.maxRetryDelay);
// Track retry metrics
this.updateMetrics('retry_attempt', { operation: operationName, attempt: String(retryAttempt + 1) });
// Wait for the calculated delay
await new Promise(resolve => setTimeout(resolve, delay));
// Increment retry attempt and try again recursively
retryAttempt++;
try {
const retryResult = await primaryFn();
// Success on retry - update metrics
this.updateMetrics('retry_success', { operation: operationName, attempt: String(retryAttempt) });
// Cache the successful result if caching is enabled
if (this.cache && cacheKey && cacheTTL) {
this.cache.set(cacheNamespace, cacheKey, retryResult, cacheTTL);
}
// Track execution time in metrics
const duration = (Date.now() - startTime) / 1000; // Convert to seconds
this.updateMetrics('operation_duration', { operation: operationName, source: 'primary' }, duration);
return {
data: retryResult,
isDegraded: false,
source: 'primary'
};
}
catch (retryError) {
// Retry also failed - log and continue with fallback
logger.warn(`Retry ${retryAttempt}/${this.maxRetryAttempts} for ${operationName} failed: ${retryError instanceof Error ? retryError.message : String(retryError)}`);
// Track retry failure metrics
this.updateMetrics('retry_failure', { operation: operationName, attempt: String(retryAttempt) });
}
}
// If we have a fallback, use it
if (fallbackConfig) {
// Activate fallback if not already active
if (!fallbackConfig.isActive) {
fallbackConfig.isActive = true;
this.emit('fallback_activated', { operationName });
this.updateMetrics('fallback_activated', { operation: operationName });
}
logger.debug(`Using fallback for ${operationName} after primary failure`);
source = 'fallback';
try {
const fallbackResult = await fallbackConfig.fallbackFn();
// Track metrics
this.updateMetrics('fallback_success', { operation: operationName });
// Track execution time in metrics
const duration = (Date.now() - startTime) / 1000; // Convert to seconds
this.updateMetrics('operation_duration', { operation: operationName, source }, duration);
return {
data: fallbackResult,
isDegraded: true,
degradationLevel: ServiceHealthStatus.PARTIALLY_DEGRADED,
source,
message: `Using fallback data due to temporary service disruption`
};
}
catch (fallbackError) {
// Fallback also failed
logger.error(`Fallback for ${operationName} failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
// Track metrics
this.updateMetrics('fallback_failure', { operation: operationName });
throw new DegradedServiceError(`All strategies failed for ${operationName}: ${error instanceof Error ? error.message : String(error)}`, {
operationName,
componentStatus: this.componentHealth.get(operationName)?.status,
attemptedStrategies: ['primary', 'cache', ...(retryAttempt > 0 ? ['retry'] : []), 'fallback']
});
}
}
// No fallback available, re-throw the original error
throw error;
}
}
/**
* Update component health status
*
* @private
* @param {string} componentName - The name of the component
* @param {ServiceHealthStatus} status - The new status
*/
updateComponentHealth(componentName, status) {
// Get current component health or create new entry
const health = this.componentHealth.get(componentName) || {
name: componentName,
status: ServiceHealthStatus.HEALTHY,
lastCheck: new Date(),
errorCount: 0
};
const previousStatus = health.status;
// Update health status
health.status = status;
health.lastCheck = new Date();
// If becoming healthy, reset error count
if (status === ServiceHealthStatus.HEALTHY) {
health.errorCount = 0;
}
// Store updated health
this.componentHealth.set(componentName, health);
// Log status change
if (previousStatus !== status) {
logger.info(`Component ${componentName} health changed from ${previousStatus} to ${status}`);
// Emit status change event
this.emit('component_status_changed', {
componentName,
previousStatus,
newStatus: status
});
// Update metrics
this.updateMetrics('component_status_changed', {
component: componentName,
status
});
}
// Recalculate overall health
this.recalculateOverallHealth();
}
/**
* Check health of all components
*
* @private
*/
checkHealth() {
logger.debug('Performing health check');
// Get all circuit breakers
const circuits = circuitBreakerRegistry.getAll();
// Update component health based on circuit breaker state
for (const circuit of circuits) {
const circuitName = circuit.getName();
const circuitState = circuit.getState();
let status = ServiceHealthStatus.HEALTHY;
if (circuitState === CircuitState.OPEN) {
status = ServiceHealthStatus.SEVERELY_DEGRADED;
}
else if (circuitState === CircuitState.HALF_OPEN) {
status = ServiceHealthStatus.PARTIALLY_DEGRADED;
}
this.updateComponentHealth(circuitName, status);
}
// Recalculate overall health
this.recalculateOverallHealth();
// Update metrics
this.updateMetrics('health_check');
}
/**
* Recalculate overall service health based on component health
*
* @private
*/
recalculateOverallHealth() {
// Default to healthy
let newHealth = ServiceHealthStatus.HEALTHY;
// Count critical component issues
let criticalComponentIssues = 0;
let nonCriticalComponentIssues = 0;
for (const [componentName, health] of this.componentHealth.entries()) {
// Skip healthy components
if (health.status === ServiceHealthStatus.HEALTHY) {
continue;
}
// Check if component has a fallback
const hasFallback = Array.from(this.fallbacks.entries())
.some(([opName, config]) => opName === componentName);
// Get the fallback config if it exists
const fallbackConfig = Array.from(this.fallbacks.entries())
.find(([opName]) => opName === componentName)?.[1];
const isCritical = fallbackConfig?.isCritical ?? false;
// Update counters based on component health and criticality
if (health.status === ServiceHealthStatus.UNAVAILABLE) {
// If component is unavailable and critical, it's a critical issue
if (isCritical && !hasFallback) {
criticalComponentIssues++;
}
else {
nonCriticalComponentIssues++;
}
}
else if (health.status === ServiceHealthStatus.SEVERELY_DEGRADED) {
// Severely degraded critical component without fallback is a critical issue
if (isCritical && !hasFallback) {
criticalComponentIssues++;
}
else {
nonCriticalComponentIssues++;
}
}
else if (health.status === ServiceHealthStatus.PARTIALLY_DEGRADED) {
// Partially degraded components count as non-critical issues
nonCriticalComponentIssues++;
}
}
// Determine overall health based on component issues
if (criticalComponentIssues > 0) {
// If any critical components are unavailable or severely degraded without fallbacks,
// the service is unavailable
newHealth = ServiceHealthStatus.UNAVAILABLE;
}
else if (nonCriticalComponentIssues > 2) {
// If more than 2 non-critical components have issues, service is severely degraded
newHealth = ServiceHealthStatus.SEVERELY_DEGRADED;
}
else if (nonCriticalComponentIssues > 0) {
// If any non-critical components have issues, service is partially degraded
newHealth = ServiceHealthStatus.PARTIALLY_DEGRADED;
}
// Update overall health if changed
if (newHealth !== this.overallHealth) {
const previousHealth = this.overallHealth;
this.overallHealth = newHealth;
logger.info(`Overall service health changed from ${previousHealth} to ${newHealth}`);
// Emit status change event
this.emit('overall_status_changed', {
previousStatus: previousHealth,
newStatus: newHealth
});
// Update metrics
this.updateMetrics('overall_status_changed', {
status: newHealth
});
}
}
/**
* Get current overall service health
*
* @returns {ServiceHealthStatus} The current overall health status
*/
getOverallHealth() {
return this.overallHealth;
}
/**
* Get health of all components
*
* @returns {ServiceComponentHealth[]} Array of component health statuses
*/
getComponentHealth() {
return Array.from(this.componentHealth.values());
}
/**
* Get health of a specific component
*
* @param {string} componentName - The name of the component
* @returns {ServiceComponentHealth | undefined} The component health or undefined if not found
*/
getComponentHealthById(componentName) {
return this.componentHealth.get(componentName);
}
/**
* Update metrics for the graceful degradation service
*
* This method attempts to update Prometheus metrics if the metrics module is available.
* If the metrics module cannot be imported, the method silently ignores the error.
*
* @private
* @param {string} event - The event type
* @param {Record<string, any>} [labels] - Additional labels for the metric
* @param {number} [value] - Value for gauge metrics
*/
async updateMetrics(event, labels, value) {
try {
const metricsRegistry = await import('./metrics.js').then(m => m.default);
switch (event) {
case 'created':
// New degradation service created
break;
case 'health_check':
// Health check performed
metricsRegistry.degradationHealthChecksTotal.inc();
break;
case 'component_status_changed':
// Component status changed
if (labels?.component && labels?.status) {
metricsRegistry.degradationComponentStatusInfo.set({
component: labels.component,
status: labels.status
}, 1);
// Set other statuses to 0
const allStatuses = Object.values(ServiceHealthStatus);
for (const status of allStatuses) {
if (status !== labels.status) {
metricsRegistry.degradationComponentStatusInfo.set({
component: labels.component,
status
}, 0);
}
}
}
break;
case 'overall_status_changed':
// Overall status changed
if (labels?.status) {
metricsRegistry.degradationOverallStatusInfo.set({
status: labels.status
}, 1);
// Set other statuses to 0
const allStatuses = Object.values(ServiceHealthStatus);
for (const status of allStatuses) {
if (status !== labels.status) {
metricsRegistry.degradationOverallStatusInfo.set({
status
}, 0);
}
}
}
break;
case 'fallback_registered':
// Fallback registered
if (labels?.operation) {
metricsRegistry.degradationFallbacksRegisteredTotal.inc({
operation: labels.operation
});
}
break;
case 'fallback_activated':
// Fallback activated
if (labels?.operation) {
metricsRegistry.degradationFallbackActivationsTotal.inc({
operation: labels.operation
});
}
break;
case 'fallback_deactivated':
// Fallback deactivated
if (labels?.operation) {
metricsRegistry.degradationFallbackDeactivationsTotal.inc({
operation: labels.operation
});
}
break;
case 'fallback_success':
// Fallback success
if (labels?.operation) {
metricsRegistry.degradationFallbackSuccessTotal.inc({
operation: labels.operation
});
}
break;
case 'fallback_failure':
// Fallback failure
if (labels?.operation) {
metricsRegistry.degradationFallbackFailureTotal.inc({
operation: labels.operation
});
}
break;
case 'operation_duration':
// Operation duration
if (labels?.operation && labels?.source && value !== undefined) {
metricsRegistry.degradationOperationDuration.observe({
operation: labels.operation,
source: labels.source
}, value);
}
break;
case 'operation_failure':
// Operation failure
if (labels?.operation) {
metricsRegistry.degradationOperationFailuresTotal.inc({
operation: labels.operation
});
}
break;
case 'cache_hit':
// Cache hit
if (labels?.operation) {
metricsRegistry.degradationCacheHitsTotal.inc({
operation: labels.operation
});
}
break;
case 'retry_attempt':
// Retry attempt
if (labels?.operation && labels?.attempt) {
metricsRegistry.degradationRetryAttemptsTotal.inc({
operation: labels.operation,
attempt: labels.attempt
});
}
break;
case 'retry_success':
// Retry success
if (labels?.operation && labels?.attempt) {
metricsRegistry.degradationRetrySuccessTotal.inc({
operation: labels.operation,
attempt: labels.attempt
});
}
break;
case 'retry_failure':
// Retry failure
if (labels?.operation && labels?.attempt) {
metricsRegistry.degradationRetryFailureTotal.inc({
operation: labels.operation,
attempt: labels.attempt
});
}
break;
}
}
catch (error) {
// Metrics module might not be available, or other error occurred
// This is non-critical, so just log at debug level
logger.debug(`Error updating graceful degradation metrics: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
/**
* Singleton instance of the graceful degradation service
*/
export const gracefulDegradation = new GracefulDegradation();
export default gracefulDegradation;
//# sourceMappingURL=graceful-degradation.js.map