Skip to content

Commit 113d884

Browse files
author
Roo Agent
committed
CHORE: Stabilisation des modifications en attente post-sauvetage
1 parent 4fc9067 commit 113d884

File tree

4 files changed

+358
-1
lines changed

4 files changed

+358
-1
lines changed

src/core/logging/FileLogger.ts

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
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

Comments
 (0)