Skip to content

Commit 8f8defd

Browse files
committed
fix: remove static logger; add session-scoped loggers
1 parent c0b8d7a commit 8f8defd

File tree

22 files changed

+407
-173
lines changed

22 files changed

+407
-173
lines changed

src/common/atlas/accessListUtils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ApiClient } from "./apiClient.js";
2-
import logger, { LogId } from "../logger.js";
2+
import { LogId } from "../logger.js";
33
import { ApiClientError } from "./apiClientError.js";
44

55
export const DEFAULT_ACCESS_LIST_COMMENT = "Added by MongoDB MCP Server to enable tool access";
@@ -30,22 +30,22 @@ export async function ensureCurrentIpInAccessList(apiClient: ApiClient, projectI
3030
params: { path: { groupId: projectId } },
3131
body: [entry],
3232
});
33-
logger.debug({
33+
apiClient.logger.debug({
3434
id: LogId.atlasIpAccessListAdded,
3535
context: "accessListUtils",
3636
message: `IP access list created: ${JSON.stringify(entry)}`,
3737
});
3838
} catch (err) {
3939
if (err instanceof ApiClientError && err.response?.status === 409) {
4040
// 409 Conflict: entry already exists, log info
41-
logger.debug({
41+
apiClient.logger.debug({
4242
id: LogId.atlasIpAccessListAdded,
4343
context: "accessListUtils",
4444
message: `IP address ${entry.ipAddress} is already present in the access list for project ${projectId}.`,
4545
});
4646
return;
4747
}
48-
logger.warning({
48+
apiClient.logger.warning({
4949
id: LogId.atlasIpAccessListAddFailure,
5050
context: "accessListUtils",
5151
message: `Error adding IP access list: ${err instanceof Error ? err.message : String(err)}`,

src/common/atlas/apiClient.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ApiClientError } from "./apiClientError.js";
44
import { paths, operations } from "./openapi.js";
55
import { CommonProperties, TelemetryEvent } from "../../telemetry/types.js";
66
import { packageInfo } from "../packageInfo.js";
7-
import logger, { LogId } from "../logger.js";
7+
import { LoggerBase, LogId } from "../logger.js";
88
import { createFetch } from "@mongodb-js/devtools-proxy-support";
99
import * as oauth from "oauth4webapi";
1010
import { Request as NodeFetchRequest } from "node-fetch";
@@ -28,7 +28,7 @@ export interface AccessToken {
2828
}
2929

3030
export class ApiClient {
31-
private options: {
31+
private readonly options: {
3232
baseUrl: string;
3333
userAgent: string;
3434
credentials?: {
@@ -93,7 +93,10 @@ export class ApiClient {
9393
},
9494
};
9595

96-
constructor(options: ApiClientOptions) {
96+
constructor(
97+
options: ApiClientOptions,
98+
public readonly logger: LoggerBase
99+
) {
97100
this.options = {
98101
...options,
99102
userAgent:
@@ -179,7 +182,7 @@ export class ApiClient {
179182
};
180183
} catch (error: unknown) {
181184
const err = error instanceof Error ? error : new Error(String(error));
182-
logger.error({
185+
this.logger.error({
183186
id: LogId.atlasConnectFailure,
184187
context: "apiClient",
185188
message: `Failed to request access token: ${err.message}`,
@@ -201,7 +204,7 @@ export class ApiClient {
201204
}
202205
} catch (error: unknown) {
203206
const err = error instanceof Error ? error : new Error(String(error));
204-
logger.error({
207+
this.logger.error({
205208
id: LogId.atlasApiRevokeFailure,
206209
context: "apiClient",
207210
message: `Failed to revoke access token: ${err.message}`,

src/common/atlas/cluster.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ClusterDescription20240805, FlexClusterDescription20241113 } from "./openapi.js";
22
import { ApiClient } from "./apiClient.js";
3-
import logger, { LogId } from "../logger.js";
3+
import { LogId } from "../logger.js";
44

55
export interface Cluster {
66
name?: string;
@@ -87,7 +87,7 @@ export async function inspectCluster(apiClient: ApiClient, projectId: string, cl
8787
return formatFlexCluster(cluster);
8888
} catch (flexError) {
8989
const err = flexError instanceof Error ? flexError : new Error(String(flexError));
90-
logger.error({
90+
apiClient.logger.error({
9191
id: LogId.atlasInspectFailure,
9292
context: "inspect-cluster",
9393
message: `error inspecting cluster: ${err.message}`,

src/common/logger.ts

Lines changed: 91 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { mongoLogId, MongoLogId, MongoLogManager, MongoLogWriter } from "mongodb
33
import redact from "mongodb-redact";
44
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
55
import { LoggingMessageNotification } from "@modelcontextprotocol/sdk/types.js";
6+
import { EventEmitter } from "events";
67

78
export type LogLevel = LoggingMessageNotification["params"]["level"];
89

@@ -55,12 +56,17 @@ interface LogPayload {
5556
context: string;
5657
message: string;
5758
noRedaction?: boolean | LoggerType | LoggerType[];
59+
attributes?: Record<string, string>;
5860
}
5961

6062
export type LoggerType = "console" | "disk" | "mcp";
6163

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";
6470

6571
public log(level: LogLevel, payload: LogPayload): void {
6672
// If no explicit value is supplied for unredacted loggers, default to "mcp"
@@ -72,7 +78,7 @@ export abstract class LoggerBase {
7278
});
7379
}
7480

75-
protected abstract type: LoggerType;
81+
protected abstract readonly type?: LoggerType;
7682

7783
protected abstract logCore(level: LogLevel, payload: LogPayload): void;
7884

@@ -92,7 +98,7 @@ export abstract class LoggerBase {
9298
if (
9399
typeof noRedaction === "object" &&
94100
Array.isArray(noRedaction) &&
95-
this.type !== null &&
101+
this.type &&
96102
noRedaction.indexOf(this.type) !== -1
97103
) {
98104
// If the consumer has supplied noRedaction: array, we skip redacting if our logger
@@ -103,78 +109,108 @@ export abstract class LoggerBase {
103109
return redact(message);
104110
}
105111

106-
info(payload: LogPayload): void {
112+
public info(payload: LogPayload): void {
107113
this.log("info", payload);
108114
}
109115

110-
error(payload: LogPayload): void {
116+
public error(payload: LogPayload): void {
111117
this.log("error", payload);
112118
}
113-
debug(payload: LogPayload): void {
119+
public debug(payload: LogPayload): void {
114120
this.log("debug", payload);
115121
}
116122

117-
notice(payload: LogPayload): void {
123+
public notice(payload: LogPayload): void {
118124
this.log("notice", payload);
119125
}
120126

121-
warning(payload: LogPayload): void {
127+
public warning(payload: LogPayload): void {
122128
this.log("warning", payload);
123129
}
124130

125-
critical(payload: LogPayload): void {
131+
public critical(payload: LogPayload): void {
126132
this.log("critical", payload);
127133
}
128134

129-
alert(payload: LogPayload): void {
135+
public alert(payload: LogPayload): void {
130136
this.log("alert", payload);
131137
}
132138

133-
emergency(payload: LogPayload): void {
139+
public emergency(payload: LogPayload): void {
134140
this.log("emergency", payload);
135141
}
136142
}
137143

138144
export class ConsoleLogger extends LoggerBase {
139-
protected type: LoggerType = "console";
145+
protected readonly type: LoggerType = "console";
140146

141147
protected logCore(level: LogLevel, payload: LogPayload): void {
142148
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+
);
144152
}
145-
}
146153

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(", ")}`;
150161
}
162+
}
151163

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;
165167

166-
await manager.cleanupOldLogFiles();
168+
public constructor(logPath: string, onError: (error: Error) => void) {
169+
super();
167170

168-
const logWriter = await manager.createLogWriter();
171+
void this.initialize(logPath, onError);
172+
}
169173

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+
}
171199
}
172200

201+
protected type: LoggerType = "disk";
202+
173203
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+
174210
const { id, context, message } = payload;
175211
const mongoDBLevel = this.mapToMongoDBLogLevel(level);
176212

177-
this.logWriter[mongoDBLevel]("MONGODB-MCP", id, context, message);
213+
this.logWriter[mongoDBLevel]("MONGODB-MCP", id, context, message, payload.attributes);
178214
}
179215

180216
private mapToMongoDBLogLevel(level: LogLevel): "info" | "warn" | "error" | "debug" | "fatal" {
@@ -199,11 +235,11 @@ export class DiskLogger extends LoggerBase {
199235
}
200236

201237
export class McpLogger extends LoggerBase {
202-
constructor(private server: McpServer) {
238+
public constructor(private readonly server: McpServer) {
203239
super();
204240
}
205241

206-
type: LoggerType = "mcp";
242+
protected readonly type: LoggerType = "mcp";
207243

208244
protected logCore(level: LogLevel, payload: LogPayload): void {
209245
// Only log if the server is connected
@@ -219,35 +255,41 @@ export class McpLogger extends LoggerBase {
219255
}
220256

221257
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;
224259

225-
private loggers: LoggerBase[] = [];
260+
private readonly loggers: LoggerBase[] = [];
261+
private readonly attributes: Record<string, string> = {};
226262

227263
constructor(...loggers: LoggerBase[]) {
228264
super();
229265

230-
this.setLoggers(...loggers);
266+
this.loggers = loggers;
231267
}
232268

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);
238271
}
239272

240273
public log(level: LogLevel, payload: LogPayload): void {
241274
// Override the public method to avoid the base logger redacting the message payload
242275
for (const logger of this.loggers) {
243-
logger.log(level, payload);
276+
logger.log(level, { ...payload, attributes: { ...this.attributes, ...payload.attributes } });
244277
}
245278
}
246279

247280
protected logCore(): void {
248281
throw new Error("logCore should never be invoked on CompositeLogger");
249282
}
283+
284+
public setAttribute(key: string, value: string): void {
285+
this.attributes[key] = value;
286+
}
250287
}
251288

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

Comments
 (0)