// Alert Service // Sends alerts for critical events and risk violations import { logger } from '@/infrastructure/monitoring/logger'; import { metricsService } from './metrics.service'; export enum AlertSeverity { LOW = 'low', MEDIUM = 'medium', HIGH = 'high', CRITICAL = 'critical', } export interface Alert { severity: AlertSeverity; message: string; dealId?: string; violationType?: string; metadata?: Record; timestamp: Date; } export class AlertService { private alertChannels: AlertChannel[] = []; constructor() { // Initialize alert channels based on environment if (process.env.SLACK_WEBHOOK_URL) { this.alertChannels.push(new SlackAlertChannel(process.env.SLACK_WEBHOOK_URL)); } if (process.env.PAGERDUTY_INTEGRATION_KEY) { this.alertChannels.push(new PagerDutyAlertChannel(process.env.PAGERDUTY_INTEGRATION_KEY)); } if (process.env.EMAIL_ALERT_RECIPIENTS) { this.alertChannels.push(new EmailAlertChannel(process.env.EMAIL_ALERT_RECIPIENTS.split(','))); } } /** * Send alert */ async sendAlert(alert: Alert): Promise { logger.error('Alert triggered', { severity: alert.severity, message: alert.message, dealId: alert.dealId, violationType: alert.violationType, }); // Record in metrics if (alert.violationType) { metricsService.recordRiskViolation(alert.violationType, alert.severity); } // Send to all channels const promises = this.alertChannels.map(channel => channel.send(alert).catch(err => { logger.error('Failed to send alert via channel', { channel: channel.constructor.name, error: err instanceof Error ? err.message : 'Unknown error', }); }) ); await Promise.allSettled(promises); } /** * Alert on risk violation */ async alertRiskViolation( violationType: string, message: string, dealId?: string, severity: AlertSeverity = AlertSeverity.HIGH ): Promise { await this.sendAlert({ severity, message: `Risk Violation: ${message}`, dealId, violationType, timestamp: new Date(), }); } /** * Alert on LTV threshold */ async alertLtvThreshold(dealId: string, currentLtv: number, maxLtv: number): Promise { const percentage = (currentLtv / maxLtv) * 100; let severity = AlertSeverity.MEDIUM; if (percentage >= 95) { severity = AlertSeverity.CRITICAL; } else if (percentage >= 85) { severity = AlertSeverity.HIGH; } await this.alertRiskViolation( 'ltv_threshold', `LTV at ${(currentLtv * 100).toFixed(2)}% (${percentage.toFixed(1)}% of max ${(maxLtv * 100).toFixed(2)}%)`, dealId, severity ); } /** * Alert on USDTz exposure */ async alertUsdtzExposure(dealId: string, exposure: number, maxExposure: number): Promise { const percentage = (exposure / maxExposure) * 100; let severity = AlertSeverity.MEDIUM; if (percentage >= 95) { severity = AlertSeverity.CRITICAL; } else if (percentage >= 85) { severity = AlertSeverity.HIGH; } await this.alertRiskViolation( 'usdtz_exposure', `USDTz exposure at $${exposure.toFixed(2)} (${percentage.toFixed(1)}% of max $${maxExposure.toFixed(2)})`, dealId, severity ); } /** * Alert on deal failure */ async alertDealFailure(dealId: string, error: string, step?: string): Promise { await this.sendAlert({ severity: AlertSeverity.HIGH, message: `Deal execution failed: ${error}`, dealId, metadata: { step }, timestamp: new Date(), }); } /** * Alert on system error */ async alertSystemError(error: string, metadata?: Record): Promise { await this.sendAlert({ severity: AlertSeverity.CRITICAL, message: `System error: ${error}`, metadata, timestamp: new Date(), }); } } // Alert Channel Interfaces interface AlertChannel { send(alert: Alert): Promise; } class SlackAlertChannel implements AlertChannel { constructor(private webhookUrl: string) {} async send(alert: Alert): Promise { const color = { [AlertSeverity.LOW]: '#36a64f', [AlertSeverity.MEDIUM]: '#ffa500', [AlertSeverity.HIGH]: '#ff6600', [AlertSeverity.CRITICAL]: '#ff0000', }[alert.severity]; const payload = { attachments: [{ color, title: `Arbitrage Alert: ${alert.severity.toUpperCase()}`, text: alert.message, fields: [ ...(alert.dealId ? [{ title: 'Deal ID', value: alert.dealId, short: true }] : []), ...(alert.violationType ? [{ title: 'Violation Type', value: alert.violationType, short: true }] : []), { title: 'Timestamp', value: alert.timestamp.toISOString(), short: true }, ], ...(alert.metadata ? { fields: [...(payload.attachments[0].fields || []), ...Object.entries(alert.metadata).map(([k, v]) => ({ title: k, value: String(v), short: true }))] } : {}), }], }; // TODO: Implement actual Slack webhook call // await fetch(this.webhookUrl, { method: 'POST', body: JSON.stringify(payload) }); logger.info('Slack alert (not implemented)', { payload }); } } class PagerDutyAlertChannel implements AlertChannel { constructor(private integrationKey: string) {} async send(alert: Alert): Promise { const severity = { [AlertSeverity.LOW]: 'info', [AlertSeverity.MEDIUM]: 'warning', [AlertSeverity.HIGH]: 'error', [AlertSeverity.CRITICAL]: 'critical', }[alert.severity]; const payload = { routing_key: this.integrationKey, event_action: 'trigger', payload: { summary: alert.message, severity, source: 'arbitrage-service', custom_details: { dealId: alert.dealId, violationType: alert.violationType, ...alert.metadata, }, }, }; // TODO: Implement actual PagerDuty API call // await fetch('https://events.pagerduty.com/v2/enqueue', { method: 'POST', body: JSON.stringify(payload) }); logger.info('PagerDuty alert (not implemented)', { payload }); } } class EmailAlertChannel implements AlertChannel { constructor(private recipients: string[]) {} async send(alert: Alert): Promise { // Only send critical/high alerts via email if (alert.severity !== AlertSeverity.CRITICAL && alert.severity !== AlertSeverity.HIGH) { return; } // TODO: Implement email sending (using nodemailer, sendgrid, etc.) logger.info('Email alert (not implemented)', { recipients: this.recipients, alert: alert.message, }); } } export const alertService = new AlertService();