Something went wrong. Try again.
Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. forked from nekomimi.pet/wisp.place-monorepo
Something went wrong. Try again.
8.6 kB · 339 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340// DIY Observability - Logs, Metrics, and Error Tracking// Typesexport interface LogEntry { id: string timestamp: Date level: 'info' | 'warn' | 'error' | 'debug' message: string service: string context?: Record<string, any> traceId?: string eventType?: string}
export interface ErrorEntry { id: string timestamp: Date message: string stack?: string service: string context?: Record<string, any> count: number // How many times this error occurred lastSeen: Date}
export interface MetricEntry { timestamp: Date path: string method: string statusCode: number duration: number // in milliseconds service: string}
export interface DatabaseStats { totalSites: number totalDomains: number totalCustomDomains: number recentSites: any[] recentDomains: any[]}
// In-memory storage with rotationconst MAX_LOGS = 5000const MAX_ERRORS = 500const MAX_METRICS = 10000
const logs: LogEntry[] = []const errors: Map<string, ErrorEntry> = new Map()const metrics: MetricEntry[] = []
// Helper to generate unique IDslet logCounter = 0let errorCounter = 0
function generateId(prefix: string, counter: number): string { return `${prefix}-${Date.now()}-${counter}`}
// Helper to extract event type from messagefunction extractEventType(message: string): string | undefined { const match = message.match(/^\[([^\]]+)\]/) return match ? match[1] : undefined}
// Log collectorexport const logCollector = { log(level: LogEntry['level'], message: string, service: string, context?: Record<string, any>, traceId?: string) { const entry: LogEntry = { id: generateId('log', logCounter++), timestamp: new Date(), level, message, service, context, traceId, eventType: extractEventType(message) }
logs.unshift(entry)
// Rotate if needed if (logs.length > MAX_LOGS) { logs.splice(MAX_LOGS) }
// Also log to console for compatibility const contextStr = context ? ` ${JSON.stringify(context)}` : '' const traceStr = traceId ? ` [trace:${traceId}]` : '' console[level === 'debug' ? 'log' : level](`[${service}] ${message}${contextStr}${traceStr}`) },
info(message: string, service: string, context?: Record<string, any>, traceId?: string) { this.log('info', message, service, context, traceId) },
warn(message: string, service: string, context?: Record<string, any>, traceId?: string) { this.log('warn', message, service, context, traceId) },
error(message: string, service: string, error?: any, context?: Record<string, any>, traceId?: string) { const ctx = { ...context } if (error instanceof Error) { ctx.error = error.message ctx.stack = error.stack } else if (error) { ctx.error = String(error) } this.log('error', message, service, ctx, traceId)
// Also track in errors errorTracker.track(message, service, error, context) },
debug(message: string, service: string, context?: Record<string, any>, traceId?: string) { if (process.env.NODE_ENV !== 'production') { this.log('debug', message, service, context, traceId) } },
getLogs(filter?: { level?: string; service?: string; limit?: number; search?: string; eventType?: string }) { let filtered = [...logs]
if (filter?.level) { filtered = filtered.filter(log => log.level === filter.level) }
if (filter?.service) { filtered = filtered.filter(log => log.service === filter.service) }
if (filter?.eventType) { filtered = filtered.filter(log => log.eventType === filter.eventType) }
if (filter?.search) { const search = filter.search.toLowerCase() filtered = filtered.filter(log => log.message.toLowerCase().includes(search) || (log.context ? JSON.stringify(log.context).toLowerCase().includes(search) : false) ) }
const limit = filter?.limit || 100 return filtered.slice(0, limit) },
clear() { logs.length = 0 }}
// Error tracker with deduplicationexport const errorTracker = { track(message: string, service: string, error?: any, context?: Record<string, any>) { const key = `${service}:${message}`
const existing = errors.get(key) if (existing) { existing.count++ existing.lastSeen = new Date() if (context) { existing.context = { ...existing.context, ...context } } } else { const entry: ErrorEntry = { id: generateId('error', errorCounter++), timestamp: new Date(), message, service, context, count: 1, lastSeen: new Date() }
if (error instanceof Error) { entry.stack = error.stack }
errors.set(key, entry)
// Rotate if needed if (errors.size > MAX_ERRORS) { const oldest = Array.from(errors.keys())[0] errors.delete(oldest) } } },
getErrors(filter?: { service?: string; limit?: number }) { let filtered = Array.from(errors.values())
if (filter?.service) { filtered = filtered.filter(err => err.service === filter.service) }
// Sort by last seen (most recent first) filtered.sort((a, b) => b.lastSeen.getTime() - a.lastSeen.getTime())
const limit = filter?.limit || 100 return filtered.slice(0, limit) },
clear() { errors.clear() }}
// Metrics collectorexport const metricsCollector = { recordRequest(path: string, method: string, statusCode: number, duration: number, service: string) { const entry: MetricEntry = { timestamp: new Date(), path, method, statusCode, duration, service }
metrics.unshift(entry)
// Rotate if needed if (metrics.length > MAX_METRICS) { metrics.splice(MAX_METRICS) } },
getMetrics(filter?: { service?: string; timeWindow?: number }) { let filtered = [...metrics]
if (filter?.service) { filtered = filtered.filter(m => m.service === filter.service) }
if (filter?.timeWindow) { const cutoff = Date.now() - filter.timeWindow filtered = filtered.filter(m => m.timestamp.getTime() > cutoff) }
return filtered },
getStats(service?: string, timeWindow: number = 3600000) { const filtered = this.getMetrics({ service, timeWindow })
if (filtered.length === 0) { return { totalRequests: 0, avgDuration: 0, p50Duration: 0, p95Duration: 0, p99Duration: 0, errorRate: 0, requestsPerMinute: 0 } }
const durations = filtered.map(m => m.duration).sort((a, b) => a - b) const totalDuration = durations.reduce((sum, d) => sum + d, 0) const errors = filtered.filter(m => m.statusCode >= 400).length
const p50 = durations[Math.floor(durations.length * 0.5)] const p95 = durations[Math.floor(durations.length * 0.95)] const p99 = durations[Math.floor(durations.length * 0.99)]
const timeWindowMinutes = timeWindow / 60000
return { totalRequests: filtered.length, avgDuration: Math.round(totalDuration / filtered.length), p50Duration: Math.round(p50), p95Duration: Math.round(p95), p99Duration: Math.round(p99), errorRate: (errors / filtered.length) * 100, requestsPerMinute: Math.round(filtered.length / timeWindowMinutes) } },
clear() { metrics.length = 0 }}
// Elysia middleware for request timingexport function observabilityMiddleware(service: string) { return { beforeHandle: ({ request }: any) => { // Store start time on request object (request as any).__startTime = Date.now() }, afterHandle: ({ request, set }: any) => { const duration = Date.now() - ((request as any).__startTime || Date.now()) const url = new URL(request.url)
metricsCollector.recordRequest( url.pathname, request.method, set.status || 200, duration, service ) }, onError: ({ request, error, set }: any) => { const duration = Date.now() - ((request as any).__startTime || Date.now()) const url = new URL(request.url)
metricsCollector.recordRequest( url.pathname, request.method, set.status || 500, duration, service )
// Don't log 404 errors const statusCode = set.status || 500 if (statusCode !== 404) { logCollector.error( `Request failed: ${request.method} ${url.pathname}`, service, error, { statusCode } ) } } }}
// Export singleton logger for easy accessexport const logger = { info: (message: string, context?: Record<string, any>) => logCollector.info(message, 'main-app', context), warn: (message: string, context?: Record<string, any>) => logCollector.warn(message, 'main-app', context), error: (message: string, error?: any, context?: Record<string, any>) => logCollector.error(message, 'main-app', error, context), debug: (message: string, context?: Record<string, any>) => logCollector.debug(message, 'main-app', context)}