Persist spans to DB via logger

Implement fire-and-forget span persistence:
- Add logSpanToDatabase and persistSpanToDatabase to logger
- Persist spans, attributes, events, and event attributes to new tables
- Wire edgeFunctionWrapper to call DB persistence after each span
- Create required tables, views, and security policies
- Ensure non-blocking and guard for missing Supabase creds
This commit is contained in:
gpt-engineer-app[bot]
2025-11-11 04:28:17 +00:00
parent 177eb540a8
commit 0dfc5ff724
3 changed files with 241 additions and 2 deletions

View File

@@ -3,6 +3,7 @@
* Prevents sensitive data exposure and provides consistent log format
*/
import { createClient } from 'jsr:@supabase/supabase-js@2';
import { formatEdgeError } from './errorFormatter.ts';
type LogLevel = 'info' | 'warn' | 'error' | 'debug';
@@ -178,7 +179,7 @@ export function injectSpanContextIntoHeaders(spanContext: SpanContext): Record<s
}
/**
* Log completed span
* Log completed span (console only)
*/
export function logSpan(span: Span): void {
const sanitizedAttributes = sanitizeContext(span.attributes);
@@ -196,6 +197,159 @@ export function logSpan(span: Span): void {
});
}
/**
* Persist span to database (fire-and-forget)
* Call this after logging the span to store it for monitoring/debugging
*/
export function logSpanToDatabase(span: Span, requestId?: string): void {
// Fire-and-forget - don't await or block on this
persistSpanToDatabase(span, requestId).catch((error) => {
edgeLogger.error('Failed to persist span to database', {
spanId: span.spanId,
traceId: span.traceId,
error: formatEdgeError(error),
});
});
}
/**
* Internal function to persist span to database
*/
async function persistSpanToDatabase(span: Span, requestId?: string): Promise<void> {
const supabaseUrl = Deno.env.get('SUPABASE_URL');
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY');
if (!supabaseUrl || !supabaseServiceKey) {
edgeLogger.warn('Skipping span persistence - Supabase credentials not configured');
return;
}
const supabase = createClient(supabaseUrl, supabaseServiceKey);
// Insert span
const { error: spanError } = await supabase
.from('request_spans')
.insert({
span_id: span.spanId,
trace_id: span.traceId,
parent_span_id: span.parentSpanId,
request_id: requestId,
name: span.name,
kind: span.kind,
start_time: new Date(span.startTime).toISOString(),
end_time: span.endTime ? new Date(span.endTime).toISOString() : null,
duration_ms: span.duration,
status: span.status,
error_type: span.error?.type,
error_message: span.error?.message,
error_stack: span.error?.stack,
});
if (spanError) {
throw new Error(`Failed to insert span: ${spanError.message}`);
}
// Insert attributes
const attributeInserts = Object.entries(span.attributes).map(([key, value]) => {
let valueType: 'string' | 'number' | 'boolean' = 'string';
let valueStr: string;
if (typeof value === 'number') {
valueType = 'number';
valueStr = String(value);
} else if (typeof value === 'boolean') {
valueType = 'boolean';
valueStr = String(value);
} else {
valueStr = String(value);
}
return {
span_id: span.spanId,
key,
value: valueStr,
value_type: valueType,
};
});
if (attributeInserts.length > 0) {
const { error: attrError } = await supabase
.from('span_attributes')
.insert(attributeInserts);
if (attrError) {
edgeLogger.warn('Failed to insert span attributes', {
spanId: span.spanId,
error: attrError.message,
});
}
}
// Insert events
for (let i = 0; i < span.events.length; i++) {
const event = span.events[i];
const { data: eventData, error: eventError } = await supabase
.from('span_events')
.insert({
span_id: span.spanId,
timestamp: new Date(event.timestamp).toISOString(),
name: event.name,
sequence_order: i,
})
.select('id')
.single();
if (eventError || !eventData) {
edgeLogger.warn('Failed to insert span event', {
spanId: span.spanId,
eventName: event.name,
error: eventError?.message,
});
continue;
}
// Insert event attributes
if (event.attributes) {
const eventAttrInserts = Object.entries(event.attributes).map(([key, value]) => {
let valueType: 'string' | 'number' | 'boolean' = 'string';
let valueStr: string;
if (typeof value === 'number') {
valueType = 'number';
valueStr = String(value);
} else if (typeof value === 'boolean') {
valueType = 'boolean';
valueStr = String(value);
} else {
valueStr = String(value);
}
return {
span_event_id: eventData.id,
key,
value: valueStr,
value_type: valueType,
};
});
if (eventAttrInserts.length > 0) {
const { error: eventAttrError } = await supabase
.from('span_event_attributes')
.insert(eventAttrInserts);
if (eventAttrError) {
edgeLogger.warn('Failed to insert event attributes', {
spanId: span.spanId,
eventName: event.name,
error: eventAttrError.message,
});
}
}
}
}
}
// Fields that should never be logged
const SENSITIVE_FIELDS = [
'password',