/** * Structured logging utility for edge functions * Prevents sensitive data exposure and provides consistent log format */ type LogLevel = 'info' | 'warn' | 'error' | 'debug'; interface LogContext { requestId?: string; // Correlation ID for tracing userId?: string; action?: string; duration?: number; // Request duration in ms traceId?: string; // Distributed tracing ID [key: string]: unknown; } // Span types for distributed tracing export interface Span { spanId: string; traceId: string; parentSpanId?: string; name: string; kind: 'SERVER' | 'CLIENT' | 'INTERNAL' | 'DATABASE'; startTime: number; endTime?: number; duration?: number; attributes: Record; events: SpanEvent[]; status: 'ok' | 'error' | 'unset'; error?: { type: string; message: string; stack?: string; }; } export interface SpanEvent { timestamp: number; name: string; attributes?: Record; } export interface SpanContext { traceId: string; spanId: string; traceFlags?: number; } // Request tracking utilities (legacy - use spans instead) export interface RequestTracking { requestId: string; start: number; traceId?: string; } export function startRequest(traceId?: string): RequestTracking { return { requestId: crypto.randomUUID(), start: Date.now(), traceId: traceId || crypto.randomUUID(), }; } export function endRequest(tracking: RequestTracking): number { return Date.now() - tracking.start; } // ============================================================================ // Span Lifecycle Functions // ============================================================================ /** * Start a new span */ export function startSpan( name: string, kind: Span['kind'], parentSpan?: SpanContext, attributes?: Record ): Span { const traceId = parentSpan?.traceId || crypto.randomUUID(); return { spanId: crypto.randomUUID(), traceId, parentSpanId: parentSpan?.spanId, name, kind, startTime: Date.now(), attributes: attributes || {}, events: [], status: 'unset', }; } /** * End a span with final status */ export function endSpan(span: Span, status?: 'ok' | 'error', error?: Error): Span { span.endTime = Date.now(); span.duration = span.endTime - span.startTime; span.status = status || 'ok'; if (error) { span.error = { type: error.name, message: error.message, stack: error.stack, }; } return span; } /** * Add event to span */ export function addSpanEvent( span: Span, name: string, attributes?: Record ): void { span.events.push({ timestamp: Date.now(), name, attributes, }); } /** * Set span attributes */ export function setSpanAttributes( span: Span, attributes: Record ): void { span.attributes = { ...span.attributes, ...attributes }; } /** * Extract span context for propagation */ export function getSpanContext(span: Span): SpanContext { return { traceId: span.traceId, spanId: span.spanId, }; } /** * Extract span context from HTTP headers (W3C Trace Context) */ export function extractSpanContextFromHeaders(headers: Headers): SpanContext | undefined { const traceparent = headers.get('traceparent'); if (!traceparent) return undefined; // Parse W3C traceparent: version-traceId-spanId-flags const parts = traceparent.split('-'); if (parts.length !== 4) return undefined; return { traceId: parts[1], spanId: parts[2], traceFlags: parseInt(parts[3], 16), }; } /** * Inject span context into headers */ export function injectSpanContextIntoHeaders(spanContext: SpanContext): Record { return { 'traceparent': `00-${spanContext.traceId}-${spanContext.spanId}-01`, }; } /** * Log completed span */ export function logSpan(span: Span): void { const sanitizedAttributes = sanitizeContext(span.attributes); const sanitizedEvents = span.events.map(e => ({ ...e, attributes: e.attributes ? sanitizeContext(e.attributes) : undefined, })); edgeLogger.info('Span completed', { span: { ...span, attributes: sanitizedAttributes, events: sanitizedEvents, }, }); } // Fields that should never be logged const SENSITIVE_FIELDS = [ 'password', 'token', 'secret', 'api_key', 'apikey', 'authorization', 'email', 'phone', 'ssn', 'credit_card', 'ip_address', 'session_id' ]; /** * Sanitize context to remove sensitive data */ export function sanitizeContext(context: LogContext): LogContext { const sanitized: LogContext = {}; for (const [key, value] of Object.entries(context)) { const lowerKey = key.toLowerCase(); // Skip sensitive fields if (SENSITIVE_FIELDS.some(field => lowerKey.includes(field))) { sanitized[key] = '[REDACTED]'; continue; } // Recursively sanitize objects if (value && typeof value === 'object' && !Array.isArray(value)) { sanitized[key] = sanitizeContext(value as LogContext); } else { sanitized[key] = value; } } return sanitized; } /** * Format log message with context */ function formatLog(level: LogLevel, message: string, context?: LogContext): string { const timestamp = new Date().toISOString(); const sanitizedContext = context ? sanitizeContext(context) : {}; return JSON.stringify({ timestamp, level, message, ...sanitizedContext }); } export const edgeLogger = { info: (message: string, context?: LogContext): void => { console.info(formatLog('info', message, context)); }, warn: (message: string, context?: LogContext): void => { console.warn(formatLog('warn', message, context)); }, error: (message: string, context?: LogContext): void => { console.error(formatLog('error', message, context)); }, debug: (message: string, context?: LogContext): void => { console.debug(formatLog('debug', message, context)); } };