forked from CherryHQ/cherry-studio-app
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLoggerService.ts
More file actions
236 lines (196 loc) Β· 6.96 KB
/
LoggerService.ts
File metadata and controls
236 lines (196 loc) Β· 6.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
import { File, Paths } from 'expo-file-system'
export type LogSourceWithContext = {
module?: string
context?: Record<string, any>
}
export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly'
// Use Expo's global __DEV__ variable. It's synchronous and readily available.
const IS_DEV = __DEV__
// The level map remains the same.
const LEVEL_MAP: Record<LogLevel, number> = {
error: 5,
warn: 4,
info: 3,
verbose: 2,
debug: 1,
silly: 0
}
// Default levels are adjusted slightly. In production, we'll log to a file by default.
const DEFAULT_CONSOLE_LEVEL = IS_DEV ? 'silly' : 'info'
const DEFAULT_FILE_LOG_LEVEL = 'warn' // Only log warnings and errors to file
export class LoggerService {
private static instance: LoggerService
// Renamed 'level' to 'consoleLevel' for clarity
private consoleLevel: LogLevel = DEFAULT_CONSOLE_LEVEL
// Renamed 'logToMainLevel' to 'fileLogLevel'
private fileLogLevel: LogLevel = DEFAULT_FILE_LOG_LEVEL
// These properties remain the same
private module: string = ''
private context: Record<string, any> = {}
// Reference to the root instance (for instances created by withContext)
private root?: LoggerService
// New properties for file logging
private logFilePath: string
private logQueue: string[] = []
private isWritingToFile = false
private constructor() {
// Define the path for our log file in the app's private document directory
this.logFilePath = `${Paths.document.uri}app.log`
console.log(`[LoggerService] Log file path: ${this.logFilePath}`)
}
public static getInstance(): LoggerService {
if (!LoggerService.instance) {
LoggerService.instance = new LoggerService()
}
return LoggerService.instance
}
// initWindowSource is removed as it's not applicable to React Native.
// withContext creates a new logger instance with specific module/context
// but shares the same log queue and file writing state with the root instance
public withContext(module: string, context?: Record<string, any>): LoggerService {
const newLogger = Object.create(this) as LoggerService
newLogger.module = module
newLogger.context = { ...this.context, ...context }
// Point to the root instance to share logQueue and isWritingToFile
newLogger.root = this.root || this
return newLogger
}
private processLog(level: LogLevel, message: string, data: any[]): void {
// --- 1. Console Logging ---
const consoleLevelNumber = LEVEL_MAP[level]
if (consoleLevelNumber >= LEVEL_MAP[this.consoleLevel]) {
const logMessage = this.module ? `[${this.module}] ${message}` : message
// Use the appropriate console method.
// In React Native, console.log can handle objects better.
switch (level) {
case 'error':
console.error(logMessage, ...data)
break
case 'warn':
console.warn(logMessage, ...data)
break
case 'info':
console.info(logMessage, ...data)
break
default: // verbose, debug, silly all map to console.log
console.log(logMessage, ...data)
break
}
}
// --- 2. File Logging ---
// Check if we should force logging to the file
const lastArg = data.length > 0 ? data[data.length - 1] : undefined
const forceLogToFile = typeof lastArg === 'object' && lastArg?.logToFile === true
if (consoleLevelNumber >= LEVEL_MAP[this.fileLogLevel] || forceLogToFile) {
const source: LogSourceWithContext = {
module: this.module
}
if (Object.keys(this.context).length > 0) {
source.context = this.context
}
// Remove the { logToFile: true } object before logging
const fileLogData = forceLogToFile ? data.slice(0, -1) : data
const logEntry = {
timestamp: new Date().toISOString(),
level,
message,
...source,
data: fileLogData
}
// Add formatted log to queue and trigger write
// Use root instance to ensure all logs are written to the same queue
const rootInstance = this.root || this
rootInstance.logQueue.push(JSON.stringify(logEntry) + '\n')
rootInstance.flushQueue()
}
}
private async flushQueue(): Promise<void> {
if (this.isWritingToFile || this.logQueue.length === 0) {
return
}
this.isWritingToFile = true
const logsToWrite = this.logQueue.splice(0).join('')
try {
const file = new File(this.logFilePath)
// Read existing content to append instead of overwrite
let existingContent = ''
if (file.exists) {
try {
existingContent = file.textSync()
} catch (readError) {
console.error('[LoggerService] Failed to read existing log file:', readError)
}
}
// Append new logs to existing content
file.write(existingContent + logsToWrite)
} catch (error) {
console.error('[LoggerService] Failed to write to log file:', error)
} finally {
this.isWritingToFile = false
// If new logs arrived during the write, process them
if (this.logQueue.length > 0) {
this.flushQueue()
}
}
}
// Public logging methods remain unchanged in their signature
public error(message: string, ...data: any[]): void {
this.processLog('error', message, data)
}
public warn(message: string, ...data: any[]): void {
this.processLog('warn', message, data)
}
public info(message: string, ...data: any[]): void {
this.processLog('info', message, data)
}
public verbose(message: string, ...data: any[]): void {
this.processLog('verbose', message, data)
}
public debug(message: string, ...data: any[]): void {
this.processLog('debug', message, data)
}
public silly(message: string, ...data: any[]): void {
this.processLog('silly', message, data)
}
// --- Level Management Methods (Updated) ---
public setConsoleLevel(level: LogLevel): void {
this.consoleLevel = level
}
public getConsoleLevel(): string {
return this.consoleLevel
}
public resetConsoleLevel(): void {
this.setConsoleLevel(DEFAULT_CONSOLE_LEVEL)
}
public setFileLogLevel(level: LogLevel): void {
this.fileLogLevel = level
}
public getFileLogLevel(): LogLevel {
return this.fileLogLevel
}
public resetFileLogLevel(): void {
this.setFileLogLevel(DEFAULT_FILE_LOG_LEVEL)
}
// --- New Utility Method for accessing the log file ---
public async getLogFileContents(): Promise<string> {
try {
const file = new File(this.logFilePath)
return file.text()
} catch (e) {
// ENOENT means file doesn't exist yet, which is fine.
if ((e as any).code === 'ENOENT') {
return ''
}
console.error('[LoggerService] Could not read log file:', e)
return ''
}
}
public async clearLogFile(): Promise<void> {
const file = new File(this.logFilePath)
file.delete()
}
public getLogFilePath(): string {
return this.logFilePath
}
}
export const loggerService = LoggerService.getInstance()