Skip to content

Commit d8f53c6

Browse files
authored
feat: include VS Code-side logs in support bundle (#916)
Append VS Code-side logs to the support bundle zip after the CLI produces it, under a `vscode-logs/` directory: the Remote SSH extension log, SSH proxy logs, and extension output channel logs (the latter two filtered to the last 3 days by mtime). Each source is collected independently and failures are warned and skipped. Uses `fflate` to read/modify/write the zip, then atomically renames over the original; on rename failure the merged zip is left as `<n>-vscode.zip` and surfaced in a warning. Closes #889
1 parent 98048f2 commit d8f53c6

5 files changed

Lines changed: 438 additions & 4 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,7 @@
597597
"axios": "1.15.0",
598598
"date-fns": "catalog:",
599599
"eventsource": "^4.1.0",
600+
"fflate": "^0.8.2",
600601
"find-process": "^2.1.1",
601602
"jsonc-parser": "^3.3.1",
602603
"openpgp": "^6.3.0",

pnpm-lock.yaml

Lines changed: 12 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { type ServiceContainer } from "./core/container";
2020
import { type MementoManager } from "./core/mementoManager";
2121
import { type PathResolver } from "./core/pathResolver";
2222
import { type SecretsManager } from "./core/secretsManager";
23+
import { appendVsCodeLogs } from "./core/supportBundleLogs";
2324
import { type DeploymentManager } from "./deployment/deploymentManager";
2425
import { CertificateError } from "./error/certificateError";
2526
import { toError } from "./error/errorUtils";
@@ -256,6 +257,18 @@ export class Commands {
256257

257258
progress.report({ message: "Collecting diagnostics..." });
258259
await cliExec.supportBundle(env, workspaceId, outputUri.fsPath, signal);
260+
261+
progress.report({ message: "Adding VS Code logs..." });
262+
await appendVsCodeLogs(
263+
outputUri.fsPath,
264+
{
265+
remoteSshLogPath: this.workspaceLogPath,
266+
proxyLogDir: this.pathResolver.getProxyLogPath(),
267+
extensionLogDir: this.pathResolver.getCodeLogDir(),
268+
},
269+
this.logger,
270+
);
271+
259272
return outputUri;
260273
},
261274
{

src/core/supportBundleLogs.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { unzip, zip } from "fflate";
2+
import * as fs from "node:fs/promises";
3+
import * as path from "node:path";
4+
import { promisify } from "node:util";
5+
6+
import { type Logger } from "../logging/logger";
7+
import { renameWithRetry } from "../util";
8+
9+
export interface LogSources {
10+
remoteSshLogPath?: string;
11+
proxyLogDir?: string;
12+
extensionLogDir?: string;
13+
}
14+
15+
// 3 days is enough context for recent issues; matching the 7-day
16+
// rotation would bloat the bundle.
17+
const LOG_MAX_AGE_MS = 3 * 24 * 60 * 60 * 1000;
18+
19+
const unzipAsync = promisify(unzip);
20+
const zipAsync = promisify(zip);
21+
22+
async function collectDirFiles(
23+
dirPath: string,
24+
logger: Logger,
25+
): Promise<Map<string, Uint8Array>> {
26+
const results = new Map<string, Uint8Array>();
27+
28+
let entries: string[];
29+
try {
30+
entries = await fs.readdir(dirPath);
31+
} catch (error) {
32+
logger.warn(`Could not read log directory ${dirPath}`, error);
33+
return results;
34+
}
35+
36+
const cutoff = Date.now() - LOG_MAX_AGE_MS;
37+
38+
await Promise.all(
39+
entries.map(async (entry) => {
40+
const filePath = path.join(dirPath, entry);
41+
try {
42+
const stat = await fs.stat(filePath);
43+
if (!stat.isFile() || stat.mtimeMs < cutoff) {
44+
return;
45+
}
46+
results.set(entry, await fs.readFile(filePath));
47+
} catch (error) {
48+
logger.warn(`Could not read log file ${filePath}`, error);
49+
}
50+
}),
51+
);
52+
53+
return results;
54+
}
55+
56+
/**
57+
* Gather log files from each source independently so a failure in one
58+
* does not affect the others.
59+
*/
60+
async function collectLogFiles(
61+
sources: LogSources,
62+
logger: Logger,
63+
): Promise<Map<string, Uint8Array>> {
64+
const files = new Map<string, Uint8Array>();
65+
66+
if (sources.remoteSshLogPath) {
67+
try {
68+
files.set(
69+
`vscode-logs/remote-ssh/${path.basename(sources.remoteSshLogPath)}`,
70+
await fs.readFile(sources.remoteSshLogPath),
71+
);
72+
} catch (error) {
73+
logger.warn("Could not read Remote SSH log", error);
74+
}
75+
}
76+
77+
if (sources.proxyLogDir) {
78+
for (const [name, data] of await collectDirFiles(
79+
sources.proxyLogDir,
80+
logger,
81+
)) {
82+
files.set(`vscode-logs/proxy/${name}`, data);
83+
}
84+
}
85+
86+
if (sources.extensionLogDir) {
87+
for (const [name, data] of await collectDirFiles(
88+
sources.extensionLogDir,
89+
logger,
90+
)) {
91+
files.set(`vscode-logs/extension/${name}`, data);
92+
}
93+
}
94+
95+
return files;
96+
}
97+
98+
/**
99+
* Best-effort: append VS Code logs to a support bundle zip.
100+
* Uses atomic rename to avoid corrupting the original bundle on failure.
101+
*/
102+
export async function appendVsCodeLogs(
103+
zipPath: string,
104+
sources: LogSources,
105+
logger: Logger,
106+
): Promise<void> {
107+
try {
108+
const logFiles = await collectLogFiles(sources, logger);
109+
if (logFiles.size === 0) {
110+
logger.info("No VS Code logs found to add to support bundle");
111+
return;
112+
}
113+
114+
logger.info(
115+
`Adding ${logFiles.size} VS Code log file(s) to support bundle`,
116+
);
117+
118+
// Write to a named temporary path first so a failure at the rename step
119+
// leaves the user with a properly named file containing VS Code logs.
120+
const parsed = path.parse(zipPath);
121+
const vscodeBundlePath = path.join(
122+
parsed.dir,
123+
`${parsed.name}-vscode${parsed.ext}`,
124+
);
125+
126+
try {
127+
const entries = await unzipAsync(await fs.readFile(zipPath));
128+
for (const [name, data] of logFiles) {
129+
entries[name] = data;
130+
}
131+
const updated = await zipAsync(entries);
132+
await fs.writeFile(vscodeBundlePath, updated);
133+
} catch (error) {
134+
logger.error("Failed to add VS Code logs to support bundle", error);
135+
try {
136+
await fs.rm(vscodeBundlePath, { force: true });
137+
} catch (cleanupError) {
138+
logger.warn(
139+
`Could not clean up partial bundle at ${vscodeBundlePath}`,
140+
cleanupError,
141+
);
142+
}
143+
return;
144+
}
145+
146+
try {
147+
await renameWithRetry(fs.rename, vscodeBundlePath, zipPath);
148+
} catch (error) {
149+
logger.warn(
150+
`Could not replace original bundle; VS Code logs saved separately at ${vscodeBundlePath}`,
151+
error,
152+
);
153+
}
154+
} catch (error) {
155+
// Best-effort: never let a failure here lose the user's bundle.
156+
logger.error("Unexpected error appending VS Code logs", error);
157+
}
158+
}

0 commit comments

Comments
 (0)