You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

284 lines
8.3 KiB

import type { ExtensionContext, OutputChannel } from 'vscode';
import { ExtensionMode, Uri, window } from 'vscode';
import { OutputLevel } from './configuration';
const emptyStr = '';
const outputChannelName = 'GitLens';
const consolePrefix = '[GitLens]';
const gitOutputChannelName = 'GitLens (Git)';
const gitConsolePrefix = '[GitLens (Git)]';
export const enum LogLevel {
Off = 'off',
Error = 'error',
Warn = 'warn',
Info = 'info',
Debug = 'debug',
}
export interface LogScope {
readonly scopeId?: number;
readonly prefix: string;
exitDetails?: string;
}
const enum OrderedLevel {
Off = 0,
Error = 1,
Warn = 2,
Info = 3,
Debug = 4,
}
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class Logger {
static readonly slowCallWarningThreshold = 500;
private static output: OutputChannel | undefined;
private static customLoggableFn: ((o: object) => string | undefined) | undefined;
static configure(context: ExtensionContext, outputLevel: OutputLevel, loggableFn?: (o: any) => string | undefined) {
this._isDebugging = context.extensionMode === ExtensionMode.Development;
this.logLevel = outputLevel;
this.customLoggableFn = loggableFn;
}
static enabled(level: LogLevel): boolean {
return this.level >= toOrderedLevel(level);
}
private static _isDebugging: boolean;
static get isDebugging() {
return this._isDebugging;
}
private static level: OrderedLevel = OrderedLevel.Off;
private static _logLevel: LogLevel = LogLevel.Off;
static get logLevel(): LogLevel {
return this._logLevel;
}
static set logLevel(value: LogLevel | OutputLevel) {
this._logLevel = fromOutputLevel(value);
this.level = toOrderedLevel(this._logLevel);
if (value === LogLevel.Off) {
this.output?.dispose();
this.output = undefined;
} else {
this.output = this.output ?? window.createOutputChannel(outputChannelName);
}
}
static debug(message: string, ...params: any[]): void;
static debug(scope: LogScope | undefined, message: string, ...params: any[]): void;
static debug(scopeOrMessage: LogScope | string | undefined, ...params: any[]): void {
if (this.level < OrderedLevel.Debug && !this.isDebugging) return;
let message;
if (typeof scopeOrMessage === 'string') {
message = scopeOrMessage;
} else {
message = params.shift();
if (scopeOrMessage != null) {
message = `${scopeOrMessage.prefix} ${message ?? emptyStr}`;
}
}
if (this.isDebugging) {
console.log(this.timestamp, consolePrefix, message ?? emptyStr, ...params);
}
if (this.output == null || this.level < OrderedLevel.Debug) return;
this.output.appendLine(`${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(true, params)}`);
}
static error(ex: Error | unknown, message?: string, ...params: any[]): void;
static error(ex: Error | unknown, scope?: LogScope, message?: string, ...params: any[]): void;
static error(ex: Error | unknown, scopeOrMessage: LogScope | string | undefined, ...params: any[]): void {
if (this.level < OrderedLevel.Error && !this.isDebugging) return;
let message;
if (scopeOrMessage == null || typeof scopeOrMessage === 'string') {
message = scopeOrMessage;
} else {
message = `${scopeOrMessage.prefix} ${params.shift() ?? emptyStr}`;
}
if (message == null) {
const stack = ex instanceof Error ? ex.stack : undefined;
if (stack) {
const match = /.*\s*?at\s(.+?)\s/.exec(stack);
if (match != null) {
message = match[1];
}
}
}
if (this.isDebugging) {
console.error(this.timestamp, consolePrefix, message ?? emptyStr, ...params, ex);
}
if (this.output == null || this.level < OrderedLevel.Error) return;
this.output.appendLine(
`${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(false, params)}\n${String(ex)}`,
);
}
static log(message: string, ...params: any[]): void;
static log(scope: LogScope | undefined, message: string, ...params: any[]): void;
static log(scopeOrMessage: LogScope | string | undefined, ...params: any[]): void {
if (this.level < OrderedLevel.Info && !this.isDebugging) return;
let message;
if (typeof scopeOrMessage === 'string') {
message = scopeOrMessage;
} else {
message = params.shift();
if (scopeOrMessage != null) {
message = `${scopeOrMessage.prefix} ${message ?? emptyStr}`;
}
}
if (this.isDebugging) {
console.log(this.timestamp, consolePrefix, message ?? emptyStr, ...params);
}
if (this.output == null || this.level < OrderedLevel.Info) return;
this.output.appendLine(`${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(false, params)}`);
}
static warn(message: string, ...params: any[]): void;
static warn(scope: LogScope | undefined, message: string, ...params: any[]): void;
static warn(scopeOrMessage: LogScope | string | undefined, ...params: any[]): void {
if (this.level < OrderedLevel.Warn && !this.isDebugging) return;
let message;
if (typeof scopeOrMessage === 'string') {
message = scopeOrMessage;
} else {
message = params.shift();
if (scopeOrMessage != null) {
message = `${scopeOrMessage.prefix} ${message ?? emptyStr}`;
}
}
if (this.isDebugging) {
console.warn(this.timestamp, consolePrefix, message ?? emptyStr, ...params);
}
if (this.output == null || this.level < OrderedLevel.Warn) return;
this.output.appendLine(`${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(false, params)}`);
}
static showOutputChannel(): void {
this.output?.show();
}
static toLoggable(p: any, sanitize?: ((key: string, value: any) => any) | undefined) {
if (typeof p !== 'object') return String(p);
if (this.customLoggableFn != null) {
const loggable = this.customLoggableFn(p);
if (loggable != null) return loggable;
}
if (p instanceof Uri) return `Uri(${p.toString(true)})`;
try {
return JSON.stringify(p, sanitize);
} catch {
return '<error>';
}
}
static toLoggableName(instance: Function | object) {
let name: string;
if (typeof instance === 'function') {
if (instance.prototype == null || instance.prototype.constructor == null) {
return instance.name;
}
name = instance.prototype.constructor.name ?? emptyStr;
} else {
name = instance.constructor?.name ?? emptyStr;
}
// Strip webpack module name (since I never name classes with an _)
const index = name.indexOf('_');
return index === -1 ? name : name.substr(index + 1);
}
private static get timestamp(): string {
return `[${new Date().toISOString().replace(/T/, ' ').slice(0, -1)}]`;
}
private static toLoggableParams(debugOnly: boolean, params: any[]) {
if (params.length === 0 || (debugOnly && this.level < OrderedLevel.Debug && !this.isDebugging)) {
return emptyStr;
}
const loggableParams = params.map(p => this.toLoggable(p)).join(', ');
return loggableParams.length !== 0 ? ` \u2014 ${loggableParams}` : emptyStr;
}
static gitOutput: OutputChannel | undefined;
static logGitCommand(command: string, duration: number, ex?: Error): void {
if (this.level < OrderedLevel.Debug && !this.isDebugging) return;
const slow = duration > Logger.slowCallWarningThreshold;
if (this.isDebugging) {
if (ex != null) {
console.error(this.timestamp, gitConsolePrefix, command ?? emptyStr, ex);
} else if (slow) {
console.warn(this.timestamp, gitConsolePrefix, command ?? emptyStr);
} else {
console.log(this.timestamp, gitConsolePrefix, command ?? emptyStr);
}
}
if (this.gitOutput == null) {
this.gitOutput = window.createOutputChannel(gitOutputChannelName);
}
this.gitOutput.appendLine(
`${this.timestamp} [${slow ? '*' : ' '}${duration.toString().padStart(6)}ms] ${command}${
ex != null ? `\n\n${ex.toString()}` : emptyStr
}`,
);
}
}
function fromOutputLevel(level: LogLevel | OutputLevel): LogLevel {
switch (level) {
case OutputLevel.Silent:
return LogLevel.Off;
case OutputLevel.Errors:
return LogLevel.Error;
case OutputLevel.Verbose:
return LogLevel.Info;
case OutputLevel.Debug:
return LogLevel.Debug;
default:
return level;
}
}
function toOrderedLevel(logLevel: LogLevel): OrderedLevel {
switch (logLevel) {
case LogLevel.Off:
return OrderedLevel.Off;
case LogLevel.Error:
return OrderedLevel.Error;
case LogLevel.Warn:
return OrderedLevel.Warn;
case LogLevel.Info:
return OrderedLevel.Info;
case LogLevel.Debug:
return OrderedLevel.Debug;
default:
return OrderedLevel.Off;
}
}