import { ONEUPTIME_URL } from "../Config"; import AIAgentAPIRequest from "./AIAgentAPIRequest"; import URL from "Common/Types/API/URL"; import API from "Common/Utils/API"; import HTTPResponse from "Common/Types/API/HTTPResponse"; import { JSONObject } from "Common/Types/JSON"; import LogSeverity from "Common/Types/Log/LogSeverity"; import logger from "Common/Server/Utils/Logger"; import OneUptimeDate from "Common/Types/Date"; export interface TaskLoggerOptions { taskId: string; context?: string; batchSize?: number; flushIntervalMs?: number; } interface LogEntry { severity: LogSeverity; message: string; timestamp: Date; } export default class TaskLogger { private taskId: string; private context: string | undefined; private logBuffer: Array = []; private batchSize: number; private flushIntervalMs: number; private flushTimer: ReturnType | null = null; private createLogUrl: URL | null = null; public constructor(options: TaskLoggerOptions) { this.taskId = options.taskId; this.context = options.context; this.batchSize = options.batchSize || 10; this.flushIntervalMs = options.flushIntervalMs || 5000; // 5 seconds default // Start periodic flush timer this.startFlushTimer(); } private getCreateLogUrl(): URL { if (!this.createLogUrl) { this.createLogUrl = URL.fromString(ONEUPTIME_URL.toString()).addRoute( "/api/ai-agent-task-log/create-log", ); } return this.createLogUrl; } private startFlushTimer(): void { this.flushTimer = setInterval(() => { this.flush().catch((err: Error) => { logger.error(`Error flushing logs: ${err.message}`); }); }, this.flushIntervalMs); } private stopFlushTimer(): void { if (this.flushTimer) { clearInterval(this.flushTimer); this.flushTimer = null; } } private formatMessage( severity: LogSeverity, message: string, timestamp: Date, ): string { const timestampStr: string = OneUptimeDate.toDateTimeLocalString(timestamp); const severityStr: string = severity.toUpperCase().padEnd(7); const contextStr: string = this.context ? `[${this.context}] ` : ""; return `[${timestampStr}] [${severityStr}] ${contextStr}${message}`; } private addToBuffer(severity: LogSeverity, message: string): void { const entry: LogEntry = { severity, message, timestamp: OneUptimeDate.getCurrentDate(), }; this.logBuffer.push(entry); // Also log locally for debugging logger.debug( `[Task ${this.taskId}] ${this.formatMessage(entry.severity, entry.message, entry.timestamp)}`, ); // Auto-flush if buffer is full if (this.logBuffer.length >= this.batchSize) { this.flush().catch((err: Error) => { logger.error(`Error auto-flushing logs: ${err.message}`); }); } } private async sendLogToServer( severity: LogSeverity, message: string, ): Promise { try { const result: HTTPResponse = await API.post({ url: this.getCreateLogUrl(), data: { ...AIAgentAPIRequest.getDefaultRequestBody(), taskId: this.taskId, severity: severity, message: message, }, }); if (!result.isSuccess()) { logger.error(`Failed to send log for task ${this.taskId}`); return false; } return true; } catch (error) { logger.error(`Error sending log for task ${this.taskId}:`); logger.error(error); return false; } } // Public logging methods public async debug(message: string): Promise { this.addToBuffer(LogSeverity.Debug, message); } public async info(message: string): Promise { this.addToBuffer(LogSeverity.Information, message); } public async warning(message: string): Promise { this.addToBuffer(LogSeverity.Warning, message); } public async error(message: string): Promise { this.addToBuffer(LogSeverity.Error, message); // Immediately flush on errors await this.flush(); } public async trace(message: string): Promise { this.addToBuffer(LogSeverity.Trace, message); } // Log output from external processes like OpenCode public async logProcessOutput( processName: string, output: string, ): Promise { const lines: Array = output.split("\n").filter((line: string) => { return line.trim().length > 0; }); for (const line of lines) { this.addToBuffer(LogSeverity.Information, `[${processName}] ${line}`); } } // Log a code block (useful for stack traces, code snippets, etc.) public async logCodeBlock( title: string, code: string, severity: LogSeverity = LogSeverity.Information, ): Promise { const formattedCode: string = `${title}:\n\`\`\`\n${code}\n\`\`\``; this.addToBuffer(severity, formattedCode); } // Flush all buffered logs to the server public async flush(): Promise { if (this.logBuffer.length === 0) { return; } // Get all entries and clear buffer const entries: Array = [...this.logBuffer]; this.logBuffer = []; // Send each log entry separately to preserve individual log lines for (const entry of entries) { const formattedMessage: string = this.formatMessage( entry.severity, entry.message, entry.timestamp, ); await this.sendLogToServer(entry.severity, formattedMessage); } } // Cleanup method - call when task is done public async dispose(): Promise { this.stopFlushTimer(); await this.flush(); } // Helper methods for common log patterns public async logStepStart(stepName: string): Promise { await this.info(`Starting: ${stepName}`); } public async logStepComplete(stepName: string): Promise { await this.info(`Completed: ${stepName}`); } public async logStepFailed(stepName: string, error: string): Promise { await this.error(`Failed: ${stepName} - ${error}`); } // Create a child logger with additional context public createChildLogger(childContext: string): TaskLogger { const fullContext: string = this.context ? `${this.context}:${childContext}` : childContext; return new TaskLogger({ taskId: this.taskId, context: fullContext, batchSize: this.batchSize, flushIntervalMs: this.flushIntervalMs, }); } }