1+ import * as fs from "fs"
2+ import * as path from "path"
3+ import { createWriteStream , WriteStream } from "fs"
4+
5+ export type LogLevel = 'INFO' | 'WARN' | 'ERROR' | 'DEBUG'
6+
7+ export interface LogMetadata {
8+ [ key : string ] : any
9+ }
10+
11+ export interface LogEntry {
12+ timestamp : string
13+ level : LogLevel
14+ component : string
15+ message : string
16+ metadata ?: LogMetadata
17+ }
18+
19+ /**
20+ * FileLogger - Système de journalisation persistante pour diagnostic des crashes webview
21+ *
22+ * Fonctionnalités :
23+ * - Logging persistant survit aux crashes de webview
24+ * - Support des niveaux de log (INFO, WARN, ERROR, DEBUG)
25+ * - Métadonnées structurées pour contexte enrichi
26+ * - Rotation automatique des logs si taille dépassée
27+ * - Thread-safe avec gestion d'erreurs gracieuse
28+ */
29+ export class FileLogger {
30+ private logFilePath : string
31+ private logStream ?: WriteStream
32+ private isInitialized : boolean = false
33+ private writeQueue : string [ ] = [ ]
34+ private isWriting : boolean = false
35+ private maxLogFileSize : number = 10 * 1024 * 1024 // 10MB par défaut
36+ private maxLogFiles : number = 5
37+
38+ constructor ( baseDir : string , filename : string = 'roo-code-debug.log' ) {
39+ // Créer le répertoire .logs dans le baseDir
40+ const logsDir = path . join ( baseDir , '.logs' )
41+ this . logFilePath = path . join ( logsDir , filename )
42+
43+ // Initialisation asynchrone pour éviter de bloquer le constructeur
44+ this . initialize ( ) . catch ( error => {
45+ console . error ( `[FileLogger] Failed to initialize: ${ error } ` )
46+ } )
47+ }
48+
49+ /**
50+ * Initialise le logger et crée le répertoire si nécessaire
51+ */
52+ private async initialize ( ) : Promise < void > {
53+ try {
54+ // Créer le répertoire .logs s'il n'existe pas
55+ const logsDir = path . dirname ( this . logFilePath )
56+ await fs . promises . mkdir ( logsDir , { recursive : true } )
57+
58+ // Vérifier si rotation nécessaire
59+ await this . checkAndRotateLog ( )
60+
61+ // Créer le stream de log
62+ this . logStream = createWriteStream ( this . logFilePath , { flags : 'a' , encoding : 'utf8' } )
63+
64+ // Gérer les erreurs du stream
65+ this . logStream . on ( 'error' , ( error ) => {
66+ console . error ( `[FileLogger] Stream error: ${ error } ` )
67+ } )
68+
69+ this . isInitialized = true
70+
71+ // Écrire les messages en attente
72+ await this . processWriteQueue ( )
73+
74+ // Log d'initialisation
75+ await this . log ( 'INFO' , 'FILE_LOGGER' , 'FileLogger initialized successfully' , {
76+ logFilePath : this . logFilePath ,
77+ timestamp : new Date ( ) . toISOString ( )
78+ } )
79+
80+ } catch ( error ) {
81+ console . error ( `[FileLogger] Initialization failed: ${ error } ` )
82+ this . isInitialized = false
83+ }
84+ }
85+
86+ /**
87+ * Vérifie la taille du fichier log et effectue une rotation si nécessaire
88+ */
89+ private async checkAndRotateLog ( ) : Promise < void > {
90+ try {
91+ const stats = await fs . promises . stat ( this . logFilePath )
92+
93+ if ( stats . size > this . maxLogFileSize ) {
94+ await this . rotateLogFiles ( )
95+ }
96+ } catch ( error ) {
97+ // Fichier n'existe pas encore, pas d'action nécessaire
98+ if ( error . code !== 'ENOENT' ) {
99+ console . error ( `[FileLogger] Error checking log file size: ${ error } ` )
100+ }
101+ }
102+ }
103+
104+ /**
105+ * Effectue la rotation des fichiers de log
106+ */
107+ private async rotateLogFiles ( ) : Promise < void > {
108+ try {
109+ const baseFilename = this . logFilePath
110+ const dir = path . dirname ( baseFilename )
111+ const ext = path . extname ( baseFilename )
112+ const name = path . basename ( baseFilename , ext )
113+
114+ // Décaler les fichiers existants (.1 -> .2, .2 -> .3, etc.)
115+ for ( let i = this . maxLogFiles - 1 ; i >= 1 ; i -- ) {
116+ const currentFile = path . join ( dir , `${ name } .${ i } ${ ext } ` )
117+ const nextFile = path . join ( dir , `${ name } .${ i + 1 } ${ ext } ` )
118+
119+ try {
120+ await fs . promises . access ( currentFile )
121+ if ( i === this . maxLogFiles - 1 ) {
122+ // Supprimer le plus ancien
123+ await fs . promises . unlink ( currentFile )
124+ } else {
125+ // Renommer vers le suivant
126+ await fs . promises . rename ( currentFile , nextFile )
127+ }
128+ } catch {
129+ // Fichier n'existe pas, continuer
130+ }
131+ }
132+
133+ // Renommer le fichier actuel vers .1
134+ const rotatedFile = path . join ( dir , `${ name } .1${ ext } ` )
135+ try {
136+ await fs . promises . rename ( baseFilename , rotatedFile )
137+ } catch ( error ) {
138+ console . error ( `[FileLogger] Error rotating main log file: ${ error } ` )
139+ }
140+
141+ } catch ( error ) {
142+ console . error ( `[FileLogger] Error during log rotation: ${ error } ` )
143+ }
144+ }
145+
146+ /**
147+ * Traite la queue d'écriture
148+ */
149+ private async processWriteQueue ( ) : Promise < void > {
150+ if ( this . isWriting || ! this . isInitialized || this . writeQueue . length === 0 ) {
151+ return
152+ }
153+
154+ this . isWriting = true
155+
156+ try {
157+ while ( this . writeQueue . length > 0 ) {
158+ const logLine = this . writeQueue . shift ( )
159+ if ( logLine && this . logStream ) {
160+ await new Promise < void > ( ( resolve , reject ) => {
161+ this . logStream ! . write ( logLine , ( error ) => {
162+ if ( error ) reject ( error )
163+ else resolve ( )
164+ } )
165+ } )
166+ }
167+ }
168+ } catch ( error ) {
169+ console . error ( `[FileLogger] Error processing write queue: ${ error } ` )
170+ } finally {
171+ this . isWriting = false
172+ }
173+ }
174+
175+ /**
176+ * Log un message avec le niveau spécifié
177+ */
178+ async log ( level : LogLevel , component : string , message : string , metadata ?: LogMetadata ) : Promise < void > {
179+ const logEntry : LogEntry = {
180+ timestamp : new Date ( ) . toISOString ( ) ,
181+ level,
182+ component,
183+ message,
184+ metadata
185+ }
186+
187+ // Formatter la ligne de log
188+ const logLine = this . formatLogEntry ( logEntry )
189+
190+ // Ajouter à la queue
191+ this . writeQueue . push ( logLine )
192+
193+ // Traiter la queue si possible
194+ if ( this . isInitialized ) {
195+ await this . processWriteQueue ( )
196+ }
197+
198+ // Aussi logger dans la console pour les erreurs
199+ if ( level === 'ERROR' || level === 'WARN' ) {
200+ console . log ( `[${ level } ] ${ component } : ${ message } ` , metadata || '' )
201+ }
202+ }
203+
204+ /**
205+ * Formate une entrée de log en ligne de texte
206+ */
207+ private formatLogEntry ( entry : LogEntry ) : string {
208+ const metadataStr = entry . metadata ? ` | ${ JSON . stringify ( entry . metadata ) } ` : ''
209+ return `[${ entry . timestamp } ] ${ entry . level } ${ entry . component } : ${ entry . message } ${ metadataStr } \n`
210+ }
211+
212+ /**
213+ * Méthodes de convenance pour chaque niveau
214+ */
215+ async info ( component : string , message : string , metadata ?: LogMetadata ) : Promise < void > {
216+ return this . log ( 'INFO' , component , message , metadata )
217+ }
218+
219+ async warn ( component : string , message : string , metadata ?: LogMetadata ) : Promise < void > {
220+ return this . log ( 'WARN' , component , message , metadata )
221+ }
222+
223+ async error ( component : string , message : string , metadata ?: LogMetadata ) : Promise < void > {
224+ return this . log ( 'ERROR' , component , message , metadata )
225+ }
226+
227+ async debug ( component : string , message : string , metadata ?: LogMetadata ) : Promise < void > {
228+ return this . log ( 'DEBUG' , component , message , metadata )
229+ }
230+
231+ /**
232+ * Force l'écriture de tous les logs en attente et ferme le stream
233+ */
234+ async dispose ( ) : Promise < void > {
235+ try {
236+ // Traiter tous les messages en attente
237+ await this . processWriteQueue ( )
238+
239+ // Log de fermeture
240+ if ( this . isInitialized ) {
241+ await this . log ( 'INFO' , 'FILE_LOGGER' , 'FileLogger disposing' , {
242+ pendingMessages : this . writeQueue . length
243+ } )
244+ }
245+
246+ // Fermer le stream
247+ if ( this . logStream ) {
248+ await new Promise < void > ( ( resolve , reject ) => {
249+ this . logStream ! . end ( ( error ) => {
250+ if ( error ) reject ( error )
251+ else resolve ( )
252+ } )
253+ } )
254+ this . logStream = undefined
255+ }
256+
257+ this . isInitialized = false
258+ } catch ( error ) {
259+ console . error ( `[FileLogger] Error during disposal: ${ error } ` )
260+ }
261+ }
262+
263+ /**
264+ * Retourne le chemin du fichier de log actuel
265+ */
266+ getLogFilePath ( ) : string {
267+ return this . logFilePath
268+ }
269+
270+ /**
271+ * Vérifie si le logger est initialisé
272+ */
273+ isReady ( ) : boolean {
274+ return this . isInitialized
275+ }
276+ }
0 commit comments