'use strict'; import { ExtensionContext, OutputChannel, Uri, window } from 'vscode'; import { extensionOutputChannelName } from './constants'; import { getCorrelationContext, getNextCorrelationId } from './system'; const emptyStr = ''; export enum TraceLevel { Silent = 'silent', Errors = 'errors', Verbose = 'verbose', Debug = 'debug', } const ConsolePrefix = `[${extensionOutputChannelName}]`; const isDebuggingRegex = /\bgitlens\b/i; export interface LogCorrelationContext { readonly correlationId?: number; readonly prefix: string; exitDetails?: string; } export class Logger { static output: OutputChannel | undefined; static customLoggableFn: ((o: object) => string | undefined) | undefined; static configure(context: ExtensionContext, level: TraceLevel, loggableFn?: (o: any) => string | undefined) { this.customLoggableFn = loggableFn; this.level = level; } private static _level: TraceLevel = TraceLevel.Silent; static get level() { return this._level; } static set level(value: TraceLevel) { this._level = value; if (value === TraceLevel.Silent) { if (this.output != null) { this.output.dispose(); this.output = undefined; } } else { this.output = this.output ?? window.createOutputChannel(extensionOutputChannelName); } } static debug(message: string, ...params: any[]): void; static debug(context: LogCorrelationContext | undefined, message: string, ...params: any[]): void; static debug(contextOrMessage: LogCorrelationContext | string | undefined, ...params: any[]): void { if (this.level !== TraceLevel.Debug && !Logger.isDebugging) return; let message; if (typeof contextOrMessage === 'string') { message = contextOrMessage; } else { message = params.shift(); if (contextOrMessage != null) { message = `${contextOrMessage.prefix} ${message ?? emptyStr}`; } } if (Logger.isDebugging) { console.log(this.timestamp, ConsolePrefix, message ?? emptyStr, ...params); } if (this.output != null && this.level === TraceLevel.Debug) { this.output.appendLine(`${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(true, params)}`); } } static error(ex: Error, message?: string, ...params: any[]): void; static error(ex: Error, context?: LogCorrelationContext, message?: string, ...params: any[]): void; static error(ex: Error, contextOrMessage: LogCorrelationContext | string | undefined, ...params: any[]): void { if (this.level === TraceLevel.Silent && !Logger.isDebugging) return; let message; if (contextOrMessage == null || typeof contextOrMessage === 'string') { message = contextOrMessage; } else { message = `${contextOrMessage.prefix} ${params.shift() ?? emptyStr}`; } if (message == null) { const stack = ex.stack; if (stack) { const match = /.*\s*?at\s(.+?)\s/.exec(stack); if (match != null) { message = match[1]; } } } if (Logger.isDebugging) { console.error(this.timestamp, ConsolePrefix, message ?? emptyStr, ...params, ex); } if (this.output != null && this.level !== TraceLevel.Silent) { this.output.appendLine( `${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(false, params)}\n${ex?.toString()}`, ); } } static getCorrelationContext() { return getCorrelationContext(); } static getNewCorrelationContext(prefix: string): LogCorrelationContext { const correlationId = getNextCorrelationId(); return { correlationId: correlationId, prefix: `[${correlationId}] ${prefix}`, }; } static log(message: string, ...params: any[]): void; static log(context: LogCorrelationContext | undefined, message: string, ...params: any[]): void; static log(contextOrMessage: LogCorrelationContext | string | undefined, ...params: any[]): void { if (this.level !== TraceLevel.Verbose && this.level !== TraceLevel.Debug && !Logger.isDebugging) { return; } let message; if (typeof contextOrMessage === 'string') { message = contextOrMessage; } else { message = params.shift(); if (contextOrMessage != null) { message = `${contextOrMessage.prefix} ${message ?? emptyStr}`; } } if (Logger.isDebugging) { console.log(this.timestamp, ConsolePrefix, message ?? emptyStr, ...params); } if (this.output != null && (this.level === TraceLevel.Verbose || this.level === TraceLevel.Debug)) { this.output.appendLine(`${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(false, params)}`); } } static logWithDebugParams(message: string, ...params: any[]): void; static logWithDebugParams(context: LogCorrelationContext | undefined, message: string, ...params: any[]): void; static logWithDebugParams(contextOrMessage: LogCorrelationContext | string | undefined, ...params: any[]): void { if (this.level !== TraceLevel.Verbose && this.level !== TraceLevel.Debug && !Logger.isDebugging) { return; } let message; if (typeof contextOrMessage === 'string') { message = contextOrMessage; } else { message = params.shift(); if (contextOrMessage != null) { message = `${contextOrMessage.prefix} ${message ?? emptyStr}`; } } if (Logger.isDebugging) { console.log(this.timestamp, ConsolePrefix, message ?? emptyStr, ...params); } if (this.output != null && (this.level === TraceLevel.Verbose || this.level === TraceLevel.Debug)) { this.output.appendLine(`${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(true, params)}`); } } static warn(message: string, ...params: any[]): void; static warn(context: LogCorrelationContext | undefined, message: string, ...params: any[]): void; static warn(contextOrMessage: LogCorrelationContext | string | undefined, ...params: any[]): void { if (this.level === TraceLevel.Silent && !Logger.isDebugging) return; let message; if (typeof contextOrMessage === 'string') { message = contextOrMessage; } else { message = params.shift(); if (contextOrMessage != null) { message = `${contextOrMessage.prefix} ${message ?? emptyStr}`; } } if (Logger.isDebugging) { console.warn(this.timestamp, ConsolePrefix, message ?? emptyStr, ...params); } if (this.output != null && this.level !== TraceLevel.Silent) { this.output.appendLine(`${this.timestamp} ${message ?? emptyStr}${this.toLoggableParams(false, params)}`); } } static showOutputChannel() { if (this.output == null) return; 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 ''; } } // eslint-disable-next-line @typescript-eslint/ban-types 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 { const now = new Date(); return `[${now .toISOString() .replace(/T/, ' ') .replace(/\..+/, emptyStr)}:${`00${now.getUTCMilliseconds()}`.slice(-3)}]`; } private static toLoggableParams(debugOnly: boolean, params: any[]) { if (params.length === 0 || (debugOnly && this.level !== TraceLevel.Debug && !Logger.isDebugging)) { return emptyStr; } const loggableParams = params.map(p => this.toLoggable(p)).join(', '); return loggableParams.length !== 0 ? ` \u2014 ${loggableParams}` : emptyStr; } private static _isDebugging: boolean | undefined; static get isDebugging() { if (this._isDebugging == null) { const env = process.env; this._isDebugging = env?.VSCODE_DEBUGGING_EXTENSION ? isDebuggingRegex.test(env.VSCODE_DEBUGGING_EXTENSION) : false; } return this._isDebugging; } static gitOutput: OutputChannel | undefined; static logGitCommand(command: string, ex?: Error): void { if (this.level !== TraceLevel.Debug) return; if (this.gitOutput == null) { this.gitOutput = window.createOutputChannel(`${extensionOutputChannelName} (Git)`); } this.gitOutput.appendLine(`${this.timestamp} ${command}${ex != null ? `\n\n${ex.toString()}` : emptyStr}`); } }