Skip to content

Commit

Permalink
Improve compund logs #237829 (#237922)
Browse files Browse the repository at this point in the history
  • Loading branch information
sandy081 authored Jan 14, 2025
1 parent ac721a2 commit a4262a0
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 129 deletions.
59 changes: 56 additions & 3 deletions src/vs/workbench/contrib/output/browser/output.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ import { localize, localize2 } from '../../../../nls.js';
import { viewFilterSubmenu } from '../../../browser/parts/views/viewFilter.js';
import { ViewAction } from '../../../browser/parts/views/viewPane.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { basename } from '../../../../base/common/resources.js';

const IMPORTED_LOG_ID_PREFIX = 'importedLog.';

// Register Service
registerSingleton(IOutputService, OutputService, InstantiationType.Delayed);
Expand Down Expand Up @@ -114,6 +118,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution {
this.registerConfigureActiveOutputLogLevelAction();
this.registerFilterActions();
this.registerExportLogsAction();
this.registerImportLogAction();
}

private registerSwitchOutputAction(): void {
Expand Down Expand Up @@ -144,7 +149,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution {
const registerOutputChannels = (channels: IOutputChannelDescriptor[]) => {
for (const channel of channels) {
const title = channel.label;
const group = channel.files && channel.files.length > 1 ? '2_compound_logs' : channel.extensionId ? '0_ext_outputchannels' : '1_core_outputchannels';
const group = (channel.files?.length && channel.files.length > 1) || channel.id.startsWith(IMPORTED_LOG_ID_PREFIX) ? '2_custom_logs' : channel.extensionId ? '0_ext_outputchannels' : '1_core_outputchannels';
registeredChannels.set(channel.id, registerAction2(class extends Action2 {
constructor() {
super({
Expand Down Expand Up @@ -186,6 +191,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution {
menu: [{
id: MenuId.ViewTitle,
when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID),
group: '2_add',
}],
});
}
Expand Down Expand Up @@ -405,7 +411,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution {
menu: [{
id: MenuId.ViewTitle,
when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID),
group: 'export',
group: '1_export',
order: 1
}],
});
Expand Down Expand Up @@ -711,7 +717,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution {
menu: [{
id: MenuId.ViewTitle,
when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID),
group: 'export',
group: '1_export',
order: 2,
}],
});
Expand Down Expand Up @@ -747,6 +753,53 @@ class OutputContribution extends Disposable implements IWorkbenchContribution {
}
}));
}

private registerImportLogAction(): void {
this._register(registerAction2(class extends Action2 {
constructor() {
super({
id: `workbench.action.importLog`,
title: nls.localize2('importLog', "Import Log..."),
f1: true,
category: Categories.Developer,
menu: [{
id: MenuId.ViewTitle,
when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID),
group: '2_add',
order: 2,
}],
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const outputService = accessor.get(IOutputService);
const fileDialogService = accessor.get(IFileDialogService);
const result = await fileDialogService.showOpenDialog({
title: nls.localize('importLogFile', "Import Log File"),
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: true,
filters: [{
name: nls.localize('logFiles', "Log Files"),
extensions: ['log']
}]
});

if (result?.length) {
const channelName = basename(result[0]);
const channelId = `${IMPORTED_LOG_ID_PREFIX}${Date.now()}`;
// Register and show the channel
Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel({
id: channelId,
label: channelName,
log: true,
files: result,
fileNames: result.map(r => basename(r).split('.')[0])
});
outputService.showChannel(channelId);
}
}
}));
}
}

Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(OutputContribution, LifecyclePhase.Restored);
Expand Down
57 changes: 6 additions & 51 deletions src/vs/workbench/contrib/output/browser/outputView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common
import { IContextKeyService, IContextKey } from '../../../../platform/contextkey/common/contextkey.js';
import { IEditorOpenContext } from '../../../common/editor.js';
import { AbstractTextResourceEditor } from '../../../browser/parts/editor/textResourceEditor.js';
import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT, LOG_ENTRY_REGEX } from '../../../services/output/common/output.js';
import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT, LOG_ENTRY_REGEX, parseLogEntries, ILogEntry } from '../../../services/output/common/output.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
Expand Down Expand Up @@ -340,12 +340,6 @@ export class OutputEditor extends AbstractTextResourceEditor {

}


interface ILogEntry {
readonly logLevel: LogLevel;
readonly lineRange: [number, number];
}

