Skip to content

Commit b6dd0f3

Browse files
authored
Refactoring logging to better support MCP server (#8533)
* Refactoring logging to better support MCP server * format
1 parent 702c1c0 commit b6dd0f3

File tree

7 files changed

+98
-120
lines changed

7 files changed

+98
-120
lines changed

firebase-vscode/src/logger-wrapper.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import { transports, format } from "winston";
66
import Transport from "winston-transport";
77
import { stripVTControlCharacters } from "node:util";
88
import { SPLAT } from "triple-beam";
9-
import { logger as cliLogger } from "../../src/logger";
10-
import { setupLoggers, tryStringify } from "../../src/utils";
9+
import { logger as cliLogger, useConsoleLoggers, useFileLogger } from "../../src/logger";
1110
import { setInquirerLogger } from "./stubs/inquirer-stub";
1211
import { getRootFolders } from "./core/config";
1312

@@ -40,7 +39,7 @@ for (const logLevel in pluginLogger) {
4039
export function logSetup() {
4140
// Log to console (use built in CLI functionality)
4241
process.env.DEBUG = "true";
43-
setupLoggers();
42+
useConsoleLoggers();
4443

4544
// Log to file
4645
// Only log to file if firebase.debug extension setting is true.
@@ -62,18 +61,7 @@ export function logSetup() {
6261
filePath = path.join(rootFolders[0], ".firebase", "logs", "vsce-debug.log");
6362
}
6463
pluginLogger.info("Logging to path", filePath);
65-
cliLogger.add(
66-
new transports.File({
67-
level: "debug",
68-
filename: filePath,
69-
format: format.printf((info) => {
70-
const segments = [info.message, ...(info[SPLAT] || [])].map(
71-
tryStringify,
72-
);
73-
return `[${info.level}] ${stripVTControlCharacters(segments.join(" "))}`;
74-
}),
75-
}),
76-
);
64+
useFileLogger(filePath);
7765
cliLogger.add(new VSCodeOutputTransport({ level: "info" }));
7866
}
7967

src/bin/cli.ts

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,15 @@ import { marked } from "marked";
55
marked.use(markedTerminal() as any);
66

77
import { CommanderStatic } from "commander";
8-
import { join } from "node:path";
9-
import { SPLAT } from "triple-beam";
10-
import { stripVTControlCharacters } from "node:util";
118
import * as fs from "node:fs";
129

1310
import { configstore } from "../configstore";
1411
import { errorOut } from "../errorOut";
1512
import { handlePreviewToggles } from "../handlePreviewToggles";
16-
import { logger } from "../logger";
13+
import { logger, useFileLogger } from "../logger";
1714
import * as client from "..";
1815
import * as fsutils from "../fsutils";
1916
import * as utils from "../utils";
20-
import * as winston from "winston";
2117

2218
import { enableExperimentsFromCliEnvVariable } from "../experiments";
2319
import { fetchMOTD } from "../fetchMOTD";
@@ -28,51 +24,13 @@ export function cli(pkg: any) {
2824
const args = process.argv.slice(2);
2925
let cmd: CommanderStatic;
3026

31-
function findAvailableLogFile(): string {
32-
const candidates = ["firebase-debug.log"];
33-
for (let i = 1; i < 10; i++) {
34-
candidates.push(`firebase-debug.${i}.log`);
35-
}
36-
37-
for (const c of candidates) {
38-
const logFilename = join(process.cwd(), c);
39-
40-
try {
41-
const fd = fs.openSync(logFilename, "r+");
42-
fs.closeSync(fd);
43-
return logFilename;
44-
} catch (e: any) {
45-
if (e.code === "ENOENT") {
46-
// File does not exist, which is fine
47-
return logFilename;
48-
}
49-
50-
// Any other error (EPERM, etc) means we won't be able to log to
51-
// this file so we skip it.
52-
}
53-
}
54-
55-
throw new Error("Unable to obtain permissions for firebase-debug.log");
56-
}
57-
58-
const logFilename = findAvailableLogFile();
59-
6027
if (!process.env.DEBUG && args.includes("--debug")) {
6128
process.env.DEBUG = "true";
6229
}
6330

6431
process.env.IS_FIREBASE_CLI = "true";
6532

66-
logger.add(
67-
new winston.transports.File({
68-
level: "debug",
69-
filename: logFilename,
70-
format: winston.format.printf((info) => {
71-
const segments = [info.message, ...(info[SPLAT] || [])].map(utils.tryStringify);
72-
return `[${info.level}] ${stripVTControlCharacters(segments.join(" "))}`;
73-
}),
74-
}),
75-
);
33+
const logFilename = useFileLogger();
7634

7735
logger.debug("-".repeat(70));
7836
logger.debug("Command: ", process.argv.join(" "));

src/bin/mcp.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
#!/usr/bin/env node
22

3-
import { silenceStdout } from "../logger";
4-
silenceStdout();
5-
3+
import { useFileLogger } from "../logger";
64
import { FirebaseMcpServer } from "../mcp/index";
75
import { parseArgs } from "util";
86
import { SERVER_FEATURES, ServerFeature } from "../mcp/types";
@@ -28,6 +26,7 @@ export async function mcp(): Promise<void> {
2826
},
2927
allowPositionals: true,
3028
});
29+
useFileLogger();
3130
const activeFeatures = (values.only || "")
3231
.split(",")
3332
.filter((f) => SERVER_FEATURES.includes(f as ServerFeature)) as ServerFeature[];

src/command.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CommanderStatic } from "commander";
33
import { first, last, size, head, keys, values } from "lodash";
44

