/** * @file observability.ts * @notice Metrics, logging, and monitoring for bridge operations */ import { ethers } from 'ethers'; export interface BridgeMetrics { totalTransfers: number; successCount: number; failureCount: number; refundCount: number; liquidityFailures: number; avgSettlementTime: number; routeMetrics: RouteMetrics[]; } export interface RouteMetrics { chainId: number; provider: string; successCount: number; failureCount: number; avgSettlementTime: number; totalVolume: string; } export interface TransferLog { transferId: string; timestamp: number; event: string; data: any; level: 'info' | 'warn' | 'error'; } export class BridgeObservability { private metrics: BridgeMetrics; private logs: TransferLog[] = []; private maxLogs: number = 10000; constructor() { this.metrics = { totalTransfers: 0, successCount: 0, failureCount: 0, refundCount: 0, liquidityFailures: 0, avgSettlementTime: 0, routeMetrics: [] }; } /** * Record transfer initiation */ recordTransferInitiated(transferId: string, data: any): void { this.metrics.totalTransfers++; this.log('info', transferId, 'TRANSFER_INITIATED', data); } /** * Record transfer success */ recordTransferSuccess(transferId: string, settlementTime: number, route: RouteMetrics): void { this.metrics.successCount++; this.updateAvgSettlementTime(settlementTime); this.updateRouteMetrics(route, true); this.log('info', transferId, 'TRANSFER_SUCCESS', { settlementTime, route }); } /** * Record transfer failure */ recordTransferFailure(transferId: string, error: string, route?: RouteMetrics): void { this.metrics.failureCount++; if (route) { this.updateRouteMetrics(route, false); } this.log('error', transferId, 'TRANSFER_FAILURE', { error, route }); } /** * Record refund */ recordRefund(transferId: string, reason: string): void { this.metrics.refundCount++; this.log('warn', transferId, 'REFUND_INITIATED', { reason }); } /** * Record liquidity failure */ recordLiquidityFailure(transferId: string, chainId: number, token: string): void { this.metrics.liquidityFailures++; this.log('error', transferId, 'LIQUIDITY_FAILURE', { chainId, token }); } /** * Update average settlement time */ private updateAvgSettlementTime(settlementTime: number): void { const total = this.metrics.successCount; this.metrics.avgSettlementTime = (this.metrics.avgSettlementTime * (total - 1) + settlementTime) / total; } /** * Update route metrics */ private updateRouteMetrics(route: RouteMetrics, success: boolean): void { const existing = this.metrics.routeMetrics.find( r => r.chainId === route.chainId && r.provider === route.provider ); if (existing) { if (success) { existing.successCount++; } else { existing.failureCount++; } existing.avgSettlementTime = (existing.avgSettlementTime * (existing.successCount + existing.failureCount - 1) + route.avgSettlementTime) / (existing.successCount + existing.failureCount); } else { this.metrics.routeMetrics.push({ ...route, successCount: success ? 1 : 0, failureCount: success ? 0 : 1 }); } } /** * Log event */ private log(level: 'info' | 'warn' | 'error', transferId: string, event: string, data: any): void { const log: TransferLog = { transferId, timestamp: Date.now(), event, data, level }; this.logs.push(log); // Keep only last maxLogs if (this.logs.length > this.maxLogs) { this.logs = this.logs.slice(-this.maxLogs); } // Emit to external logging system (e.g., Loki, CloudWatch) this.emitLog(log); } /** * Emit log to external system */ private emitLog(log: TransferLog): void { // In production, send to logging service console.log(`[${log.level.toUpperCase()}] ${log.event}`, log); } /** * Get current metrics */ getMetrics(): BridgeMetrics { return { ...this.metrics }; } /** * Get success rate */ getSuccessRate(): number { const total = this.metrics.successCount + this.metrics.failureCount; if (total === 0) return 0; return (this.metrics.successCount / total) * 100; } /** * Get refund rate */ getRefundRate(): number { if (this.metrics.totalTransfers === 0) return 0; return (this.metrics.refundCount / this.metrics.totalTransfers) * 100; } /** * Get logs for transfer */ getTransferLogs(transferId: string): TransferLog[] { return this.logs.filter(log => log.transferId === transferId); } /** * Get logs by level */ getLogsByLevel(level: 'info' | 'warn' | 'error', limit: number = 100): TransferLog[] { return this.logs .filter(log => log.level === level) .slice(-limit) .reverse(); } /** * Export metrics for Prometheus */ exportPrometheusMetrics(): string { const lines: string[] = []; lines.push(`# HELP bridge_total_transfers Total number of bridge transfers`); lines.push(`# TYPE bridge_total_transfers counter`); lines.push(`bridge_total_transfers ${this.metrics.totalTransfers}`); lines.push(`# HELP bridge_success_count Number of successful transfers`); lines.push(`# TYPE bridge_success_count counter`); lines.push(`bridge_success_count ${this.metrics.successCount}`); lines.push(`# HELP bridge_failure_count Number of failed transfers`); lines.push(`# TYPE bridge_failure_count counter`); lines.push(`bridge_failure_count ${this.metrics.failureCount}`); lines.push(`# HELP bridge_success_rate Success rate percentage`); lines.push(`# TYPE bridge_success_rate gauge`); lines.push(`bridge_success_rate ${this.getSuccessRate()}`); lines.push(`# HELP bridge_avg_settlement_time Average settlement time in seconds`); lines.push(`# TYPE bridge_avg_settlement_time gauge`); lines.push(`bridge_avg_settlement_time ${this.metrics.avgSettlementTime}`); return lines.join('\n'); } }