Files
thrilltrack-explorer/supabase/functions/_shared/logger.ts
gpt-engineer-app[bot] 2d65f13b85 Connect to Lovable Cloud
Add centralized errorFormatter to convert various error types into readable messages, and apply it across edge functions. Replace String(error) usage with formatEdgeError, update relevant imports, fix a throw to use toError, and enhance logger to log formatted errors. Includes new errorFormatter.ts and widespread updates to 18+ edge functions plus logger integration.
2025-11-10 18:09:15 +00:00

273 lines
6.0 KiB
TypeScript

/**
* Structured logging utility for edge functions
* Prevents sensitive data exposure and provides consistent log format
*/
import { formatEdgeError } from './errorFormatter.ts';
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<string, unknown>;
events: SpanEvent[];
status: 'ok' | 'error' | 'unset';
error?: {
type: string;
message: string;
stack?: string;
};
}
export interface SpanEvent {
timestamp: number;
name: string;
attributes?: Record<string, unknown>;
}
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<string, unknown>
): 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?: unknown): Span {
span.endTime = Date.now();
span.duration = span.endTime - span.startTime;
span.status = status || 'ok';
if (error) {
const err = error instanceof Error ? error : new Error(formatEdgeError(error));
span.error = {
type: err.name,
message: err.message,
stack: err.stack,
};
}
return span;
}
/**
* Add event to span
*/
export function addSpanEvent(
span: Span,
name: string,
attributes?: Record<string, unknown>
): void {
span.events.push({
timestamp: Date.now(),
name,
attributes,
});
}
/**
* Set span attributes
*/
export function setSpanAttributes(
span: Span,
attributes: Record<string, unknown>
): 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<string, string> {
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));
}
};