Skip to content

Commit 70d9963

Browse files
jaggederestclaude
andcommitted
feat: implement debug logging with sensitive data masking
Add comprehensive debug logging throughout the codebase to aid in troubleshooting connection issues: - Add process error handlers (uncaughtException, unhandledRejection) - Add process signal handlers (SIGTERM, SIGINT, SIGHUP) - Add memory pressure monitoring with heap usage alerts - Implement sensitive data masking for logs (SSH keys, passwords, tokens) - Add debug logging to SSH config operations with masked output - Add connection lifecycle logging with unique connection IDs - Add retry logic and backoff timing logs - Add workspace disconnection event logging - Refactor logger to separate vscode dependencies using adapter pattern - Create ConfigProvider interface for test isolation - Reorganize files into logical directories (config/, logging/, test/) - All debug logs gated by coder.verbose flag 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9c67183 commit 70d9963

20 files changed

+644
-130
lines changed

src/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import { getProxyForUrl } from "./proxy";
1717
import { Storage } from "./storage";
1818
import { expandPath } from "./util";
1919

20+
// TODO: Add WebSocket connection logging
21+
// TODO: Add HTTP API call logging
22+
// TODO: Add certificate validation logging
23+
// TODO: Add token refresh logging
24+
2025
export const coderSessionTokenHeader = "Coder-Session-Token";
2126

