Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
sandy081 authored Feb 2, 2023
1 parent b3ef59e commit d37a079
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 34 deletions.
4 changes: 4 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ function configureCommandlineSwitchesSync(cliArgs) {
case 'log-level':
if (typeof argvValue === 'string') {
process.argv.push('--log', argvValue);
} else if (Array.isArray(argvValue)) {
for (const value of argvValue) {
process.argv.push('--log', value);
}
}
break;
}
Expand Down
148 changes: 148 additions & 0 deletions src/vs/workbench/contrib/logs/common/defaultLogLevels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ILogService, ILoggerService, LogLevel, LogLevelToString, getLogLevel, parseLogLevel } from 'vs/platform/log/common/log';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IFileService } from 'vs/platform/files/common/files';
import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing';
import { isString, isUndefined } from 'vs/base/common/types';
import { EXTENSION_IDENTIFIER_WITH_LOG_REGEX } from 'vs/platform/environment/common/environmentService';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { parse } from 'vs/base/common/json';

interface ParsedArgvLogLevels {
default?: LogLevel;
extensions?: [string, LogLevel][];
}

export type DefaultLogLevels = Required<Readonly<ParsedArgvLogLevels>>;

export const IDefaultLogLevelsService = createDecorator<IDefaultLogLevelsService>('IDefaultLogLevelsService');

export interface IDefaultLogLevelsService {

readonly _serviceBrand: undefined;

getDefaultLogLevels(): Promise<DefaultLogLevels>;

setDefaultLogLevel(logLevel: LogLevel, extensionId?: string): Promise<void>;
}

class DefaultLogLevelsService implements IDefaultLogLevelsService {

_serviceBrand: undefined;

constructor(
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IFileService private readonly fileService: IFileService,
@IJSONEditingService private readonly jsonEditingService: IJSONEditingService,
@ILogService private readonly logService: ILogService,
@ILoggerService private readonly loggerService: ILoggerService,
) {
}

async getDefaultLogLevels(): Promise<DefaultLogLevels> {
const argvLogLevel = await this._parseLogLevelsFromArgv();
return {
default: argvLogLevel?.default ?? this._getDefaultLogLevelFromEnv(),
extensions: argvLogLevel?.extensions ?? this._getExtensionsDefaultLogLevelsFromEnv()
};
}

async setDefaultLogLevel(defaultLogLevel: LogLevel, extensionId?: string): Promise<void> {
const argvLogLevel = await this._parseLogLevelsFromArgv() ?? {};
if (extensionId) {
extensionId = extensionId.toLowerCase();
const argvLogLevel = await this._parseLogLevelsFromArgv() ?? {};
const currentDefaultLogLevel = this._getDefaultLogLevel(argvLogLevel, extensionId);
argvLogLevel.extensions = argvLogLevel.extensions ?? [];
const extension = argvLogLevel.extensions.find(([extension]) => extension === extensionId);
if (extension) {
extension[1] = defaultLogLevel;
} else {
argvLogLevel.extensions.push([extensionId, defaultLogLevel]);
}
await this._writeLogLevelsToArgv(argvLogLevel);
const extensionLoggers = [...this.loggerService.getRegisteredLoggers()].filter(logger => logger.extensionId && logger.extensionId.toLowerCase() === extensionId);
for (const { resource } of extensionLoggers) {
if (this.loggerService.getLogLevel(resource) === currentDefaultLogLevel) {
this.loggerService.setLogLevel(resource, defaultLogLevel);
}
}
} else {
const currentLogLevel = this._getDefaultLogLevel(argvLogLevel);
argvLogLevel.default = defaultLogLevel;
await this._writeLogLevelsToArgv(argvLogLevel);
if (this.loggerService.getLogLevel() === currentLogLevel) {
this.loggerService.setLogLevel(defaultLogLevel);
}
}
}

private _getDefaultLogLevel(argvLogLevels: ParsedArgvLogLevels, extension?: string): LogLevel {
if (extension) {
const extensionLogLevel = argvLogLevels.extensions?.find(([extensionId]) => extensionId === extension);
if (extensionLogLevel) {
return extensionLogLevel[1];
}
}
return argvLogLevels.default ?? getLogLevel(this.environmentService);
}

private async _writeLogLevelsToArgv(logLevels: ParsedArgvLogLevels): Promise<void> {
const logLevelsValue: string[] = [];
if (logLevels.default) {
logLevelsValue.push(LogLevelToString(logLevels.default));
}
for (const [extension, logLevel] of logLevels.extensions ?? []) {
logLevelsValue.push(`${extension}:${LogLevelToString(logLevel)}`);
}
await this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['log-level'], value: logLevelsValue.length ? logLevelsValue : undefined }], true);
}

