|
/** |
|
* Failover Metrics and Observability |
|
* |
|
* Comprehensive metrics collection and monitoring for Aurora failover events, |
|
* system health, and performance tracking across all regions. |
|
*/ |
|
|
|
class FailoverMetrics { |
|
constructor(options = {}) { |
|
this.options = { |
|
retentionPeriodMs: options.retentionPeriodMs || 24 * 60 * 60 * 1000, // 24 hours |
|
aggregationIntervalMs: options.aggregationIntervalMs || 60 * 1000, // 1 minute |
|
maxEvents: options.maxEvents || 10000, |
|
enableAutoAggregation: options.enableAutoAggregation !== false |
|
}; |
|
|
|
// Core metrics |
|
this.metrics = { |
|
failoverEvents: 0, |
|
fallbackRequests: 0, |
|
queuedOperations: 0, |
|
circuitBreakerTrips: 0, |
|
recoveryAttempts: 0, |
|
successfulRecoveries: 0, |
|
failedRecoveries: 0 |
|
}; |
|
|
|
// Per-region metrics |
|
this.regionMetrics = {}; |
|
|
|
// Event tracking |
|
this.events = []; |
|
this.aggregatedMetrics = []; |
|
|
|
// Performance tracking |
|
this.performanceMetrics = { |
|
latencyHistogram: {}, |
|
errorRates: {}, |
|
throughput: {} |
|
}; |
|
|
|
// Health check tracking |
|
this.healthMetrics = {}; |
|
|
|
if (this.options.enableAutoAggregation) { |
|
this.startAutoAggregation(); |
|
} |
|
} |
|
|
|
/** |
|
* Record a failover event |
|
* @param {string} fromRegion - Region failed over from |
|
* @param {string} toRegion - Region failed over to |
|
* @param {Object} metadata - Additional event metadata |
|
*/ |
|
recordFailover(fromRegion, toRegion, metadata = {}) { |
|
this.metrics.failoverEvents++; |
|
this.incrementRegionMetric(fromRegion, 'failoversFrom'); |
|
this.incrementRegionMetric(toRegion, 'failoversTo'); |
|
|
|
const event = { |
|
type: 'failover', |
|
timestamp: Date.now(), |
|
fromRegion, |
|
toRegion, |
|
metadata, |
|
id: this.generateEventId() |
|
}; |
|
|
|
this.addEvent(event); |
|
|
|
console.log(`FAILOVER METRIC: ${fromRegion} → ${toRegion}`, { |
|
eventId: event.id, |
|
timestamp: new Date(event.timestamp).toISOString(), |
|
...metadata |
|
}); |
|
} |
|
|
|
/** |
|
* Record a fallback request |
|
* @param {string} region - Region handling the fallback request |
|
* @param {Object} requestMetadata - Request metadata |
|
*/ |
|
recordFallbackRequest(region, requestMetadata = {}) { |
|
this.metrics.fallbackRequests++; |
|
this.incrementRegionMetric(region, 'fallbackRequests'); |
|
|
|
const event = { |
|
type: 'fallback_request', |
|
timestamp: Date.now(), |
|
region, |
|
metadata: requestMetadata, |
|
id: this.generateEventId() |
|
}; |
|
|
|
this.addEvent(event); |
|
} |
|
|
|
/** |
|
* Record a queued write operation |
|
* @param {Object} operationMetadata - Operation metadata |
|
*/ |
|
recordQueuedOperation(operationMetadata = {}) { |
|
this.metrics.queuedOperations++; |
|
|
|
const event = { |
|
type: 'queued_operation', |
|
timestamp: Date.now(), |
|
metadata: operationMetadata, |
|
id: this.generateEventId() |
|
}; |
|
|
|
this.addEvent(event); |
|
} |
|
|
|
/** |
|
* Record a circuit breaker trip |
|
* @param {string} region - Region where circuit breaker tripped |
|
* @param {Object} circuitBreakerState - Circuit breaker state |
|
*/ |
|
recordCircuitBreakerTrip(region, circuitBreakerState = {}) { |
|
this.metrics.circuitBreakerTrips++; |
|
this.incrementRegionMetric(region, 'circuitBreakerTrips'); |
|
|
|
const event = { |
|
type: 'circuit_breaker_trip', |
|
timestamp: Date.now(), |
|
region, |
|
metadata: circuitBreakerState, |
|
id: this.generateEventId() |
|
}; |
|
|
|
this.addEvent(event); |
|
} |
|
|
|
/** |
|
* Record a recovery attempt |
|
* @param {string} region - Region being recovered |
|
* @param {boolean} successful - Whether recovery was successful |
|
* @param {Object} recoveryMetadata - Recovery metadata |
|
*/ |
|
recordRecoveryAttempt(region, successful, recoveryMetadata = {}) { |
|
this.metrics.recoveryAttempts++; |
|
|
|
if (successful) { |
|
this.metrics.successfulRecoveries++; |
|
this.incrementRegionMetric(region, 'successfulRecoveries'); |
|
} else { |
|
this.metrics.failedRecoveries++; |
|
this.incrementRegionMetric(region, 'failedRecoveries'); |
|
} |
|
|
|
const event = { |
|
type: 'recovery_attempt', |
|
timestamp: Date.now(), |
|
region, |
|
successful, |
|
metadata: recoveryMetadata, |
|
id: this.generateEventId() |
|
}; |
|
|
|
this.addEvent(event); |
|
} |
|
|
|
/** |
|
* Record latency for a region |
|
* @param {string} region - Region |
|
* @param {number} latencyMs - Latency in milliseconds |
|
* @param {string} operation - Operation type (optional) |
|
*/ |
|
recordLatency(region, latencyMs, operation = 'general') { |
|
if (!this.performanceMetrics.latencyHistogram[region]) { |
|
this.performanceMetrics.latencyHistogram[region] = {}; |
|
} |
|
|
|
if (!this.performanceMetrics.latencyHistogram[region][operation]) { |
|
this.performanceMetrics.latencyHistogram[region][operation] = []; |
|
} |
|
|
|
this.performanceMetrics.latencyHistogram[region][operation].push({ |
|
latency: latencyMs, |
|
timestamp: Date.now() |
|
}); |
|
|
|
// Keep only recent latency data |
|
const cutoff = Date.now() - this.options.retentionPeriodMs; |
|
this.performanceMetrics.latencyHistogram[region][operation] = |
|
this.performanceMetrics.latencyHistogram[region][operation] |
|
.filter(entry => entry.timestamp > cutoff); |
|
} |
|
|
|
/** |
|
* Record an error for a region |
|
* @param {string} region - Region where error occurred |
|
* @param {string} errorType - Type of error |
|
* @param {Object} errorMetadata - Error metadata |
|
*/ |
|
recordError(region, errorType, errorMetadata = {}) { |
|
if (!this.performanceMetrics.errorRates[region]) { |
|
this.performanceMetrics.errorRates[region] = {}; |
|
} |
|
|
|
if (!this.performanceMetrics.errorRates[region][errorType]) { |
|
this.performanceMetrics.errorRates[region][errorType] = []; |
|
} |
|
|
|
this.performanceMetrics.errorRates[region][errorType].push({ |
|
timestamp: Date.now(), |
|
metadata: errorMetadata |
|
}); |
|
|
|
// Keep only recent error data |
|
const cutoff = Date.now() - this.options.retentionPeriodMs; |
|
this.performanceMetrics.errorRates[region][errorType] = |
|
this.performanceMetrics.errorRates[region][errorType] |
|
.filter(entry => entry.timestamp > cutoff); |
|
|
|
const event = { |
|
type: 'error', |
|
timestamp: Date.now(), |
|
region, |
|
errorType, |
|
metadata: errorMetadata, |
|
id: this.generateEventId() |
|
}; |
|
|
|
this.addEvent(event); |
|
} |
|
|
|
/** |
|
* Record health check result |
|
* @param {string} region - Region |
|
* @param {boolean} healthy - Health status |
|
* @param {Object} healthData - Health check data |
|
*/ |
|
recordHealthCheck(region, healthy, healthData = {}) { |
|
if (!this.healthMetrics[region]) { |
|
this.healthMetrics[region] = { |
|
checks: [], |
|
lastHealthy: null, |
|
lastUnhealthy: null, |
|
consecutiveFailures: 0, |
|
consecutiveSuccesses: 0 |
|
}; |
|
} |
|
|
|
const regionHealth = this.healthMetrics[region]; |
|
const now = Date.now(); |
|
|
|
regionHealth.checks.push({ |
|
timestamp: now, |
|
healthy, |
|
data: healthData |
|
}); |
|
|
|
if (healthy) { |
|
regionHealth.lastHealthy = now; |
|
regionHealth.consecutiveSuccesses++; |
|
regionHealth.consecutiveFailures = 0; |
|
} else { |
|
regionHealth.lastUnhealthy = now; |
|
regionHealth.consecutiveFailures++; |
|
regionHealth.consecutiveSuccesses = 0; |
|
} |
|
|
|
// Keep only recent health checks |
|
const cutoff = now - this.options.retentionPeriodMs; |
|
regionHealth.checks = regionHealth.checks.filter(check => check.timestamp > cutoff); |
|
} |
|
|
|
/** |
|
* Get current metrics summary |
|
* @returns {Object} - Metrics summary |
|
*/ |
|
getMetricsSummary() { |
|
return { |
|
core: { ...this.metrics }, |
|
regions: { ...this.regionMetrics }, |
|
eventCount: this.events.length, |
|
aggregationCount: this.aggregatedMetrics.length, |
|
dataRetentionPeriodMs: this.options.retentionPeriodMs, |
|
lastUpdated: Date.now() |
|
}; |
|
} |
|
|
|
/** |
|
* Get performance metrics for a region |
|
* @param {string} region - Region to get metrics for |
|
* @returns {Object} - Performance metrics |
|
*/ |
|
getRegionPerformanceMetrics(region) { |
|
const latencyData = this.performanceMetrics.latencyHistogram[region] || {}; |
|
const errorData = this.performanceMetrics.errorRates[region] || {}; |
|
const healthData = this.healthMetrics[region] || {}; |
|
|
|
// Calculate latency statistics |
|
const latencyStats = {}; |
|
Object.keys(latencyData).forEach(operation => { |
|
const latencies = latencyData[operation].map(entry => entry.latency); |
|
if (latencies.length > 0) { |
|
latencyStats[operation] = { |
|
count: latencies.length, |
|
min: Math.min(...latencies), |
|
max: Math.max(...latencies), |
|
avg: latencies.reduce((sum, l) => sum + l, 0) / latencies.length, |
|
p95: this.calculatePercentile(latencies, 95), |
|
p99: this.calculatePercentile(latencies, 99) |
|
}; |
|
} |
|
}); |
|
|
|
// Calculate error rates |
|
const errorStats = {}; |
|
Object.keys(errorData).forEach(errorType => { |
|
const errors = errorData[errorType]; |
|
const recentErrors = errors.filter(e => e.timestamp > Date.now() - 60 * 60 * 1000); // Last hour |
|
errorStats[errorType] = { |
|
total: errors.length, |
|
lastHour: recentErrors.length, |
|
rate: recentErrors.length / 60 // errors per minute |
|
}; |
|
}); |
|
|
|
// Calculate health statistics |
|
const healthStats = { |
|
totalChecks: healthData.checks?.length || 0, |
|
consecutiveFailures: healthData.consecutiveFailures || 0, |
|
consecutiveSuccesses: healthData.consecutiveSuccesses || 0, |
|
lastHealthy: healthData.lastHealthy, |
|
lastUnhealthy: healthData.lastUnhealthy |
|
}; |
|
|
|
if (healthData.checks && healthData.checks.length > 0) { |
|
const recentChecks = healthData.checks.filter(c => c.timestamp > Date.now() - 60 * 60 * 1000); |
|
const healthyChecks = recentChecks.filter(c => c.healthy).length; |
|
healthStats.healthRate = recentChecks.length > 0 ? healthyChecks / recentChecks.length : 0; |
|
} |
|
|
|
return { |
|
region, |
|
latency: latencyStats, |
|
errors: errorStats, |
|
health: healthStats, |
|
timestamp: Date.now() |
|
}; |
|
} |
|
|
|
/** |
|
* Get events within a time range |
|
* @param {number} startTime - Start timestamp |
|
* @param {number} endTime - End timestamp |
|
* @param {string[]} eventTypes - Filter by event types (optional) |
|
* @returns {Array} - Filtered events |
|
*/ |
|
getEvents(startTime, endTime = Date.now(), eventTypes = []) { |
|
let filteredEvents = this.events.filter(event => |
|
event.timestamp >= startTime && event.timestamp <= endTime |
|
); |
|
|
|
if (eventTypes.length > 0) { |
|
filteredEvents = filteredEvents.filter(event => |
|
eventTypes.includes(event.type) |
|
); |
|
} |
|
|
|
return filteredEvents.sort((a, b) => b.timestamp - a.timestamp); |
|
} |
|
|
|
/** |
|
* Get aggregated metrics for a time period |
|
* @param {number} periodMs - Time period in milliseconds |
|
* @returns {Object} - Aggregated metrics |
|
*/ |
|
getAggregatedMetrics(periodMs = 60 * 60 * 1000) { // Default: 1 hour |
|
const endTime = Date.now(); |
|
const startTime = endTime - periodMs; |
|
|
|
const periodEvents = this.getEvents(startTime, endTime); |
|
|
|
const aggregated = { |
|
period: { startTime, endTime, durationMs: periodMs }, |
|
events: { |
|
total: periodEvents.length, |
|
byType: {} |
|
}, |
|
regions: {} |
|
}; |
|
|
|
// Aggregate events by type |
|
periodEvents.forEach(event => { |
|
if (!aggregated.events.byType[event.type]) { |
|
aggregated.events.byType[event.type] = 0; |
|
} |
|
aggregated.events.byType[event.type]++; |
|
|
|
// Aggregate by region |
|
const region = event.region || event.fromRegion || event.toRegion; |
|
if (region) { |
|
if (!aggregated.regions[region]) { |
|
aggregated.regions[region] = { events: 0, types: {} }; |
|
} |
|
aggregated.regions[region].events++; |
|
|
|
if (!aggregated.regions[region].types[event.type]) { |
|
aggregated.regions[region].types[event.type] = 0; |
|
} |
|
aggregated.regions[region].types[event.type]++; |
|
} |
|
}); |
|
|
|
return aggregated; |
|
} |
|
|
|
/** |
|
* Start automatic metric aggregation |
|
*/ |
|
startAutoAggregation() { |
|
if (this.aggregationInterval) { |
|
return; |
|
} |
|
|
|
this.aggregationInterval = setInterval(() => { |
|
this.performAggregation(); |
|
this.cleanup(); |
|
}, this.options.aggregationIntervalMs); |
|
|
|
console.log('Auto-aggregation started'); |
|
} |
|
|
|
/** |
|
* Stop automatic metric aggregation |
|
*/ |
|
stopAutoAggregation() { |
|
if (this.aggregationInterval) { |
|
clearInterval(this.aggregationInterval); |
|
this.aggregationInterval = null; |
|
console.log('Auto-aggregation stopped'); |
|
} |
|
} |
|
|
|
/** |
|
* Perform metric aggregation |
|
*/ |
|
performAggregation() { |
|
const aggregated = this.getAggregatedMetrics(this.options.aggregationIntervalMs); |
|
aggregated.timestamp = Date.now(); |
|
|
|
this.aggregatedMetrics.push(aggregated); |
|
|
|
// Keep only recent aggregations |
|
const cutoff = Date.now() - this.options.retentionPeriodMs; |
|
this.aggregatedMetrics = this.aggregatedMetrics.filter(agg => agg.timestamp > cutoff); |
|
} |
|
|
|
/** |
|
* Clean up old data |
|
*/ |
|
cleanup() { |
|
const cutoff = Date.now() - this.options.retentionPeriodMs; |
|
|
|
// Clean up events |
|
this.events = this.events.filter(event => event.timestamp > cutoff); |
|
|
|
// Clean up performance metrics |
|
Object.keys(this.performanceMetrics.latencyHistogram).forEach(region => { |
|
Object.keys(this.performanceMetrics.latencyHistogram[region]).forEach(operation => { |
|
this.performanceMetrics.latencyHistogram[region][operation] = |
|
this.performanceMetrics.latencyHistogram[region][operation] |
|
.filter(entry => entry.timestamp > cutoff); |
|
}); |
|
}); |
|
|
|
Object.keys(this.performanceMetrics.errorRates).forEach(region => { |
|
Object.keys(this.performanceMetrics.errorRates[region]).forEach(errorType => { |
|
this.performanceMetrics.errorRates[region][errorType] = |
|
this.performanceMetrics.errorRates[region][errorType] |
|
.filter(entry => entry.timestamp > cutoff); |
|
}); |
|
}); |
|
|
|
// Clean up health metrics |
|
Object.keys(this.healthMetrics).forEach(region => { |
|
if (this.healthMetrics[region].checks) { |
|
this.healthMetrics[region].checks = |
|
this.healthMetrics[region].checks.filter(check => check.timestamp > cutoff); |
|
} |
|
}); |
|
} |
|
|
|
/** |
|
* Helper methods |
|
*/ |
|
|
|
incrementRegionMetric(region, metric) { |
|
if (!this.regionMetrics[region]) { |
|
this.regionMetrics[region] = {}; |
|
} |
|
|
|
if (!this.regionMetrics[region][metric]) { |
|
this.regionMetrics[region][metric] = 0; |
|
} |
|
|
|
this.regionMetrics[region][metric]++; |
|
} |
|
|
|
addEvent(event) { |
|
this.events.push(event); |
|
|
|
// Prevent memory issues by limiting event count |
|
if (this.events.length > this.options.maxEvents) { |
|
this.events = this.events.slice(-this.options.maxEvents); |
|
} |
|
} |
|
|
|
generateEventId() { |
|
return `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; |
|
} |
|
|
|
calculatePercentile(values, percentile) { |
|
const sorted = [...values].sort((a, b) => a - b); |
|
const index = Math.ceil(sorted.length * percentile / 100) - 1; |
|
return sorted[index] || 0; |
|
} |
|
|
|
/** |
|
* Export metrics for external monitoring systems |
|
* @returns {Object} - Exportable metrics |
|
*/ |
|
exportMetrics() { |
|
return { |
|
timestamp: Date.now(), |
|
metrics: this.getMetricsSummary(), |
|
regions: Object.keys(this.regionMetrics).map(region => |
|
this.getRegionPerformanceMetrics(region) |
|
), |
|
recentEvents: this.getEvents(Date.now() - 60 * 60 * 1000), // Last hour |
|
aggregated: this.aggregatedMetrics.slice(-24) // Last 24 aggregations |
|
}; |
|
} |
|
|
|
/** |
|
* Reset all metrics (useful for testing) |
|
*/ |
|
reset() { |
|
this.metrics = { |
|
failoverEvents: 0, |
|
fallbackRequests: 0, |
|
queuedOperations: 0, |
|
circuitBreakerTrips: 0, |
|
recoveryAttempts: 0, |
|
successfulRecoveries: 0, |
|
failedRecoveries: 0 |
|
}; |
|
|
|
this.regionMetrics = {}; |
|
this.events = []; |
|
this.aggregatedMetrics = []; |
|
this.performanceMetrics = { |
|
latencyHistogram: {}, |
|
errorRates: {}, |
|
throughput: {} |
|
}; |
|
this.healthMetrics = {}; |
|
} |
|
} |
|
|
|
module.exports = { FailoverMetrics }; |