You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
5.9 KiB
237 lines
5.9 KiB
/**
|
|
* Centralized logging utility for Node Banana
|
|
*
|
|
* Features:
|
|
* - Session-based logging (one log file per workflow execution)
|
|
* - Automatic rotation (keeps last 10 sessions)
|
|
* - Privacy-aware (truncates prompts, logs image metadata not full data)
|
|
* - Structured JSON format
|
|
*/
|
|
|
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
|
|
export type LogCategory =
|
|
| 'workflow.start'
|
|
| 'workflow.end'
|
|
| 'workflow.error'
|
|
| 'workflow.validation'
|
|
| 'node.execution'
|
|
| 'node.error'
|
|
| 'api.gemini'
|
|
| 'api.openai'
|
|
| 'api.llm'
|
|
| 'api.error'
|
|
| 'file.save'
|
|
| 'file.load'
|
|
| 'file.error'
|
|
| 'connection.validation'
|
|
| 'state.change'
|
|
| 'system';
|
|
|
|
export interface LogEntry {
|
|
timestamp: string;
|
|
level: LogLevel;
|
|
category: LogCategory;
|
|
message: string;
|
|
context?: Record<string, any>;
|
|
error?: {
|
|
message: string;
|
|
stack?: string;
|
|
name?: string;
|
|
};
|
|
}
|
|
|
|
export interface LogSession {
|
|
sessionId: string;
|
|
startTime: string;
|
|
endTime?: string;
|
|
entries: LogEntry[];
|
|
}
|
|
|
|
// Type for server-only operations
|
|
type LoggerServer = {
|
|
saveSession: (session: LogSession) => Promise<void>;
|
|
rotateLogFiles: () => Promise<void>;
|
|
};
|
|
|
|
class Logger {
|
|
private currentSession: LogSession | null = null;
|
|
private isClient: boolean;
|
|
|
|
constructor() {
|
|
this.isClient = typeof window !== 'undefined';
|
|
}
|
|
|
|
/**
|
|
* Initialize a new logging session for a workflow execution
|
|
* Note: On client side, this just tracks in memory. File writing happens server-side in API routes.
|
|
*/
|
|
async startSession(): Promise<string> {
|
|
const sessionId = this.generateSessionId();
|
|
const startTime = new Date().toISOString();
|
|
|
|
this.currentSession = {
|
|
sessionId,
|
|
startTime,
|
|
entries: [],
|
|
};
|
|
|
|
this.log('info', 'system', `Session started: ${sessionId}`);
|
|
|
|
return sessionId;
|
|
}
|
|
|
|
/**
|
|
* End the current session
|
|
* Note: On client side, this just clears memory. File writing happens server-side in API routes.
|
|
*/
|
|
async endSession(): Promise<void> {
|
|
if (!this.currentSession) {
|
|
return;
|
|
}
|
|
|
|
this.currentSession.endTime = new Date().toISOString();
|
|
this.log('info', 'system', `Session ended: ${this.currentSession.sessionId}`);
|
|
|
|
this.currentSession = null;
|
|
}
|
|
|
|
/**
|
|
* Get the current session (useful for server-side code to save it)
|
|
*/
|
|
getCurrentSession(): LogSession | null {
|
|
return this.currentSession;
|
|
}
|
|
|
|
/**
|
|
* Log a message
|
|
*/
|
|
log(
|
|
level: LogLevel,
|
|
category: LogCategory,
|
|
message: string,
|
|
context?: Record<string, any>,
|
|
error?: Error
|
|
): void {
|
|
const entry: LogEntry = {
|
|
timestamp: new Date().toISOString(),
|
|
level,
|
|
category,
|
|
message,
|
|
};
|
|
|
|
if (context) {
|
|
entry.context = this.sanitizeContext(context);
|
|
}
|
|
|
|
if (error) {
|
|
entry.error = {
|
|
message: error.message,
|
|
stack: error.stack,
|
|
name: error.name,
|
|
};
|
|
}
|
|
|
|
// Add to current session if exists
|
|
if (this.currentSession) {
|
|
this.currentSession.entries.push(entry);
|
|
}
|
|
|
|
// Also log to console for development
|
|
const consoleMethod = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log';
|
|
console[consoleMethod](`[${category}] ${message}`, context || '', error || '');
|
|
}
|
|
|
|
/**
|
|
* Convenience methods
|
|
*/
|
|
info(category: LogCategory, message: string, context?: Record<string, any>): void {
|
|
this.log('info', category, message, context);
|
|
}
|
|
|
|
warn(category: LogCategory, message: string, context?: Record<string, any>): void {
|
|
this.log('warn', category, message, context);
|
|
}
|
|
|
|
error(category: LogCategory, message: string, context?: Record<string, any>, error?: Error): void {
|
|
this.log('error', category, message, context, error);
|
|
}
|
|
|
|
/**
|
|
* Get the current session ID
|
|
*/
|
|
getSessionId(): string | null {
|
|
return this.currentSession?.sessionId || null;
|
|
}
|
|
|
|
/**
|
|
* Generate a unique session ID
|
|
*/
|
|
private generateSessionId(): string {
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
|
const random = Math.random().toString(36).substring(2, 8);
|
|
return `exec-${timestamp}-${random}`;
|
|
}
|
|
|
|
/**
|
|
* Sanitize context to protect privacy and reduce log size
|
|
*/
|
|
private sanitizeContext(context: Record<string, any>): Record<string, any> {
|
|
const sanitized: Record<string, any> = {};
|
|
|
|
for (const [key, value] of Object.entries(context)) {
|
|
// Truncate prompts to 200 characters
|
|
if (key === 'prompt' && typeof value === 'string') {
|
|
sanitized[key] = value.length > 200 ? value.substring(0, 200) + '...[truncated]' : value;
|
|
}
|
|
// Convert image data URIs to metadata
|
|
else if (key === 'image' || key === 'images') {
|
|
if (typeof value === 'string' && value.startsWith('data:image')) {
|
|
sanitized[key] = this.extractImageMetadata(value);
|
|
} else if (Array.isArray(value)) {
|
|
sanitized[key] = value.map(img =>
|
|
typeof img === 'string' && img.startsWith('data:image')
|
|
? this.extractImageMetadata(img)
|
|
: img
|
|
);
|
|
} else {
|
|
sanitized[key] = value;
|
|
}
|
|
}
|
|
// Handle nested objects
|
|
else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
sanitized[key] = this.sanitizeContext(value);
|
|
}
|
|
// Keep other values as-is
|
|
else {
|
|
sanitized[key] = value;
|
|
}
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
/**
|
|
* Extract metadata from image data URI
|
|
*/
|
|
private extractImageMetadata(dataUri: string): object {
|
|
const match = dataUri.match(/^data:image\/(\w+);base64,(.+)$/);
|
|
if (!match) {
|
|
return { error: 'Invalid image data URI' };
|
|
}
|
|
|
|
const [, format, base64Data] = match;
|
|
const sizeInBytes = base64Data.length * 0.75; // Approximate size from base64
|
|
const sizeInKB = Math.round(sizeInBytes / 1024);
|
|
|
|
return {
|
|
format,
|
|
sizeKB: sizeInKB,
|
|
isDataURI: true,
|
|
};
|
|
}
|
|
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const logger = new Logger();
|
|
|