Files
thrilltrack-explorer/supabase/functions/_shared/logger.ts
gpt-engineer-app[bot] 96adb2b15e Connect to Lovable Cloud
Implement distributed tracing across edge functions:
- Introduce span types and utilities (logger.ts enhancements)
- Replace request tracking with span-based tracing in approval and rejection flows
- Propagate and manage W3C trace context in edge tracking
- Add span visualization scaffolding (spanVisualizer.ts) and admin TraceViewer UI (TraceViewer.tsx)
- Create tracing-related type definitions and support files
- Prepare RPC call logging with span context and retries
2025-11-10 14:40:44 +00:00

270 lines
5.9 KiB
TypeScript

/**
* 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<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?: 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<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));
}
};