Skip to content

[WIP] Add debug logging using the new logging system #547

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
/coverage/
*.vsix
yarn-error.log
.DS_Store
51 changes: 51 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,54 @@
- Error handling: wrap and type errors appropriately
- Use async/await for promises, avoid explicit Promise construction where possible
- Test files must be named `*.test.ts` and use Vitest

## Development Workflow: Spec → Code

THESE INSTRUCTIONS ARE CRITICAL!

They dramatically improve the quality of the work you create.

### Phase 1: Requirements First

When asked to implement any feature or make changes, ALWAYS start by asking:
"Should I create a Spec for this task first?"

IFF user agrees:

- Create a markdown file in `.claude/scopes/FeatureName.md`
- Interview the user to clarify:
- Purpose & user problem
- Success criteria
- Scope & constraints
- Technical considerations
- Out of scope items

### Phase 2: Review & Refine

After drafting the Spec:

- Present it to the user
- Ask: "Does this capture your intent? Any changes needed?"
- Iterate until user approves
- End with: "Spec looks good? Type 'GO!' when ready to implement"

### Phase 3: Implementation

ONLY after user types "GO!" or explicitly approves:

- Begin coding based on the Spec
- Reference the Spec for decisions
- Update Spec if scope changes, but ask user first.

### File Organization

```

.claude/
├── scopes/
│ ├── FeatureName.md # Shared/committed Specs
│ └── OtherFeature.md # Other Specs, for future or past work

```

**Remember: Think first, ask clarifying questions, _then_ code. The Spec is your north star.**
7 changes: 6 additions & 1 deletion src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import { getProxyForUrl } from "./proxy";
import { Storage } from "./storage";
import { expandPath } from "./util";

// TODO: Add WebSocket connection logging
// TODO: Add HTTP API call logging
// TODO: Add certificate validation logging
// TODO: Add token refresh logging

export const coderSessionTokenHeader = "Coder-Session-Token";