private async _parseLogLevelsFromArgv(): Promise<ParsedArgvLogLevels | undefined> {
const result: ParsedArgvLogLevels = { extensions: [] };
try {
const content = await this.fileService.readFile(this.environmentService.argvResource);
const argv: { 'log-level'?: string | string[] } = parse(content.value.toString());
const logLevels = isString(argv['log-level']) ? [argv['log-level']] : Array.isArray(argv['log-level']) ? argv['log-level'] : [];
for (const extensionLogLevel of logLevels) {
const matches = EXTENSION_IDENTIFIER_WITH_LOG_REGEX.exec(extensionLogLevel);
if (matches && matches[1] && matches[2]) {
const logLevel = parseLogLevel(matches[2]);
if (!isUndefined(logLevel)) {
result.extensions?.push([matches[1].toLowerCase(), logLevel]);
}
} else {
const logLevel = parseLogLevel(extensionLogLevel);
if (!isUndefined(logLevel)) {
result.default = logLevel;
}
}
}
} catch (error) {
this.logService.error(error);
}
return !isUndefined(result.default) || result.extensions?.length ? result : undefined;
}

private _getDefaultLogLevelFromEnv(): LogLevel {
return getLogLevel(this.environmentService);
}

private _getExtensionsDefaultLogLevelsFromEnv(): [string, LogLevel][] {
const result: [string, LogLevel][] = [];
for (const [extension, logLevelValue] of this.environmentService.extensionLogLevel ?? []) {
const logLevel = parseLogLevel(logLevelValue);
if (!isUndefined(logLevel)) {
result.push([extension, logLevel]);
}
}
return result;
}
}

registerSingleton(IDefaultLogLevelsService, DefaultLogLevelsService, InstantiationType.Delayed);
16 changes: 15 additions & 1 deletion src/vs/workbench/contrib/logs/common/logs.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as
import { IFileService, whenProviderRegistered } from 'vs/platform/files/common/files';
import { IOutputChannelRegistry, IOutputService, Extensions } from 'vs/workbench/services/output/common/output';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { ILogService, ILoggerResource, ILoggerService } from 'vs/platform/log/common/log';
import { ILogService, ILoggerResource, ILoggerService, LogLevel } from 'vs/platform/log/common/log';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { rendererLogId, showWindowLogActionId } from 'vs/workbench/common/logConstants';
import { createCancelablePromise, timeout } from 'vs/base/common/async';
import { CancellationError, getErrorMessage, isCancellationError } from 'vs/base/common/errors';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IDefaultLogLevelsService } from 'vs/workbench/contrib/logs/common/defaultLogLevels';

registerAction2(class extends Action2 {
constructor() {
Expand All @@ -35,6 +36,19 @@ registerAction2(class extends Action2 {
}
});

registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.action.setDefaultLogLevel',
title: { value: nls.localize('setDefaultLogLevel', "Set Default Log Level"), original: 'Set Default Log Level' },
category: Categories.Developer,
});
}
run(servicesAccessor: ServicesAccessor, logLevel: LogLevel, extensionId?: string): Promise<void> {
return servicesAccessor.get(IDefaultLogLevelsService).setDefaultLogLevel(logLevel, extensionId);
}
});