export class FilterController extends Disposable implements IEditorContribution {

public static readonly ID = 'output.editor.contrib.filterController';
Expand Down Expand Up @@ -413,30 +407,8 @@ export class FilterController extends Disposable implements IEditorContribution
}

private computeLogEntriesIncremental(model: ITextModel, fromLine: number): void {
if (!this.logEntries) {
return;
}

const lineCount = model.getLineCount();
for (let lineNumber = fromLine; lineNumber <= lineCount; lineNumber++) {
const lineContent = model.getLineContent(lineNumber);
const match = LOG_ENTRY_REGEX.exec(lineContent);
if (match) {
const logLevel = this.parseLogLevel(match[3]);
const startLine = lineNumber;
let endLine = lineNumber;

while (endLine < lineCount) {
const nextLineContent = model.getLineContent(endLine + 1);
if (model.getLineFirstNonWhitespaceColumn(endLine + 1) === 0 || LOG_ENTRY_REGEX.test(nextLineContent)) {
break;
}
endLine++;
}

this.logEntries.push({ logLevel, lineRange: [startLine, endLine] });
lineNumber = endLine;
}
if (this.logEntries) {
this.logEntries = this.logEntries.concat(parseLogEntries(model, fromLine));
}
}

Expand All @@ -456,17 +428,17 @@ export class FilterController extends Disposable implements IEditorContribution
for (let i = from; i < this.logEntries.length; i++) {
const entry = this.logEntries[i];
if (hasLogLevelFilter && !this.shouldShowEntry(entry, filters)) {
this.hiddenAreas.push(new Range(entry.lineRange[0], 1, entry.lineRange[1], model.getLineMaxColumn(entry.lineRange[1])));
this.hiddenAreas.push(entry.range);
continue;
}
if (filters.text) {
const matches = model.findMatches(filters.text, new Range(entry.lineRange[0], 1, entry.lineRange[1], model.getLineLastNonWhitespaceColumn(entry.lineRange[1])), false, false, null, false);
const matches = model.findMatches(filters.text, entry.range, false, false, null, false);
if (matches.length) {
for (const match of matches) {
findMatchesDecorations.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION });
}
} else {
this.hiddenAreas.push(new Range(entry.lineRange[0], 1, entry.lineRange[1], model.getLineMaxColumn(entry.lineRange[1])));
this.hiddenAreas.push(entry.range);
}
}
}
Expand Down Expand Up @@ -509,21 +481,4 @@ export class FilterController extends Disposable implements IEditorContribution
}
return true;
}

private parseLogLevel(level: string): LogLevel {
switch (level.toLowerCase()) {
case 'trace':
return LogLevel.Trace;
case 'debug':
return LogLevel.Debug;
case 'info':
return LogLevel.Info;
case 'warning':
return LogLevel.Warning;
case 'error':
return LogLevel.Error;
default:
throw new Error(`Unknown log level: ${level}`);
}
}
}
106 changes: 31 additions & 75 deletions src/vs/workbench/contrib/output/common/outputChannelModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import { Range } from '../../../../editor/common/core/range.js';
import { VSBuffer } from '../../../../base/common/buffer.js';
import { ILogger, ILoggerService, ILogService } from '../../../../platform/log/common/log.js';
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
import { LOG_ENTRY_REGEX, OutputChannelUpdateMode } from '../../../services/output/common/output.js';
import { LOG_ENTRY_REGEX, LOG_MIME, OutputChannelUpdateMode, parseLogEntryAt } from '../../../services/output/common/output.js';
import { isCancellationError } from '../../../../base/common/errors.js';
import { binarySearch } from '../../../../base/common/arrays.js';
import { TextModel } from '../../../../editor/common/model/textModel.js';

