@@ -26,12 +26,79 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
26
// Configure logging
27
27
const LOG_DIR = path . join ( __dirname , '../logs' ) ;
28
28
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
29
31
30
32
// Ensure log directory exists
31
33
if ( ! fs . existsSync ( LOG_DIR ) ) {
32
34
fs . mkdirSync ( LOG_DIR , { recursive : true } ) ;
33
35
}
34
36
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
+
35
102
// Helper function to convert LogLine to string
36
103
function logLineToString ( logLine : LogLine ) : string {
37
104
const timestamp = logLine . timestamp ? new Date ( logLine . timestamp ) . toISOString ( ) : new Date ( ) . toISOString ( ) ;
@@ -239,16 +306,35 @@ let stagehand: Stagehand | undefined;
239
306
let serverInstance : Server | undefined ;
240
307
const consoleLogs : string [ ] = [ ] ;
241
308
const operationLogs : string [ ] = [ ] ;
309
+ const MAX_OPERATION_LOGS = 1000 ; // Prevent operation logs from growing too large
242
310
243
311
function log ( message : string , level : 'info' | 'error' | 'debug' = 'info' ) {
244
312
const timestamp = new Date ( ) . toISOString ( ) ;
245
313
const logMessage = `[${ timestamp } ] [${ level . toUpperCase ( ) } ] ${ message } ` ;
314
+
315
+ // Manage operation logs with size limit
246
316
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
+ }
247
328
248
- // Write to file
249
- fs . appendFileSync ( LOG_FILE , logMessage + '\n' ) ;
329
+ // Queue log for async writing
330
+ logQueue . push ( logMessage ) ;
250
331
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
252
338
if ( process . env . DEBUG || level === 'error' ) {
253
339
console . error ( logMessage ) ;
254
340
}
@@ -262,6 +348,26 @@ function log(message: string, level: 'info' | 'error' | 'debug' = 'info') {
262
348
}
263
349
}
264
350
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
+
265
371
function logRequest ( type : string , params : any ) {
266
372
const requestLog = {
267
373
timestamp : new Date ( ) . toISOString ( ) ,
@@ -327,15 +433,13 @@ async function handleToolCall(
327
433
} ,
328
434
{
329
435
type : "text" ,
330
- text : `Operation logs:\n${ operationLogs . join ( "\n" ) } ` ,
436
+ text : `Operation logs:\n${ formatLogResponse ( operationLogs ) } ` ,
331
437
} ,
332
438
] ,
333
439
isError : true ,
334
440
} ;
335
441
}
336
442
337
-
338
-
339
443
switch ( name ) {
340
444
case "stagehand_navigate" :
341
445
try {
@@ -359,7 +463,7 @@ async function handleToolCall(
359
463
} ,
360
464
{
361
465
type : "text" ,
362
- text : `Operation logs:\n${ operationLogs . join ( "\n" ) } ` ,
466
+ text : `Operation logs:\n${ formatLogResponse ( operationLogs ) } ` ,
363
467
} ,
364
468
] ,
365
469
isError : true ,
@@ -392,7 +496,7 @@ async function handleToolCall(
392
496
} ,
393
497
{
394
498
type : "text" ,
395
- text : `Operation logs:\n${ operationLogs . join ( "\n" ) } ` ,
499
+ text : `Operation logs:\n${ formatLogResponse ( operationLogs ) } ` ,
396
500
} ,
397
501
] ,
398
502
isError : true ,
@@ -428,7 +532,7 @@ async function handleToolCall(
428
532
} ,
429
533
{
430
534
type : "text" ,
431
- text : `Operation logs:\n${ operationLogs . join ( "\n" ) } ` ,
535
+ text : `Operation logs:\n${ formatLogResponse ( operationLogs ) } ` ,
432
536
} ,
433
537
] ,
434
538
isError : true ,
@@ -459,7 +563,7 @@ async function handleToolCall(
459
563
} ,
460
564
{
461
565
type : "text" ,
462
- text : `Operation logs:\n${ operationLogs . join ( "\n" ) } ` ,
566
+ text : `Operation logs:\n${ formatLogResponse ( operationLogs ) } ` ,
463
567
} ,
464
568
] ,
465
569
isError : true ,
@@ -510,7 +614,7 @@ async function handleToolCall(
510
614
} ,
511
615
{
512
616
type : "text" ,
513
- text : `Operation logs:\n${ operationLogs . join ( "\n" ) } ` ,
617
+ text : `Operation logs:\n${ formatLogResponse ( operationLogs ) } ` ,
514
618
} ,
515
619
] ,
516
620
isError : true ,
@@ -526,7 +630,7 @@ async function handleToolCall(
526
630
} ,
527
631
{
528
632
type : "text" ,
529
- text : `Operation logs:\n${ operationLogs . join ( "\n" ) } ` ,
633
+ text : `Operation logs:\n${ formatLogResponse ( operationLogs ) } ` ,
530
634
} ,
531
635
] ,
532
636
isError : true ,
@@ -652,3 +756,26 @@ runServer().catch((error) => {
652
756
const errorMsg = error instanceof Error ? error . message : String ( error ) ;
653
757
console . error ( errorMsg ) ;
654
758
} ) ;
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