/**
Expand Down Expand Up @@ -105,7 +110,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);
},
);

Expand Down
5 changes: 2 additions & 3 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as vscode from "vscode";
import { makeCoderSdk, needToken } from "./api";
import { extractAgents } from "./api-helper";
import { CertificateError } from "./error";
import { logger } from "./logger";
import { Storage } from "./storage";
import { toRemoteAuthority, toSafeHost } from "./util";
import { OpenableTreeItem } from "./workspacesProvider";
Expand Down Expand Up @@ -245,9 +246,7 @@ 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}`,
);
logger.info(`Failed to log in to Coder server: ${message}`);
} else {
this.vscodeProposed.window.showErrorMessage(
"Failed to log in to Coder server",
Expand Down
1 change: 1 addition & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { VSCodeConfigProvider } from "./vscodeConfigProvider";
18 changes: 18 additions & 0 deletions src/config/vscodeConfigProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as vscode from "vscode";
import { ConfigProvider } from "../logger";

export class VSCodeConfigProvider implements ConfigProvider {
getVerbose(): boolean {
const config = vscode.workspace.getConfiguration("coder");
return config.get<boolean>("verbose", false);
}

onVerboseChange(callback: () => void): { dispose: () => void } {
const disposable = vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration("coder.verbose")) {
callback();
}
});
return disposable;
}
}
29 changes: 16 additions & 13 deletions src/error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@ const isElectron =
// TODO: Remove the vscode mock once we revert the testing framework.
beforeAll(() => {
vi.mock("vscode", () => {
return {};
return {
workspace: {
getConfiguration: vi.fn().mockReturnValue({
get: vi.fn().mockReturnValue(false),
}),
onDidChangeConfiguration: vi.fn().mockReturnValue({
dispose: vi.fn(),
}),
},
};
});
});

const logger = {
writeToCoderOutputChannel(message: string) {
throw new Error(message);
},
};

const disposers: (() => void)[] = [];
afterAll(() => {
disposers.forEach((d) => d());
Expand Down Expand Up @@ -89,7 +92,7 @@ it("detects partial chains", async () => {
try {
await request;
} catch (error) {
const wrapped = await CertificateError.maybeWrap(error, address, logger);
const wrapped = await CertificateError.maybeWrap(error, address);
expect(wrapped instanceof CertificateError).toBeTruthy();
expect((wrapped as CertificateError).x509Err).toBe(X509_ERR.PARTIAL_CHAIN);
}
Expand Down Expand Up @@ -126,7 +129,7 @@ it("detects self-signed certificates without signing capability", async () => {
try {
await request;
} catch (error) {
const wrapped = await CertificateError.maybeWrap(error, address, logger);
const wrapped = await CertificateError.maybeWrap(error, address);
expect(wrapped instanceof CertificateError).toBeTruthy();
expect((wrapped as CertificateError).x509Err).toBe(X509_ERR.NON_SIGNING);
}
Expand Down Expand Up @@ -157,7 +160,7 @@ it("detects self-signed certificates", async () => {
try {
await request;
} catch (error) {
const wrapped = await CertificateError.maybeWrap(error, address, logger);
const wrapped = await CertificateError.maybeWrap(error, address);
expect(wrapped instanceof CertificateError).toBeTruthy();
expect((wrapped as CertificateError).x509Err).toBe(X509_ERR.UNTRUSTED_LEAF);
}
Expand Down Expand Up @@ -200,7 +203,7 @@ it("detects an untrusted chain", async () => {
try {
await request;
} catch (error) {
const wrapped = await CertificateError.maybeWrap(error, address, logger);
const wrapped = await CertificateError.maybeWrap(error, address);
expect(wrapped instanceof CertificateError).toBeTruthy();
expect((wrapped as CertificateError).x509Err).toBe(
X509_ERR.UNTRUSTED_CHAIN,
Expand Down Expand Up @@ -243,11 +246,11 @@ it("falls back with different error", async () => {
servername: "localhost",
}),
});
await expect(request).rejects.toMatch(/failed with status code 500/);
await expect(request).rejects.toThrow(/failed with status code 500/);
try {
await request;
} catch (error) {
const wrapped = await CertificateError.maybeWrap(error, "1", logger);
const wrapped = await CertificateError.maybeWrap(error, "1");
expect(wrapped instanceof CertificateError).toBeFalsy();
expect((wrapped as Error).message).toMatch(/failed with status code 500/);
}
Expand Down
8 changes: 2 additions & 6 deletions src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
Expand All @@ -47,7 +44,6 @@ export class CertificateError extends Error {
static async maybeWrap<T>(
err: T,
address: string,
logger: Logger,
): Promise<CertificateError | T> {
if (isAxiosError(err)) {
switch (err.code) {
Expand All @@ -59,7 +55,7 @@ export class CertificateError extends Error {
await CertificateError.determineVerifyErrorCause(address);
return new CertificateError(err.message, cause);
} catch (error) {
logger.writeToCoderOutputChannel(
logger.info(
`Failed to parse certificate from ${address}: ${error}`,
);
break;
Expand Down
79 changes: 65 additions & 14 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import * as vscode from "vscode";
import { makeCoderSdk, needToken } from "./api";
import { errToStr } from "./api-helper";
import { Commands } from "./commands";
import { VSCodeConfigProvider } from "./config";
import { CertificateError, getErrorDetail } from "./error";
import { logger } from "./logger";
import { OutputChannelAdapter } from "./logging";
import { Remote } from "./remote";
import { Storage } from "./storage";
import { toSafeHost } from "./util";
Expand Down Expand Up @@ -48,6 +51,60 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
}

const output = vscode.window.createOutputChannel("Coder");

// Initialize logger with the output channel and config provider
logger.initialize(new OutputChannelAdapter(output));
logger.setConfigProvider(new VSCodeConfigProvider());

// Set up error handlers for uncaught exceptions and unhandled rejections
process.on("uncaughtException", (error) => {
logger.debug(`[process#global] error: Uncaught exception - ${error.stack}`);
// Don't crash the extension - let VS Code handle it
});

process.on("unhandledRejection", (reason, promise) => {
logger.debug(
`[process#global] error: Unhandled rejection at ${promise} - reason: ${reason}`,
);
});

// Set up process signal handlers
const signals: NodeJS.Signals[] = ["SIGTERM", "SIGINT", "SIGHUP"];
signals.forEach((signal) => {
process.on(signal, () => {
logger.debug(`[process#global] disconnect: Received signal ${signal}`);
});
});

// Set up memory pressure monitoring
let memoryCheckInterval: NodeJS.Timeout | undefined;
const checkMemoryPressure = () => {
const usage = process.memoryUsage();
const heapUsedPercent = (usage.heapUsed / usage.heapTotal) * 100;
if (heapUsedPercent > 90) {
logger.debug(
`[process#global] error: High memory usage detected - heap used: ${heapUsedPercent.toFixed(
1,
)}% (${Math.round(usage.heapUsed / 1024 / 1024)}MB / ${Math.round(
usage.heapTotal / 1024 / 1024,
)}MB)`,
);
}
};

// Check memory every 30 seconds when verbose logging is enabled
const configProvider = new VSCodeConfigProvider();
if (configProvider.getVerbose()) {
memoryCheckInterval = setInterval(checkMemoryPressure, 30000);
ctx.subscriptions.push({
dispose: () => {
if (memoryCheckInterval) {
clearInterval(memoryCheckInterval);
}
},
});
}

const storage = new Storage(
output,
ctx.globalState,
Expand Down Expand Up @@ -317,7 +374,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
}
} catch (ex) {
if (ex instanceof CertificateError) {
storage.writeToCoderOutputChannel(ex.x509Err || ex.message);
logger.info(ex.x509Err || ex.message);
await ex.showModal("Failed to open workspace");
} else if (isAxiosError(ex)) {
const msg = getErrorMessage(ex, "None");
Expand All @@ -326,7 +383,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
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);
logger.info(message);
await vscodeProposed.window.showErrorMessage(
"Failed to open workspace",
{
Expand All @@ -337,7 +394,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
);
} else {
const message = errToStr(ex, "No error message was provided");
storage.writeToCoderOutputChannel(message);
logger.info(message);
await vscodeProposed.window.showErrorMessage(
"Failed to open workspace",
{
Expand All @@ -356,14 +413,12 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
// See if the plugin client is authenticated.
const baseUrl = restClient.getAxiosInstance().defaults.baseURL;
if (baseUrl) {
storage.writeToCoderOutputChannel(
`Logged in to ${baseUrl}; checking credentials`,
);
logger.info(`Logged in to ${baseUrl}; checking credentials`);
restClient
.getAuthenticatedUser()
.then(async (user) => {
if (user && user.roles) {
storage.writeToCoderOutputChannel("Credentials are valid");
logger.info("Credentials are valid");
vscode.commands.executeCommand(
"setContext",
"coder.authenticated",
Expand All @@ -381,17 +436,13 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
myWorkspacesProvider.fetchAndRefresh();
allWorkspacesProvider.fetchAndRefresh();
} else {
storage.writeToCoderOutputChannel(
`No error, but got unexpected response: ${user}`,
);
logger.info(`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}`,
);
logger.info(`Failed to check user authentication: ${error.message}`);
vscode.window.showErrorMessage(
`Failed to check user authentication: ${error.message}`,
);
Expand All @@ -400,7 +451,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
vscode.commands.executeCommand("setContext", "coder.loaded", true);
});
} else {
storage.writeToCoderOutputChannel("Not currently logged in");
logger.info("Not currently logged in");
vscode.commands.executeCommand("setContext", "coder.loaded", true);

// Handle autologin, if not already logged in.
Expand Down
Loading