diff --git a/extensions/gitpod-remote/src/extension.ts b/extensions/gitpod-remote/src/extension.ts index 76ef63d6a0f10..ef06446b911d5 100644 --- a/extensions/gitpod-remote/src/extension.ts +++ b/extensions/gitpod-remote/src/extension.ts @@ -16,12 +16,13 @@ export async function activate(context: vscode.ExtensionContext) { if (!gitpodContext) { return; } + if (vscode.extensions.getExtension('gitpod.gitpod')) { try { await util.promisify(cp.exec)('code --uninstall-extension gitpod.gitpod'); vscode.commands.executeCommand('workbench.action.reloadWindow'); } catch (e) { - gitpodContext.output.appendLine('failed to uninstall gitpod.gitpod: ' + e); + gitpodContext.logger.error('failed to uninstall gitpod.gitpod:', e); } return; } @@ -43,6 +44,11 @@ export async function activate(context: vscode.ExtensionContext) { // and gitpod.gitpod to disable auto tunneling from the current local machine. vscode.commands.executeCommand('gitpod.api.autoTunnel', gitpodContext.info.getGitpodHost(), gitpodContext.info.getInstanceId(), false); + // For collecting logs, will be called by gitpod-desktop extension; + context.subscriptions.push(vscode.commands.registerCommand('__gitpod.getGitpodRemoteLogsUri', () => { + return context.logUri; + })); + // TODO // - auth? // - .gitpod.yml validations @@ -87,7 +93,7 @@ export function openWorkspaceLocation(context: GitpodExtensionContext): boolean } export async function installInitialExtensions(context: GitpodExtensionContext): Promise { - context.output.appendLine('installing initial extensions...'); + context.logger.info('installing initial extensions...'); const extensions: (vscode.Uri | string)[] = []; try { const workspaceContextUri = vscode.Uri.parse(context.info.getWorkspaceContextUrl()); @@ -125,10 +131,10 @@ export async function installInitialExtensions(context: GitpodExtensionContext): } } } catch (e) { - context.output.appendLine('failed to detect workspace context dependent extensions:' + e); + context.logger.error('failed to detect workspace context dependent extensions:', e); console.error('failed to detect workspace context dependent extensions:', e); } - context.output.appendLine('initial extensions: ' + extensions); + context.logger.info('initial extensions:', extensions); if (extensions.length) { let cause; try { @@ -138,11 +144,11 @@ export async function installInitialExtensions(context: GitpodExtensionContext): cause = e; } if (cause) { - context.output.appendLine('failed to install initial extensions: ' + cause); + context.logger.error('failed to install initial extensions:', cause); console.error('failed to install initial extensions: ', cause); } } - context.output.appendLine('initial extensions installed'); + context.logger.info('initial extensions installed'); } export function registerHearbeat(context: GitpodExtensionContext): void { @@ -152,11 +158,13 @@ export function registerHearbeat(context: GitpodExtensionContext): void { }; const sendHeartBeat = async (wasClosed?: true) => { const suffix = wasClosed ? 'was closed heartbeat' : 'heartbeat'; + if (wasClosed) { + context.logger.trace('sending ' + suffix); + } try { - context.output.appendLine('sending ' + suffix); await context.gitpod.server.sendHeartBeat({ instanceId: context.info.getInstanceId(), wasClosed }); } catch (err) { - context.output.appendLine(`failed to send ${suffix}: ` + err); + context.logger.error(`failed to send ${suffix}:`, err); console.error(`failed to send ${suffix}`, err); } }; diff --git a/extensions/gitpod-shared/src/common/logger.ts b/extensions/gitpod-shared/src/common/logger.ts new file mode 100644 index 0000000000000..0f71ef88014ae --- /dev/null +++ b/extensions/gitpod-shared/src/common/logger.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +type LogLevel = 'Trace' | 'Info' | 'Error' | 'Warn' | 'Log'; + +export default class Log { + private output: vscode.OutputChannel; + + constructor(name: string) { + this.output = vscode.window.createOutputChannel(name); + } + + private data2String(data: any): string { + if (data instanceof Error) { + return data.stack || data.message; + } + if (data.success === false && data.message) { + return data.message; + } + return data.toString(); + } + + public trace(message: string, data?: any): void { + this.logLevel('Trace', message, data); + } + + public info(message: string, data?: any): void { + this.logLevel('Info', message, data); + } + + public error(message: string, data?: any): void { + this.logLevel('Error', message, data); + } + + public warn(message: string, data?: any): void { + this.logLevel('Warn', message, data); + } + + public log(message: string, data?: any): void { + this.logLevel('Log', message, data); + } + + public logLevel(level: LogLevel, message: string, data?: any): void { + this.output.appendLine(`[${level} - ${this.now()}] ${message}`); + if (data) { + this.output.appendLine(this.data2String(data)); + } + } + + private now(): string { + const now = new Date(); + return padLeft(now.getUTCHours() + '', 2, '0') + + ':' + padLeft(now.getMinutes() + '', 2, '0') + + ':' + padLeft(now.getUTCSeconds() + '', 2, '0') + '.' + now.getMilliseconds(); + } + + public show() { + this.output.show(); + } +} + +function padLeft(s: string, n: number, pad = ' ') { + return pad.repeat(Math.max(0, n - s.length)) + s; +} diff --git a/extensions/gitpod-shared/src/extension.ts b/extensions/gitpod-shared/src/extension.ts index 5b77872b3e5e0..d076439e95f83 100644 --- a/extensions/gitpod-shared/src/extension.ts +++ b/extensions/gitpod-shared/src/extension.ts @@ -14,18 +14,15 @@ export async function setupGitpodContext(context: vscode.ExtensionContext): Prom } const gitpodContext = await createGitpodExtensionContext(context); + vscode.commands.executeCommand('setContext', 'gitpod.inWorkspace', !!gitpodContext); if (!gitpodContext) { - vscode.commands.executeCommand('setContext', 'gitpod.inWorkspace', false); return undefined; } - vscode.commands.executeCommand('setContext', 'gitpod.inWorkspace', true); + + logContextInfo(gitpodContext); vscode.commands.executeCommand('setContext', 'gitpod.ideAlias', gitpodContext.info.getIdeAlias()); - if (vscode.env.uiKind === vscode.UIKind.Web) { - vscode.commands.executeCommand('setContext', 'gitpod.UIKind', 'web'); - } else if (vscode.env.uiKind === vscode.UIKind.Desktop) { - vscode.commands.executeCommand('setContext', 'gitpod.UIKind', 'desktop'); - } + vscode.commands.executeCommand('setContext', 'gitpod.UIKind', vscode.env.uiKind === vscode.UIKind.Web ? 'web' : 'desktop'); registerUsageAnalytics(gitpodContext); registerWorkspaceCommands(gitpodContext); @@ -40,3 +37,15 @@ function registerUsageAnalytics(context: GitpodExtensionContext): void { context.fireAnalyticsEvent({ eventName: 'vscode_session', properties: {} }); } +function logContextInfo(context: GitpodExtensionContext) { + context.logger.info(`VSCODE_MACHINE_ID: ${vscode.env.machineId}`); + context.logger.info(`VSCODE_SESSION_ID: ${vscode.env.sessionId}`); + context.logger.info(`VSCODE_VERSION: ${vscode.version}`); + context.logger.info(`VSCODE_APP_NAME: ${vscode.env.appName}`); + context.logger.info(`VSCODE_APP_HOST: ${vscode.env.appHost}`); + context.logger.info(`VSCODE_UI_KIND: ${vscode.env.uiKind === vscode.UIKind.Web ? 'web' : 'desktop'}`); + + context.logger.info(`GITPOD_WORKSPACE_CONTEXT_URL: ${context.info.getWorkspaceContextUrl()}`); + context.logger.info(`GITPOD_INSTANCE_ID: ${context.info.getInstanceId()}`); + context.logger.info(`GITPOD_WORKSPACE_URL: ${context.info.getWorkspaceUrl()}`); +} diff --git a/extensions/gitpod-shared/src/features.ts b/extensions/gitpod-shared/src/features.ts index b62a9a67f8a0e..77227ece697d1 100644 --- a/extensions/gitpod-shared/src/features.ts +++ b/extensions/gitpod-shared/src/features.ts @@ -35,6 +35,7 @@ import WebSocket = require('ws'); import { BaseGitpodAnalyticsEventPropeties, GitpodAnalyticsEvent } from './analytics'; import * as uuid from 'uuid'; import { RemoteTrackMessage } from '@gitpod/gitpod-protocol/lib/analytics'; +import Log from './common/logger'; export class SupervisorConnection { readonly deadlines = { @@ -94,7 +95,7 @@ export class GitpodExtensionContext implements vscode.ExtensionContext { readonly user: Promise, readonly instanceListener: Promise, readonly workspaceOwned: Promise, - readonly output: vscode.OutputChannel, + readonly logger: Log, readonly ipcHookCli: string | undefined ) { this.workspaceContextUrl = vscode.Uri.parse(info.getWorkspaceContextUrl()); @@ -168,7 +169,7 @@ export class GitpodExtensionContext implements vscode.ExtensionContext { await Promise.allSettled(this.pendingWillCloseSocket.map(f => f())); webSocket.close(); } catch (e) { - this.output.appendLine('failed to dispose context: ' + e); + this.logger.error('failed to dispose context:', e); console.error('failed to dispose context:', e); } })(); @@ -193,20 +194,20 @@ export class GitpodExtensionContext implements vscode.ExtensionContext { } }; if (this.devMode && vscode.env.uiKind === vscode.UIKind.Web) { - this.output.appendLine(`ANALYTICS: ${JSON.stringify(msg)} `); + this.logger.trace(`ANALYTICS: ${JSON.stringify(msg)} `); return Promise.resolve(); } try { await this.gitpod.server.trackEvent(msg); } catch (e) { - this.output.appendLine('failed to track event: ' + e); + this.logger.error('failed to track event:', e); console.error('failed to track event:', e); } } } export async function createGitpodExtensionContext(context: vscode.ExtensionContext): Promise { - const output = vscode.window.createOutputChannel('Gitpod Workspace'); + const logger = new Log('Gitpod Workspace'); const devMode = context.extensionMode === vscode.ExtensionMode.Development || !!process.env['VSCODE_DEV']; const supervisor = new SupervisorConnection(context); @@ -222,7 +223,7 @@ export async function createGitpodExtensionContext(context: vscode.ExtensionCont contentAvailable = result.getAvailable(); } catch (e) { if (e.code === grpc.status.UNAVAILABLE) { - output.appendLine('It does not look like we are running in a Gitpod workspace, supervisor is not available.'); + logger.info('It does not look like we are running in a Gitpod workspace, supervisor is not available.'); return undefined; } console.error('cannot maintain connection to supervisor', e); @@ -308,7 +309,7 @@ export async function createGitpodExtensionContext(context: vscode.ExtensionCont return workspaceOwned; })(); - const ipcHookCli = installCLIProxy(context, output); + const ipcHookCli = installCLIProxy(context, logger); const config = await import('./gitpod-plugin-model'); return new GitpodExtensionContext( @@ -324,7 +325,7 @@ export async function createGitpodExtensionContext(context: vscode.ExtensionCont pendingGetUser, pendingInstanceListener, pendingWorkspaceOwned, - output, + logger, ipcHookCli ); } @@ -722,7 +723,7 @@ export function registerDefaultLayout(context: GitpodExtensionContext): void { } } -function installCLIProxy(context: vscode.ExtensionContext, output: vscode.OutputChannel): string | undefined { +function installCLIProxy(context: vscode.ExtensionContext, logger: Log): string | undefined { const vscodeIpcHookCli = process.env['VSCODE_IPC_HOOK_CLI']; if (!vscodeIpcHookCli) { return undefined; @@ -772,7 +773,7 @@ function installCLIProxy(context: vscode.ExtensionContext, output: vscode.Output fs.promises.unlink(ipcHookCli) )); }).catch(e => { - output.appendLine('failed to start cli proxy: ' + e); + logger.error('failed to start cli proxy: ' + e); console.error('failed to start cli proxy:' + e); }); @@ -815,6 +816,7 @@ export async function registerTasks(context: GitpodExtensionContext, createTermi }); } catch (err) { if (!('code' in err && err.code === grpc.status.CANCELLED)) { + context.logger.error('code server: listening task updates failed:', err); console.error('code server: listening task updates failed:', err); } } finally { @@ -824,6 +826,11 @@ export async function registerTasks(context: GitpodExtensionContext, createTermi await new Promise(resolve => setTimeout(resolve, 1000)); } } + context.logger.trace('Task status:', [...tasks.values()].map(status => { + const stateMap = { [TaskState.OPENING]: 'CLOSED', [TaskState.RUNNING]: 'RUNNING', [TaskState.CLOSED]: 'CLOSED' }; + return `\t${status.getTerminal()} => ${stateMap[status.getState()]}`; + }).join('\n')); + if (token.isCancellationRequested) { return; } @@ -837,6 +844,7 @@ export async function registerTasks(context: GitpodExtensionContext, createTermi taskTerminals.set(term.getAlias(), term); } } catch (e) { + context.logger.error('failed to list task terminals:', e); console.error('failed to list task terminals:', e); } @@ -922,6 +930,7 @@ function createTaskPty(alias: string, context: GitpodExtensionContext, contextTo } catch (e) { notFound = 'code' in e && e.code === grpc.status.NOT_FOUND; if (!token.isCancellationRequested && !notFound && !('code' in e && e.code === grpc.status.CANCELLED)) { + context.logger.error(`${alias} terminal: listening failed:`, e); console.error(`${alias} terminal: listening failed:`, e); } } finally { @@ -931,9 +940,16 @@ function createTaskPty(alias: string, context: GitpodExtensionContext, contextTo return; } if (notFound) { + context.logger.trace(`${alias} terminal not found`); onDidCloseEmitter.fire(); - } else if (typeof exitCode === 'number') { + tokenSource.cancel(); + return; + } + if (typeof exitCode === 'number') { + context.logger.trace(`${alias} terminal exited with ${exitCode}`); onDidCloseEmitter.fire(exitCode); + tokenSource.cancel(); + return; } await new Promise(resolve => setTimeout(resolve, 2000)); } @@ -958,10 +974,12 @@ function createTaskPty(alias: string, context: GitpodExtensionContext, contextTo await util.promisify(context.supervisor.terminal.shutdown.bind(context.supervisor.terminal, request, context.supervisor.metadata, { deadline: Date.now() + context.supervisor.deadlines.short }))(); + context.logger.trace(`${alias} terminal closed`); } catch (e) { if (e && e.code === grpc.status.NOT_FOUND) { // Swallow, the pty has already been killed } else { + context.logger.error(`${alias} terminal: shutdown failed:`, e); console.error(`${alias} terminal: shutdown failed:`, e); } } @@ -985,6 +1003,7 @@ function createTaskPty(alias: string, context: GitpodExtensionContext, contextTo }))(); } catch (e) { if (e && e.code !== grpc.status.NOT_FOUND) { + context.logger.error(`${alias} terminal: write failed:`, e); console.error(`${alias} terminal: write failed:`, e); } } @@ -1012,6 +1031,7 @@ function createTaskPty(alias: string, context: GitpodExtensionContext, contextTo }))(); } catch (e) { if (e && e.code !== grpc.status.NOT_FOUND) { + context.logger.error(`${alias} terminal: resize failed:`, e); console.error(`${alias} terminal: resize failed:`, e); } } @@ -1066,7 +1086,7 @@ async function updateIpcHookCli(context: GitpodExtensionContext): Promise req.end(); }); } catch (e) { - context.output.appendLine('Failed to update gitpod ipc hook cli: ' + e); + context.logger.error('Failed to update gitpod ipc hook cli:', e); console.error('Failed to update gitpod ipc hook cli:', e); } } diff --git a/extensions/gitpod-web/package.json b/extensions/gitpod-web/package.json index 8aa1aff937a10..7514290873b57 100644 --- a/extensions/gitpod-web/package.json +++ b/extensions/gitpod-web/package.json @@ -505,6 +505,6 @@ "vscode-jsonrpc": "^5.0.1", "vscode-nls": "^5.0.0", "yauzl": "^2.9.2", - "yazl": "^2.4.3" + "yazl": "^2.5.1" } } diff --git a/extensions/gitpod-web/src/extension.ts b/extensions/gitpod-web/src/extension.ts index 76fd02d5de750..fdd59435430eb 100644 --- a/extensions/gitpod-web/src/extension.ts +++ b/extensions/gitpod-web/src/extension.ts @@ -32,8 +32,6 @@ export async function activate(context: vscode.ExtensionContext) { return; } - logContextInfo(gitpodContext); - registerDesktop(); registerAuth(gitpodContext); registerPorts(gitpodContext); @@ -67,14 +65,6 @@ export function deactivate() { return gitpodContext.dispose(); } -function logContextInfo(context: GitpodExtensionContext) { - const output = context.output; - - output.appendLine(`GITPOD_WORKSPACE_CONTEXT_URL: ${context.info.getWorkspaceContextUrl()}`); - output.appendLine(`GITPOD_INSTANCE_ID: ${context.info.getInstanceId()}`); - output.appendLine(`GITPOD_WORKSPACE_URL: ${context.info.getWorkspaceUrl()}`); -} - export function registerAuth(context: GitpodExtensionContext): void { type Keytar = { getPassword: typeof keytarType['getPassword']; @@ -515,6 +505,7 @@ export function registerPorts(context: GitpodExtensionContext): void { }); } catch (err) { if (!('code' in err && err.code === grpc.status.CANCELLED)) { + context.logger.error('cannot maintain connection to supervisor', err); console.error('cannot maintain connection to supervisor', err); } } finally { diff --git a/extensions/gitpod/package.json b/extensions/gitpod/package.json index 02c2127b7ac07..33b0e61283f59 100644 --- a/extensions/gitpod/package.json +++ b/extensions/gitpod/package.json @@ -31,6 +31,15 @@ "*", "onCommand:gitpod.api.autoTunnel" ], + "contributes": { + "commands": [ + { + "command": "gitpod.collectLogs", + "category": "Gitpod", + "title": "Collect all logs" + } + ] + }, "main": "./out/extension.js", "scripts": { "compile": "gulp compile-extension:gitpod", @@ -40,14 +49,16 @@ "devDependencies": { "@types/node": "16.x", "@types/node-fetch": "^2.5.12", - "@types/tmp": "^0.2.1" + "@types/tmp": "^0.2.1", + "@types/yazl": "^2.4.2" }, "dependencies": { "@gitpod/local-app-api-grpcweb": "main", "@improbable-eng/grpc-web-node-http-transport": "^0.14.0", "node-fetch": "2.6.7", "tmp": "^0.2.1", - "vscode-nls": "^5.0.0" + "vscode-nls": "^5.0.0", + "yazl": "^2.5.1" }, "extensionDependencies": [ "ms-vscode-remote.remote-ssh" diff --git a/extensions/gitpod/src/collectLogs.ts b/extensions/gitpod/src/collectLogs.ts new file mode 100644 index 0000000000000..9db8971d104c5 --- /dev/null +++ b/extensions/gitpod/src/collectLogs.ts @@ -0,0 +1,102 @@ +import * as path from 'path'; +import * as yazl from 'yazl'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as vscode from 'vscode'; + +interface IFile { + path: string; + contents: Buffer | string; +} + +function zip(zipPath: string, files: IFile[]): Promise { + return new Promise((c, e) => { + const zip = new yazl.ZipFile(); + files.forEach(f => { + if (f.contents) { + zip.addBuffer(typeof f.contents === 'string' ? Buffer.from(f.contents, 'utf8') : f.contents, f.path); + } + }); + zip.end(); + + const zipStream = fs.createWriteStream(zipPath); + zip.outputStream.pipe(zipStream); + + zip.outputStream.once('error', e); + zipStream.once('error', e); + zipStream.once('finish', () => c(zipPath)); + }); +} + +async function traverseFolder(folderUri: vscode.Uri, files: IFile[], token: vscode.CancellationToken) { + if (token.isCancellationRequested) { + return; + } + + const children = await vscode.workspace.fs.readDirectory(folderUri); + for (const [name, type] of children) { + if (token.isCancellationRequested) { + return; + } + + if (type === vscode.FileType.File) { + const filePath = path.posix.join(folderUri.path, name); + const fileContent = await vscode.workspace.fs.readFile(folderUri.with({ path: filePath })); + if (fileContent.byteLength > 0) { + files.push({ + path: filePath, + contents: Buffer.from(fileContent) + }); + } + } else if (type === vscode.FileType.Directory) { + const folderPath = path.posix.join(folderUri.path, name); + await traverseFolder(folderUri.with({ path: folderPath }), files, token); + } + } +} + +export async function collectLogs(context: vscode.ExtensionContext) { + const saveUri = await vscode.window.showSaveDialog({ + title: 'Choose save location ...', + defaultUri: vscode.Uri.file(path.posix.join(os.homedir(), `vscode-desktop-logs-${new Date().toISOString().replace(/-|:|\.\d+Z$/g, '')}.zip`)), + }); + if (!saveUri) { + return; + } + + let extRemoteLogsUri: vscode.Uri | undefined; + try { + // Invoke command from gitpot-remote extension + extRemoteLogsUri = await vscode.commands.executeCommand('__gitpod.getGitpodRemoteLogsUri'); + } catch { + // Ignore if not found + } + const extLocalLogsUri = context.logUri; + + const remoteLogsUri = extRemoteLogsUri?.with({ path: path.posix.dirname(path.posix.dirname(extRemoteLogsUri.path)) }); + const localLogsUri = extLocalLogsUri.with({ path: path.posix.dirname(path.posix.dirname(extLocalLogsUri.path)) }); + + return vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: 'Exporting logs to zip file ...', + cancellable: true + }, async (_, token) => { + const remoteLogFiles: IFile[] = []; + if (remoteLogsUri) { + await traverseFolder(remoteLogsUri, remoteLogFiles, token); + remoteLogFiles.forEach(file => file.path = path.posix.join('./remote', path.posix.relative(remoteLogsUri.path, file.path))); + if (token.isCancellationRequested) { + return; + } + } + + const localLogFiles: IFile[] = []; + await traverseFolder(localLogsUri, localLogFiles, token); + localLogFiles.forEach(file => file.path = path.posix.join('./local', path.posix.relative(localLogsUri.path, file.path))); + if (token.isCancellationRequested) { + return; + } + + return zip(saveUri.fsPath, remoteLogFiles.concat(localLogFiles)); + }); +} diff --git a/extensions/gitpod/src/extension.ts b/extensions/gitpod/src/extension.ts index 9aaa44b635222..9485573ab3db6 100644 --- a/extensions/gitpod/src/extension.ts +++ b/extensions/gitpod/src/extension.ts @@ -16,6 +16,7 @@ import * as tmp from 'tmp'; import * as path from 'path'; import * as vscode from 'vscode'; import { grpc } from '@improbable-eng/grpc-web'; +import { collectLogs } from './collectLogs'; interface SSHConnectionParams { workspaceId: string; @@ -434,6 +435,17 @@ export async function activate(context: vscode.ExtensionContext) { } })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.collectLogs', async () => { + try { + await collectLogs(context); + } catch (e) { + const outputMessage = `Error collecting logs: ${e}`; + vscode.window.showErrorMessage(outputMessage); + log(outputMessage); + // logger.error(outputMessage); + } + })); + if (vscode.env.remoteName === undefined || context.extension.extensionKind !== vscode.ExtensionKind.UI) { return; } diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 2bd7b3bf2f351..d3f6c5768ac1d 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -1046,7 +1046,7 @@ yauzl@^2.9.2: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yazl@^2.4.3: +yazl@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35" integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== diff --git a/src/vs/gitpod/browser/workbench/contrib/exportLogs.contribution.ts b/src/vs/gitpod/browser/workbench/contrib/exportLogs.contribution.ts index 703012cd466ec..b0f4e3bd37e44 100644 --- a/src/vs/gitpod/browser/workbench/contrib/exportLogs.contribution.ts +++ b/src/vs/gitpod/browser/workbench/contrib/exportLogs.contribution.ts @@ -151,7 +151,7 @@ registerAction2(class ExportLogsAction extends Action2 { ); if (bufferData) { - triggerDownload(bufferData, 'vscodeLogs.zip'); + triggerDownload(bufferData, `vscode-web-logs-${new Date().toISOString().replace(/-|:|\.\d+Z$/g, '')}.zip`); } } });