From 36b388e7a6fa10582bca182728395f0ce262cb65 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 15 Jul 2025 23:54:32 +0300 Subject: [PATCH 1/4] Use LogOutputChannel to log messages --- src/extension.ts | 2 +- src/storage.ts | 65 +++++++++++++++++++++--------------------------- 2 files changed, 29 insertions(+), 38 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 05eb7319..29a09b48 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -47,7 +47,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { ); } - const output = vscode.window.createOutputChannel("Coder"); + const output = vscode.window.createOutputChannel("Coder", { log: true }); const storage = new Storage( output, ctx.globalState, diff --git a/src/storage.ts b/src/storage.ts index 8453bc5d..ad4bfa5b 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -14,7 +14,7 @@ const MAX_URLS = 10; export class Storage { constructor( - private readonly output: vscode.OutputChannel, + private readonly output: vscode.LogOutputChannel, private readonly memento: vscode.Memento, private readonly secrets: vscode.SecretStorage, private readonly globalStorageUri: vscode.Uri, @@ -129,55 +129,52 @@ export class Storage { const enableDownloads = vscode.workspace.getConfiguration().get("coder.enableDownloads") !== false; - this.output.appendLine( + this.output.info( `Downloads are ${enableDownloads ? "enabled" : "disabled"}`, ); // Get the build info to compare with the existing binary version, if any, // and to log for debugging. const buildInfo = await restClient.getBuildInfo(); - this.output.appendLine(`Got server version: ${buildInfo.version}`); + this.output.info(`Got server version: ${buildInfo.version}`); // Check if there is an existing binary and whether it looks valid. If it // is valid and matches the server, or if it does not match the server but // downloads are disabled, we can return early. const binPath = path.join(this.getBinaryCachePath(label), cli.name()); - this.output.appendLine(`Using binary path: ${binPath}`); + this.output.info(`Using binary path: ${binPath}`); const stat = await cli.stat(binPath); if (stat === undefined) { - this.output.appendLine("No existing binary found, starting download"); + this.output.info("No existing binary found, starting download"); } else { - this.output.appendLine( - `Existing binary size is ${prettyBytes(stat.size)}`, - ); + this.output.info(`Existing binary size is ${prettyBytes(stat.size)}`); try { const version = await cli.version(binPath); - this.output.appendLine(`Existing binary version is ${version}`); + this.output.info(`Existing binary version is ${version}`); // If we have the right version we can avoid the request entirely. if (version === buildInfo.version) { - this.output.appendLine( + this.output.info( "Using existing binary since it matches the server version", ); return binPath; } else if (!enableDownloads) { - this.output.appendLine( + this.output.info( "Using existing binary even though it does not match the server version because downloads are disabled", ); return binPath; } - this.output.appendLine( + this.output.info( "Downloading since existing binary does not match the server version", ); } catch (error) { - this.output.appendLine( - `Unable to get version of existing binary: ${error}`, + this.output.error( + `Unable to get version of existing binary: ${error}. Downloading new binary instead`, ); - this.output.appendLine("Downloading new binary instead"); } } if (!enableDownloads) { - this.output.appendLine( + this.output.error( "Unable to download CLI because downloads are disabled", ); throw new Error("Unable to download CLI because downloads are disabled"); @@ -187,9 +184,9 @@ export class Storage { const removed = await cli.rmOld(binPath); removed.forEach(({ fileName, error }) => { if (error) { - this.output.appendLine(`Failed to remove ${fileName}: ${error}`); + this.output.error(`Failed to remove ${fileName}: ${error}`); } else { - this.output.appendLine(`Removed ${fileName}`); + this.output.info(`Removed ${fileName}`); } }); @@ -202,12 +199,12 @@ export class Storage { configSource && String(configSource).trim().length > 0 ? String(configSource) : "/bin/" + binName; - this.output.appendLine(`Downloading binary from: ${binSource}`); + this.output.info(`Downloading binary from: ${binSource}`); // Ideally we already caught that this was the right version and returned // early, but just in case set the ETag. const etag = stat !== undefined ? await cli.eTag(binPath) : ""; - this.output.appendLine(`Using ETag: ${etag}`); + this.output.info(`Using ETag: ${etag}`); // Make the download request. const controller = new AbortController(); @@ -223,20 +220,18 @@ export class Storage { // Ignore all errors so we can catch a 404! validateStatus: () => true, }); - this.output.appendLine(`Got status code ${resp.status}`); + this.output.info(`Got status code ${resp.status}`); switch (resp.status) { case 200: { const rawContentLength = resp.headers["content-length"]; const contentLength = Number.parseInt(rawContentLength); if (Number.isNaN(contentLength)) { - this.output.appendLine( + this.output.error( `Got invalid or missing content length: ${rawContentLength}`, ); } else { - this.output.appendLine( - `Got content length: ${prettyBytes(contentLength)}`, - ); + this.output.info(`Got content length: ${prettyBytes(contentLength)}`); } // Download to a temporary file. @@ -317,11 +312,11 @@ export class Storage { // False means the user canceled, although in practice it appears we // would not get this far because VS Code already throws on cancelation. if (!completed) { - this.output.appendLine("User aborted download"); + this.output.error("User aborted download"); throw new Error("User aborted download"); } - this.output.appendLine( + this.output.info( `Downloaded ${prettyBytes(written)} to ${path.basename(tempFile)}`, ); @@ -331,35 +326,31 @@ export class Storage { if (stat !== undefined) { const oldBinPath = binPath + ".old-" + Math.random().toString(36).substring(8); - this.output.appendLine( + this.output.info( `Moving existing binary to ${path.basename(oldBinPath)}`, ); await fs.rename(binPath, oldBinPath); } // Then move the temporary binary into the right place. - this.output.appendLine( - `Moving downloaded file to ${path.basename(binPath)}`, - ); + this.output.info(`Moving downloaded file to ${path.basename(binPath)}`); await fs.mkdir(path.dirname(binPath), { recursive: true }); await fs.rename(tempFile, binPath); // For debugging, to see if the binary only partially downloaded. const newStat = await cli.stat(binPath); - this.output.appendLine( + this.output.info( `Downloaded binary size is ${prettyBytes(newStat?.size || 0)}`, ); // Make sure we can execute this new binary. const version = await cli.version(binPath); - this.output.appendLine(`Downloaded binary version is ${version}`); + this.output.info(`Downloaded binary version is ${version}`); return binPath; } case 304: { - this.output.appendLine( - "Using existing binary since server returned a 304", - ); + this.output.info("Using existing binary since server returned a 304"); return binPath; } case 404: { @@ -508,7 +499,7 @@ export class Storage { } public writeToCoderOutputChannel(message: string) { - this.output.appendLine(`[${new Date().toISOString()}] ${message}`); + this.output.info(message); // We don't want to focus on the output here, because the // Coder server is designed to restart gracefully for users // because of P2P connections, and we don't want to draw From c4908309095343dd7cd7e6fb9a347eff562ae504 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 16 Jul 2025 18:39:31 +0300 Subject: [PATCH 2/4] Use multiple args instead of string interpolation + Downgrader logging from error to warn --- src/storage.ts | 47 +++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/storage.ts b/src/storage.ts index ad4bfa5b..01a19fe0 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -129,28 +129,26 @@ export class Storage { const enableDownloads = vscode.workspace.getConfiguration().get("coder.enableDownloads") !== false; - this.output.info( - `Downloads are ${enableDownloads ? "enabled" : "disabled"}`, - ); + this.output.info("Downloads are", enableDownloads ? "enabled" : "disabled"); // Get the build info to compare with the existing binary version, if any, // and to log for debugging. const buildInfo = await restClient.getBuildInfo(); - this.output.info(`Got server version: ${buildInfo.version}`); + this.output.info("Got server version", buildInfo.version); // Check if there is an existing binary and whether it looks valid. If it // is valid and matches the server, or if it does not match the server but // downloads are disabled, we can return early. const binPath = path.join(this.getBinaryCachePath(label), cli.name()); - this.output.info(`Using binary path: ${binPath}`); + this.output.info("Using binary path", binPath); const stat = await cli.stat(binPath); if (stat === undefined) { this.output.info("No existing binary found, starting download"); } else { - this.output.info(`Existing binary size is ${prettyBytes(stat.size)}`); + this.output.info("Existing binary size is", prettyBytes(stat.size)); try { const version = await cli.version(binPath); - this.output.info(`Existing binary version is ${version}`); + this.output.info("Existing binary version is", version); // If we have the right version we can avoid the request entirely. if (version === buildInfo.version) { this.output.info( @@ -167,16 +165,14 @@ export class Storage { "Downloading since existing binary does not match the server version", ); } catch (error) { - this.output.error( + this.output.warn( `Unable to get version of existing binary: ${error}. Downloading new binary instead`, ); } } if (!enableDownloads) { - this.output.error( - "Unable to download CLI because downloads are disabled", - ); + this.output.warn("Unable to download CLI because downloads are disabled"); throw new Error("Unable to download CLI because downloads are disabled"); } @@ -184,9 +180,9 @@ export class Storage { const removed = await cli.rmOld(binPath); removed.forEach(({ fileName, error }) => { if (error) { - this.output.error(`Failed to remove ${fileName}: ${error}`); + this.output.warn(`Failed to remove ${fileName}`, error); } else { - this.output.info(`Removed ${fileName}`); + this.output.info("Removed", fileName); } }); @@ -199,12 +195,12 @@ export class Storage { configSource && String(configSource).trim().length > 0 ? String(configSource) : "/bin/" + binName; - this.output.info(`Downloading binary from: ${binSource}`); + this.output.info("Downloading binary from", binSource); // Ideally we already caught that this was the right version and returned // early, but just in case set the ETag. const etag = stat !== undefined ? await cli.eTag(binPath) : ""; - this.output.info(`Using ETag: ${etag}`); + this.output.info("Using ETag", etag); // Make the download request. const controller = new AbortController(); @@ -220,18 +216,19 @@ export class Storage { // Ignore all errors so we can catch a 404! validateStatus: () => true, }); - this.output.info(`Got status code ${resp.status}`); + this.output.info("Got status code", resp.status); switch (resp.status) { case 200: { const rawContentLength = resp.headers["content-length"]; const contentLength = Number.parseInt(rawContentLength); if (Number.isNaN(contentLength)) { - this.output.error( - `Got invalid or missing content length: ${rawContentLength}`, + this.output.warn( + "Got invalid or missing content length", + rawContentLength, ); } else { - this.output.info(`Got content length: ${prettyBytes(contentLength)}`); + this.output.info("Got content length", prettyBytes(contentLength)); } // Download to a temporary file. @@ -312,7 +309,7 @@ export class Storage { // False means the user canceled, although in practice it appears we // would not get this far because VS Code already throws on cancelation. if (!completed) { - this.output.error("User aborted download"); + this.output.warn("User aborted download"); throw new Error("User aborted download"); } @@ -327,25 +324,27 @@ export class Storage { const oldBinPath = binPath + ".old-" + Math.random().toString(36).substring(8); this.output.info( - `Moving existing binary to ${path.basename(oldBinPath)}`, + "Moving existing binary to", + path.basename(oldBinPath), ); await fs.rename(binPath, oldBinPath); } // Then move the temporary binary into the right place. - this.output.info(`Moving downloaded file to ${path.basename(binPath)}`); + this.output.info("Moving downloaded file to", path.basename(binPath)); await fs.mkdir(path.dirname(binPath), { recursive: true }); await fs.rename(tempFile, binPath); // For debugging, to see if the binary only partially downloaded. const newStat = await cli.stat(binPath); this.output.info( - `Downloaded binary size is ${prettyBytes(newStat?.size || 0)}`, + "Downloaded binary size is", + prettyBytes(newStat?.size || 0), ); // Make sure we can execute this new binary. const version = await cli.version(binPath); - this.output.info(`Downloaded binary version is ${version}`); + this.output.info("Downloaded binary version is", version); return binPath; } From 9d54465bcb207fccee91b09673a428a43e32f40a Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 16 Jul 2025 19:03:52 +0300 Subject: [PATCH 3/4] Replaced "writeToCoderOutputChannel" with proper logging --- src/api.ts | 2 +- src/commands.ts | 5 +-- src/error.test.ts | 15 ++++++--- src/error.ts | 9 ++--- src/extension.ts | 22 +++++------- src/headers.test.ts | 13 ++++--- src/headers.ts | 13 +++---- src/inbox.ts | 8 ++--- src/logger.ts | 7 ++++ src/remote.ts | 71 +++++++++++++++------------------------ src/storage.ts | 17 +++------- src/workspaceMonitor.ts | 6 ++-- src/workspacesProvider.ts | 2 +- 13 files changed, 84 insertions(+), 106 deletions(-) create mode 100644 src/logger.ts diff --git a/src/api.ts b/src/api.ts index 22de2618..96b49673 100644 --- a/src/api.ts +++ b/src/api.ts @@ -105,7 +105,7 @@ export function makeCoderSdk( restClient.getAxiosInstance().interceptors.response.use( (r) => r, async (err) => { - throw await CertificateError.maybeWrap(err, baseUrl, storage); + throw await CertificateError.maybeWrap(err, baseUrl, storage.output); }, ); diff --git a/src/commands.ts b/src/commands.ts index d6734376..4373228c 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -245,8 +245,9 @@ export class Commands { } catch (err) { const message = getErrorMessage(err, "no response from the server"); if (isAutologin) { - this.storage.writeToCoderOutputChannel( - `Failed to log in to Coder server: ${message}`, + this.storage.output.warn( + "Failed to log in to Coder server:", + message, ); } else { this.vscodeProposed.window.showErrorMessage( diff --git a/src/error.test.ts b/src/error.test.ts index 3c4a50c3..4bbb9395 100644 --- a/src/error.test.ts +++ b/src/error.test.ts @@ -4,6 +4,7 @@ import https from "https"; import * as path from "path"; import { afterAll, beforeAll, it, expect, vi } from "vitest"; import { CertificateError, X509_ERR, X509_ERR_CODE } from "./error"; +import { Logger } from "./logger"; // Before each test we make a request to sanity check that we really get the // error we are expecting, then we run it through CertificateError. @@ -23,10 +24,16 @@ beforeAll(() => { }); }); -const logger = { - writeToCoderOutputChannel(message: string) { - throw new Error(message); - }, +const throwingLog = (message: string) => { + throw new Error(message); +}; + +const logger: Logger = { + trace: throwingLog, + debug: throwingLog, + info: throwingLog, + warn: throwingLog, + error: throwingLog, }; const disposers: (() => void)[] = []; diff --git a/src/error.ts b/src/error.ts index 53cc3389..5fa07294 100644 --- a/src/error.ts +++ b/src/error.ts @@ -3,6 +3,7 @@ import { isApiError, isApiErrorResponse } from "coder/site/src/api/errors"; import * as forge from "node-forge"; import * as tls from "tls"; import * as vscode from "vscode"; +import { Logger } from "./logger"; // X509_ERR_CODE represents error codes as returned from BoringSSL/OpenSSL. export enum X509_ERR_CODE { @@ -21,10 +22,6 @@ export enum X509_ERR { UNTRUSTED_CHAIN = "Your Coder deployment's certificate chain does not appear to be trusted by this system. The root of the certificate chain must be added to this system's trust store. ", } -export interface Logger { - writeToCoderOutputChannel(message: string): void; -} - interface KeyUsage { keyCertSign: boolean; } @@ -59,9 +56,7 @@ export class CertificateError extends Error { await CertificateError.determineVerifyErrorCause(address); return new CertificateError(err.message, cause); } catch (error) { - logger.writeToCoderOutputChannel( - `Failed to parse certificate from ${address}: ${error}`, - ); + logger.warn(`Failed to parse certificate from ${address}`, error); break; } case X509_ERR_CODE.DEPTH_ZERO_SELF_SIGNED_CERT: diff --git a/src/extension.ts b/src/extension.ts index 29a09b48..96f110c5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -317,7 +317,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { } } catch (ex) { if (ex instanceof CertificateError) { - storage.writeToCoderOutputChannel(ex.x509Err || ex.message); + storage.output.warn(ex.x509Err || ex.message); await ex.showModal("Failed to open workspace"); } else if (isAxiosError(ex)) { const msg = getErrorMessage(ex, "None"); @@ -326,7 +326,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { const method = ex.config?.method?.toUpperCase() || "request"; const status = ex.response?.status || "None"; const message = `API ${method} to '${urlString}' failed.\nStatus code: ${status}\nMessage: ${msg}\nDetail: ${detail}`; - storage.writeToCoderOutputChannel(message); + storage.output.warn(message); await vscodeProposed.window.showErrorMessage( "Failed to open workspace", { @@ -337,7 +337,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { ); } else { const message = errToStr(ex, "No error message was provided"); - storage.writeToCoderOutputChannel(message); + storage.output.warn(message); await vscodeProposed.window.showErrorMessage( "Failed to open workspace", { @@ -356,14 +356,12 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { // See if the plugin client is authenticated. const baseUrl = restClient.getAxiosInstance().defaults.baseURL; if (baseUrl) { - storage.writeToCoderOutputChannel( - `Logged in to ${baseUrl}; checking credentials`, - ); + storage.output.info(`Logged in to ${baseUrl}; checking credentials`); restClient .getAuthenticatedUser() .then(async (user) => { if (user && user.roles) { - storage.writeToCoderOutputChannel("Credentials are valid"); + storage.output.info("Credentials are valid"); vscode.commands.executeCommand( "setContext", "coder.authenticated", @@ -381,17 +379,13 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { myWorkspacesProvider.fetchAndRefresh(); allWorkspacesProvider.fetchAndRefresh(); } else { - storage.writeToCoderOutputChannel( - `No error, but got unexpected response: ${user}`, - ); + storage.output.warn("No error, but got unexpected response", user); } }) .catch((error) => { // This should be a failure to make the request, like the header command // errored. - storage.writeToCoderOutputChannel( - `Failed to check user authentication: ${error.message}`, - ); + storage.output.warn("Failed to check user authentication", error); vscode.window.showErrorMessage( `Failed to check user authentication: ${error.message}`, ); @@ -400,7 +394,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { vscode.commands.executeCommand("setContext", "coder.loaded", true); }); } else { - storage.writeToCoderOutputChannel("Not currently logged in"); + storage.output.info("Not currently logged in"); vscode.commands.executeCommand("setContext", "coder.loaded", true); // Handle autologin, if not already logged in. diff --git a/src/headers.test.ts b/src/headers.test.ts index 5cf333f5..669a8d74 100644 --- a/src/headers.test.ts +++ b/src/headers.test.ts @@ -2,11 +2,14 @@ import * as os from "os"; import { it, expect, describe, beforeEach, afterEach, vi } from "vitest"; import { WorkspaceConfiguration } from "vscode"; import { getHeaderCommand, getHeaders } from "./headers"; - -const logger = { - writeToCoderOutputChannel() { - // no-op - }, +import { Logger } from "./logger"; + +const logger: Logger = { + trace: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, }; it("should return no headers", async () => { diff --git a/src/headers.ts b/src/headers.ts index 4d4b5f44..e61bfa81 100644 --- a/src/headers.ts +++ b/src/headers.ts @@ -2,12 +2,9 @@ import * as cp from "child_process"; import * as os from "os"; import * as util from "util"; import type { WorkspaceConfiguration } from "vscode"; +import { Logger } from "./logger"; import { escapeCommandArg } from "./util"; -export interface Logger { - writeToCoderOutputChannel(message: string): void; -} - interface ExecException { code?: number; stderr?: string; @@ -78,11 +75,9 @@ export async function getHeaders( }); } catch (error) { if (isExecException(error)) { - logger.writeToCoderOutputChannel( - `Header command exited unexpectedly with code ${error.code}`, - ); - logger.writeToCoderOutputChannel(`stdout: ${error.stdout}`); - logger.writeToCoderOutputChannel(`stderr: ${error.stderr}`); + logger.warn("Header command exited unexpectedly with code", error.code); + logger.warn("stdout:", error.stdout); + logger.warn("stderr:", error.stderr); throw new Error( `Header command exited unexpectedly with code ${error.code}`, ); diff --git a/src/inbox.ts b/src/inbox.ts index 709dfbd8..0ec79720 100644 --- a/src/inbox.ts +++ b/src/inbox.ts @@ -63,7 +63,7 @@ export class Inbox implements vscode.Disposable { }); this.#socket.on("open", () => { - this.#storage.writeToCoderOutputChannel("Listening to Coder Inbox"); + this.#storage.output.info("Listening to Coder Inbox"); }); this.#socket.on("error", (error) => { @@ -86,9 +86,7 @@ export class Inbox implements vscode.Disposable { dispose() { if (!this.#disposed) { - this.#storage.writeToCoderOutputChannel( - "No longer listening to Coder Inbox", - ); + this.#storage.output.info("No longer listening to Coder Inbox"); this.#socket.close(); this.#disposed = true; } @@ -99,6 +97,6 @@ export class Inbox implements vscode.Disposable { error, "Got empty error while monitoring Coder Inbox", ); - this.#storage.writeToCoderOutputChannel(message); + this.#storage.output.error(message); } } diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 00000000..30bf0ec6 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,7 @@ +export interface Logger { + trace(message: string, ...args: unknown[]): void; + debug(message: string, ...args: unknown[]): void; + info(message: string, ...args: unknown[]): void; + warn(message: string, ...args: unknown[]): void; + error(message: string, ...args: unknown[]): void; +} diff --git a/src/remote.ts b/src/remote.ts index 4a13ae56..761b3d67 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -117,9 +117,7 @@ export class Remote { case "starting": case "stopping": writeEmitter = initWriteEmitterAndTerminal(); - this.storage.writeToCoderOutputChannel( - `Waiting for ${workspaceName}...`, - ); + this.storage.output.info(`Waiting for ${workspaceName}...`); workspace = await waitForBuild( restClient, writeEmitter, @@ -131,9 +129,7 @@ export class Remote { return undefined; } writeEmitter = initWriteEmitterAndTerminal(); - this.storage.writeToCoderOutputChannel( - `Starting ${workspaceName}...`, - ); + this.storage.output.info(`Starting ${workspaceName}...`); workspace = await startWorkspaceIfStoppedOrFailed( restClient, globalConfigDir, @@ -150,9 +146,7 @@ export class Remote { return undefined; } writeEmitter = initWriteEmitterAndTerminal(); - this.storage.writeToCoderOutputChannel( - `Starting ${workspaceName}...`, - ); + this.storage.output.info(`Starting ${workspaceName}...`); workspace = await startWorkspaceIfStoppedOrFailed( restClient, globalConfigDir, @@ -175,8 +169,9 @@ export class Remote { ); } } - this.storage.writeToCoderOutputChannel( - `${workspaceName} status is now ${workspace.latest_build.status}`, + this.storage.output.info( + `${workspaceName} status is now`, + workspace.latest_build.status, ); } return workspace; @@ -243,12 +238,8 @@ export class Remote { return; } - this.storage.writeToCoderOutputChannel( - `Using deployment URL: ${baseUrlRaw}`, - ); - this.storage.writeToCoderOutputChannel( - `Using deployment label: ${parts.label || "n/a"}`, - ); + this.storage.output.info("Using deployment URL", baseUrlRaw); + this.storage.output.info("Using deployment label", parts.label || "n/a"); // We could use the plugin client, but it is possible for the user to log // out or log into a different deployment while still connected, which would @@ -314,15 +305,14 @@ export class Remote { // Next is to find the workspace from the URI scheme provided. let workspace: Workspace; try { - this.storage.writeToCoderOutputChannel( - `Looking for workspace ${workspaceName}...`, - ); + this.storage.output.info(`Looking for workspace ${workspaceName}...`); workspace = await workspaceRestClient.getWorkspaceByOwnerAndName( parts.username, parts.workspace, ); - this.storage.writeToCoderOutputChannel( - `Found workspace ${workspaceName} with status ${workspace.latest_build.status}`, + this.storage.output.info( + `Found workspace ${workspaceName} with status`, + workspace.latest_build.status, ); this.commands.workspace = workspace; } catch (error) { @@ -404,9 +394,7 @@ export class Remote { this.commands.workspace = workspace; // Pick an agent. - this.storage.writeToCoderOutputChannel( - `Finding agent for ${workspaceName}...`, - ); + this.storage.output.info(`Finding agent for ${workspaceName}...`); const gotAgent = await this.commands.maybeAskAgent(workspace, parts.agent); if (!gotAgent) { // User declined to pick an agent. @@ -414,12 +402,13 @@ export class Remote { return; } let agent = gotAgent; // Reassign so it cannot be undefined in callbacks. - this.storage.writeToCoderOutputChannel( - `Found agent ${agent.name} with status ${agent.status}`, + this.storage.output.info( + `Found agent ${agent.name} with status`, + agent.status, ); // Do some janky setting manipulation. - this.storage.writeToCoderOutputChannel("Modifying settings..."); + this.storage.output.info("Modifying settings..."); const remotePlatforms = this.vscodeProposed.workspace .getConfiguration() .get>("remote.SSH.remotePlatform", {}); @@ -491,9 +480,7 @@ export class Remote { // write here is not necessarily catastrophic since the user will be // asked for the platform and the default timeout might be sufficient. mungedPlatforms = mungedConnTimeout = false; - this.storage.writeToCoderOutputChannel( - `Failed to configure settings: ${ex}`, - ); + this.storage.output.warn("Failed to configure settings", ex); } } @@ -521,9 +508,7 @@ export class Remote { // Wait for the agent to connect. if (agent.status === "connecting") { - this.storage.writeToCoderOutputChannel( - `Waiting for ${workspaceName}/${agent.name}...`, - ); + this.storage.output.info(`Waiting for ${workspaceName}/${agent.name}...`); await vscode.window.withProgress( { title: "Waiting for the agent to connect...", @@ -552,8 +537,9 @@ export class Remote { }); }, ); - this.storage.writeToCoderOutputChannel( - `Agent ${agent.name} status is now ${agent.status}`, + this.storage.output.info( + `Agent ${agent.name} status is now`, + agent.status, ); } @@ -584,7 +570,7 @@ export class Remote { // If we didn't write to the SSH config file, connecting would fail with // "Host not found". try { - this.storage.writeToCoderOutputChannel("Updating SSH config..."); + this.storage.output.info("Updating SSH config..."); await this.updateSSHConfig( workspaceRestClient, parts.label, @@ -594,9 +580,7 @@ export class Remote { featureSet, ); } catch (error) { - this.storage.writeToCoderOutputChannel( - `Failed to configure SSH: ${error}`, - ); + this.storage.output.warn("Failed to configure SSH", error); throw error; } @@ -633,7 +617,7 @@ export class Remote { }), ); - this.storage.writeToCoderOutputChannel("Remote setup complete"); + this.storage.output.info("Remote setup complete"); // Returning the URL and token allows the plugin to authenticate its own // client, for example to display the list of workspaces belonging to this @@ -674,8 +658,9 @@ export class Remote { return ""; } await fs.mkdir(logDir, { recursive: true }); - this.storage.writeToCoderOutputChannel( - `SSH proxy diagnostics are being written to ${logDir}`, + this.storage.output.info( + "SSH proxy diagnostics are being written to", + logDir, ); return ` --log-dir ${escapeCommandArg(logDir)}`; } diff --git a/src/storage.ts b/src/storage.ts index 01a19fe0..206dbce3 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -14,7 +14,7 @@ const MAX_URLS = 10; export class Storage { constructor( - private readonly output: vscode.LogOutputChannel, + public readonly output: vscode.LogOutputChannel, private readonly memento: vscode.Memento, private readonly secrets: vscode.SecretStorage, private readonly globalStorageUri: vscode.Uri, @@ -180,7 +180,7 @@ export class Storage { const removed = await cli.rmOld(binPath); removed.forEach(({ fileName, error }) => { if (error) { - this.output.warn(`Failed to remove ${fileName}`, error); + this.output.warn("Failed to remove", fileName, error); } else { this.output.info("Removed", fileName); } @@ -314,7 +314,8 @@ export class Storage { } this.output.info( - `Downloaded ${prettyBytes(written)} to ${path.basename(tempFile)}`, + `Downloaded ${prettyBytes(written)} to`, + path.basename(tempFile), ); // Move the old binary to a backup location first, just in case. And, @@ -497,14 +498,6 @@ export class Storage { : path.join(this.globalStorageUri.fsPath, "url"); } - public writeToCoderOutputChannel(message: string) { - this.output.info(message); - // We don't want to focus on the output here, because the - // Coder server is designed to restart gracefully for users - // because of P2P connections, and we don't want to draw - // attention to it. - } - /** * Configure the CLI for the deployment with the provided label. * @@ -604,7 +597,7 @@ export class Storage { return getHeaders( url, getHeaderCommand(vscode.workspace.getConfiguration()), - this, + this.output, ); } } diff --git a/src/workspaceMonitor.ts b/src/workspaceMonitor.ts index 18df50b2..5935a1f3 100644 --- a/src/workspaceMonitor.ts +++ b/src/workspaceMonitor.ts @@ -42,7 +42,7 @@ export class WorkspaceMonitor implements vscode.Disposable { this.name = `${workspace.owner_name}/${workspace.name}`; const url = this.restClient.getAxiosInstance().defaults.baseURL; const watchUrl = new URL(`${url}/api/v2/workspaces/${workspace.id}/watch`); - this.storage.writeToCoderOutputChannel(`Monitoring ${this.name}...`); + this.storage.output.info(`Monitoring ${this.name}...`); const eventSource = new EventSource(watchUrl.toString(), { fetch: createStreamingFetchAdapter(this.restClient.getAxiosInstance()), @@ -85,7 +85,7 @@ export class WorkspaceMonitor implements vscode.Disposable { */ dispose() { if (!this.disposed) { - this.storage.writeToCoderOutputChannel(`Unmonitoring ${this.name}...`); + this.storage.output.info(`Unmonitoring ${this.name}...`); this.statusBarItem.dispose(); this.eventSource.close(); this.disposed = true; @@ -202,7 +202,7 @@ export class WorkspaceMonitor implements vscode.Disposable { error, "Got empty error while monitoring workspace", ); - this.storage.writeToCoderOutputChannel(message); + this.storage.output.error(message); } private updateContext(workspace: Workspace) { diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index a77b31ad..64b74e7d 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -96,7 +96,7 @@ export class WorkspaceProvider */ private async fetch(): Promise { if (vscode.env.logLevel <= vscode.LogLevel.Debug) { - this.storage.writeToCoderOutputChannel( + this.storage.output.info( `Fetching workspaces: ${this.getWorkspacesQuery || "no filter"}...`, ); } From 029711f4c8931f91e6836a55a606f5168b245193 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Thu, 17 Jul 2025 18:09:15 +0300 Subject: [PATCH 4/4] Add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f07f13fb..4089ec4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Update `/openDevContainer` to support all dev container features when hostPath and configFile are provided. +- Coder output panel enhancements: All log entries now include timestamps, and you can filter messages by log level directly in the panel ## [v1.9.2](https://github.com/coder/vscode-coder/releases/tag/v1.9.2) 2025-06-25