Skip to content

Commit ab2680c

Browse files
add longer running logs rotation
1 parent 331e646 commit ab2680c

File tree

1 file changed

+139
-12
lines changed

1 file changed

+139
-12
lines changed

stagehand/src/index.ts

Lines changed: 139 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,79 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
2626
// Configure logging
2727
const LOG_DIR = path.join(__dirname, '../logs');
2828
const LOG_FILE = path.join(LOG_DIR, `stagehand-${new Date().toISOString().split('T')[0]}.log`);
29+
const MAX_LOG_FILES = 10; // Maximum number of log files to keep
30+
const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB max log file size
2931

3032
// Ensure log directory exists
3133
if (!fs.existsSync(LOG_DIR)) {
3234
fs.mkdirSync(LOG_DIR, { recursive: true });
3335
}
3436

37+
// Setup log rotation management
38+
function setupLogRotation() {
39+
try {
40+
// Check if current log file exceeds max size
41+
if (fs.existsSync(LOG_FILE) && fs.statSync(LOG_FILE).size > MAX_LOG_SIZE) {
42+
const timestamp = new Date().toISOString().replace(/:/g, '-');
43+
const rotatedLogFile = path.join(LOG_DIR, `stagehand-${timestamp}.log`);
44+
fs.renameSync(LOG_FILE, rotatedLogFile);
45+
}
46+
47+
// Clean up old log files if we have too many
48+
const logFiles = fs.readdirSync(LOG_DIR)
49+
.filter(file => file.startsWith('stagehand-') && file.endsWith('.log'))
50+
.map(file => path.join(LOG_DIR, file))
51+
.sort((a, b) => fs.statSync(b).mtime.getTime() - fs.statSync(a).mtime.getTime());
52+
53+
if (logFiles.length > MAX_LOG_FILES) {
54+
logFiles.slice(MAX_LOG_FILES).forEach(file => {
55+
try {
56+
fs.unlinkSync(file);
57+
} catch (err) {
58+
console.error(`Failed to delete old log file ${file}:`, err);
59+
}
60+
});
61+
}
62+
} catch (err) {
63+
console.error('Error in log rotation:', err);
64+
}
65+
}
66+
67+
// Run log rotation on startup
68+
setupLogRotation();
69+
70+
// Queue for batching log writes
71+
let logQueue: string[] = [];
72+
let logWriteTimeout: NodeJS.Timeout | null = null;
73+
const LOG_FLUSH_INTERVAL = 1000; // Flush logs every second
74+
75+
// Flush logs to disk asynchronously
76+
async function flushLogs() {
77+
if (logQueue.length === 0) return;
78+
79+
const logsToWrite = logQueue.join('\n') + '\n';
80+
logQueue = [];
81+
logWriteTimeout = null;
82+
83+
try {
84+
await fs.promises.appendFile(LOG_FILE, logsToWrite);
85+
86+
// Check if we need to rotate logs after write
87+
const stats = await fs.promises.stat(LOG_FILE);
88+
if (stats.size > MAX_LOG_SIZE) {
89+
setupLogRotation();
90+
}
91+
} catch (err) {
92+
console.error('Failed to write logs to file:', err);
93+
// If write fails, try to use sync version as fallback
94+
try {
95+
fs.appendFileSync(LOG_FILE, logsToWrite);
96+
} catch (syncErr) {
97+
console.error('Failed to write logs synchronously:', syncErr);
98+
}
99+
}
100+
}
101+
35102
// Helper function to convert LogLine to string
36103
function logLineToString(logLine: LogLine): string {
37104
const timestamp = logLine.timestamp ? new Date(logLine.timestamp).toISOString() : new Date().toISOString();
@@ -239,16 +306,35 @@ let stagehand: Stagehand | undefined;
239306
let serverInstance: Server | undefined;
240307
const consoleLogs: string[] = [];
241308
const operationLogs: string[] = [];
309+
const MAX_OPERATION_LOGS = 1000; // Prevent operation logs from growing too large
242310

243311
function log(message: string, level: 'info' | 'error' | 'debug' = 'info') {
244312
const timestamp = new Date().toISOString();
245313
const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
314+
315+
// Manage operation logs with size limit
246316
operationLogs.push(logMessage);
317+
if (operationLogs.length > MAX_OPERATION_LOGS) {
318+
// Keep most recent logs but trim the middle to maintain context
319+
const half = Math.floor(MAX_OPERATION_LOGS / 2);
320+
// Keep first 100 and last (MAX_OPERATION_LOGS - 100) logs
321+
const firstLogs = operationLogs.slice(0, 100);
322+
const lastLogs = operationLogs.slice(operationLogs.length - (MAX_OPERATION_LOGS - 100));
323+
operationLogs.length = 0;
324+
operationLogs.push(...firstLogs);
325+
operationLogs.push(`[...${operationLogs.length - MAX_OPERATION_LOGS} logs truncated...]`);
326+
operationLogs.push(...lastLogs);
327+
}
247328

248-
// Write to file
249-
fs.appendFileSync(LOG_FILE, logMessage + '\n');
329+
// Queue log for async writing
330+
logQueue.push(logMessage);
250331

251-
// Console output to stderr
332+
// Setup timer to flush logs if not already scheduled
333+
if (!logWriteTimeout) {
334+
logWriteTimeout = setTimeout(flushLogs, LOG_FLUSH_INTERVAL);
335+
}
336+
337+
// Console output to stderr for debugging
252338
if (process.env.DEBUG || level === 'error') {
253339
console.error(logMessage);
254340
}
@@ -262,6 +348,26 @@ function log(message: string, level: 'info' | 'error' | 'debug' = 'info') {
262348
}
263349
}
264350

351+
// Add log rotation check periodically
352+
setInterval(() => {
353+
setupLogRotation();
354+
}, 15 * 60 * 1000); // Check every 15 minutes
355+
356+
function formatLogResponse(logs: string[]): string {
357+
if (logs.length <= 100) {
358+
return logs.join("\n");
359+
}
360+
361+
// For very long logs, include first and last parts with truncation notice
362+
const first = logs.slice(0, 50);
363+
const last = logs.slice(-50);
364+
return [
365+
...first,
366+
`\n... ${logs.length - 100} more log entries (truncated) ...\n`,
367+
...last
368+
].join("\n");
369+
}
370+
265371
function logRequest(type: string, params: any) {
266372
const requestLog = {
267373
timestamp: new Date().toISOString(),
@@ -327,15 +433,13 @@ async function handleToolCall(
327433
},
328434
{
329435
type: "text",
330-
text: `Operation logs:\n${operationLogs.join("\n")}`,
436+
text: `Operation logs:\n${formatLogResponse(operationLogs)}`,
331437
},
332438
],
333439
isError: true,
334440
};
335441
}
336442