2227
/**

src/config/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { VSCodeConfigProvider } from "./vscodeConfigProvider";

src/config/vscodeConfigProvider.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as vscode from "vscode";
2+
import { ConfigProvider } from "../logger";
3+
4+
export class VSCodeConfigProvider implements ConfigProvider {
5+
getVerbose(): boolean {
6+
const config = vscode.workspace.getConfiguration("coder");
7+
return config.get<boolean>("verbose", false);
8+
}
9+
10+
onVerboseChange(callback: () => void): { dispose: () => void } {
11+
const disposable = vscode.workspace.onDidChangeConfiguration((e) => {
12+
if (e.affectsConfiguration("coder.verbose")) {
13+
callback();
14+
}
15+
});
16+
return disposable;
17+
}
18+
}

src/error.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ it("falls back with different error", async () => {
246246
servername: "localhost",
247247
}),
248248
});
249-
await expect(request).rejects.toMatch(/failed with status code 500/);
249+
await expect(request).rejects.toThrow(/failed with status code 500/);
250250
try {
251251
await request;
252252
} catch (error) {

src/extension.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import * as vscode from "vscode";
66
import { makeCoderSdk, needToken } from "./api";
77
import { errToStr } from "./api-helper";
88
import { Commands } from "./commands";
9+
import { VSCodeConfigProvider } from "./config";
910
import { CertificateError, getErrorDetail } from "./error";
1011
import { logger } from "./logger";
12+
import { OutputChannelAdapter } from "./logging";
1113
import { Remote } from "./remote";
1214
import { Storage } from "./storage";
1315
import { toSafeHost } from "./util";
@@ -50,8 +52,58 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
5052

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

53-
// Initialize logger with the output channel
54-
logger.initialize(output);
55+
// Initialize logger with the output channel and config provider
56+
logger.initialize(new OutputChannelAdapter(output));
57+
logger.setConfigProvider(new VSCodeConfigProvider());
58+
59+
// Set up error handlers for uncaught exceptions and unhandled rejections
60+
process.on("uncaughtException", (error) => {
61+
logger.debug(`[process#global] error: Uncaught exception - ${error.stack}`);
62+
// Don't crash the extension - let VS Code handle it
63+
});
64+
65+
process.on("unhandledRejection", (reason, promise) => {
66+
logger.debug(
67+
`[process#global] error: Unhandled rejection at ${promise} - reason: ${reason}`,
68+
);
69+
});
70+
71+
// Set up process signal handlers
72+
const signals: NodeJS.Signals[] = ["SIGTERM", "SIGINT", "SIGHUP"];
73+
signals.forEach((signal) => {
74+
process.on(signal, () => {
75+
logger.debug(`[process#global] disconnect: Received signal ${signal}`);
76+
});
77+
});
78+
79+
// Set up memory pressure monitoring
80+
let memoryCheckInterval: NodeJS.Timeout | undefined;
81+
const checkMemoryPressure = () => {
82+
const usage = process.memoryUsage();
83+
const heapUsedPercent = (usage.heapUsed / usage.heapTotal) * 100;
84+
if (heapUsedPercent > 90) {
85+
logger.debug(
86+
`[process#global] error: High memory usage detected - heap used: ${heapUsedPercent.toFixed(
87+
1,
88+
)}% (${Math.round(usage.heapUsed / 1024 / 1024)}MB / ${Math.round(
89+
usage.heapTotal / 1024 / 1024,
90+
)}MB)`,
91+
);
92+
}
93+
};
94+
95+
// Check memory every 30 seconds when verbose logging is enabled
96+
const configProvider = new VSCodeConfigProvider();
97+
if (configProvider.getVerbose()) {
98+
memoryCheckInterval = setInterval(checkMemoryPressure, 30000);
99+
ctx.subscriptions.push({
100+
dispose: () => {
101+
if (memoryCheckInterval) {
102+
clearInterval(memoryCheckInterval);
103+
}
104+
},
105+
});
106+
}
55107

56108
const storage = new Storage(
57109
output,

src/headers.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,25 @@ it("should return headers", async () => {
7171
it("should error on malformed or empty lines", async () => {
7272
await expect(
7373
getHeaders("localhost", "printf 'foo=bar\\r\\n\\r\\n'"),
74-
).rejects.toMatch(/Malformed/);
74+
).rejects.toThrow(/Malformed/);
7575
await expect(
7676
getHeaders("localhost", "printf '\\r\\nfoo=bar'"),
77-
).rejects.toMatch(/Malformed/);
78-
await expect(getHeaders("localhost", "printf '=foo'")).rejects.toMatch(
77+
).rejects.toThrow(/Malformed/);
78+
await expect(getHeaders("localhost", "printf '=foo'")).rejects.toThrow(
7979
/Malformed/,
8080
);
81-
await expect(getHeaders("localhost", "printf 'foo'")).rejects.toMatch(
81+
await expect(getHeaders("localhost", "printf 'foo'")).rejects.toThrow(
8282
/Malformed/,
8383
);
84-
await expect(getHeaders("localhost", "printf ' =foo'")).rejects.toMatch(
84+
await expect(getHeaders("localhost", "printf ' =foo'")).rejects.toThrow(
8585
/Malformed/,
8686
);
87-
await expect(getHeaders("localhost", "printf 'foo =bar'")).rejects.toMatch(
87+
await expect(getHeaders("localhost", "printf 'foo =bar'")).rejects.toThrow(
8888
/Malformed/,
8989
);
9090
await expect(
9191
getHeaders("localhost", "printf 'foo foo=bar'"),
92-
).rejects.toMatch(/Malformed/);
92+
).rejects.toThrow(/Malformed/);
9393
});
9494

9595
it("should have access to environment variables", async () => {
@@ -105,7 +105,7 @@ it("should have access to environment variables", async () => {
105105
});
106106

107107
it("should error on non-zero exit", async () => {
108-
await expect(getHeaders("localhost", "exit 10")).rejects.toMatch(
108+
await expect(getHeaders("localhost", "exit 10")).rejects.toThrow(
109109
/exited unexpectedly with code 10/,
110110
);
111111
});

src/logger.test.ts

Lines changed: 76 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,9 @@ vi.mock("vscode", () => ({
1616
}));
1717

1818
import * as vscode from "vscode";
19-
import {
20-
ArrayAdapter,
21-
LogLevel,
22-
NoOpAdapter,
23-
OutputChannelAdapter,
24-
logger,
25-
} from "./logger";
19+
import { ArrayAdapter, LogLevel, NoOpAdapter, logger } from "./logger";
20+
import { OutputChannelAdapter } from "./logging";
21+
import { TestConfigProvider } from "./test";
2622

2723
describe("Logger", () => {
2824
beforeEach(() => {
@@ -191,79 +187,56 @@ describe("Logger", () => {
191187

192188
describe("Configuration", () => {
193189
it("should read verbose setting on initialization", () => {
194-
const mockConfig = {
195-
get: vi.fn().mockReturnValue(true),
196-
};
197-
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(
198-
mockConfig as unknown as vscode.WorkspaceConfiguration,
199-
);
200-
201-
logger.reset(); // Reset triggers re-initialization
202-
203-
expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("coder");
204-
expect(mockConfig.get).toHaveBeenCalledWith("verbose", false);
205-
});
206-
207-
it("should update log level when configuration changes", () => {
208-
let configChangeCallback: (
209-
e: vscode.ConfigurationChangeEvent,
210-
) => void = () => {};
211-
const mockDisposable = { dispose: vi.fn() };
212-
213-
vi.mocked(vscode.workspace.onDidChangeConfiguration).mockImplementation(
214-
(callback) => {
215-
configChangeCallback = callback;
216-
return mockDisposable as vscode.Disposable;
217-
},
218-
);
190+
const adapter = new ArrayAdapter();
191+
const configProvider = new TestConfigProvider();
219192

220-
const mockConfig = {
221-
get: vi.fn().mockReturnValue(false),
222-
};
223-
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(
224-
mockConfig as unknown as vscode.WorkspaceConfiguration,
225-
);
193+
logger.setAdapter(adapter);
194+
logger.setConfigProvider(configProvider);
226195

227-
logger.reset();
196+
// Test with verbose = false
197+
configProvider.setVerbose(false);
198+
logger.debug("Debug message");
199+
logger.info("Info message");
228200

229-
// Change config to verbose
230-
mockConfig.get.mockReturnValue(true);
231-
configChangeCallback({
232-
affectsConfiguration: (section: string) => section === "coder.verbose",
233-
} as vscode.ConfigurationChangeEvent);
201+
let logs = adapter.getSnapshot();
202+
expect(logs.length).toBe(1); // Only info should be logged
203+
expect(logs[0]).toContain("Info message");
234204

235-
// Verify it reads the new config
236-
expect(mockConfig.get).toHaveBeenCalledWith("verbose", false);
205+
// Clear and test with verbose = true
206+
adapter.clear();
207+
configProvider.setVerbose(true);
208+
logger.debug("Debug message 2");
209+
logger.info("Info message 2");
210+
211+
logs = adapter.getSnapshot();
212+
expect(logs.length).toBe(2); // Both should be logged
213+
expect(logs[0]).toContain("Debug message 2");
214+
expect(logs[1]).toContain("Info message 2");
237215
});
238216

239-
it("should ignore non-coder.verbose configuration changes", () => {
240-
let configChangeCallback: (
241-
e: vscode.ConfigurationChangeEvent,
242-
) => void = () => {};
243-
vi.mocked(vscode.workspace.onDidChangeConfiguration).mockImplementation(
244-
(callback) => {
245-
configChangeCallback = callback;
246-
return { dispose: vi.fn() } as unknown as vscode.Disposable;
247-
},
248-
);
217+
it("should update log level when configuration changes", () => {
218+
const adapter = new ArrayAdapter();
219+
const configProvider = new TestConfigProvider();
249220

250-
const mockConfig = {
251-
get: vi.fn().mockReturnValue(false),
252-
};
253-
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(
254-
mockConfig as unknown as vscode.WorkspaceConfiguration,
255-
);
221+
logger.setAdapter(adapter);
222+
logger.setConfigProvider(configProvider);
256223

257-
logger.reset();
258-
mockConfig.get.mockClear();
224+
// Start with verbose = false
225+
configProvider.setVerbose(false);
226+
logger.debug("Debug 1");
227+
logger.info("Info 1");
228+
229+
let logs = adapter.getSnapshot();
230+
expect(logs.length).toBe(1); // Only info
259231

260-
// Trigger non-verbose config change
261-
configChangeCallback({
262-
affectsConfiguration: (section: string) => section === "other.setting",
263-
} as vscode.ConfigurationChangeEvent);
232+
// Change to verbose = true
233+
adapter.clear();
234+
configProvider.setVerbose(true);
235+
logger.debug("Debug 2");
236+
logger.info("Info 2");
264237

265-
// Should not re-read config
266-
expect(mockConfig.get).not.toHaveBeenCalled();
238+
logs = adapter.getSnapshot();
239+
expect(logs.length).toBe(2); // Both logged
267240
});
268241
});
269242

@@ -345,15 +318,33 @@ describe("Logger", () => {
345318
});
346319

347320
it("should dispose configuration listener on reset", () => {
348-
const mockDisposable = { dispose: vi.fn() };
349-
vi.mocked(vscode.workspace.onDidChangeConfiguration).mockReturnValue(
350-
mockDisposable as unknown as vscode.Disposable,
351-
);
321+
const adapter = new ArrayAdapter();
322+
const configProvider = new TestConfigProvider();
323+
324+
logger.setAdapter(adapter);
325+
logger.setConfigProvider(configProvider);
326+
327+
// Track if dispose was called
328+
let disposed = false;
329+
const originalOnVerboseChange =
330+
configProvider.onVerboseChange.bind(configProvider);
331+
configProvider.onVerboseChange = (callback: () => void) => {
332+
const result = originalOnVerboseChange(callback);
333+
return {
334+
dispose: () => {
335+
disposed = true;
336+
result.dispose();
337+
},
338+
};
339+
};
352340

341+
// Re-set config provider to register the wrapped listener
342+
logger.setConfigProvider(configProvider);
343+
344+
// Reset should dispose the listener
353345
logger.reset();
354-
logger.reset(); // Second reset should dispose the first listener
355346

356-
expect(mockDisposable.dispose).toHaveBeenCalled();
347+
expect(disposed).toBe(true);
357348
});
358349
});
359350

@@ -364,7 +355,12 @@ describe("Logger", () => {
364355
clear: vi.fn(),
365356
} as unknown as vscode.OutputChannel;
366357

367-
logger.initialize(mockChannel);
358+
const adapter = new OutputChannelAdapter(mockChannel);
359+
logger.initialize(adapter);
360+
361+
// Set up config provider for test
362+
const configProvider = new TestConfigProvider();
363+
logger.setConfigProvider(configProvider);
368364

369365
// Verify we can log after initialization
370366
logger.info("Test message");
@@ -373,9 +369,10 @@ describe("Logger", () => {
373369

374370
it("should throw if already initialized", () => {
375371
const mockChannel = {} as vscode.OutputChannel;
376-
logger.initialize(mockChannel);
372+
const adapter = new OutputChannelAdapter(mockChannel);
373+
logger.initialize(adapter);
377374

378-
expect(() => logger.initialize(mockChannel)).toThrow(
375+
expect(() => logger.initialize(adapter)).toThrow(
379376
"Logger already initialized",
380377
);
381378
});

0 commit comments

Comments
 (0)