55
import { FirebaseError } from "./error";
6-
import { getInheritedOption, setupLoggers, withTimeout } from "./utils";
6+
import { getInheritedOption, withTimeout } from "./utils";
77
import { loadRC } from "./rc";
88
import { Config } from "./config";
99
import { configstore } from "./configstore";
@@ -13,6 +13,7 @@ import { selectAccount, setActiveAccount } from "./auth";
1313
import { getProject } from "./management/projects";
1414
import { requireAuth } from "./requireAuth";
1515
import { Options } from "./options";
16+
import { useConsoleLoggers } from "./logger";
1617

1718
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1819
type ActionFunction = (...args: any[]) => any;
@@ -311,7 +312,7 @@ export class Command {
311312
if (getInheritedOption(options, "json")) {
312313
options.nonInteractive = true;
313314
} else {
314-
setupLoggers();
315+
useConsoleLoggers();
315316
}
316317

317318
if (getInheritedOption(options, "config")) {

src/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import * as program from "commander";
22
import * as clc from "colorette";
33
import * as leven from "leven";
44

5-
import { logger } from "./logger";
6-
import { setupLoggers } from "./utils";
5+
import { logger, useConsoleLoggers } from "./logger";
76

87
const pkg = require("../package.json");
98

@@ -76,7 +75,7 @@ const RENAMED_COMMANDS: Record<string, string> = {
7675

7776
// Default handler, this is called when no other command action matches.
7877
program.action((_, args) => {
79-
setupLoggers();
78+
useConsoleLoggers();
8079

8180
const cmd = args[0];
8281
logger.error(clc.bold(clc.red("Error:")), clc.bold(cmd), "is not a Firebase command");

src/logger.ts

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import * as winston from "winston";
22
import * as Transport from "winston-transport";
3+
import { EventEmitter } from "events";
4+
import * as path from "path";
5+
import * as fs from "fs";
6+
import { SPLAT } from "triple-beam";
7+
import { stripVTControlCharacters } from "util";
38

49
import { isVSCodeExtension } from "./vsCodeUtils";
5-
import { EventEmitter } from "events";
610

711
/**
812
* vsceLogEmitter passes CLI logs along to VSCode.
@@ -119,6 +123,42 @@ function maybeUseVSCodeLogger(logger: winston.Logger): winston.Logger {
119123
return logger;
120124
}
121125

126+
export function findAvailableLogFile(): string {
127+
const candidates = ["firebase-debug.log"];
128+
for (let i = 1; i < 10; i++) {
129+
candidates.push(`firebase-debug.${i}.log`);
130+
}
131+
132+
for (const c of candidates) {
133+
const logFilename = path.join(process.cwd(), c);
134+
try {
135+
const fd = fs.openSync(logFilename, "r+");
136+
fs.closeSync(fd);
137+
return logFilename;
138+
} catch (e: any) {
139+
if (e.code === "ENOENT") {
140+
// File does not exist, which is fine
141+
return logFilename;
142+
}
143+
// Any other error (EPERM, etc) means we won't be able to log to
144+
// this file so we skip it.
145+
}
146+
}
147+
throw new Error("Unable to obtain permissions for firebase-debug.log");
148+
}
149+
150+
function tryStringify(value: any) {
151+
if (typeof value === "string") {
152+
return value;
153+
}
154+
155+
try {
156+
return JSON.stringify(value);
157+
} catch {
158+
return value;
159+
}
160+
}
161+
122162
const rawLogger = winston.createLogger();
123163
// Set a default silent logger to suppress logs during tests
124164
rawLogger.add(
@@ -135,12 +175,52 @@ rawLogger.exitOnError = false;
135175
// allow error parameters.
136176
// Casting looks super dodgy, but it should be safe because we know the underlying code
137177
// handles all parameter types we care about.
138-
export let logger: Logger = maybeUseVSCodeLogger(
178+
export const logger: Logger = maybeUseVSCodeLogger(
139179
annotateDebugLines(expandErrors(rawLogger)),
140180
) as unknown as Logger;
141181

142-
export function silenceStdout() {
143-
logger = winston.createLogger({
144-
silent: true,
145-
}) as unknown as Logger;
182+
/**
183+
* Sets up logging to the firebase-debug.log file.
184+
*/
185+
export function useFileLogger(logFile?: string): string {
186+
const logFileName = logFile ?? findAvailableLogFile();
187+
logger.add(
188+
new winston.transports.File({
189+
level: "debug",
190+
filename: logFileName,
191+
format: winston.format.printf((info) => {
192+
const segments = [info.message, ...(info[SPLAT] || [])].map(tryStringify);
193+
return `[${info.level}] ${stripVTControlCharacters(segments.join(" "))}`;
194+
}),
195+
}),
196+
);
197+
return logFileName;
198+
}
199+
200+
/**
201+
* Sets up logging to the command line.
202+
*/
203+
export function useConsoleLoggers(): void {
204+
if (process.env.DEBUG) {
205+
logger.add(
206+
new winston.transports.Console({
207+
level: "debug",
208+
format: winston.format.printf((info) => {
209+
const segments = [info.message, ...(info[SPLAT] || [])].map(tryStringify);
210+
return `${stripVTControlCharacters(segments.join(" "))}`;
211+
}),
212+
}),
213+
);
214+
} else if (process.env.IS_FIREBASE_CLI) {
215+
logger.add(
216+
new winston.transports.Console({
217+
level: "info",
218+
format: winston.format.printf((info) =>
219+
[info.message, ...(info[SPLAT] || [])]
220+
.filter((chunk) => typeof chunk === "string")
221+
.join(" "),
222+
),
223+
}),
224+
);
225+
}
146226
}

src/utils.ts

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@ import * as open from "open";
1212
import * as ora from "ora";
1313
import * as process from "process";
1414
import { Readable } from "stream";
15-
import * as winston from "winston";
16-
import { SPLAT } from "triple-beam";
1715
import { AssertionError } from "assert";
18-
import { stripVTControlCharacters } from "node:util";
1916
import { getPortPromise as getPort } from "portfinder";
2017

2118
import { configstore } from "./configstore";
@@ -468,22 +465,6 @@ export async function promiseProps(obj: any): Promise<any> {
468465
return Promise.all(promises).then(() => resultObj);
469466
}
470467

471-
/**
472-
* Attempts to call JSON.stringify on an object, if it throws return the original value
473-
* @param value
474-
*/
475-
export function tryStringify(value: any) {
476-
if (typeof value === "string") {
477-
return value;
478-
}
479-
480-
try {
481-
return JSON.stringify(value);
482-
} catch {
483-
return value;
484-
}
485-
}
486-
487468
/**
488469
* Attempts to call JSON.parse on an object, if it throws return the original value
489470
* @param value
@@ -500,34 +481,6 @@ export function tryParse(value: any) {
500481
}
501482
}
502483

503-
/**
504-
*
505-
*/
506-
export function setupLoggers() {
507-
if (process.env.DEBUG) {
508-
logger.add(
509-
new winston.transports.Console({
510-
level: "debug",
511-
format: winston.format.printf((info) => {
512-
const segments = [info.message, ...(info[SPLAT] || [])].map(tryStringify);
513-
return `${stripVTControlCharacters(segments.join(" "))}`;
514-
}),
515-
}),
516-
);
517-
} else if (process.env.IS_FIREBASE_CLI) {
518-
logger.add(
519-
new winston.transports.Console({
520-
level: "info",
521-
format: winston.format.printf((info) =>
522-
[info.message, ...(info[SPLAT] || [])]
523-
.filter((chunk) => typeof chunk === "string")
524-
.join(" "),
525-
),
526-
}),
527-
);
528-
}
529-
}
530-
531484
/**
532485
* Runs a given function inside a spinner with a message
533486
*/

0 commit comments

Comments
 (0)