11import { EventEmitter } from "node:events" ;
2- import { mkdir , appendFile } from "node:fs/promises" ;
2+ import { appendFile , mkdir , rename , rm , stat } from "node:fs/promises" ;
33import { join } from "node:path" ;
44import type { LogEntry , LogSource , LogStream , RuntimePaths } from "../../shared/contracts" ;
55
66const MAX_BUFFERED_LOGS = 1000 ;
7+ const MAX_LOG_FILE_BYTES = 10 * 1024 * 1024 ;
8+ const MAX_LOG_FILE_BACKUPS = 5 ;
79
810function formatLogLine ( entry : LogEntry ) : string {
911 const timestamp = new Date ( entry . timestamp ) . toISOString ( ) ;
@@ -12,6 +14,7 @@ function formatLogLine(entry: LogEntry): string {
1214
1315export class LogStore extends EventEmitter {
1416 private readonly entries : LogEntry [ ] = [ ] ;
17+ private writeQueue = Promise . resolve ( ) ;
1518
1619 constructor ( private readonly paths : RuntimePaths ) {
1720 super ( ) ;
@@ -54,8 +57,47 @@ export class LogStore extends EventEmitter {
5457 }
5558
5659 private writeEntry ( entry : LogEntry ) : void {
57- void mkdir ( this . paths . logsRoot , { recursive : true } )
58- . then ( ( ) => appendFile ( this . getServiceLogPath ( entry . source ) , formatLogLine ( entry ) , "utf8" ) )
60+ const line = formatLogLine ( entry ) ;
61+ this . writeQueue = this . writeQueue
62+ . catch ( ( ) => undefined )
63+ . then ( ( ) => this . writeLine ( entry . source , line ) )
5964 . catch ( ( ) => undefined ) ;
6065 }
66+
67+ private async writeLine ( source : LogSource , line : string ) : Promise < void > {
68+ await mkdir ( this . paths . logsRoot , { recursive : true } ) ;
69+
70+ const logPath = this . getServiceLogPath ( source ) ;
71+ await this . rotateLogFileIfNeeded ( logPath , Buffer . byteLength ( line , "utf8" ) ) ;
72+ await appendFile ( logPath , line , "utf8" ) ;
73+ }
74+
75+ private async rotateLogFileIfNeeded ( logPath : string , incomingBytes : number ) : Promise < void > {
76+ const currentSize = await this . getFileSize ( logPath ) ;
77+ if ( currentSize <= 0 || currentSize + incomingBytes <= MAX_LOG_FILE_BYTES ) {
78+ return ;
79+ }
80+
81+ await rm ( `${ logPath } .${ MAX_LOG_FILE_BACKUPS } ` , { force : true } ) ;
82+ for ( let index = MAX_LOG_FILE_BACKUPS - 1 ; index >= 1 ; index -= 1 ) {
83+ await this . renameIfExists ( `${ logPath } .${ index } ` , `${ logPath } .${ index + 1 } ` ) ;
84+ }
85+ await this . renameIfExists ( logPath , `${ logPath } .1` ) ;
86+ }
87+
88+ private async getFileSize ( path : string ) : Promise < number > {
89+ try {
90+ return ( await stat ( path ) ) . size ;
91+ } catch {
92+ return 0 ;
93+ }
94+ }
95+
96+ private async renameIfExists ( from : string , to : string ) : Promise < void > {
97+ try {
98+ await rename ( from , to ) ;
99+ } catch {
100+ // Missing or locked old log files should not block new logs from being written.
101+ }
102+ }
61103}
0 commit comments