class LogOutputChannels extends Disposable implements IWorkbenchContribution {

constructor(
Expand Down
96 changes: 64 additions & 32 deletions src/vs/workbench/contrib/logs/common/logsActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@

import * as nls from 'vs/nls';
import { Action } from 'vs/base/common/actions';
import { ILoggerService, LogLevel, getLogLevel, isLogLevel, parseLogLevel } from 'vs/platform/log/common/log';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { ILoggerService, LogLevel, isLogLevel } from 'vs/platform/log/common/log';
import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { dirname, basename, isEqual } from 'vs/base/common/resources';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IOutputService } from 'vs/workbench/services/output/common/output';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { extensionTelemetryLogChannelId, telemetryLogChannelId } from 'vs/platform/telemetry/common/telemetryUtils';
import { IDefaultLogLevelsService } from 'vs/workbench/contrib/logs/common/defaultLogLevels';
import { Codicon } from 'vs/base/common/codicons';
import { ThemeIcon } from 'vs/base/common/themables';
import { DisposableStore } from 'vs/base/common/lifecycle';

type LogLevelQuickPickItem = IQuickPickItem & { level: LogLevel };
type LogChannelQuickPickItem = IQuickPickItem & { id: string; resource: URI; extensionId?: string };
Expand All @@ -29,7 +31,7 @@ export class SetLogLevelAction extends Action {
@IQuickInputService private readonly quickInputService: IQuickInputService,
@ILoggerService private readonly loggerService: ILoggerService,
@IOutputService private readonly outputService: IOutputService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IDefaultLogLevelsService private readonly defaultLogLevelsService: IDefaultLogLevelsService,
) {
super(id, label);
}
Expand All @@ -46,6 +48,7 @@ export class SetLogLevelAction extends Action {
}

private async selectLogLevelOrChannel(): Promise<LogChannelQuickPickItem | LogLevel | null> {
const defaultLogLevels = await this.defaultLogLevelsService.getDefaultLogLevels();
const extensionLogs: LogChannelQuickPickItem[] = [], logs: LogChannelQuickPickItem[] = [];
const logLevel = this.loggerService.getLogLevel();
for (const channel of this.outputService.getChannelDescriptors()) {
Expand All @@ -62,40 +65,79 @@ export class SetLogLevelAction extends Action {
}
const entries: (LogLevelQuickPickItem | LogChannelQuickPickItem | IQuickPickSeparator)[] = [];
entries.push({ type: 'separator', label: nls.localize('all', "All") });
entries.push(...this.getLogLevelEntries(this.getDefaultLogLevel(), this.loggerService.getLogLevel()));
entries.push(...this.getLogLevelEntries(defaultLogLevels.default, this.loggerService.getLogLevel()));
entries.push({ type: 'separator', label: nls.localize('loggers', "Logs") });
entries.push(...logs.sort((a, b) => a.label.localeCompare(b.label)));
if (extensionLogs.length && logs.length) {
entries.push({ type: 'separator', label: nls.localize('extensionLogs', "Extension Logs") });
}
entries.push(...extensionLogs.sort((a, b) => a.label.localeCompare(b.label)));
const entry = await this.quickInputService.pick(entries, { placeHolder: nls.localize('selectlog', "Set Log Level") });
return entry
? (<LogLevelQuickPickItem>entry).level ?? <LogChannelQuickPickItem>entry
: null;

return new Promise((resolve, reject) => {
const disposables = new DisposableStore();
const quickPick = this.quickInputService.createQuickPick();
quickPick.placeholder = nls.localize('selectlog', "Set Log Level");
quickPick.items = entries;
let selectedItem: IQuickPickItem | undefined;
disposables.add(quickPick.onDidTriggerItemButton(e => {
quickPick.hide();
this.defaultLogLevelsService.setDefaultLogLevel((<LogLevelQuickPickItem>e.item).level);
}));
disposables.add(quickPick.onDidAccept(e => {
selectedItem = quickPick.selectedItems[0];
quickPick.hide();
}));
disposables.add(quickPick.onDidHide(() => {
const result = selectedItem ? (<LogLevelQuickPickItem>selectedItem).level ?? <LogChannelQuickPickItem>selectedItem : null;
disposables.dispose();
resolve(result);
}));
quickPick.show();
});
}

private async setLogLevelForChannel(logChannel: LogChannelQuickPickItem): Promise<void> {
const defaultLogLevel = this.getDefaultLogLevel(logChannel.extensionId);
const defaultLogLevels = await this.defaultLogLevelsService.getDefaultLogLevels();
const defaultLogLevel = defaultLogLevels.extensions.find(e => e[0] === logChannel.extensionId?.toLowerCase())?.[1] ?? defaultLogLevels.default;
const currentLogLevel = this.loggerService.getLogLevel(logChannel.resource) ?? defaultLogLevel;
const entries = this.getLogLevelEntries(defaultLogLevel, currentLogLevel);

const entry = await this.quickInputService.pick(entries, { placeHolder: logChannel ? nls.localize('selectLogLevelFor', " {0}: Select log level", logChannel?.label) : nls.localize('selectLogLevel', "Select log level"), activeItem: entries[this.loggerService.getLogLevel()] });
if (entry) {
this.loggerService.setLogLevel(logChannel.resource, entry.level);
}
return new Promise((resolve, reject) => {
const disposables = new DisposableStore();
const quickPick = this.quickInputService.createQuickPick();
quickPick.placeholder = logChannel ? nls.localize('selectLogLevelFor', " {0}: Select log level", logChannel?.label) : nls.localize('selectLogLevel', "Select log level");
quickPick.items = entries;
quickPick.activeItems = [entries[this.loggerService.getLogLevel()]];
let selectedItem: LogLevelQuickPickItem | undefined;
disposables.add(quickPick.onDidTriggerItemButton(e => {
quickPick.hide();
this.defaultLogLevelsService.setDefaultLogLevel((<LogLevelQuickPickItem>e.item).level, logChannel.extensionId);
}));
disposables.add(quickPick.onDidAccept(e => {
selectedItem = quickPick.selectedItems[0] as LogLevelQuickPickItem;
quickPick.hide();
}));
disposables.add(quickPick.onDidHide(() => {
if (selectedItem) {
this.loggerService.setLogLevel(logChannel.resource, selectedItem.level);
}
disposables.dispose();
resolve();
}));
quickPick.show();
});
}

private getLogLevelEntries(defaultLogLevel: LogLevel, currentLogLevel: LogLevel): LogLevelQuickPickItem[] {
const button: IQuickInputButton = { iconClass: ThemeIcon.asClassName(Codicon.arrowSwap), tooltip: nls.localize('resetLogLevel', "Set as Default Log Level") };
return [
{ label: this.getLabel(LogLevel.Trace, currentLogLevel), level: LogLevel.Trace, description: this.getDescription(LogLevel.Trace, defaultLogLevel) },
{ label: this.getLabel(LogLevel.Debug, currentLogLevel), level: LogLevel.Debug, description: this.getDescription(LogLevel.Debug, defaultLogLevel) },
{ label: this.getLabel(LogLevel.Info, currentLogLevel), level: LogLevel.Info, description: this.getDescription(LogLevel.Info, defaultLogLevel) },
{ label: this.getLabel(LogLevel.Warning, currentLogLevel), level: LogLevel.Warning, description: this.getDescription(LogLevel.Warning, defaultLogLevel) },
{ label: this.getLabel(LogLevel.Error, currentLogLevel), level: LogLevel.Error, description: this.getDescription(LogLevel.Error, defaultLogLevel) },
{ label: this.getLabel(LogLevel.Off, currentLogLevel), level: LogLevel.Off, description: this.getDescription(LogLevel.Off, defaultLogLevel) },
{ label: this.getLabel(LogLevel.Trace, currentLogLevel), level: LogLevel.Trace, description: this.getDescription(LogLevel.Trace, defaultLogLevel), buttons: defaultLogLevel !== LogLevel.Trace ? [button] : undefined },
{ label: this.getLabel(LogLevel.Debug, currentLogLevel), level: LogLevel.Debug, description: this.getDescription(LogLevel.Debug, defaultLogLevel), buttons: defaultLogLevel !== LogLevel.Debug ? [button] : undefined },
{ label: this.getLabel(LogLevel.Info, currentLogLevel), level: LogLevel.Info, description: this.getDescription(LogLevel.Info, defaultLogLevel), buttons: defaultLogLevel !== LogLevel.Info ? [button] : undefined },
{ label: this.getLabel(LogLevel.Warning, currentLogLevel), level: LogLevel.Warning, description: this.getDescription(LogLevel.Warning, defaultLogLevel), buttons: defaultLogLevel !== LogLevel.Warning ? [button] : undefined },
{ label: this.getLabel(LogLevel.Error, currentLogLevel), level: LogLevel.Error, description: this.getDescription(LogLevel.Error, defaultLogLevel), buttons: defaultLogLevel !== LogLevel.Error ? [button] : undefined },
{ label: this.getLabel(LogLevel.Off, currentLogLevel), level: LogLevel.Off, description: this.getDescription(LogLevel.Off, defaultLogLevel), buttons: defaultLogLevel !== LogLevel.Off ? [button] : undefined },
];

}

private getLabel(level: LogLevel, current?: LogLevel): string {
Expand All @@ -115,16 +157,6 @@ export class SetLogLevelAction extends Action {
return defaultLogLevel === level ? nls.localize('default', "Default") : undefined;
}

private getDefaultLogLevel(extensionId?: string): LogLevel {
let logLevel: LogLevel | undefined;
if (extensionId) {
const logLevelValue = this.environmentService.extensionLogLevel?.find(([id]) => areSameExtensions({ id }, { id: extensionId }))?.[1];
if (logLevelValue) {
logLevel = parseLogLevel(logLevelValue);
}
}
return logLevel ?? getLogLevel(this.environmentService);
}
}

export class OpenWindowSessionLogFileAction extends Action {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/electron-sandbox/desktop.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ import { applicationConfigurationNodeBase } from 'vs/workbench/common/configurat
}
},
'log-level': {
type: 'string',
type: ['string', 'array'],
description: localize('argv.logLevel', "Log level to use. Default is 'info'. Allowed values are 'error', 'warn', 'info', 'debug', 'trace', 'off'.")
}
}
Expand Down

0 comments on commit d37a079

Please sign in to comment.