diff --git a/src/lib/monitoring/types.ts b/src/lib/monitoring/types.ts new file mode 100644 index 0000000..e8ee187 --- /dev/null +++ b/src/lib/monitoring/types.ts @@ -0,0 +1,148 @@ +// === System Health === +export interface SystemHealth { + cpuUsagePercent: number + memoryUsedMB: number + memoryTotalMB: number + memoryUsagePercent: number + diskUsedGB: number + diskTotalGB: number + diskUsagePercent: number + loadAvg1: number + loadAvg5: number + uptime: number // seconds +} + +// === Service Statuses === +export type ServiceStatusType = 'online' | 'warning' | 'offline' + +export interface ProcessStatus { + status: ServiceStatusType + pid: number + memoryMB: number + uptimeSeconds: number + restarts: number +} + +export interface PostgresqlStatus { + status: ServiceStatusType + connections: number + maxConnections: number + latencyMs: number +} + +export interface PgBouncerStatus { + status: ServiceStatusType + activeConnections: number + waitingClients: number + poolSize: number +} + +export interface RedisStatus { + status: ServiceStatusType + memoryUsedMB: number + connectedClients: number + opsPerSec: number +} + +export interface ServiceStatuses { + payload: ProcessStatus + queueWorker: ProcessStatus + postgresql: PostgresqlStatus + pgbouncer: PgBouncerStatus + redis: RedisStatus +} + +// === External Statuses === +export interface SmtpStatus { + status: ServiceStatusType + lastCheck: string // ISO date + responseTimeMs: number +} + +export type OAuthStatusType = 'ok' | 'expiring_soon' | 'expired' | 'error' + +export interface OAuthTokenStatus { + status: OAuthStatusType + tokensTotal: number + tokensExpiringSoon: number + tokensExpired: number +} + +export interface CronJobStatus { + lastRun: string // ISO date + status: 'ok' | 'failed' | 'unknown' +} + +export interface CronStatuses { + communitySync: CronJobStatus + tokenRefresh: CronJobStatus + youtubeSync: CronJobStatus +} + +export interface ExternalStatuses { + smtp: SmtpStatus + metaOAuth: OAuthTokenStatus + youtubeOAuth: OAuthTokenStatus + cronJobs: CronStatuses +} + +// === Performance === +export interface PerformanceMetrics { + avgResponseTimeMs: number + p95ResponseTimeMs: number + p99ResponseTimeMs: number + errorRate: number // 0-1 + requestsPerMinute: number +} + +// === Full Snapshot === +export interface SystemMetrics { + timestamp: string // ISO date + system: SystemHealth + services: ServiceStatuses + external: ExternalStatuses + performance: PerformanceMetrics +} + +// === SSE Events (discriminated union) === +export type MonitoringEvent = + | { type: 'health'; data: SystemHealth } + | { type: 'service'; data: Partial } + | { type: 'alert'; data: AlertEvent } + | { type: 'log'; data: LogEvent } + | { type: 'performance'; data: PerformanceMetrics } + +export interface AlertEvent { + id: string + ruleId: string + metric: string + value: number + threshold: number + severity: AlertSeverity + message: string + timestamp: string +} + +export interface LogEvent { + id: string + level: LogLevel + source: LogSource + message: string + timestamp: string + context?: Record +} + +// === Enums as union types === +export type AlertCondition = 'gt' | 'lt' | 'eq' | 'gte' | 'lte' +export type AlertSeverity = 'warning' | 'error' | 'critical' +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal' +export type LogSource = 'payload' | 'queue-worker' | 'cron' | 'email' | 'oauth' | 'sync' + +// === Performance Tracker Entry === +export interface PerformanceEntry { + timestamp: number // Date.now() + method: string + path: string + statusCode: number + durationMs: number +} diff --git a/tests/unit/monitoring/types.test.ts b/tests/unit/monitoring/types.test.ts new file mode 100644 index 0000000..fabb04d --- /dev/null +++ b/tests/unit/monitoring/types.test.ts @@ -0,0 +1,93 @@ +import { describe, it, expect } from 'vitest' +import type { + SystemMetrics, + MonitoringEvent, + AlertCondition, + LogLevel, + LogSource, +} from '@/lib/monitoring/types' + +describe('Monitoring Types', () => { + it('SystemMetrics has all required sections', () => { + const metrics: SystemMetrics = { + timestamp: new Date().toISOString(), + system: { + cpuUsagePercent: 23, + memoryUsedMB: 4200, + memoryTotalMB: 8192, + memoryUsagePercent: 51.3, + diskUsedGB: 30, + diskTotalGB: 50, + diskUsagePercent: 60, + loadAvg1: 0.5, + loadAvg5: 0.8, + uptime: 1209600, + }, + services: { + payload: { + status: 'online', + pid: 1234, + memoryMB: 512, + uptimeSeconds: 86400, + restarts: 0, + }, + queueWorker: { + status: 'online', + pid: 5678, + memoryMB: 256, + uptimeSeconds: 86400, + restarts: 0, + }, + postgresql: { status: 'online', connections: 12, maxConnections: 50, latencyMs: 2 }, + pgbouncer: { status: 'online', activeConnections: 8, waitingClients: 0, poolSize: 20 }, + redis: { status: 'online', memoryUsedMB: 48, connectedClients: 5, opsPerSec: 120 }, + }, + external: { + smtp: { + status: 'online', + lastCheck: new Date().toISOString(), + responseTimeMs: 180, + }, + metaOAuth: { status: 'ok', tokensTotal: 2, tokensExpiringSoon: 1, tokensExpired: 0 }, + youtubeOAuth: { status: 'ok', tokensTotal: 3, tokensExpiringSoon: 0, tokensExpired: 0 }, + cronJobs: { + communitySync: { lastRun: new Date().toISOString(), status: 'ok' }, + tokenRefresh: { lastRun: new Date().toISOString(), status: 'ok' }, + youtubeSync: { lastRun: new Date().toISOString(), status: 'ok' }, + }, + }, + performance: { + avgResponseTimeMs: 120, + p95ResponseTimeMs: 350, + p99ResponseTimeMs: 800, + errorRate: 0.02, + requestsPerMinute: 45, + }, + } + + expect(metrics.system.cpuUsagePercent).toBe(23) + expect(metrics.services.payload.status).toBe('online') + expect(metrics.external.smtp.status).toBe('online') + expect(metrics.performance.avgResponseTimeMs).toBe(120) + }) + + it('MonitoringEvent types are exhaustive', () => { + const events: MonitoringEvent['type'][] = ['health', 'service', 'alert', 'log', 'performance'] + expect(events).toHaveLength(5) + }) + + it('AlertCondition covers all comparison operators', () => { + const conditions: AlertCondition[] = ['gt', 'lt', 'eq', 'gte', 'lte'] + expect(conditions).toHaveLength(5) + }) + + it('LogLevel covers all severity levels', () => { + const levels: LogLevel[] = ['debug', 'info', 'warn', 'error', 'fatal'] + expect(levels).toHaveLength(5) + }) + + it('LogSource covers all system components', () => { + const sources: LogSource[] = ['payload', 'queue-worker', 'cron', 'email', 'oauth', 'sync'] + expect(sources).toHaveLength(6) + }) +})