@@ -3,6 +3,7 @@ import { mongoLogId, MongoLogId, MongoLogManager, MongoLogWriter } from "mongodb
3
3
import redact from "mongodb-redact" ;
4
4
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
5
5
import { LoggingMessageNotification } from "@modelcontextprotocol/sdk/types.js" ;
6
+ import { EventEmitter } from "events" ;
6
7
7
8
export type LogLevel = LoggingMessageNotification [ "params" ] [ "level" ] ;
8
9
@@ -55,12 +56,17 @@ interface LogPayload {
55
56
context : string ;
56
57
message : string ;
57
58
noRedaction ?: boolean | LoggerType | LoggerType [ ] ;
59
+ attributes ?: Record < string , string > ;
58
60
}
59
61
60
62
export type LoggerType = "console" | "disk" | "mcp" ;
61
63
62
- export abstract class LoggerBase {
63
- private defaultUnredactedLogger : LoggerType = "mcp" ;
64
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
+ type EventMap < T > = Record < keyof T , any [ ] > | DefaultEventMap ;
66
+ type DefaultEventMap = [ never ] ;
67
+
68
+ export abstract class LoggerBase < T extends EventMap < T > = DefaultEventMap > extends EventEmitter < T > {
69
+ private readonly defaultUnredactedLogger : LoggerType = "mcp" ;
64
70
65
71
public log ( level : LogLevel , payload : LogPayload ) : void {
66
72
// If no explicit value is supplied for unredacted loggers, default to "mcp"
@@ -72,7 +78,7 @@ export abstract class LoggerBase {
72
78
} ) ;
73
79
}
74
80
75
- protected abstract type : LoggerType ;
81
+ protected abstract readonly type ? : LoggerType ;
76
82
77
83
protected abstract logCore ( level : LogLevel , payload : LogPayload ) : void ;
78
84
@@ -92,7 +98,7 @@ export abstract class LoggerBase {
92
98
if (
93
99
typeof noRedaction === "object" &&
94
100
Array . isArray ( noRedaction ) &&
95
- this . type !== null &&
101
+ this . type &&
96
102
noRedaction . indexOf ( this . type ) !== - 1
97
103
) {
98
104
// If the consumer has supplied noRedaction: array, we skip redacting if our logger
@@ -103,78 +109,108 @@ export abstract class LoggerBase {
103
109
return redact ( message ) ;
104
110
}
105
111
106
- info ( payload : LogPayload ) : void {
112
+ public info ( payload : LogPayload ) : void {
107
113
this . log ( "info" , payload ) ;
108
114
}
109
115
110
- error ( payload : LogPayload ) : void {
116
+ public error ( payload : LogPayload ) : void {
111
117
this . log ( "error" , payload ) ;
112
118
}
113
- debug ( payload : LogPayload ) : void {
119
+ public debug ( payload : LogPayload ) : void {
114
120
this . log ( "debug" , payload ) ;
115
121
}
116
122
117
- notice ( payload : LogPayload ) : void {
123
+ public notice ( payload : LogPayload ) : void {
118
124
this . log ( "notice" , payload ) ;
119
125
}
120
126
121
- warning ( payload : LogPayload ) : void {
127
+ public warning ( payload : LogPayload ) : void {
122
128
this . log ( "warning" , payload ) ;
123
129
}
124
130
125
- critical ( payload : LogPayload ) : void {
131
+ public critical ( payload : LogPayload ) : void {
126
132
this . log ( "critical" , payload ) ;
127
133
}
128
134
129
- alert ( payload : LogPayload ) : void {
135
+ public alert ( payload : LogPayload ) : void {
130
136
this . log ( "alert" , payload ) ;
131
137
}
132
138
133
- emergency ( payload : LogPayload ) : void {
139
+ public emergency ( payload : LogPayload ) : void {
134
140
this . log ( "emergency" , payload ) ;
135
141
}
136
142
}
137
143
138
144
export class ConsoleLogger extends LoggerBase {
139
- protected type : LoggerType = "console" ;
145
+ protected readonly type : LoggerType = "console" ;
140
146
141
147
protected logCore ( level : LogLevel , payload : LogPayload ) : void {
142
148
const { id, context, message } = payload ;
143
- console . error ( `[${ level . toUpperCase ( ) } ] ${ id . __value } - ${ context } : ${ message } (${ process . pid } )` ) ;
149
+ console . error (
150
+ `[${ level . toUpperCase ( ) } ]${ id . __value } - ${ context } : ${ message } (${ process . pid } ${ this . serializeAttributes ( payload . attributes ) } )`
151
+ ) ;
144
152
}
145
- }
146
153
147
- export class DiskLogger extends LoggerBase {
148
- private constructor ( private logWriter : MongoLogWriter ) {
149
- super ( ) ;
154
+ private serializeAttributes ( attributes ?: Record < string , string > ) : string {
155
+ if ( ! attributes || Object . keys ( attributes ) . length === 0 ) {
156
+ return "" ;
157
+ }
158
+ return `, ${ Object . entries ( attributes )
159
+ . map ( ( [ key , value ] ) => `${ key } =${ value } ` )
160
+ . join ( ", " ) } `;
150
161
}
162
+ }
151
163
152
- protected type : LoggerType = "disk" ;
153
-
154
- static async fromPath ( logPath : string ) : Promise < DiskLogger > {
155
- await fs . mkdir ( logPath , { recursive : true } ) ;
156
-
157
- const manager = new MongoLogManager ( {
158
- directory : logPath ,
159
- retentionDays : 30 ,
160
- onwarn : console . warn ,
161
- onerror : console . error ,
162
- gzip : false ,
163
- retentionGB : 1 ,
164
- } ) ;
164
+ export class DiskLogger extends LoggerBase < { initialized : [ ] } > {
165
+ private bufferedMessages : { level : LogLevel ; payload : LogPayload } [ ] = [ ] ;
166
+ private logWriter ?: MongoLogWriter ;
165
167
166
- await manager . cleanupOldLogFiles ( ) ;
168
+ public constructor ( logPath : string , onError : ( error : Error ) => void ) {
169
+ super ( ) ;
167
170
168
- const logWriter = await manager . createLogWriter ( ) ;
171
+ void this . initialize ( logPath , onError ) ;
172
+ }
169
173
170
- return new DiskLogger ( logWriter ) ;
174
+ private async initialize ( logPath : string , onError : ( error : Error ) => void ) : Promise < void > {
175
+ try {
176
+ await fs . mkdir ( logPath , { recursive : true } ) ;
177
+
178
+ const manager = new MongoLogManager ( {
179
+ directory : logPath ,
180
+ retentionDays : 30 ,
181
+ onwarn : console . warn ,
182
+ onerror : console . error ,
183
+ gzip : false ,
184
+ retentionGB : 1 ,
185
+ } ) ;
186
+
187
+ await manager . cleanupOldLogFiles ( ) ;
188
+
189
+ this . logWriter = await manager . createLogWriter ( ) ;
190
+
191
+ for ( const message of this . bufferedMessages ) {
192
+ this . logCore ( message . level , message . payload ) ;
193
+ }
194
+ this . bufferedMessages = [ ] ;
195
+ this . emit ( "initialized" ) ;
196
+ } catch ( error : unknown ) {
197
+ onError ( error as Error ) ;
198
+ }
171
199
}
172
200
201
+ protected type : LoggerType = "disk" ;
202
+
173
203
protected logCore ( level : LogLevel , payload : LogPayload ) : void {
204
+ if ( ! this . logWriter ) {
205
+ // If the log writer is not initialized, buffer the message
206
+ this . bufferedMessages . push ( { level, payload } ) ;
207
+ return ;
208
+ }
209
+
174
210
const { id, context, message } = payload ;
175
211
const mongoDBLevel = this . mapToMongoDBLogLevel ( level ) ;
176
212
177
- this . logWriter [ mongoDBLevel ] ( "MONGODB-MCP" , id , context , message ) ;
213
+ this . logWriter [ mongoDBLevel ] ( "MONGODB-MCP" , id , context , message , payload . attributes ) ;
178
214
}
179
215
180
216
private mapToMongoDBLogLevel ( level : LogLevel ) : "info" | "warn" | "error" | "debug" | "fatal" {
@@ -199,11 +235,11 @@ export class DiskLogger extends LoggerBase {
199
235
}
200
236
201
237
export class McpLogger extends LoggerBase {
202
- constructor ( private server : McpServer ) {
238
+ public constructor ( private readonly server : McpServer ) {
203
239
super ( ) ;
204
240
}
205
241
206
- type : LoggerType = "mcp" ;
242
+ protected readonly type : LoggerType = "mcp" ;
207
243
208
244
protected logCore ( level : LogLevel , payload : LogPayload ) : void {
209
245
// Only log if the server is connected
@@ -219,35 +255,41 @@ export class McpLogger extends LoggerBase {
219
255
}
220
256
221
257
export class CompositeLogger extends LoggerBase {
222
- // This is not a real logger type - it should not be used anyway.
223
- protected type : LoggerType = "composite" as unknown as LoggerType ;
258
+ protected readonly type ?: LoggerType ;
224
259
225
- private loggers : LoggerBase [ ] = [ ] ;
260
+ private readonly loggers : LoggerBase [ ] = [ ] ;
261
+ private readonly attributes : Record < string , string > = { } ;
226
262
227
263
constructor ( ...loggers : LoggerBase [ ] ) {
228
264
super ( ) ;
229
265
230
- this . setLoggers ( ... loggers ) ;
266
+ this . loggers = loggers ;
231
267
}
232
268
233
- setLoggers ( ...loggers : LoggerBase [ ] ) : void {
234
- if ( loggers . length === 0 ) {
235
- throw new Error ( "At least one logger must be provided" ) ;
236
- }
237
- this . loggers = [ ...loggers ] ;
269
+ public addLogger ( logger : LoggerBase ) : void {
270
+ this . loggers . push ( logger ) ;
238
271
}
239
272
240
273
public log ( level : LogLevel , payload : LogPayload ) : void {
241
274
// Override the public method to avoid the base logger redacting the message payload
242
275
for ( const logger of this . loggers ) {
243
- logger . log ( level , payload ) ;
276
+ logger . log ( level , { ... payload , attributes : { ... this . attributes , ... payload . attributes } } ) ;
244
277
}
245
278
}
246
279
247
280
protected logCore ( ) : void {
248
281
throw new Error ( "logCore should never be invoked on CompositeLogger" ) ;
249
282
}
283
+
284
+ public setAttribute ( key : string , value : string ) : void {
285
+ this . attributes [ key ] = value ;
286
+ }
250
287
}
251
288
252
- const logger = new CompositeLogger ( new ConsoleLogger ( ) ) ;
253
- export default logger ;
289
+ export class NullLogger extends LoggerBase {
290
+ protected type ?: LoggerType ;
291
+
292
+ protected logCore ( ) : void {
293
+ // No-op logger, does not log anything
294
+ }
295
+ }
0 commit comments