337-
338-
339443
switch (name) {
340444
case "stagehand_navigate":
341445
try {
@@ -359,7 +463,7 @@ async function handleToolCall(
359463
},
360464
{
361465
type: "text",
362-
text: `Operation logs:\n${operationLogs.join("\n")}`,
466+
text: `Operation logs:\n${formatLogResponse(operationLogs)}`,
363467
},
364468
],
365469
isError: true,
@@ -392,7 +496,7 @@ async function handleToolCall(
392496
},
393497
{
394498
type: "text",
395-
text: `Operation logs:\n${operationLogs.join("\n")}`,
499+
text: `Operation logs:\n${formatLogResponse(operationLogs)}`,
396500
},
397501
],
398502
isError: true,
@@ -428,7 +532,7 @@ async function handleToolCall(
428532
},
429533
{
430534
type: "text",
431-
text: `Operation logs:\n${operationLogs.join("\n")}`,
535+
text: `Operation logs:\n${formatLogResponse(operationLogs)}`,
432536
},
433537
],
434538
isError: true,
@@ -459,7 +563,7 @@ async function handleToolCall(
459563
},
460564
{
461565
type: "text",
462-
text: `Operation logs:\n${operationLogs.join("\n")}`,
566+
text: `Operation logs:\n${formatLogResponse(operationLogs)}`,
463567
},
464568
],
465569
isError: true,
@@ -510,7 +614,7 @@ async function handleToolCall(
510614
},
511615
{
512616
type: "text",
513-
text: `Operation logs:\n${operationLogs.join("\n")}`,
617+
text: `Operation logs:\n${formatLogResponse(operationLogs)}`,
514618
},
515619
],
516620
isError: true,
@@ -526,7 +630,7 @@ async function handleToolCall(
526630
},
527631
{
528632
type: "text",
529-
text: `Operation logs:\n${operationLogs.join("\n")}`,
633+
text: `Operation logs:\n${formatLogResponse(operationLogs)}`,
530634
},
531635
],
532636
isError: true,
@@ -652,3 +756,26 @@ runServer().catch((error) => {
652756
const errorMsg = error instanceof Error ? error.message : String(error);
653757
console.error(errorMsg);
654758
});
759+
760+
// Make sure logs are flushed when the process exits
761+
process.on('exit', () => {
762+
if (logQueue.length > 0) {
763+
try {
764+
fs.appendFileSync(LOG_FILE, logQueue.join('\n') + '\n');
765+
} catch (err) {
766+
console.error('Failed to flush logs on exit:', err);
767+
}
768+
}
769+
});
770+
771+
process.on('SIGINT', () => {
772+
// Flush logs and exit
773+
if (logQueue.length > 0) {
774+
try {
775+
fs.appendFileSync(LOG_FILE, logQueue.join('\n') + '\n');
776+
} catch (err) {
777+
console.error('Failed to flush logs on SIGINT:', err);
778+
}
779+
}
780+
process.exit(0);
781+
});

0 commit comments

Comments
 (0)