export interface IOutputChannelModel extends IDisposable {
readonly onDispose: Event<void>;
Expand Down Expand Up @@ -168,6 +168,7 @@ class MultiFileContentProvider extends Disposable implements IContentProvider {

constructor(
filesInfos: IOutputChannelFileInfo[],
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IFileService fileService: IFileService,
@ILogService logService: ILogService,
) {
Expand All @@ -194,12 +195,37 @@ class MultiFileContentProvider extends Disposable implements IContentProvider {

async getContent(): Promise<{ readonly content: string; readonly consume: () => void }> {
const outputs = await Promise.all(this.fileOutputs.map(output => output.getContent()));
const content = combineLogEntries(outputs);
const content = this.combineLogEntries(outputs);
return {
content,
consume: () => outputs.forEach(({ consume }) => consume())
};
}

private combineLogEntries(outputs: { content: string; name: string }[]): string {

const logEntries: [number, string][] = [];

for (const { content, name } of outputs) {
const model = this.instantiationService.createInstance(TextModel, content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null);
for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber++) {
const logEntry = parseLogEntryAt(model, lineNumber);
if (!logEntry) {
continue;
}
lineNumber = logEntry.range.endLineNumber;
const content = model.getValueInRange(logEntry.range).replace(LOG_ENTRY_REGEX, `$1 [${name}] $2`);
logEntries.push([logEntry.timestamp, content]);
}
}

let result = '';
for (const [, content] of logEntries.sort((a, b) => a[0] - b[0])) {
result += content + '\n';
}
return result;
}

}

export abstract class AbstractFileOutputChannelModel extends Disposable implements IOutputChannelModel {
Expand Down Expand Up @@ -440,8 +466,9 @@ export class MultiFileOutputChannelModel extends AbstractFileOutputChannelModel
@IModelService modelService: IModelService,
@ILogService logService: ILogService,
@IEditorWorkerService editorWorkerService: IEditorWorkerService,
@IInstantiationService instantiationService: IInstantiationService,
) {
const multifileOutput = new MultiFileContentProvider(filesInfos, fileService, logService);
const multifileOutput = new MultiFileContentProvider(filesInfos, instantiationService, fileService, logService);
super(modelUri, language, multifileOutput, modelService, editorWorkerService);
this.multifileOutput = this._register(multifileOutput);
}
Expand Down Expand Up @@ -549,74 +576,3 @@ export class DelegatedOutputChannelModel extends Disposable implements IOutputCh
this.outputChannelModel.then(outputChannelModel => outputChannelModel.replace(value));
}
}

function combineLogEntries(outputs: { content: string; name: string }[]): string {
const timestampEntries: Date[] = [];
const combinedEntries: string[] = [];

let startTimestampOfLastOutput: Date | undefined;
let endTimestampOfLastOutput: Date | undefined;

for (const output of outputs) {
let startTimestamp: Date | undefined;
let timestamp: Date | undefined;
const logEntries = output.content.split('\n');
for (let index = 0; index < logEntries.length; index++) {
const entry = logEntries[index];
if (!entry.trim()) {
continue;
}
timestamp = new Date(entry.match(LOG_ENTRY_REGEX)?.[1]!);
if (!startTimestamp) {
startTimestamp = timestamp;
}
const entriesToAdd = [entry.replace(LOG_ENTRY_REGEX, `$1 [${output.name}] $2`)];
const timestampsToAdd = [timestamp];

if (startTimestampOfLastOutput && timestamp < startTimestampOfLastOutput) {
for (index = index + 1; index < logEntries.length; index++) {
const entry = logEntries[index];
if (!entry.trim()) {
continue;
}
timestamp = new Date(entry.match(LOG_ENTRY_REGEX)?.[1]!);
if (timestamp > startTimestampOfLastOutput) {
index--;
break;
}
entriesToAdd.push(entry.replace(LOG_ENTRY_REGEX, `$1 [${output.name}] $2`));
timestampsToAdd.push(timestamp);
}
combinedEntries.unshift(...entriesToAdd);
timestampEntries.unshift(...timestampsToAdd);
continue;
}

if (endTimestampOfLastOutput && timestamp > endTimestampOfLastOutput) {
for (index = index + 1; index < logEntries.length; index++) {
const entry = logEntries[index];
if (!entry.trim()) {
continue;
}
timestamp = new Date(entry.match(LOG_ENTRY_REGEX)?.[1]!);
entriesToAdd.push(entry.replace(LOG_ENTRY_REGEX, `$1 [${output.name}] $2`));
timestampsToAdd.push(timestamp);
}
combinedEntries.push(...entriesToAdd);
timestampEntries.push(...timestampsToAdd);
break;
}

const idx = binarySearch(timestampEntries, timestamp, (a, b) => a.getTime() - b.getTime());
const insertionIndex = idx < 0 ? ~idx : idx;
combinedEntries.splice(insertionIndex, 0, ...entriesToAdd);
timestampEntries.splice(insertionIndex, 0, ...timestampsToAdd);
}

startTimestampOfLastOutput = startTimestamp;
endTimestampOfLastOutput = timestamp;
}
// Add new empty line at the end
combinedEntries.push('');
return combinedEntries.join('\n');
}
Loading

0 comments on commit a4262a0

Please sign in to comment.