| 20 | const MAX_SIZE_BYTES = 10 * 1024 * 1024 // 10MB |
| 21 | |
| 22 | class AppLogger { |
| 23 | private logFile: string |
| 24 | private oldLogFile: string |
| 25 | private threshold: LogLevel |
| 26 | |
| 27 | constructor(logsDir: string) { |
| 28 | this.logFile = path.join(logsDir, 'app.log') |
| 29 | this.oldLogFile = path.join(logsDir, 'app.old.log') |
| 30 | this.threshold = resolveThreshold() |
| 31 | } |
| 32 | |
| 33 | debug(scope: string, message: string, data?: unknown): void { |
| 34 | this.write('DEBUG', scope, message, data) |
| 35 | } |
| 36 | |
| 37 | info(scope: string, message: string, data?: unknown): void { |
| 38 | this.write('INFO', scope, message, data) |
| 39 | } |
| 40 | |
| 41 | warn(scope: string, message: string, data?: unknown): void { |
| 42 | this.write('WARN', scope, message, data) |
| 43 | } |
| 44 | |
| 45 | /** `data` may be an Error; its name/message/stack are extracted automatically. */ |
| 46 | error(scope: string, message: string, data?: unknown): void { |
| 47 | this.write('ERROR', scope, message, data) |
| 48 | } |
| 49 | |
| 50 | getLogPath(): string { |
| 51 | return this.logFile |
| 52 | } |
| 53 | |
| 54 | private write(level: LogLevel, scope: string, message: string, data?: unknown): void { |
| 55 | if (LEVEL_ORDER[level] < LEVEL_ORDER[this.threshold]) return |
| 56 | |
| 57 | let line = `[${new Date().toISOString()}] [${level}] [${scope}] ${message}` |
| 58 | |
| 59 | try { |
| 60 | const tail = formatData(data) |
| 61 | if (tail) line += `\n${tail}` |
| 62 | line += '\n' |
| 63 | ensureDir(path.dirname(this.logFile)) |
| 64 | this.rotateIfNeeded() |
| 65 | fs.appendFileSync(this.logFile, line, 'utf-8') |
| 66 | } catch (err) { |
| 67 | // Logging must never break the app; surface to console only. |
| 68 | console.error('[AppLogger] Failed to write log:', err) |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | // ponytail: rename-based rotation, atomic, no rewrite, ~20MB total ceiling |
| 73 | private rotateIfNeeded(): void { |
| 74 | let size = 0 |
| 75 | try { |
| 76 | size = fs.statSync(this.logFile).size |
| 77 | } catch { |
| 78 | return // file doesn't exist yet |
| 79 | } |
nothing calls this directly
no outgoing calls
no test coverage detected