diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index d78a171bf..f3327f4de 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -35,6 +35,7 @@ import { PochiWebviewSidebar } from "./integrations/webview"; import { PochiTaskEditorProvider } from "./integrations/webview/webview-panel"; import { type AuthClient, createAuthClient } from "./lib/auth-client"; import { FileLogger } from "./lib/file-logger"; +import { HeartbeatLogger } from "./lib/heartbeat-logger"; import { getLogger } from "./lib/logger"; import { PostInstallActions } from "./lib/post-install-actions"; import { WorkspaceScope } from "./lib/workspace-scoped"; @@ -79,6 +80,7 @@ export async function activate(context: vscode.ExtensionContext) { container.resolve(CommandManager); container.resolve(DiffOriginContentProvider); container.resolve(PostInstallActions); + container.resolve(HeartbeatLogger); container.resolve(FileLogger); container.resolve(TerminalLinkProvider); container.resolve(DiffChangesContentProvider); diff --git a/packages/vscode/src/integrations/configuration.ts b/packages/vscode/src/integrations/configuration.ts index 7972237fd..dc51e067b 100644 --- a/packages/vscode/src/integrations/configuration.ts +++ b/packages/vscode/src/integrations/configuration.ts @@ -179,6 +179,7 @@ const PochiAdvanceSettings = z.object({ }) .optional(), webviewLogLevel: z.string().optional(), + enabledFileLogger: z.boolean().optional(), }); export type PochiAdvanceSettings = z.infer; diff --git a/packages/vscode/src/lib/file-logger.ts b/packages/vscode/src/lib/file-logger.ts index 22bc3bb77..726f236ff 100644 --- a/packages/vscode/src/lib/file-logger.ts +++ b/packages/vscode/src/lib/file-logger.ts @@ -1,59 +1,70 @@ import * as os from "node:os"; +import path from "node:path"; +// biome-ignore lint/style/useImportType: needed for dependency injection +import { PochiConfiguration } from "@/integrations/configuration"; import { getExtensionLogger } from "@vscode-logging/logger"; import { inject, injectable, singleton } from "tsyringe"; -import * as vscode from "vscode"; - -const HeartbeatInterval = 120_000; // 2 minutes +import type * as vscode from "vscode"; @injectable() @singleton() -export class FileLogger implements vscode.Disposable { +export class FileLogger { private rootLogger; - private disposables: vscode.Disposable[] = []; constructor( @inject("vscode.ExtensionContext") context: vscode.ExtensionContext, + private readonly pochiConfiguration: PochiConfiguration, ) { + const logFilePath = path.join(os.homedir(), ".pochi", "logs"); this.rootLogger = getExtensionLogger({ extName: context.extension.id, level: "info", - logPath: context.logUri.fsPath, + logPath: logFilePath, sourceLocationTracking: false, logOutputChannel: undefined, // no log to output channel logConsole: false, // no log to console }); - - this.startHeartbeat(); } getLogger(tag: string) { return this.rootLogger.getChildLogger({ label: tag }); } - private startHeartbeat() { - const logger = this.getLogger("Heartbeat"); - logger.info("Starting heartbeat check.", { - osHost: os.hostname(), - machineId: vscode.env.machineId, - remoteName: vscode.env.remoteName, - }); + handleLog(name: string | undefined, level: string, args: unknown[]) { + const enabledFileLogger = + !!this.pochiConfiguration.advancedSettings.value.enabledFileLogger; + if (!enabledFileLogger) { + return; + } - const timmer = setInterval(() => { - logger.info("Heartbeat check."); - }, HeartbeatInterval); - this.disposables.push({ - dispose: () => { - logger.info("Stopping heartbeat check."); - clearInterval(timmer); - }, - }); - } + const logger = this.getLogger(name || "FileLog"); + const message = + typeof args[0] === "string" ? args[0] : JSON.stringify(args[0]); + const remainArgs = args.slice(1); - dispose() { - for (const d of this.disposables) { - d.dispose(); + switch (level) { + case "INFO": + logger.info(message, ...remainArgs); + break; + case "WARN": + logger.warn(message, ...remainArgs); + break; + case "ERROR": + logger.error(message, ...remainArgs); + break; + case "FATAL": + logger.fatal(message, ...remainArgs); + break; + case "DEBUG": + logger.debug(message, ...remainArgs); + break; + case "TRACE": + logger.trace(message, ...remainArgs); + break; + default: + logger.info(message, ...remainArgs); + break; } - this.disposables = []; } } diff --git a/packages/vscode/src/lib/heartbeat-logger.ts b/packages/vscode/src/lib/heartbeat-logger.ts new file mode 100644 index 000000000..f845ee110 --- /dev/null +++ b/packages/vscode/src/lib/heartbeat-logger.ts @@ -0,0 +1,59 @@ +import * as os from "node:os"; +import { getExtensionLogger } from "@vscode-logging/logger"; +import { inject, injectable, singleton } from "tsyringe"; +import * as vscode from "vscode"; + +const HeartbeatInterval = 120_000; // 2 minutes + +@injectable() +@singleton() +export class HeartbeatLogger implements vscode.Disposable { + private rootLogger; + private disposables: vscode.Disposable[] = []; + + constructor( + @inject("vscode.ExtensionContext") + context: vscode.ExtensionContext, + ) { + this.rootLogger = getExtensionLogger({ + extName: context.extension.id, + level: "info", + logPath: context.logUri.fsPath, + sourceLocationTracking: false, + logOutputChannel: undefined, // no log to output channel + logConsole: false, // no log to console + }); + + this.startHeartbeat(); + } + + getLogger(tag: string) { + return this.rootLogger.getChildLogger({ label: tag }); + } + + private startHeartbeat() { + const logger = this.getLogger("Heartbeat"); + logger.info("Starting heartbeat check.", { + osHost: os.hostname(), + machineId: vscode.env.machineId, + remoteName: vscode.env.remoteName, + }); + + const timmer = setInterval(() => { + logger.info("Heartbeat check."); + }, HeartbeatInterval); + this.disposables.push({ + dispose: () => { + logger.info("Stopping heartbeat check."); + clearInterval(timmer); + }, + }); + } + + dispose() { + for (const d of this.disposables) { + d.dispose(); + } + this.disposables = []; + } +} diff --git a/packages/vscode/src/lib/logger.ts b/packages/vscode/src/lib/logger.ts index d9166f8fb..88c57209f 100644 --- a/packages/vscode/src/lib/logger.ts +++ b/packages/vscode/src/lib/logger.ts @@ -1,6 +1,8 @@ import { window } from "vscode"; export { getLogger } from "@getpochi/common"; import { attachTransport } from "@getpochi/common"; +import { container } from "tsyringe"; +import { FileLogger } from "./file-logger"; const outputChannel = window.createOutputChannel("Pochi", { log: true }); @@ -11,6 +13,26 @@ attachTransport((args, meta) => { message = `[${meta.name}] ${message}`; } + // Check if the second argument contains { toFile: true } + const shouldLogToFile = + typeof args[1] === "object" && + args[1] !== null && + "toFile" in args[1] && + args[1].toFile === true; + + if (shouldLogToFile) { + try { + const fileLogger = container.resolve(FileLogger); + fileLogger.handleLog(meta.name, meta.logLevelName, [ + message, + ...remainArgs, + ]); + } catch { + // ignore + } + return; + } + switch (meta.logLevelName) { case "SILLY": outputChannel.trace(message, ...remainArgs);