From 143b13f3a3f77282570fab479b1ee2d3e4d22ff0 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 14 Apr 2020 15:47:18 -0600 Subject: [PATCH 01/30] Combine the logger init functions. --- src/client/logging/logger.ts | 80 ++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/src/client/logging/logger.ts b/src/client/logging/logger.ts index 55246130a738..c239ca8c9721 100644 --- a/src/client/logging/logger.ts +++ b/src/client/logging/logger.ts @@ -15,8 +15,7 @@ import { LogLevel } from './types'; // Initialize the loggers as soon as this module is imported. const consoleLogger = createLogger(); const fileLogger = createLogger(); -initializeConsoleLogger(); -initializeFileLogger(); +initialize(); // Convert from LogLevel enum to node console "stream" (logger method). const logLevelMap = { @@ -44,52 +43,51 @@ function logToFile(logLevel: LogLevel, ...args: any[]) { } /** - * Initialize the logger for console. - * We do two things here: - * - Anything written to the logger will be displayed in the console window as well - * This is the behavior of the extension when running it. - * When running tests on CI, we might not want this behavior, as it'll pollute the - * test output with logging (as mentioned this is optional). - * Messages logged using our logger will be prefixed with `Python Extension: ....` for console window. - * This way, its easy to identify messages specific to the python extension. - * - Monkey patch the console.log and similar methods to send messages to the file logger. - * When running UI tests or similar, and we want to see everything that was dumped into `console window`, - * then we need to hijack the console logger. - * To do this we need to monkey patch the console methods. - * This is optional (generally done when running tests on CI). + * Initialize the logger. + * + * For console we do two things here: + * - Anything written to the logger will be displayed in the console + * window as well This is the behavior of the extension when running + * it. When running tests on CI, we might not want this behavior, as + * it'll pollute the test output with logging (as mentioned this is + * optional). Messages logged using our logger will be prefixed with + * `Python Extension: ....` for console window. This way, its easy + * to identify messages specific to the python extension. + * - Monkey patch the console.log and similar methods to send messages + * to the file logger. When running UI tests or similar, and we want + * to see everything that was dumped into `console window`, then we + * need to hijack the console logger. To do this we need to monkey + * patch the console methods. This is optional (generally done when + * running tests on CI). + * + * For the logfile: + * - we send all logging output to a log file. We log to the file + * only if a file has been specified as an env variable. Currently + * this is setup on CI servers. */ -function initializeConsoleLogger() { +function initialize() { // Hijack `console.log` when running tests on CI. if (process.env.VSC_PYTHON_LOG_FILE && process.env.TF_BUILD) { monkeypatchConsole(logToFile); } - - if (isTestExecution() && !process.env.VSC_PYTHON_FORCE_LOGGING) { - // Do not log to console if running tests on CI and we're not asked to do so. - return; + // Do not log to console if running tests on CI and we're not + // asked to do so. + if (!isTestExecution() || process.env.VSC_PYTHON_FORCE_LOGGING) { + // Rest of this stuff is just to instantiate the console logger. + // I.e. when we use our logger, ensure we also log to the + // console (for end users). + const formatter = getFormatter({ + // In CI there's no need for the label. + label: process.env.TF_BUILD ? undefined : 'Python Extension:' + }); + const transport = getConsoleTransport(logToConsole, formatter); + consoleLogger.add(transport as any); } - // Rest of this stuff is just to instantiate the console logger. - // I.e. when we use our logger, ensure we also log to the console (for end users). - const formatter = getFormatter({ - // In CI there's no need for the label. - label: process.env.TF_BUILD ? undefined : 'Python Extension:' - }); - const transport = getConsoleTransport(logToConsole, formatter); - consoleLogger.add(transport as any); -} - -/** - * Send all logging output to a log file. - * We log to the file only if a file has been specified as an env variable. - * Currently this is setup on CI servers. - */ -function initializeFileLogger() { const logfile = process.env.VSC_PYTHON_LOG_FILE; - if (!logfile) { - return; + if (logfile) { + const formatter = getFormatter(); + const transport = getFileTransport(logfile, formatter); + fileLogger.add(transport); } - const formatter = getFormatter(); - const transport = getFileTransport(logfile, formatter); - fileLogger.add(transport); } From d6a0c53cac8970f7cca87883574079b071244a30 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 14 Apr 2020 15:58:41 -0600 Subject: [PATCH 02/30] Factor out valueToLogString(). --- src/client/logging/util.ts | 59 +++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/src/client/logging/util.ts b/src/client/logging/util.ts index 2cab65bed14d..2813870a9a87 100644 --- a/src/client/logging/util.ts +++ b/src/client/logging/util.ts @@ -5,28 +5,37 @@ // tslint:disable-next-line:no-any type Arguments = any[]; +function valueToLogString(value: unknown, kind: string): string { + if (value === undefined) { + return 'undefined'; + } + if (value === null) { + return 'null'; + } + try { + // tslint:disable-next-line:no-any + if (value && (value as any).fsPath) { + // tslint:disable-next-line:no-any + return ``; + } + return JSON.stringify(value); + } catch { + return `<${kind} cannot be serialized for logging>`; + } +} + // Convert the given array of values (func call arguments) into a string // suitable to be used in a log message. export function argsToLogString(args: Arguments): string { + if (!args) { + return ''; + } try { - return (args || []) - .map((item, index) => { - if (item === undefined) { - return `Arg ${index + 1}: undefined`; - } - if (item === null) { - return `Arg ${index + 1}: null`; - } - try { - if (item && item.fsPath) { - return `Arg ${index + 1}: `; - } - return `Arg ${index + 1}: ${JSON.stringify(item)}`; - } catch { - return `Arg ${index + 1}: `; - } - }) - .join(', '); + const argStrings = args.map((item, index) => { + const valueString = valueToLogString(item, 'argument'); + return `Arg ${index + 1}: ${valueString}`; + }); + return argStrings.join(', '); } catch { return ''; } @@ -35,16 +44,6 @@ export function argsToLogString(args: Arguments): string { // Convert the given return value into a string // suitable to be used in a log message. export function returnValueToLogString(returnValue: unknown): string { - const returnValueMessage = 'Return Value: '; - if (returnValue === undefined) { - return `${returnValueMessage}undefined`; - } - if (returnValue === null) { - return `${returnValueMessage}null`; - } - try { - return `${returnValueMessage}${JSON.stringify(returnValue)}`; - } catch { - return `${returnValueMessage}`; - } + const valueString = valueToLogString(returnValue, 'Return value'); + return `Return Value: ${valueString}`; } From 2f7461a23e3cef7e72247b0d4b420e73995e6dec Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 14 Apr 2020 16:04:40 -0600 Subject: [PATCH 03/30] Move ConsoleTransport to module scope. --- src/client/logging/transports.ts | 37 ++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/client/logging/transports.ts b/src/client/logging/transports.ts index 03b779b5ce33..b0665ed8616e 100644 --- a/src/client/logging/transports.ts +++ b/src/client/logging/transports.ts @@ -13,25 +13,34 @@ import { ConsoleStreams } from './types'; // tslint:disable-next-line: no-var-requires no-require-imports const TransportStream = require('winston-transport'); +const formattedMessage = Symbol.for('message'); + +// A winston-compatible transport type. +class ConsoleTransport extends TransportStream { + constructor( + // logToConsole() is used to emit the log entry. + private readonly logToConsole: (stream: ConsoleStreams, ...args: any[]) => void, + options?: any + ) { + super(options); + } + + public log?(info: { level: string; message: string; [formattedMessage]: string }, next: () => void): any { + setImmediate(() => this.emit('logged', info)); + const msg = info[formattedMessage] || info.message; + this.logToConsole(info.level as any, msg); + if (next) { + next(); + } + } +} + // Create a console-targeting transport that can be added to a winston logger. export function getConsoleTransport( logToConsole: (stream: ConsoleStreams, ...args: any[]) => void, formatter: logform.Format ) { - const formattedMessage = Symbol.for('message'); - class ConsoleTransport extends TransportStream { - constructor(options?: any) { - super(options); - } - public log?(info: { level: string; message: string; [formattedMessage]: string }, next: () => void): any { - setImmediate(() => this.emit('logged', info)); - logToConsole(info.level as any, info[formattedMessage] || info.message); - if (next) { - next(); - } - } - } - return new ConsoleTransport({ + return new ConsoleTransport(logToConsole, { // We minimize customization. format: formatter }); From f99c320b4e59ac94822c1bcb3b51df209bb3ce96 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 14 Apr 2020 16:39:24 -0600 Subject: [PATCH 04/30] Factor out a multi-logger log() function. --- src/client/logging/logger.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/client/logging/logger.ts b/src/client/logging/logger.ts index c239ca8c9721..5dd2cda3374a 100644 --- a/src/client/logging/logger.ts +++ b/src/client/logging/logger.ts @@ -24,22 +24,24 @@ const logLevelMap = { [LogLevel.Warning]: 'warn' }; +interface ILogger { + transports: unknown[]; + log(level: string, message: string): void; +} + // Emit a log message derived from the args to all enabled transports. -export function _log(logLevel: LogLevel, ...args: any[]) { - if (consoleLogger.transports.length > 0) { - const message = args.length === 0 ? '' : util.format(args[0], ...args.slice(1)); - consoleLogger.log(logLevelMap[logLevel], message); +function log(loggers: ILogger[], logLevel: LogLevel, args: any[]) { + for (const logger of loggers) { + if (logger.transports.length > 0) { + const message = args.length === 0 ? '' : util.format(args[0], ...args.slice(1)); + logger.log(logLevelMap[logLevel], message); + } } - logToFile(logLevel, ...args); } -// Emit a log message derived from the args to a file, if enabled. -function logToFile(logLevel: LogLevel, ...args: any[]) { - if (fileLogger.transports.length === 0) { - return; - } - const message = args.length === 0 ? '' : util.format(args[0], ...args.slice(1)); - fileLogger.log(logLevelMap[logLevel], message); +// Emit a log message derived from the args to all enabled transports. +export function _log(logLevel: LogLevel, ...args: any[]) { + log([consoleLogger, fileLogger], logLevel, args); } /** @@ -68,6 +70,9 @@ function logToFile(logLevel: LogLevel, ...args: any[]) { function initialize() { // Hijack `console.log` when running tests on CI. if (process.env.VSC_PYTHON_LOG_FILE && process.env.TF_BUILD) { + function logToFile(logLevel: LogLevel, ...args: any[]) { + log([fileLogger], logLevel, args); + } monkeypatchConsole(logToFile); } // Do not log to console if running tests on CI and we're not From b06fcce0eb3d5429e7ced67238cbf3e863f9c6d6 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 14 Apr 2020 18:16:30 -0600 Subject: [PATCH 05/30] Make log levels more consistent. --- src/client/logging/_console.ts | 49 ++++++++++----- src/client/logging/_trace.ts | 13 ++-- src/client/logging/formatters.ts | 43 ++++++++----- src/client/logging/levels.ts | 100 +++++++++++++++++++++++++++++++ src/client/logging/logger.ts | 19 +++--- src/client/logging/transports.ts | 16 ++--- src/client/logging/types.ts | 10 ---- 7 files changed, 185 insertions(+), 65 deletions(-) create mode 100644 src/client/logging/levels.ts diff --git a/src/client/logging/_console.ts b/src/client/logging/_console.ts index a628c94e852b..df2305e287f8 100644 --- a/src/client/logging/_console.ts +++ b/src/client/logging/_console.ts @@ -4,21 +4,32 @@ // tslint:disable:no-console no-any -import { ConsoleStreams, LogLevel } from './types'; +import { LogLevel } from './levels'; + +// The logging "streams" (methods) of the node console. +type ConsoleStream = 'log' | 'error' | 'warn' | 'info' | 'debug' | 'trace'; + +// Convert from LogLevel enum to node console "stream" (logger method). +const streamByLevel: { [K in LogLevel]: ConsoleStream } = { + [LogLevel.Error]: 'error', + [LogLevel.Warn]: 'warn', + [LogLevel.Info]: 'info', + [LogLevel.Debug]: 'debug', + [LogLevel.Trace]: 'trace' +}; const logMethods = { log: Symbol.for('log'), - info: Symbol.for('info'), error: Symbol.for('error'), + warn: Symbol.for('warn'), + info: Symbol.for('info'), debug: Symbol.for('debug'), - warn: Symbol.for('warn') + trace: Symbol.for('trace') }; // Log a message based on "args" to the given console "stream". -export function logToConsole(stream: ConsoleStreams, ...args: any[]) { - if (['info', 'error', 'warn', 'log', 'debug'].indexOf(stream) === -1) { - stream = 'log'; - } +export function logToConsole(level: LogLevel | undefined, ...args: any[]) { + const stream = (level ? streamByLevel[level] : undefined) || 'log'; // Further below we monkeypatch the console.log, etc methods. const fn = (console as any)[logMethods[stream]] || console[stream] || console.log; fn(...args); @@ -44,31 +55,37 @@ export function monkeypatchConsole(logToFile: (logLevel: LogLevel, ...args: any[ // tslint:disable-next-line: no-function-expression console.log = function () { const args = Array.prototype.slice.call(arguments); - logToConsole('log', ...args); - logToFile(LogLevel.Information, ...args); + logToConsole(undefined, ...args); + logToFile(LogLevel.Info, ...args); }; // tslint:disable-next-line: no-function-expression console.info = function () { const args = Array.prototype.slice.call(arguments); - logToConsole('info', ...args); - logToFile(LogLevel.Information, ...args); + logToConsole(LogLevel.Info, ...args); + logToFile(LogLevel.Info, ...args); }; // tslint:disable-next-line: no-function-expression console.warn = function () { const args = Array.prototype.slice.call(arguments); - logToConsole('warn', ...args); - logToFile(LogLevel.Warning, ...args); + logToConsole(LogLevel.Warn, ...args); + logToFile(LogLevel.Warn, ...args); }; // tslint:disable-next-line: no-function-expression console.error = function () { const args = Array.prototype.slice.call(arguments); - logToConsole('error', ...args); + logToConsole(LogLevel.Error, ...args); logToFile(LogLevel.Error, ...args); }; // tslint:disable-next-line: no-function-expression console.debug = function () { const args = Array.prototype.slice.call(arguments); - logToConsole('debug', ...args); - logToFile(LogLevel.Information, ...args); + logToConsole(LogLevel.Debug, ...args); + logToFile(LogLevel.Info, ...args); + }; + // tslint:disable-next-line: no-function-expression + console.trace = function () { + const args = Array.prototype.slice.call(arguments); + logToConsole(LogLevel.Trace, ...args); + logToFile(LogLevel.Info, ...args); }; } diff --git a/src/client/logging/_trace.ts b/src/client/logging/_trace.ts index fca65c199735..e85976e3dc24 100644 --- a/src/client/logging/_trace.ts +++ b/src/client/logging/_trace.ts @@ -6,12 +6,13 @@ import { StopWatch } from '../common/utils/stopWatch'; import { sendTelemetryEvent } from '../telemetry'; +import { LogLevel } from './levels'; import { _log as log } from './logger'; -import { LogLevel, TraceOptions } from './types'; +import { TraceOptions } from './types'; import { argsToLogString, returnValueToLogString } from './util'; export function traceVerbose(...args: any[]) { - log(LogLevel.Information, ...args); + log(LogLevel.Info, ...args); } export function traceError(...args: any[]) { @@ -19,11 +20,11 @@ export function traceError(...args: any[]) { } export function traceInfo(...args: any[]) { - log(LogLevel.Information, ...args); + log(LogLevel.Info, ...args); } export function traceWarning(...args: any[]) { - log(LogLevel.Warning, ...args); + log(LogLevel.Warn, ...args); } export namespace traceDecorators { @@ -37,7 +38,7 @@ export namespace traceDecorators { return trace(message); } export function warn(message: string) { - return trace(message, TraceOptions.Arguments | TraceOptions.ReturnValue, LogLevel.Warning); + return trace(message, TraceOptions.Arguments | TraceOptions.ReturnValue, LogLevel.Warn); } } @@ -76,7 +77,7 @@ function trace(message: string, opts: TraceOptions = TraceOptions.None, logLevel log(LogLevel.Error, messagesToLog.join(', '), ex); sendTelemetryEvent('ERROR' as any, undefined, undefined, ex); } else { - log(LogLevel.Information, messagesToLog.join(', ')); + log(LogLevel.Info, messagesToLog.join(', ')); } } const timer = new StopWatch(); diff --git a/src/client/logging/formatters.ts b/src/client/logging/formatters.ts index 1867186d09c2..5051cd46374a 100644 --- a/src/client/logging/formatters.ts +++ b/src/client/logging/formatters.ts @@ -3,28 +3,43 @@ 'use strict'; import { format } from 'winston'; +import { getLevel, LogLevel, LogLevelName } from './levels'; import { FormatterOptions } from './types'; const TIMESTAMP = 'YYYY-MM-DD HH:mm:ss'; -// Return a consistent representation of the given log level. -// // Pascal casing is used so log files get highlighted when viewing // in VSC and other editors. -function normalizeLevel(level: string): string { - return `${level.substring(0, 1).toUpperCase()}${level.substring(1)}`; +const formattedLogLevels: { [K in LogLevel]: string } = { + [LogLevel.Error]: 'Error', + [LogLevel.Warn]: 'Warn', + [LogLevel.Info]: 'Info', + [LogLevel.Debug]: 'Debug', + [LogLevel.Trace]: 'Trace' +}; + +// Return a consistent representation of the given log level. +function normalizeLevel(name: LogLevelName): string { + const level = getLevel(name); + if (level) { + const norm = formattedLogLevels[level]; + if (norm) { + return norm; + } + } + return `${name.substring(0, 1).toUpperCase()}${name.substring(1).toLowerCase()}`; } // Return a log entry that can be emitted as-is. -function formatMessage(level: string, timestamp: string, message: string): string { - level = normalizeLevel(level); - return `${level} ${timestamp}: ${message}`; +function formatMessage(level: LogLevelName, timestamp: string, message: string): string { + const levelFormatted = normalizeLevel(level); + return `${levelFormatted} ${timestamp}: ${message}`; } // Return a log entry that can be emitted as-is. -function formatLabeledMessage(level: string, timestamp: string, label: string, message: string): string { - level = normalizeLevel(level); - return `${level} ${label} ${timestamp}: ${message}`; +function formatLabeledMessage(level: LogLevelName, timestamp: string, label: string, message: string): string { + const levelFormatted = normalizeLevel(level); + return `${levelFormatted} ${label} ${timestamp}: ${message}`; } // Return a minimal format object that can be used with a "winston" @@ -33,8 +48,8 @@ function getMinimalFormatter() { return format.combine( format.timestamp({ format: TIMESTAMP }), format.printf( - // a minimal message - ({ level, message, timestamp }) => formatMessage(level, timestamp, message) + // tslint:disable-next-line:no-any + ({ level, message, timestamp }) => formatMessage(level as any, timestamp, message) ) ); } @@ -46,8 +61,8 @@ function getLabeledFormatter(label_: string) { format.label({ label: label_ }), format.timestamp({ format: TIMESTAMP }), format.printf( - // mostly a minimal message - ({ level, message, label, timestamp }) => formatLabeledMessage(level, timestamp, label, message) + // tslint:disable-next-line:no-any + ({ level, message, label, timestamp }) => formatLabeledMessage(level as any, timestamp, label, message) ) ); } diff --git a/src/client/logging/levels.ts b/src/client/logging/levels.ts new file mode 100644 index 000000000000..d857946a6c75 --- /dev/null +++ b/src/client/logging/levels.ts @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as winston from 'winston'; + +//====================== +// Our custom log levels + +export enum LogLevel { + // Larger numbers are higher priority. + Error = 40, + Warn = 30, + Info = 20, + Debug = 10, + Trace = 5 +} +export type LogLevelName = 'ERROR' | 'WARNING' | 'INFORMATION' | 'DEBUG' | 'DEBUG-TRACE'; +const logLevelMap: { [K in LogLevel]: LogLevelName } = { + [LogLevel.Error]: 'ERROR', + [LogLevel.Warn]: 'WARNING', + [LogLevel.Info]: 'INFORMATION', + [LogLevel.Debug]: 'DEBUG', + [LogLevel.Trace]: 'DEBUG-TRACE' +}; +// This can be used for winston.LoggerOptions.levels. +export const configLevels: winston.config.AbstractConfigSetLevels = { + ERROR: 0, + WARNING: 1, + INFORMATION: 2, + DEBUG: 4, + 'DEBUG-TRACE': 5 +}; + +//====================== +// Other log levels + +// The level names from winston/config.npm. +// See: https://www.npmjs.com/package/winston#logging-levels +type NPMLogLevelName = 'error' | 'warn' | 'info' | 'verbose' | 'debug' | 'silly'; +const npmLogLevelMap: { [K in LogLevel]: NPMLogLevelName } = { + [LogLevel.Error]: 'error', + [LogLevel.Warn]: 'warn', + [LogLevel.Info]: 'info', + [LogLevel.Debug]: 'debug', + [LogLevel.Trace]: 'silly' +}; + +//====================== +// lookup functions + +// Convert from LogLevel enum to the proper level name. +export function resolveLevelName( + level: LogLevel, + // Default to configLevels. + levels?: winston.config.AbstractConfigSetLevels +): string | undefined { + if (levels === undefined) { + return getLevelName(level); + } else if (levels === configLevels) { + return getLevelName(level); + } else if (levels === winston.config.npm.levels) { + return npmLogLevelMap[level]; + } else { + return undefined; + } +} +export function getLevelName(level: LogLevel): LogLevelName | undefined { + return logLevelMap[level]; +} + +// Convert from a level name to the actual level. +export function resolveLevel( + levelName: string, + // Default to configLevels. + levels?: winston.config.AbstractConfigSetLevels +): LogLevel | undefined { + let levelMap: { [K in LogLevel]: string }; + if (levels === undefined) { + levelMap = logLevelMap; + } else if (levels === configLevels) { + levelMap = logLevelMap; + } else if (levels === winston.config.npm.levels) { + levelMap = npmLogLevelMap; + } else { + return undefined; + } + for (const level of Object.keys(levelMap)) { + if (typeof level === 'string') { + continue; + } + if (logLevelMap[level] === levelName) { + return level; + } + } + return undefined; +} +export function getLevel(name: LogLevelName): LogLevel | undefined { + return resolveLevel(name); +} diff --git a/src/client/logging/logger.ts b/src/client/logging/logger.ts index 5dd2cda3374a..dceb610524ad 100644 --- a/src/client/logging/logger.ts +++ b/src/client/logging/logger.ts @@ -5,27 +5,21 @@ // tslint:disable:no-any import * as util from 'util'; -import { createLogger } from 'winston'; +import * as winston from 'winston'; import { isTestExecution } from '../common/constants'; import { logToConsole, monkeypatchConsole } from './_console'; import { getFormatter } from './formatters'; +import { LogLevel, resolveLevelName } from './levels'; import { getConsoleTransport, getFileTransport } from './transports'; -import { LogLevel } from './types'; // Initialize the loggers as soon as this module is imported. -const consoleLogger = createLogger(); -const fileLogger = createLogger(); +const consoleLogger = winston.createLogger(); +const fileLogger = winston.createLogger(); initialize(); -// Convert from LogLevel enum to node console "stream" (logger method). -const logLevelMap = { - [LogLevel.Error]: 'error', - [LogLevel.Information]: 'info', - [LogLevel.Warning]: 'warn' -}; - interface ILogger { transports: unknown[]; + levels: winston.config.AbstractConfigSetLevels; log(level: string, message: string): void; } @@ -34,7 +28,8 @@ function log(loggers: ILogger[], logLevel: LogLevel, args: any[]) { for (const logger of loggers) { if (logger.transports.length > 0) { const message = args.length === 0 ? '' : util.format(args[0], ...args.slice(1)); - logger.log(logLevelMap[logLevel], message); + const levelName: string = resolveLevelName(logLevel, logger.levels) || ''; + logger.log(levelName, message); } } } diff --git a/src/client/logging/transports.ts b/src/client/logging/transports.ts index b0665ed8616e..a79b15e45a2b 100644 --- a/src/client/logging/transports.ts +++ b/src/client/logging/transports.ts @@ -6,9 +6,9 @@ import * as logform from 'logform'; import * as path from 'path'; -import { transports } from 'winston'; +import * as winston from 'winston'; import { EXTENSION_ROOT_DIR } from '../constants'; -import { ConsoleStreams } from './types'; +import { LogLevel, resolveLevel } from './levels'; // tslint:disable-next-line: no-var-requires no-require-imports const TransportStream = require('winston-transport'); @@ -19,16 +19,18 @@ const formattedMessage = Symbol.for('message'); class ConsoleTransport extends TransportStream { constructor( // logToConsole() is used to emit the log entry. - private readonly logToConsole: (stream: ConsoleStreams, ...args: any[]) => void, - options?: any + private readonly logToConsole: (level: LogLevel | undefined, ...args: any[]) => void, + options?: any, + private readonly levels?: winston.config.AbstractConfigSetLevels ) { super(options); } public log?(info: { level: string; message: string; [formattedMessage]: string }, next: () => void): any { setImmediate(() => this.emit('logged', info)); + const level = resolveLevel(info.level, this.levels); const msg = info[formattedMessage] || info.message; - this.logToConsole(info.level as any, msg); + this.logToConsole(level, msg); if (next) { next(); } @@ -37,7 +39,7 @@ class ConsoleTransport extends TransportStream { // Create a console-targeting transport that can be added to a winston logger. export function getConsoleTransport( - logToConsole: (stream: ConsoleStreams, ...args: any[]) => void, + logToConsole: (level: LogLevel | undefined, ...args: any[]) => void, formatter: logform.Format ) { return new ConsoleTransport(logToConsole, { @@ -51,7 +53,7 @@ export function getFileTransport(logfile: string, formatter: logform.Format) { if (!path.isAbsolute(logfile)) { logfile = path.join(EXTENSION_ROOT_DIR, logfile); } - return new transports.File({ + return new winston.transports.File({ format: formatter, filename: logfile, handleExceptions: true diff --git a/src/client/logging/types.ts b/src/client/logging/types.ts index d769bff5e189..b0490794b2f9 100644 --- a/src/client/logging/types.ts +++ b/src/client/logging/types.ts @@ -2,16 +2,6 @@ // Licensed under the MIT License. 'use strict'; -// The various supported log levels. The number value matches (npm) winston. -export enum LogLevel { - Error = 0, - Warning = 1, - Information = 2 -} - -// The logging "streams" (methods) of the node console. -export type ConsoleStreams = 'log' | 'error' | 'warn' | 'info' | 'debug'; - // Knobs used when creating a formatter. export type FormatterOptions = { label?: string; From f6f0386a2f20a31c47b0e03cd74c9241515d804f Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 14 Apr 2020 18:24:16 -0600 Subject: [PATCH 06/30] Drop global disable for "no-any". --- src/client/logging/_console.ts | 13 ++++++++++--- src/client/logging/_trace.ts | 6 ++++-- src/client/logging/logger.ts | 12 +++++++----- src/client/logging/transports.ts | 7 ++++--- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/client/logging/_console.ts b/src/client/logging/_console.ts index df2305e287f8..ab2500be600e 100644 --- a/src/client/logging/_console.ts +++ b/src/client/logging/_console.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. 'use strict'; -// tslint:disable:no-console no-any +// tslint:disable:no-console import { LogLevel } from './levels'; @@ -27,10 +27,14 @@ const logMethods = { trace: Symbol.for('trace') }; +// tslint:disable-next-line:no-any +type Arguments = any[]; + // Log a message based on "args" to the given console "stream". -export function logToConsole(level: LogLevel | undefined, ...args: any[]) { +export function logToConsole(level: LogLevel | undefined, ...args: Arguments) { const stream = (level ? streamByLevel[level] : undefined) || 'log'; // Further below we monkeypatch the console.log, etc methods. + // tslint:disable-next-line:no-any const fn = (console as any)[logMethods[stream]] || console[stream] || console.log; fn(...args); } @@ -43,14 +47,17 @@ export function logToConsole(level: LogLevel | undefined, ...args: any[]) { * on CI so we can see everything logged to the console window * (via the logs). */ -export function monkeypatchConsole(logToFile: (logLevel: LogLevel, ...args: any[]) => void) { +export function monkeypatchConsole(logToFile: (logLevel: LogLevel, ...args: Arguments) => void) { // Keep track of the original functions before we monkey patch them. // Using symbols guarantee the properties will be unique & prevents clashing with names other code/library may create or have created. + // tslint:disable:no-any (console as any)[logMethods.log] = console.log; (console as any)[logMethods.info] = console.info; (console as any)[logMethods.error] = console.error; (console as any)[logMethods.debug] = console.debug; + (console as any)[logMethods.trace] = console.trace; (console as any)[logMethods.warn] = console.warn; + // tslint:enable:no-any // tslint:disable-next-line: no-function-expression console.log = function () { diff --git a/src/client/logging/_trace.ts b/src/client/logging/_trace.ts index e85976e3dc24..45cd9873b79f 100644 --- a/src/client/logging/_trace.ts +++ b/src/client/logging/_trace.ts @@ -2,8 +2,6 @@ // Licensed under the MIT License. 'use strict'; -// tslint:disable:no-any - import { StopWatch } from '../common/utils/stopWatch'; import { sendTelemetryEvent } from '../telemetry'; import { LogLevel } from './levels'; @@ -11,18 +9,22 @@ import { _log as log } from './logger'; import { TraceOptions } from './types'; import { argsToLogString, returnValueToLogString } from './util'; +// tslint:disable-next-line:no-any export function traceVerbose(...args: any[]) { log(LogLevel.Info, ...args); } +// tslint:disable-next-line:no-any export function traceError(...args: any[]) { log(LogLevel.Error, ...args); } +// tslint:disable-next-line:no-any export function traceInfo(...args: any[]) { log(LogLevel.Info, ...args); } +// tslint:disable-next-line:no-any export function traceWarning(...args: any[]) { log(LogLevel.Warn, ...args); } diff --git a/src/client/logging/logger.ts b/src/client/logging/logger.ts index dceb610524ad..49d57815de17 100644 --- a/src/client/logging/logger.ts +++ b/src/client/logging/logger.ts @@ -2,8 +2,6 @@ // Licensed under the MIT License. 'use strict'; -// tslint:disable:no-any - import * as util from 'util'; import * as winston from 'winston'; import { isTestExecution } from '../common/constants'; @@ -17,6 +15,9 @@ const consoleLogger = winston.createLogger(); const fileLogger = winston.createLogger(); initialize(); +// tslint:disable-next-line:no-any +type Arguments = any[]; + interface ILogger { transports: unknown[]; levels: winston.config.AbstractConfigSetLevels; @@ -24,7 +25,7 @@ interface ILogger { } // Emit a log message derived from the args to all enabled transports. -function log(loggers: ILogger[], logLevel: LogLevel, args: any[]) { +function log(loggers: ILogger[], logLevel: LogLevel, args: Arguments) { for (const logger of loggers) { if (logger.transports.length > 0) { const message = args.length === 0 ? '' : util.format(args[0], ...args.slice(1)); @@ -35,7 +36,7 @@ function log(loggers: ILogger[], logLevel: LogLevel, args: any[]) { } // Emit a log message derived from the args to all enabled transports. -export function _log(logLevel: LogLevel, ...args: any[]) { +export function _log(logLevel: LogLevel, ...args: Arguments) { log([consoleLogger, fileLogger], logLevel, args); } @@ -65,7 +66,7 @@ export function _log(logLevel: LogLevel, ...args: any[]) { function initialize() { // Hijack `console.log` when running tests on CI. if (process.env.VSC_PYTHON_LOG_FILE && process.env.TF_BUILD) { - function logToFile(logLevel: LogLevel, ...args: any[]) { + function logToFile(logLevel: LogLevel, ...args: Arguments) { log([fileLogger], logLevel, args); } monkeypatchConsole(logToFile); @@ -81,6 +82,7 @@ function initialize() { label: process.env.TF_BUILD ? undefined : 'Python Extension:' }); const transport = getConsoleTransport(logToConsole, formatter); + // tslint:disable-next-line:no-any consoleLogger.add(transport as any); } diff --git a/src/client/logging/transports.ts b/src/client/logging/transports.ts index a79b15e45a2b..657d6892c281 100644 --- a/src/client/logging/transports.ts +++ b/src/client/logging/transports.ts @@ -2,8 +2,6 @@ // Licensed under the MIT License. 'use strict'; -// tslint:disable:no-any - import * as logform from 'logform'; import * as path from 'path'; import * as winston from 'winston'; @@ -18,14 +16,16 @@ const formattedMessage = Symbol.for('message'); // A winston-compatible transport type. class ConsoleTransport extends TransportStream { constructor( - // logToConsole() is used to emit the log entry. + // tslint:disable-next-line:no-any private readonly logToConsole: (level: LogLevel | undefined, ...args: any[]) => void, + // tslint:disable-next-line:no-any options?: any, private readonly levels?: winston.config.AbstractConfigSetLevels ) { super(options); } + // tslint:disable-next-line:no-any public log?(info: { level: string; message: string; [formattedMessage]: string }, next: () => void): any { setImmediate(() => this.emit('logged', info)); const level = resolveLevel(info.level, this.levels); @@ -39,6 +39,7 @@ class ConsoleTransport extends TransportStream { // Create a console-targeting transport that can be added to a winston logger. export function getConsoleTransport( + // tslint:disable-next-line:no-any logToConsole: (level: LogLevel | undefined, ...args: any[]) => void, formatter: logform.Format ) { From e9552feb172aea7719fe1b95c35e280a643211bf Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 14 Apr 2020 18:40:33 -0600 Subject: [PATCH 07/30] Factor out formatMessages() for use in trace(). --- src/client/logging/_trace.ts | 44 ++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/client/logging/_trace.ts b/src/client/logging/_trace.ts index 45cd9873b79f..1dc06f61c4a0 100644 --- a/src/client/logging/_trace.ts +++ b/src/client/logging/_trace.ts @@ -44,6 +44,32 @@ export namespace traceDecorators { } } +function formatMessages( + opts: TraceOptions, + message: string, + kind: string, + name: string, + elapsed: number, + // tslint:disable-next-line:no-any + args: any[], + // tslint:disable-next-line:no-any + returnValue?: any +): string { + const messages = [message]; + messages.push( + `${kind} name = ${name}`.trim(), + `completed in ${elapsed}ms`, + `has a ${returnValue ? 'truthy' : 'falsy'} return value` + ); + if ((opts & TraceOptions.Arguments) === TraceOptions.Arguments) { + messages.push(argsToLogString(args)); + } + if ((opts & TraceOptions.ReturnValue) === TraceOptions.ReturnValue) { + messages.push(returnValueToLogString(returnValue)); + } + return messages.join(', '); +} + function trace(message: string, opts: TraceOptions = TraceOptions.None, logLevel?: LogLevel) { // tslint:disable-next-line:no-function-expression no-any return function (_: Object, __: string, descriptor: TypedPropertyDescriptor) { @@ -63,23 +89,13 @@ function trace(message: string, opts: TraceOptions = TraceOptions.None, logLevel } // tslint:disable-next-line:no-any function writeToLog(elapsedTime: number, returnValue?: any, ex?: Error) { - const messagesToLog = [message]; - messagesToLog.push( - `Class name = ${className}, completed in ${elapsedTime}ms, has a ${ - returnValue ? 'truthy' : 'falsy' - } return value` - ); - if ((opts & TraceOptions.Arguments) === TraceOptions.Arguments) { - messagesToLog.push(argsToLogString(args)); - } - if ((opts & TraceOptions.ReturnValue) === TraceOptions.ReturnValue) { - messagesToLog.push(returnValueToLogString(returnValue)); - } + const formatted = formatMessages(opts, message, 'Class', className, elapsedTime, args, returnValue); if (ex) { - log(LogLevel.Error, messagesToLog.join(', '), ex); + log(LogLevel.Error, formatted, ex); + // tslint:disable-next-line:no-any sendTelemetryEvent('ERROR' as any, undefined, undefined, ex); } else { - log(LogLevel.Info, messagesToLog.join(', ')); + log(LogLevel.Info, formatted); } } const timer = new StopWatch(); From f5f79badc2bcb4722ed2c66a21059ab12ac2e6cf Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 5 May 2020 12:45:55 -0600 Subject: [PATCH 08/30] Factor out LogInfo. --- src/client/logging/_trace.ts | 41 +++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/client/logging/_trace.ts b/src/client/logging/_trace.ts index 1dc06f61c4a0..e1d6f11b553a 100644 --- a/src/client/logging/_trace.ts +++ b/src/client/logging/_trace.ts @@ -30,23 +30,35 @@ export function traceWarning(...args: any[]) { } export namespace traceDecorators { - export function verbose(message: string, opts: TraceOptions = TraceOptions.Arguments | TraceOptions.ReturnValue) { - return trace(message, opts); + const DEFAULT_OPTS: TraceOptions = TraceOptions.Arguments | TraceOptions.ReturnValue; + + export function verbose(message: string, opts: TraceOptions = DEFAULT_OPTS) { + return trace({ message, opts }); } export function error(message: string) { - return trace(message, TraceOptions.Arguments | TraceOptions.ReturnValue, LogLevel.Error); + const opts = DEFAULT_OPTS; + const level = LogLevel.Error; + return trace({ message, opts, level }); } export function info(message: string) { - return trace(message); + const opts = TraceOptions.None; + return trace({ message, opts }); } export function warn(message: string) { - return trace(message, TraceOptions.Arguments | TraceOptions.ReturnValue, LogLevel.Warn); + const opts = DEFAULT_OPTS; + const level = LogLevel.Warn; + return trace({ message, opts, level }); } } +type LogInfo = { + opts: TraceOptions; + message: string; + level?: LogLevel; +}; + function formatMessages( - opts: TraceOptions, - message: string, + info: LogInfo, kind: string, name: string, elapsed: number, @@ -55,22 +67,22 @@ function formatMessages( // tslint:disable-next-line:no-any returnValue?: any ): string { - const messages = [message]; + const messages = [info.message]; messages.push( `${kind} name = ${name}`.trim(), `completed in ${elapsed}ms`, `has a ${returnValue ? 'truthy' : 'falsy'} return value` ); - if ((opts & TraceOptions.Arguments) === TraceOptions.Arguments) { + if ((info.opts & TraceOptions.Arguments) === TraceOptions.Arguments) { messages.push(argsToLogString(args)); } - if ((opts & TraceOptions.ReturnValue) === TraceOptions.ReturnValue) { + if ((info.opts & TraceOptions.ReturnValue) === TraceOptions.ReturnValue) { messages.push(returnValueToLogString(returnValue)); } return messages.join(', '); } -function trace(message: string, opts: TraceOptions = TraceOptions.None, logLevel?: LogLevel) { +function trace(logInfo: LogInfo) { // tslint:disable-next-line:no-function-expression no-any return function (_: Object, __: string, descriptor: TypedPropertyDescriptor) { const originalMethod = descriptor.value; @@ -79,17 +91,16 @@ function trace(message: string, opts: TraceOptions = TraceOptions.None, logLevel const className = _ && _.constructor ? _.constructor.name : ''; // tslint:disable-next-line:no-any function writeSuccess(elapsedTime: number, returnValue: any) { - if (logLevel === LogLevel.Error) { - return; + if (logInfo.level === undefined || logInfo.level > LogLevel.Error) { + writeToLog(elapsedTime, returnValue); } - writeToLog(elapsedTime, returnValue); } function writeError(elapsedTime: number, ex: Error) { writeToLog(elapsedTime, undefined, ex); } // tslint:disable-next-line:no-any function writeToLog(elapsedTime: number, returnValue?: any, ex?: Error) { - const formatted = formatMessages(opts, message, 'Class', className, elapsedTime, args, returnValue); + const formatted = formatMessages(logInfo, 'Class', className, elapsedTime, args, returnValue); if (ex) { log(LogLevel.Error, formatted, ex); // tslint:disable-next-line:no-any From 9ba1d34096aecf5418745d6ce9fa0ce0cfc190a1 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 5 May 2020 12:46:29 -0600 Subject: [PATCH 09/30] Factor out CallInfo. --- src/client/logging/_trace.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/client/logging/_trace.ts b/src/client/logging/_trace.ts index e1d6f11b553a..be5b238446b4 100644 --- a/src/client/logging/_trace.ts +++ b/src/client/logging/_trace.ts @@ -57,24 +57,28 @@ type LogInfo = { level?: LogLevel; }; +type CallInfo = { + kind: string; + name: string; + // tslint:disable-next-line:no-any + args: any[]; +}; + function formatMessages( info: LogInfo, - kind: string, - name: string, + call: CallInfo, elapsed: number, // tslint:disable-next-line:no-any - args: any[], - // tslint:disable-next-line:no-any returnValue?: any ): string { const messages = [info.message]; messages.push( - `${kind} name = ${name}`.trim(), + `${call.kind} name = ${call.name}`.trim(), `completed in ${elapsed}ms`, `has a ${returnValue ? 'truthy' : 'falsy'} return value` ); if ((info.opts & TraceOptions.Arguments) === TraceOptions.Arguments) { - messages.push(argsToLogString(args)); + messages.push(argsToLogString(call.args)); } if ((info.opts & TraceOptions.ReturnValue) === TraceOptions.ReturnValue) { messages.push(returnValueToLogString(returnValue)); @@ -88,7 +92,11 @@ function trace(logInfo: LogInfo) { const originalMethod = descriptor.value; // tslint:disable-next-line:no-function-expression no-any descriptor.value = function (...args: any[]) { - const className = _ && _.constructor ? _.constructor.name : ''; + const callInfo = { + kind: 'Class', + name: _ && _.constructor ? _.constructor.name : '', + args + }; // tslint:disable-next-line:no-any function writeSuccess(elapsedTime: number, returnValue: any) { if (logInfo.level === undefined || logInfo.level > LogLevel.Error) { @@ -100,7 +108,7 @@ function trace(logInfo: LogInfo) { } // tslint:disable-next-line:no-any function writeToLog(elapsedTime: number, returnValue?: any, ex?: Error) { - const formatted = formatMessages(logInfo, 'Class', className, elapsedTime, args, returnValue); + const formatted = formatMessages(logInfo, callInfo, elapsedTime, returnValue); if (ex) { log(LogLevel.Error, formatted, ex); // tslint:disable-next-line:no-any From 124d7a5e801bfd9eb8929a3775de90fd3160d05b Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 5 May 2020 13:06:03 -0600 Subject: [PATCH 10/30] Factor out TraceInfo. --- src/client/logging/_trace.ts | 55 ++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/client/logging/_trace.ts b/src/client/logging/_trace.ts index be5b238446b4..28742f465ada 100644 --- a/src/client/logging/_trace.ts +++ b/src/client/logging/_trace.ts @@ -64,24 +64,25 @@ type CallInfo = { args: any[]; }; -function formatMessages( - info: LogInfo, - call: CallInfo, - elapsed: number, +type TraceInfo = CallInfo & { + elapsed: number; // tslint:disable-next-line:no-any - returnValue?: any -): string { + returnValue?: any; + err?: Error; +}; + +function formatMessages(info: LogInfo, traced: TraceInfo): string { const messages = [info.message]; messages.push( - `${call.kind} name = ${call.name}`.trim(), - `completed in ${elapsed}ms`, - `has a ${returnValue ? 'truthy' : 'falsy'} return value` + `${traced.kind} name = ${traced.name}`.trim(), + `completed in ${traced.elapsed}ms`, + `has a ${traced.returnValue ? 'truthy' : 'falsy'} return value` ); if ((info.opts & TraceOptions.Arguments) === TraceOptions.Arguments) { - messages.push(argsToLogString(call.args)); + messages.push(argsToLogString(traced.args)); } if ((info.opts & TraceOptions.ReturnValue) === TraceOptions.ReturnValue) { - messages.push(returnValueToLogString(returnValue)); + messages.push(returnValueToLogString(traced.returnValue)); } return messages.join(', '); } @@ -92,27 +93,27 @@ function trace(logInfo: LogInfo) { const originalMethod = descriptor.value; // tslint:disable-next-line:no-function-expression no-any descriptor.value = function (...args: any[]) { - const callInfo = { + const call = { kind: 'Class', name: _ && _.constructor ? _.constructor.name : '', args }; // tslint:disable-next-line:no-any - function writeSuccess(elapsedTime: number, returnValue: any) { + function writeSuccess(info: TraceInfo) { if (logInfo.level === undefined || logInfo.level > LogLevel.Error) { - writeToLog(elapsedTime, returnValue); + writeToLog(info); } } - function writeError(elapsedTime: number, ex: Error) { - writeToLog(elapsedTime, undefined, ex); + function writeError(info: TraceInfo) { + writeToLog(info); } // tslint:disable-next-line:no-any - function writeToLog(elapsedTime: number, returnValue?: any, ex?: Error) { - const formatted = formatMessages(logInfo, callInfo, elapsedTime, returnValue); - if (ex) { - log(LogLevel.Error, formatted, ex); + function writeToLog(info: TraceInfo) { + const formatted = formatMessages(logInfo, info); + if (info.err) { + log(LogLevel.Error, formatted, info.err); // tslint:disable-next-line:no-any - sendTelemetryEvent('ERROR' as any, undefined, undefined, ex); + sendTelemetryEvent('ERROR' as any, undefined, undefined, info.err); } else { log(LogLevel.Info, formatted); } @@ -127,18 +128,22 @@ function trace(logInfo: LogInfo) { // tslint:disable-next-line:prefer-type-cast (result as Promise) .then((data) => { - writeSuccess(timer.elapsedTime, data); + const info = { ...call, elapsed: timer.elapsedTime, returnValue: data }; + writeSuccess(info); return data; }) .catch((ex) => { - writeError(timer.elapsedTime, ex); + const info = { ...call, elapsed: timer.elapsedTime, err: ex }; + writeError(info); }); } else { - writeSuccess(timer.elapsedTime, result); + const info = { ...call, elapsed: timer.elapsedTime, returnValue: result }; + writeSuccess(info); } return result; } catch (ex) { - writeError(timer.elapsedTime, ex); + const info = { ...call, elapsed: timer.elapsedTime, err: ex }; + writeError(info); throw ex; } }; From 28f2b66ad84e12248131603d6aa0226467a9180a Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 5 May 2020 13:16:22 -0600 Subject: [PATCH 11/30] Factor out logResult(). --- src/client/logging/_trace.ts | 46 ++++++++++++++---------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/src/client/logging/_trace.ts b/src/client/logging/_trace.ts index 28742f465ada..737925e1fb69 100644 --- a/src/client/logging/_trace.ts +++ b/src/client/logging/_trace.ts @@ -87,6 +87,20 @@ function formatMessages(info: LogInfo, traced: TraceInfo): string { return messages.join(', '); } +function logResult(info: LogInfo, traced: TraceInfo) { + const formatted = formatMessages(info, traced); + if (traced.err === undefined) { + // The call did not fail. + if (!info.level || info.level > LogLevel.Error) { + log(LogLevel.Info, formatted); + } + } else { + log(LogLevel.Error, formatted, traced.err); + // tslint:disable-next-line:no-any + sendTelemetryEvent('ERROR' as any, undefined, undefined, traced.err); + } +} + function trace(logInfo: LogInfo) { // tslint:disable-next-line:no-function-expression no-any return function (_: Object, __: string, descriptor: TypedPropertyDescriptor) { @@ -98,26 +112,6 @@ function trace(logInfo: LogInfo) { name: _ && _.constructor ? _.constructor.name : '', args }; - // tslint:disable-next-line:no-any - function writeSuccess(info: TraceInfo) { - if (logInfo.level === undefined || logInfo.level > LogLevel.Error) { - writeToLog(info); - } - } - function writeError(info: TraceInfo) { - writeToLog(info); - } - // tslint:disable-next-line:no-any - function writeToLog(info: TraceInfo) { - const formatted = formatMessages(logInfo, info); - if (info.err) { - log(LogLevel.Error, formatted, info.err); - // tslint:disable-next-line:no-any - sendTelemetryEvent('ERROR' as any, undefined, undefined, info.err); - } else { - log(LogLevel.Info, formatted); - } - } const timer = new StopWatch(); try { // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any @@ -128,22 +122,18 @@ function trace(logInfo: LogInfo) { // tslint:disable-next-line:prefer-type-cast (result as Promise) .then((data) => { - const info = { ...call, elapsed: timer.elapsedTime, returnValue: data }; - writeSuccess(info); + logResult(logInfo, { ...call, elapsed: timer.elapsedTime, returnValue: data }); return data; }) .catch((ex) => { - const info = { ...call, elapsed: timer.elapsedTime, err: ex }; - writeError(info); + logResult(logInfo, { ...call, elapsed: timer.elapsedTime, err: ex }); }); } else { - const info = { ...call, elapsed: timer.elapsedTime, returnValue: result }; - writeSuccess(info); + logResult(logInfo, { ...call, elapsed: timer.elapsedTime, returnValue: result }); } return result; } catch (ex) { - const info = { ...call, elapsed: timer.elapsedTime, err: ex }; - writeError(info); + logResult(logInfo, { ...call, elapsed: timer.elapsedTime, err: ex }); throw ex; } }; From 423326c3d9ed736078f76ae08dcc82d246144dfe Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 5 May 2020 13:31:13 -0600 Subject: [PATCH 12/30] Add isPromise(). --- src/client/common/utils/async.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/client/common/utils/async.ts b/src/client/common/utils/async.ts index 8ecd281a862d..030ac00b7c10 100644 --- a/src/client/common/utils/async.ts +++ b/src/client/common/utils/async.ts @@ -27,6 +27,16 @@ export async function waitForPromise(promise: Promise, timeout: number): P }); } +// tslint:disable-next-line: no-any +export function isThenable(v: any): v is Thenable { + return typeof v?.then === 'function'; +} + +// tslint:disable-next-line: no-any +export function isPromise(v: any): v is Promise { + return typeof v?.then === 'function' && typeof v?.catch === 'function'; +} + //====================== // Deferred From d9dbbca015d8e8f897a3d3d89f7c32dfc017987d Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 5 May 2020 13:38:41 -0600 Subject: [PATCH 13/30] Factor out withTrace(). --- src/client/logging/_trace.ts | 59 +++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/client/logging/_trace.ts b/src/client/logging/_trace.ts index 737925e1fb69..44ba040e6596 100644 --- a/src/client/logging/_trace.ts +++ b/src/client/logging/_trace.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. 'use strict'; +import { isPromise } from '../common/utils/async'; import { StopWatch } from '../common/utils/stopWatch'; import { sendTelemetryEvent } from '../telemetry'; import { LogLevel } from './levels'; @@ -101,41 +102,49 @@ function logResult(info: LogInfo, traced: TraceInfo) { } } +function withTrace(call: CallInfo, logInfo: LogInfo, run: () => T): T { + const timer = new StopWatch(); + try { + // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any + const result = run(); + + // If method being wrapped returns a promise then wait for it. + if (isPromise(result)) { + // tslint:disable-next-line:prefer-type-cast + (result as Promise) + .then((data) => { + logResult(logInfo, { ...call, elapsed: timer.elapsedTime, returnValue: data }); + return data; + }) + .catch((ex) => { + logResult(logInfo, { ...call, elapsed: timer.elapsedTime, err: ex }); + }); + } else { + logResult(logInfo, { ...call, elapsed: timer.elapsedTime, returnValue: result }); + } + return result; + } catch (ex) { + logResult(logInfo, { ...call, elapsed: timer.elapsedTime, err: ex }); + throw ex; + } +} + function trace(logInfo: LogInfo) { // tslint:disable-next-line:no-function-expression no-any return function (_: Object, __: string, descriptor: TypedPropertyDescriptor) { const originalMethod = descriptor.value; // tslint:disable-next-line:no-function-expression no-any descriptor.value = function (...args: any[]) { - const call = { + const info = { kind: 'Class', name: _ && _.constructor ? _.constructor.name : '', args }; - const timer = new StopWatch(); - try { - // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any - const result = originalMethod.apply(this, args); - // If method being wrapped returns a promise then wait for it. - // tslint:disable-next-line:no-unsafe-any - if (result && typeof result.then === 'function' && typeof result.catch === 'function') { - // tslint:disable-next-line:prefer-type-cast - (result as Promise) - .then((data) => { - logResult(logInfo, { ...call, elapsed: timer.elapsedTime, returnValue: data }); - return data; - }) - .catch((ex) => { - logResult(logInfo, { ...call, elapsed: timer.elapsedTime, err: ex }); - }); - } else { - logResult(logInfo, { ...call, elapsed: timer.elapsedTime, returnValue: result }); - } - return result; - } catch (ex) { - logResult(logInfo, { ...call, elapsed: timer.elapsedTime, err: ex }); - throw ex; - } + // tslint:disable-next-line:no-this-assignment no-invalid-this + const scope = this; + return withTrace(info, logInfo, () => { + return originalMethod.apply(scope, args); + }); }; return descriptor; From 3637a40cea136c8385b6067a2e641a94d3ff932f Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 5 May 2020 14:08:18 -0600 Subject: [PATCH 14/30] Move trace() into the traceDecorators namespace (not exported). --- src/client/logging/_trace.ts | 44 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/client/logging/_trace.ts b/src/client/logging/_trace.ts index 44ba040e6596..d8772d3d3142 100644 --- a/src/client/logging/_trace.ts +++ b/src/client/logging/_trace.ts @@ -33,6 +33,28 @@ export function traceWarning(...args: any[]) { export namespace traceDecorators { const DEFAULT_OPTS: TraceOptions = TraceOptions.Arguments | TraceOptions.ReturnValue; + function trace(logInfo: LogInfo) { + // tslint:disable-next-line:no-function-expression no-any + return function (_: Object, __: string, descriptor: TypedPropertyDescriptor) { + const originalMethod = descriptor.value; + // tslint:disable-next-line:no-function-expression no-any + descriptor.value = function (...args: any[]) { + const traced = { + kind: 'Class', + name: _ && _.constructor ? _.constructor.name : '', + args + }; + // tslint:disable-next-line:no-this-assignment no-invalid-this + const scope = this; + return withTrace(traced, logInfo, () => { + return originalMethod.apply(scope, args); + }); + }; + + return descriptor; + }; + } + export function verbose(message: string, opts: TraceOptions = DEFAULT_OPTS) { return trace({ message, opts }); } @@ -128,25 +150,3 @@ function withTrace(call: CallInfo, logInfo: LogInfo, run: () => T): T { throw ex; } } - -function trace(logInfo: LogInfo) { - // tslint:disable-next-line:no-function-expression no-any - return function (_: Object, __: string, descriptor: TypedPropertyDescriptor) { - const originalMethod = descriptor.value; - // tslint:disable-next-line:no-function-expression no-any - descriptor.value = function (...args: any[]) { - const info = { - kind: 'Class', - name: _ && _.constructor ? _.constructor.name : '', - args - }; - // tslint:disable-next-line:no-this-assignment no-invalid-this - const scope = this; - return withTrace(info, logInfo, () => { - return originalMethod.apply(scope, args); - }); - }; - - return descriptor; - }; -} From fff02488961c14ebe98d1aef19c4203b9e7164b4 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 5 May 2020 14:26:36 -0600 Subject: [PATCH 15/30] Factor out tracing() (i.e. a "context manager"). --- src/client/common/utils/misc.ts | 39 ++++++++++++++++++++++++++ src/client/logging/_trace.ts | 49 ++++++--------------------------- 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/client/common/utils/misc.ts b/src/client/common/utils/misc.ts index 1f21921fe6f6..dc3dbdc70e90 100644 --- a/src/client/common/utils/misc.ts +++ b/src/client/common/utils/misc.ts @@ -4,6 +4,8 @@ import { Uri } from 'vscode'; import { InterpreterUri } from '../installer/types'; import { IAsyncDisposable, IDisposable, Resource } from '../types'; +import { isPromise } from './async'; +import { StopWatch } from './stopWatch'; // tslint:disable-next-line:no-empty export function noop() {} @@ -38,6 +40,43 @@ export async function usingAsync( } } +// Information about a traced function/method call. +export type TraceInfo = { + elapsed: number; // milliseconds + // Either returnValue or err will be set. + // tslint:disable-next-line:no-any + returnValue?: any; + err?: Error; +}; + +// Call run(), call log() with the trace info, and return the result. +export function tracing(log: (t: TraceInfo) => void, run: () => T): T { + const timer = new StopWatch(); + try { + // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any + const result = run(); + + // If method being wrapped returns a promise then wait for it. + if (isPromise(result)) { + // tslint:disable-next-line:prefer-type-cast + (result as Promise) + .then((data) => { + log({ elapsed: timer.elapsedTime, returnValue: data }); + return data; + }) + .catch((ex) => { + log({ elapsed: timer.elapsedTime, err: ex }); + }); + } else { + log({ elapsed: timer.elapsedTime, returnValue: result }); + } + return result; + } catch (ex) { + log({ elapsed: timer.elapsedTime, err: ex }); + throw ex; + } +} + /** * Checking whether something is a Resource (Uri/undefined). * Using `instanceof Uri` doesn't always work as the object is not an instance of Uri (at least not in tests). diff --git a/src/client/logging/_trace.ts b/src/client/logging/_trace.ts index d8772d3d3142..a05b9f32d135 100644 --- a/src/client/logging/_trace.ts +++ b/src/client/logging/_trace.ts @@ -2,8 +2,7 @@ // Licensed under the MIT License. 'use strict'; -import { isPromise } from '../common/utils/async'; -import { StopWatch } from '../common/utils/stopWatch'; +import { TraceInfo, tracing } from '../common/utils/misc'; import { sendTelemetryEvent } from '../telemetry'; import { LogLevel } from './levels'; import { _log as log } from './logger'; @@ -39,14 +38,14 @@ export namespace traceDecorators { const originalMethod = descriptor.value; // tslint:disable-next-line:no-function-expression no-any descriptor.value = function (...args: any[]) { - const traced = { + const call = { kind: 'Class', name: _ && _.constructor ? _.constructor.name : '', args }; // tslint:disable-next-line:no-this-assignment no-invalid-this const scope = this; - return withTrace(traced, logInfo, () => { + return withTrace(call, logInfo, () => { return originalMethod.apply(scope, args); }); }; @@ -87,22 +86,15 @@ type CallInfo = { args: any[]; }; -type TraceInfo = CallInfo & { - elapsed: number; - // tslint:disable-next-line:no-any - returnValue?: any; - err?: Error; -}; - -function formatMessages(info: LogInfo, traced: TraceInfo): string { +function formatMessages(info: LogInfo, call: CallInfo, traced: TraceInfo): string { const messages = [info.message]; messages.push( - `${traced.kind} name = ${traced.name}`.trim(), + `${call.kind} name = ${call.name}`.trim(), `completed in ${traced.elapsed}ms`, `has a ${traced.returnValue ? 'truthy' : 'falsy'} return value` ); if ((info.opts & TraceOptions.Arguments) === TraceOptions.Arguments) { - messages.push(argsToLogString(traced.args)); + messages.push(argsToLogString(call.args)); } if ((info.opts & TraceOptions.ReturnValue) === TraceOptions.ReturnValue) { messages.push(returnValueToLogString(traced.returnValue)); @@ -110,8 +102,8 @@ function formatMessages(info: LogInfo, traced: TraceInfo): string { return messages.join(', '); } -function logResult(info: LogInfo, traced: TraceInfo) { - const formatted = formatMessages(info, traced); +function logResult(info: LogInfo, call: CallInfo, traced: TraceInfo) { + const formatted = formatMessages(info, call, traced); if (traced.err === undefined) { // The call did not fail. if (!info.level || info.level > LogLevel.Error) { @@ -125,28 +117,5 @@ function logResult(info: LogInfo, traced: TraceInfo) { } function withTrace(call: CallInfo, logInfo: LogInfo, run: () => T): T { - const timer = new StopWatch(); - try { - // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any - const result = run(); - - // If method being wrapped returns a promise then wait for it. - if (isPromise(result)) { - // tslint:disable-next-line:prefer-type-cast - (result as Promise) - .then((data) => { - logResult(logInfo, { ...call, elapsed: timer.elapsedTime, returnValue: data }); - return data; - }) - .catch((ex) => { - logResult(logInfo, { ...call, elapsed: timer.elapsedTime, err: ex }); - }); - } else { - logResult(logInfo, { ...call, elapsed: timer.elapsedTime, returnValue: result }); - } - return result; - } catch (ex) { - logResult(logInfo, { ...call, elapsed: timer.elapsedTime, err: ex }); - throw ex; - } + return tracing((traced) => logResult(logInfo, call, traced), run); } From d112de52fab2be95326a7d52d8e3db71482ac02f Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 5 May 2020 15:03:50 -0600 Subject: [PATCH 16/30] Factor out a trace() decorator factory. --- src/client/common/utils/decorators.ts | 35 +++++++++++++++++++++++++++ src/client/logging/_trace.ts | 34 +++----------------------- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/client/common/utils/decorators.ts b/src/client/common/utils/decorators.ts index e0e85bfded1b..512f31e65e19 100644 --- a/src/client/common/utils/decorators.ts +++ b/src/client/common/utils/decorators.ts @@ -8,6 +8,7 @@ import { traceError, traceVerbose } from '../logger'; import { Resource } from '../types'; import { createDeferred, Deferred } from './async'; import { getCacheKeyFromFunctionArgs, getGlobalCacheStore, InMemoryInterpreterSpecificCache } from './cacheUtils'; +import { TraceInfo, tracing } from './misc'; // tslint:disable-next-line:no-require-imports no-var-requires const _debounce = require('lodash/debounce') as typeof import('lodash/debounce'); @@ -225,3 +226,37 @@ export function displayProgress(title: string, location = ProgressLocation.Windo }; }; } + +// Information about a function/method call. +export type CallInfo = { + kind: string; // "Class", etc. + name: string; + // tslint:disable-next-line:no-any + args: any[]; +}; + +// Return a decorator that traces the decorated function. +export function trace(log: (c: CallInfo, t: TraceInfo) => void) { + // tslint:disable-next-line:no-function-expression no-any + return function (_: Object, __: string, descriptor: TypedPropertyDescriptor) { + const originalMethod = descriptor.value; + // tslint:disable-next-line:no-function-expression no-any + descriptor.value = function (...args: any[]) { + const call = { + kind: 'Class', + name: _ && _.constructor ? _.constructor.name : '', + args + }; + // tslint:disable-next-line:no-this-assignment no-invalid-this + const scope = this; + return tracing( + // "log()" + (t) => log(call, t), + // "run()" + () => originalMethod.apply(scope, args) + ); + }; + + return descriptor; + }; +} diff --git a/src/client/logging/_trace.ts b/src/client/logging/_trace.ts index a05b9f32d135..828c8cfcd7fc 100644 --- a/src/client/logging/_trace.ts +++ b/src/client/logging/_trace.ts @@ -2,7 +2,8 @@ // Licensed under the MIT License. 'use strict'; -import { TraceInfo, tracing } from '../common/utils/misc'; +import { CallInfo, trace as traceDecorator } from '../common/utils/decorators'; +import { TraceInfo, tracing as _tracing } from '../common/utils/misc'; import { sendTelemetryEvent } from '../telemetry'; import { LogLevel } from './levels'; import { _log as log } from './logger'; @@ -33,25 +34,7 @@ export namespace traceDecorators { const DEFAULT_OPTS: TraceOptions = TraceOptions.Arguments | TraceOptions.ReturnValue; function trace(logInfo: LogInfo) { - // tslint:disable-next-line:no-function-expression no-any - return function (_: Object, __: string, descriptor: TypedPropertyDescriptor) { - const originalMethod = descriptor.value; - // tslint:disable-next-line:no-function-expression no-any - descriptor.value = function (...args: any[]) { - const call = { - kind: 'Class', - name: _ && _.constructor ? _.constructor.name : '', - args - }; - // tslint:disable-next-line:no-this-assignment no-invalid-this - const scope = this; - return withTrace(call, logInfo, () => { - return originalMethod.apply(scope, args); - }); - }; - - return descriptor; - }; + return traceDecorator((call, traced) => logResult(logInfo, call, traced)); } export function verbose(message: string, opts: TraceOptions = DEFAULT_OPTS) { @@ -79,13 +62,6 @@ type LogInfo = { level?: LogLevel; }; -type CallInfo = { - kind: string; - name: string; - // tslint:disable-next-line:no-any - args: any[]; -}; - function formatMessages(info: LogInfo, call: CallInfo, traced: TraceInfo): string { const messages = [info.message]; messages.push( @@ -115,7 +91,3 @@ function logResult(info: LogInfo, call: CallInfo, traced: TraceInfo) { sendTelemetryEvent('ERROR' as any, undefined, undefined, traced.err); } } - -function withTrace(call: CallInfo, logInfo: LogInfo, run: () => T): T { - return tracing((traced) => logResult(logInfo, call, traced), run); -} From 318e6b237e5d0db97f8176d5065dd5f0b313d075 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 5 May 2020 16:14:22 -0600 Subject: [PATCH 17/30] Add normalizeCall(). --- src/client/logging/_trace.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/client/logging/_trace.ts b/src/client/logging/_trace.ts index 828c8cfcd7fc..0470490328aa 100644 --- a/src/client/logging/_trace.ts +++ b/src/client/logging/_trace.ts @@ -34,7 +34,7 @@ export namespace traceDecorators { const DEFAULT_OPTS: TraceOptions = TraceOptions.Arguments | TraceOptions.ReturnValue; function trace(logInfo: LogInfo) { - return traceDecorator((call, traced) => logResult(logInfo, call, traced)); + return traceDecorator((call, traced) => logResult(logInfo, traced, call)); } export function verbose(message: string, opts: TraceOptions = DEFAULT_OPTS) { @@ -62,7 +62,22 @@ type LogInfo = { level?: LogLevel; }; -function formatMessages(info: LogInfo, call: CallInfo, traced: TraceInfo): string { +function normalizeCall(call: CallInfo): CallInfo { + let { kind, name, args } = call; + if (!kind || kind === '') { + kind = 'Function'; + } + if (!name || name === '') { + name = ''; + } + if (!args) { + args = []; + } + return { kind, name, args }; +} + +function formatMessages(info: LogInfo, traced: TraceInfo, call?: CallInfo): string { + call = normalizeCall(call!); const messages = [info.message]; messages.push( `${call.kind} name = ${call.name}`.trim(), @@ -78,8 +93,8 @@ function formatMessages(info: LogInfo, call: CallInfo, traced: TraceInfo): strin return messages.join(', '); } -function logResult(info: LogInfo, call: CallInfo, traced: TraceInfo) { - const formatted = formatMessages(info, call, traced); +function logResult(info: LogInfo, traced: TraceInfo, call?: CallInfo) { + const formatted = formatMessages(info, traced, call); if (traced.err === undefined) { // The call did not fail. if (!info.level || info.level > LogLevel.Error) { From 52ceb8d99dbbf5c21d909425bf1d3170ae65d31d Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 5 May 2020 16:16:20 -0600 Subject: [PATCH 18/30] Add tracing() (i.e. a "context manager"). --- src/client/logging/_trace.ts | 5 +++++ src/client/logging/index.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/src/client/logging/_trace.ts b/src/client/logging/_trace.ts index 0470490328aa..0b3559e075dc 100644 --- a/src/client/logging/_trace.ts +++ b/src/client/logging/_trace.ts @@ -56,6 +56,11 @@ export namespace traceDecorators { } } +// This is like a "context manager" that logs tracing info. +export function tracing(logInfo: LogInfo, run: () => T, call?: CallInfo): T { + return _tracing((traced) => logResult(logInfo, traced, call), run); +} + type LogInfo = { opts: TraceOptions; message: string; diff --git a/src/client/logging/index.ts b/src/client/logging/index.ts index 6da41c569a13..e0913c57c818 100644 --- a/src/client/logging/index.ts +++ b/src/client/logging/index.ts @@ -8,6 +8,7 @@ export { traceDecorators, traceError, traceInfo, + tracing, traceVerbose, traceWarning } from './_trace'; From 8a3513f150ce6a54d340a5964cd312dc0526faa8 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 6 May 2020 12:59:55 -0600 Subject: [PATCH 19/30] Add a TODO about re-throwing the error. --- src/client/common/utils/misc.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client/common/utils/misc.ts b/src/client/common/utils/misc.ts index dc3dbdc70e90..972e197d3eba 100644 --- a/src/client/common/utils/misc.ts +++ b/src/client/common/utils/misc.ts @@ -66,6 +66,9 @@ export function tracing(log: (t: TraceInfo) => void, run: () => T): T { }) .catch((ex) => { log({ elapsed: timer.elapsedTime, err: ex }); + // tslint:disable-next-line:no-suspicious-comment + // TODO(GH-11645) Re-throw the error like we do + // in the non-Promise case. }); } else { log({ elapsed: timer.elapsedTime, returnValue: result }); From 3cd3ad2673837a2f6de32d0b3027ae9355a95720 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 6 May 2020 13:14:45 -0600 Subject: [PATCH 20/30] Use a more specific type assertion. --- src/client/logging/formatters.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/client/logging/formatters.ts b/src/client/logging/formatters.ts index 5051cd46374a..f67dc7b0b5e0 100644 --- a/src/client/logging/formatters.ts +++ b/src/client/logging/formatters.ts @@ -48,8 +48,8 @@ function getMinimalFormatter() { return format.combine( format.timestamp({ format: TIMESTAMP }), format.printf( - // tslint:disable-next-line:no-any - ({ level, message, timestamp }) => formatMessage(level as any, timestamp, message) + // This relies on the timestamp formatter we added above: + ({ level, message, timestamp }) => formatMessage(level as LogLevelName, timestamp, message) ) ); } @@ -61,8 +61,9 @@ function getLabeledFormatter(label_: string) { format.label({ label: label_ }), format.timestamp({ format: TIMESTAMP }), format.printf( - // tslint:disable-next-line:no-any - ({ level, message, label, timestamp }) => formatLabeledMessage(level as any, timestamp, label, message) + // This relies on the label and timestamp formatters we added above: + ({ level, message, label, timestamp }) => + formatLabeledMessage(level as LogLevelName, timestamp, label, message) ) ); } From 0748f29d934595ff9ada14a2cae9cb160ab5c0aa Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 6 May 2020 15:39:50 -0600 Subject: [PATCH 21/30] Split up initialize(). --- src/client/logging/logger.ts | 57 +++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/src/client/logging/logger.ts b/src/client/logging/logger.ts index 49d57815de17..2e9e0203dfa2 100644 --- a/src/client/logging/logger.ts +++ b/src/client/logging/logger.ts @@ -4,7 +4,8 @@ import * as util from 'util'; import * as winston from 'winston'; -import { isTestExecution } from '../common/constants'; +import * as Transport from 'winston-transport'; +import { isCI, isTestExecution } from '../common/constants'; import { logToConsole, monkeypatchConsole } from './_console'; import { getFormatter } from './formatters'; import { LogLevel, resolveLevelName } from './levels'; @@ -64,32 +65,46 @@ export function _log(logLevel: LogLevel, ...args: Arguments) { * this is setup on CI servers. */ function initialize() { - // Hijack `console.log` when running tests on CI. - if (process.env.VSC_PYTHON_LOG_FILE && process.env.TF_BUILD) { - function logToFile(logLevel: LogLevel, ...args: Arguments) { - log([fileLogger], logLevel, args); + if (process.env.VSC_PYTHON_LOG_FILE) { + initializeFileLogging(fileLogger, process.env.VSC_PYTHON_LOG_FILE); + if (isCI) { + hijackConsole(fileLogger); } - monkeypatchConsole(logToFile); } - // Do not log to console if running tests on CI and we're not + + // Do not log to console if running tests and we're not // asked to do so. if (!isTestExecution() || process.env.VSC_PYTHON_FORCE_LOGGING) { - // Rest of this stuff is just to instantiate the console logger. - // I.e. when we use our logger, ensure we also log to the - // console (for end users). - const formatter = getFormatter({ + if (isCI) { // In CI there's no need for the label. - label: process.env.TF_BUILD ? undefined : 'Python Extension:' - }); - const transport = getConsoleTransport(logToConsole, formatter); - // tslint:disable-next-line:no-any - consoleLogger.add(transport as any); + initializeConsoleLogging(consoleLogger); + } else { + initializeConsoleLogging(consoleLogger, 'Python Extension:'); + } } +} + +interface ILoggerTransports { + add(transport: Transport): void; +} - const logfile = process.env.VSC_PYTHON_LOG_FILE; - if (logfile) { - const formatter = getFormatter(); - const transport = getFileTransport(logfile, formatter); - fileLogger.add(transport); +function initializeFileLogging(logger: ILoggerTransports, logfile: string) { + const formatter = getFormatter(); + const transport = getFileTransport(logfile, formatter); + logger.add(transport); +} + +function hijackConsole(logger: ILogger) { + function logToFile(logLevel: LogLevel, ...args: Arguments) { + log([logger], logLevel, args); } + monkeypatchConsole(logToFile); +} + +function initializeConsoleLogging(logger: ILoggerTransports, label?: string) { + // When we use our logger, ensure we also log to the console (for end users). + const formatter = getFormatter({ label }); + const transport = getConsoleTransport(logToConsole, formatter); + // tslint:disable-next-line:no-any + logger.add(transport as any); } From 6aa479328e3db5974aac5d4111bc333b369ddc1b Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Thu, 7 May 2020 14:23:38 -0600 Subject: [PATCH 22/30] Move things out of types.ts. --- src/client/common/logger.ts | 2 +- src/client/logging/_trace.ts | 8 +++++++- src/client/logging/formatters.ts | 6 +++++- src/client/logging/index.ts | 5 +++-- src/client/logging/types.ts | 15 --------------- 5 files changed, 16 insertions(+), 20 deletions(-) delete mode 100644 src/client/logging/types.ts diff --git a/src/client/common/logger.ts b/src/client/common/logger.ts index f43a633ffd45..ac118305bdae 100644 --- a/src/client/common/logger.ts +++ b/src/client/common/logger.ts @@ -1,10 +1,10 @@ export { // aliases: // (temporarily for backward compatibility and to avoid churn) + TraceOptions as LogOptions, traceDecorators, traceError, traceInfo, traceVerbose, traceWarning } from '../logging/_trace'; -export { TraceOptions as LogOptions } from '../logging/types'; diff --git a/src/client/logging/_trace.ts b/src/client/logging/_trace.ts index 0b3559e075dc..97aa41b3d02d 100644 --- a/src/client/logging/_trace.ts +++ b/src/client/logging/_trace.ts @@ -7,9 +7,15 @@ import { TraceInfo, tracing as _tracing } from '../common/utils/misc'; import { sendTelemetryEvent } from '../telemetry'; import { LogLevel } from './levels'; import { _log as log } from './logger'; -import { TraceOptions } from './types'; import { argsToLogString, returnValueToLogString } from './util'; +// The information we want to log. +export enum TraceOptions { + None = 0, + Arguments = 1, + ReturnValue = 2 +} + // tslint:disable-next-line:no-any export function traceVerbose(...args: any[]) { log(LogLevel.Info, ...args); diff --git a/src/client/logging/formatters.ts b/src/client/logging/formatters.ts index f67dc7b0b5e0..b3dd4e52761f 100644 --- a/src/client/logging/formatters.ts +++ b/src/client/logging/formatters.ts @@ -4,10 +4,14 @@ import { format } from 'winston'; import { getLevel, LogLevel, LogLevelName } from './levels'; -import { FormatterOptions } from './types'; const TIMESTAMP = 'YYYY-MM-DD HH:mm:ss'; +// Knobs used when creating a formatter. +export type FormatterOptions = { + label?: string; +}; + // Pascal casing is used so log files get highlighted when viewing // in VSC and other editors. const formattedLogLevels: { [K in LogLevel]: string } = { diff --git a/src/client/logging/index.ts b/src/client/logging/index.ts index e0913c57c818..272c8dceffc7 100644 --- a/src/client/logging/index.ts +++ b/src/client/logging/index.ts @@ -5,10 +5,11 @@ export { // aliases // (for convenience) + TraceOptions, traceDecorators, traceError, traceInfo, - tracing, traceVerbose, - traceWarning + traceWarning, + tracing } from './_trace'; diff --git a/src/client/logging/types.ts b/src/client/logging/types.ts deleted file mode 100644 index b0490794b2f9..000000000000 --- a/src/client/logging/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -// Knobs used when creating a formatter. -export type FormatterOptions = { - label?: string; -}; - -// The information we want to log. -export enum TraceOptions { - None = 0, - Arguments = 1, - ReturnValue = 2 -} From 1bfa529dded3694c96875f2f065dbbeb1d3bfa90 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 6 May 2020 18:02:18 -0600 Subject: [PATCH 23/30] Store the console funcs on ConsoleTransport. --- src/client/logging/_console.ts | 2 +- src/client/logging/logger.ts | 4 ++-- src/client/logging/transports.ts | 40 +++++++++++++++++++++----------- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/client/logging/_console.ts b/src/client/logging/_console.ts index ab2500be600e..621354927bf0 100644 --- a/src/client/logging/_console.ts +++ b/src/client/logging/_console.ts @@ -31,7 +31,7 @@ const logMethods = { type Arguments = any[]; // Log a message based on "args" to the given console "stream". -export function logToConsole(level: LogLevel | undefined, ...args: Arguments) { +function logToConsole(level: LogLevel | undefined, ...args: Arguments) { const stream = (level ? streamByLevel[level] : undefined) || 'log'; // Further below we monkeypatch the console.log, etc methods. // tslint:disable-next-line:no-any diff --git a/src/client/logging/logger.ts b/src/client/logging/logger.ts index 2e9e0203dfa2..4739aa5d4081 100644 --- a/src/client/logging/logger.ts +++ b/src/client/logging/logger.ts @@ -6,7 +6,7 @@ import * as util from 'util'; import * as winston from 'winston'; import * as Transport from 'winston-transport'; import { isCI, isTestExecution } from '../common/constants'; -import { logToConsole, monkeypatchConsole } from './_console'; +import { monkeypatchConsole } from './_console'; import { getFormatter } from './formatters'; import { LogLevel, resolveLevelName } from './levels'; import { getConsoleTransport, getFileTransport } from './transports'; @@ -104,7 +104,7 @@ function hijackConsole(logger: ILogger) { function initializeConsoleLogging(logger: ILoggerTransports, label?: string) { // When we use our logger, ensure we also log to the console (for end users). const formatter = getFormatter({ label }); - const transport = getConsoleTransport(logToConsole, formatter); + const transport = getConsoleTransport(formatter); // tslint:disable-next-line:no-any logger.add(transport as any); } diff --git a/src/client/logging/transports.ts b/src/client/logging/transports.ts index 657d6892c281..098f23bc6b78 100644 --- a/src/client/logging/transports.ts +++ b/src/client/logging/transports.ts @@ -5,19 +5,29 @@ import * as logform from 'logform'; import * as path from 'path'; import * as winston from 'winston'; +import * as Transport from 'winston-transport'; import { EXTENSION_ROOT_DIR } from '../constants'; import { LogLevel, resolveLevel } from './levels'; -// tslint:disable-next-line: no-var-requires no-require-imports -const TransportStream = require('winston-transport'); - const formattedMessage = Symbol.for('message'); // A winston-compatible transport type. -class ConsoleTransport extends TransportStream { +// We do not use transports.ConsoleTransport because it cannot +// adapt to our custom log levels very well. +class ConsoleTransport extends Transport { + // tslint:disable:no-console + // tslint:disable-next-line:no-any + private static funcByLevel: { [K in LogLevel]: (...args: any[]) => void } = { + [LogLevel.Error]: console.error, + [LogLevel.Warn]: console.warn, + [LogLevel.Info]: console.info, + [LogLevel.Debug]: console.debug, + [LogLevel.Trace]: console.trace + }; + private static defaultFunc = console.log; + // tslint:enable:no-console + constructor( - // tslint:disable-next-line:no-any - private readonly logToConsole: (level: LogLevel | undefined, ...args: any[]) => void, // tslint:disable-next-line:no-any options?: any, private readonly levels?: winston.config.AbstractConfigSetLevels @@ -35,22 +45,26 @@ class ConsoleTransport extends TransportStream { next(); } } + + private logToConsole(level?: LogLevel, msg?: string) { + let func = ConsoleTransport.defaultFunc; + if (level) { + func = ConsoleTransport.funcByLevel[level] || func; + } + func(msg); + } } // Create a console-targeting transport that can be added to a winston logger. -export function getConsoleTransport( - // tslint:disable-next-line:no-any - logToConsole: (level: LogLevel | undefined, ...args: any[]) => void, - formatter: logform.Format -) { - return new ConsoleTransport(logToConsole, { +export function getConsoleTransport(formatter: logform.Format): Transport { + return new ConsoleTransport({ // We minimize customization. format: formatter }); } // Create a file-targeting transport that can be added to a winston logger. -export function getFileTransport(logfile: string, formatter: logform.Format) { +export function getFileTransport(logfile: string, formatter: logform.Format): Transport { if (!path.isAbsolute(logfile)) { logfile = path.join(EXTENSION_ROOT_DIR, logfile); } From 5b54214969c88bde297f23aac82f643af261bba3 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Thu, 7 May 2020 14:36:19 -0600 Subject: [PATCH 24/30] Simplify console logging. --- src/client/logging/_console.ts | 62 +++++----------------------------- src/client/logging/logger.ts | 10 +++--- 2 files changed, 13 insertions(+), 59 deletions(-) diff --git a/src/client/logging/_console.ts b/src/client/logging/_console.ts index 621354927bf0..e4b2e999cf0d 100644 --- a/src/client/logging/_console.ts +++ b/src/client/logging/_console.ts @@ -6,39 +6,9 @@ import { LogLevel } from './levels'; -// The logging "streams" (methods) of the node console. -type ConsoleStream = 'log' | 'error' | 'warn' | 'info' | 'debug' | 'trace'; - -// Convert from LogLevel enum to node console "stream" (logger method). -const streamByLevel: { [K in LogLevel]: ConsoleStream } = { - [LogLevel.Error]: 'error', - [LogLevel.Warn]: 'warn', - [LogLevel.Info]: 'info', - [LogLevel.Debug]: 'debug', - [LogLevel.Trace]: 'trace' -}; - -const logMethods = { - log: Symbol.for('log'), - error: Symbol.for('error'), - warn: Symbol.for('warn'), - info: Symbol.for('info'), - debug: Symbol.for('debug'), - trace: Symbol.for('trace') -}; - // tslint:disable-next-line:no-any type Arguments = any[]; -// Log a message based on "args" to the given console "stream". -function logToConsole(level: LogLevel | undefined, ...args: Arguments) { - const stream = (level ? streamByLevel[level] : undefined) || 'log'; - // Further below we monkeypatch the console.log, etc methods. - // tslint:disable-next-line:no-any - const fn = (console as any)[logMethods[stream]] || console[stream] || console.log; - fn(...args); -} - /** * What we're doing here is monkey patching the console.log so we can * send everything sent to console window into our logs. This is only @@ -47,52 +17,36 @@ function logToConsole(level: LogLevel | undefined, ...args: Arguments) { * on CI so we can see everything logged to the console window * (via the logs). */ -export function monkeypatchConsole(logToFile: (logLevel: LogLevel, ...args: Arguments) => void) { - // Keep track of the original functions before we monkey patch them. - // Using symbols guarantee the properties will be unique & prevents clashing with names other code/library may create or have created. - // tslint:disable:no-any - (console as any)[logMethods.log] = console.log; - (console as any)[logMethods.info] = console.info; - (console as any)[logMethods.error] = console.error; - (console as any)[logMethods.debug] = console.debug; - (console as any)[logMethods.trace] = console.trace; - (console as any)[logMethods.warn] = console.warn; - // tslint:enable:no-any - +// tslint:disable-next-line:no-any +export function monkeypatchConsole(log: (logLevel: LogLevel, ...args: Arguments) => void) { // tslint:disable-next-line: no-function-expression console.log = function () { const args = Array.prototype.slice.call(arguments); - logToConsole(undefined, ...args); - logToFile(LogLevel.Info, ...args); + log((undefined as unknown) as LogLevel, ...args); }; // tslint:disable-next-line: no-function-expression console.info = function () { const args = Array.prototype.slice.call(arguments); - logToConsole(LogLevel.Info, ...args); - logToFile(LogLevel.Info, ...args); + log(LogLevel.Info, ...args); }; // tslint:disable-next-line: no-function-expression console.warn = function () { const args = Array.prototype.slice.call(arguments); - logToConsole(LogLevel.Warn, ...args); - logToFile(LogLevel.Warn, ...args); + log(LogLevel.Warn, ...args); }; // tslint:disable-next-line: no-function-expression console.error = function () { const args = Array.prototype.slice.call(arguments); - logToConsole(LogLevel.Error, ...args); - logToFile(LogLevel.Error, ...args); + log(LogLevel.Error, ...args); }; // tslint:disable-next-line: no-function-expression console.debug = function () { const args = Array.prototype.slice.call(arguments); - logToConsole(LogLevel.Debug, ...args); - logToFile(LogLevel.Info, ...args); + log(LogLevel.Info, ...args); }; // tslint:disable-next-line: no-function-expression console.trace = function () { const args = Array.prototype.slice.call(arguments); - logToConsole(LogLevel.Trace, ...args); - logToFile(LogLevel.Info, ...args); + log(LogLevel.Info, ...args); }; } diff --git a/src/client/logging/logger.ts b/src/client/logging/logger.ts index 4739aa5d4081..c4ded514db2c 100644 --- a/src/client/logging/logger.ts +++ b/src/client/logging/logger.ts @@ -68,7 +68,7 @@ function initialize() { if (process.env.VSC_PYTHON_LOG_FILE) { initializeFileLogging(fileLogger, process.env.VSC_PYTHON_LOG_FILE); if (isCI) { - hijackConsole(fileLogger); + hijackConsole(consoleLogger, fileLogger); } } @@ -94,11 +94,11 @@ function initializeFileLogging(logger: ILoggerTransports, logfile: string) { logger.add(transport); } -function hijackConsole(logger: ILogger) { - function logToFile(logLevel: LogLevel, ...args: Arguments) { - log([logger], logLevel, args); +function hijackConsole(...loggers: ILogger[]) { + function logToAll(logLevel: LogLevel, ...args: Arguments) { + log(loggers, logLevel, args); } - monkeypatchConsole(logToFile); + monkeypatchConsole(logToAll); } function initializeConsoleLogging(logger: ILoggerTransports, label?: string) { From 33760d05eb96aad7439abfda7804e1b264d20f72 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Thu, 7 May 2020 16:56:43 -0600 Subject: [PATCH 25/30] Simplify better. --- src/client/logging/_console.ts | 64 +++++++++++++++++----------------- src/client/logging/logger.ts | 18 ++++++++-- 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/client/logging/_console.ts b/src/client/logging/_console.ts index e4b2e999cf0d..6e07ef967751 100644 --- a/src/client/logging/_console.ts +++ b/src/client/logging/_console.ts @@ -5,9 +5,18 @@ // tslint:disable:no-console import { LogLevel } from './levels'; +// Ensure that the console functions are bound before monkeypatching. +import './transports'; -// tslint:disable-next-line:no-any -type Arguments = any[]; +// The logging "streams" (methods) of the node console. +enum ConsoleStream { + Log = 'log', + Error = 'error', + Warn = 'warn', + Info = 'info', + Debug = 'debug', + Trace = 'trace' +} /** * What we're doing here is monkey patching the console.log so we can @@ -18,35 +27,26 @@ type Arguments = any[]; * (via the logs). */ // tslint:disable-next-line:no-any -export function monkeypatchConsole(log: (logLevel: LogLevel, ...args: Arguments) => void) { - // tslint:disable-next-line: no-function-expression - console.log = function () { - const args = Array.prototype.slice.call(arguments); - log((undefined as unknown) as LogLevel, ...args); - }; - // tslint:disable-next-line: no-function-expression - console.info = function () { - const args = Array.prototype.slice.call(arguments); - log(LogLevel.Info, ...args); - }; - // tslint:disable-next-line: no-function-expression - console.warn = function () { - const args = Array.prototype.slice.call(arguments); - log(LogLevel.Warn, ...args); - }; - // tslint:disable-next-line: no-function-expression - console.error = function () { - const args = Array.prototype.slice.call(arguments); - log(LogLevel.Error, ...args); - }; - // tslint:disable-next-line: no-function-expression - console.debug = function () { - const args = Array.prototype.slice.call(arguments); - log(LogLevel.Info, ...args); - }; - // tslint:disable-next-line: no-function-expression - console.trace = function () { - const args = Array.prototype.slice.call(arguments); - log(LogLevel.Info, ...args); +export function monkeypatchConsole(log: (logLevel: LogLevel, ...args: any[]) => void) { + const levels: { [key: string]: LogLevel } = { + error: LogLevel.Error, + warn: LogLevel.Warn }; + // tslint:disable-next-line:no-any + const consoleAny: any = console; + for (const stream of Object.values(ConsoleStream)) { + // Using symbols guarantee the properties will be unique & prevents + // clashing with names other code/library may create or have created. + // We could use a closure but it's a bit trickier. + const sym = Symbol.for(stream); + consoleAny[sym] = consoleAny[stream]; + // tslint:disable-next-line: no-function-expression + consoleAny[stream] = function () { + const args = Array.prototype.slice.call(arguments); + const fn = consoleAny[sym]; + fn(...args); + const level = levels[stream] || LogLevel.Info; + log(level, ...args); + }; + } } diff --git a/src/client/logging/logger.ts b/src/client/logging/logger.ts index c4ded514db2c..7b67b4d3a7a2 100644 --- a/src/client/logging/logger.ts +++ b/src/client/logging/logger.ts @@ -25,12 +25,24 @@ interface ILogger { log(level: string, message: string): void; } +function getLevelName(level: LogLevel, logger: ILogger): string { + const levelName = resolveLevelName(level, logger.levels); + if (levelName) { + return levelName; + } else if (logger === consoleLogger) { + // XXX Hard-coding this is fragile: + return 'silly'; + } else { + return resolveLevelName(LogLevel.Info, logger.levels) || 'info'; + } +} + // Emit a log message derived from the args to all enabled transports. function log(loggers: ILogger[], logLevel: LogLevel, args: Arguments) { for (const logger of loggers) { if (logger.transports.length > 0) { const message = args.length === 0 ? '' : util.format(args[0], ...args.slice(1)); - const levelName: string = resolveLevelName(logLevel, logger.levels) || ''; + const levelName = getLevelName(logLevel, logger); logger.log(levelName, message); } } @@ -68,7 +80,7 @@ function initialize() { if (process.env.VSC_PYTHON_LOG_FILE) { initializeFileLogging(fileLogger, process.env.VSC_PYTHON_LOG_FILE); if (isCI) { - hijackConsole(consoleLogger, fileLogger); + sendConsoleToLogger(fileLogger); } } @@ -94,7 +106,7 @@ function initializeFileLogging(logger: ILoggerTransports, logfile: string) { logger.add(transport); } -function hijackConsole(...loggers: ILogger[]) { +function sendConsoleToLogger(...loggers: ILogger[]) { function logToAll(logLevel: LogLevel, ...args: Arguments) { log(loggers, logLevel, args); } From b7a6e5127a5611da21a8d617f36e18e6ae295ea4 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Thu, 7 May 2020 17:07:58 -0600 Subject: [PATCH 26/30] Compose the log message just once. --- src/client/logging/_console.ts | 14 +++----------- src/client/logging/logger.ts | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/client/logging/_console.ts b/src/client/logging/_console.ts index 6e07ef967751..fbb0d826a7a6 100644 --- a/src/client/logging/_console.ts +++ b/src/client/logging/_console.ts @@ -8,16 +8,6 @@ import { LogLevel } from './levels'; // Ensure that the console functions are bound before monkeypatching. import './transports'; -// The logging "streams" (methods) of the node console. -enum ConsoleStream { - Log = 'log', - Error = 'error', - Warn = 'warn', - Info = 'info', - Debug = 'debug', - Trace = 'trace' -} - /** * What we're doing here is monkey patching the console.log so we can * send everything sent to console window into our logs. This is only @@ -28,13 +18,15 @@ enum ConsoleStream { */ // tslint:disable-next-line:no-any export function monkeypatchConsole(log: (logLevel: LogLevel, ...args: any[]) => void) { + // The logging "streams" (methods) of the node console. + const streams = ['log', 'error', 'warn', 'info', 'debug', 'trace']; const levels: { [key: string]: LogLevel } = { error: LogLevel.Error, warn: LogLevel.Warn }; // tslint:disable-next-line:no-any const consoleAny: any = console; - for (const stream of Object.values(ConsoleStream)) { + for (const stream of streams) { // Using symbols guarantee the properties will be unique & prevents // clashing with names other code/library may create or have created. // We could use a closure but it's a bit trickier. diff --git a/src/client/logging/logger.ts b/src/client/logging/logger.ts index 7b67b4d3a7a2..8a585670d2f2 100644 --- a/src/client/logging/logger.ts +++ b/src/client/logging/logger.ts @@ -39,9 +39,9 @@ function getLevelName(level: LogLevel, logger: ILogger): string { // Emit a log message derived from the args to all enabled transports. function log(loggers: ILogger[], logLevel: LogLevel, args: Arguments) { + const message = args.length === 0 ? '' : util.format(args[0], ...args.slice(1)); for (const logger of loggers) { if (logger.transports.length > 0) { - const message = args.length === 0 ? '' : util.format(args[0], ...args.slice(1)); const levelName = getLevelName(logLevel, logger); logger.log(levelName, message); } From cc6c1c4d6af2b5be05cbbbc28a4c5a2ede8c1b47 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Thu, 7 May 2020 17:19:04 -0600 Subject: [PATCH 27/30] Alias from the "public" namespace. --- src/client/common/logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/common/logger.ts b/src/client/common/logger.ts index ac118305bdae..1b3d53b8b1e8 100644 --- a/src/client/common/logger.ts +++ b/src/client/common/logger.ts @@ -7,4 +7,4 @@ export { traceInfo, traceVerbose, traceWarning -} from '../logging/_trace'; +} from '../logging'; From 9ecbff74b6488b029da657deab362e7fb62ea13f Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Thu, 7 May 2020 19:39:53 -0600 Subject: [PATCH 28/30] Factor out _global.ts. --- src/client/common/logger.ts | 14 +- src/client/logging/_console.ts | 4 +- src/client/logging/_global.ts | 133 ++++++++++++++++++ src/client/logging/index.ts | 11 +- src/client/logging/logger.ts | 154 +++++++++------------ src/client/logging/{_trace.ts => trace.ts} | 60 ++------ src/client/logging/transports.ts | 12 +- src/client/logging/util.ts | 2 +- 8 files changed, 231 insertions(+), 159 deletions(-) create mode 100644 src/client/logging/_global.ts rename src/client/logging/{_trace.ts => trace.ts} (53%) diff --git a/src/client/common/logger.ts b/src/client/common/logger.ts index 1b3d53b8b1e8..d332dd6028e5 100644 --- a/src/client/common/logger.ts +++ b/src/client/common/logger.ts @@ -1,10 +1,10 @@ +// These are all just temporary aliases, for backward compatibility +// and to avoid churn. export { - // aliases: - // (temporarily for backward compatibility and to avoid churn) - TraceOptions as LogOptions, traceDecorators, - traceError, - traceInfo, - traceVerbose, - traceWarning + logError as traceError, + logInfo as traceInfo, + logVerbose as traceVerbose, + logWarning as traceWarning } from '../logging'; +export { TraceOptions as LogOptions } from '../logging/trace'; diff --git a/src/client/logging/_console.ts b/src/client/logging/_console.ts index fbb0d826a7a6..f37758012112 100644 --- a/src/client/logging/_console.ts +++ b/src/client/logging/_console.ts @@ -7,6 +7,7 @@ import { LogLevel } from './levels'; // Ensure that the console functions are bound before monkeypatching. import './transports'; +import { Arguments } from './util'; /** * What we're doing here is monkey patching the console.log so we can @@ -16,8 +17,7 @@ import './transports'; * on CI so we can see everything logged to the console window * (via the logs). */ -// tslint:disable-next-line:no-any -export function monkeypatchConsole(log: (logLevel: LogLevel, ...args: any[]) => void) { +export function monkeypatchConsole(log: (logLevel: LogLevel, ...args: Arguments) => void) { // The logging "streams" (methods) of the node console. const streams = ['log', 'error', 'warn', 'info', 'debug', 'trace']; const levels: { [key: string]: LogLevel } = { diff --git a/src/client/logging/_global.ts b/src/client/logging/_global.ts new file mode 100644 index 000000000000..bd932a143abe --- /dev/null +++ b/src/client/logging/_global.ts @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { isCI, isTestExecution } from '../common/constants'; +import { CallInfo } from '../common/utils/decorators'; +import { monkeypatchConsole } from './_console'; +import { LogLevel } from './levels'; +import { configureLogger, createLogger, ILogger, LoggerConfig, logToAll } from './logger'; +import { createTracingDecorator, LogInfo, TraceOptions, tracing as _tracing } from './trace'; +import { Arguments } from './util'; + +const consoleLogger = createLogger(); +const fileLogger = createLogger(); +initialize(); + +/** + * Initialize the logger. + * + * For console we do two things here: + * - Anything written to the logger will be displayed in the console + * window as well This is the behavior of the extension when running + * it. When running tests on CI, we might not want this behavior, as + * it'll pollute the test output with logging (as mentioned this is + * optional). Messages logged using our logger will be prefixed with + * `Python Extension: ....` for console window. This way, its easy + * to identify messages specific to the python extension. + * - Monkey patch the console.log and similar methods to send messages + * to the file logger. When running UI tests or similar, and we want + * to see everything that was dumped into `console window`, then we + * need to hijack the console logger. To do this we need to monkey + * patch the console methods. This is optional (generally done when + * running tests on CI). + * + * For the logfile: + * - we send all logging output to a log file. We log to the file + * only if a file has been specified as an env variable. Currently + * this is setup on CI servers. + */ +function initialize() { + const config: LoggerConfig = {}; + let nonConsole = false; + + // Do not log to console if running tests and we're not + // asked to do so. + if (!isTestExecution() || process.env.VSC_PYTHON_FORCE_LOGGING) { + config.console = {}; + // In CI there's no need for the label. + if (!isCI) { + config.console.label = 'Python Extension:'; + } + } + configureLogger(consoleLogger, config); + delete config.console; + if (process.env.VSC_PYTHON_LOG_FILE) { + config.file = { + logfile: process.env.VSC_PYTHON_LOG_FILE + }; + nonConsole = true; + } + configureLogger(fileLogger, config); + + if (isCI && nonConsole) { + delete config.console; + // Send console.*() to the non-console loggers. + sendConsoleToLogger( + // This is a separate logger that matches our config but + // does not do any console logging. + createLogger(config) + ); + } +} + +const loggers = [consoleLogger, fileLogger]; + +function sendConsoleToLogger(logger: ILogger) { + function _log(logLevel: LogLevel, ...args: Arguments) { + logToAll([logger], logLevel, args); + } + monkeypatchConsole(_log); +} + +// Emit a log message derived from the args to all enabled transports. +export function log(logLevel: LogLevel, ...args: Arguments) { + logToAll(loggers, logLevel, args); +} + +// tslint:disable-next-line:no-any +export function logVerbose(...args: any[]) { + log(LogLevel.Info, ...args); +} + +// tslint:disable-next-line:no-any +export function logError(...args: any[]) { + log(LogLevel.Error, ...args); +} + +// tslint:disable-next-line:no-any +export function logInfo(...args: any[]) { + log(LogLevel.Info, ...args); +} + +// tslint:disable-next-line:no-any +export function logWarning(...args: any[]) { + log(LogLevel.Warn, ...args); +} + +// This is like a "context manager" that logs tracing info. +export function tracing(info: LogInfo, run: () => T, call?: CallInfo): T { + return _tracing(loggers, info, run, call); +} + +export namespace traceDecorators { + const DEFAULT_OPTS: TraceOptions = TraceOptions.Arguments | TraceOptions.ReturnValue; + + export function verbose(message: string, opts: TraceOptions = DEFAULT_OPTS) { + return createTracingDecorator(loggers, { message, opts }); + } + export function error(message: string) { + const opts = DEFAULT_OPTS; + const level = LogLevel.Error; + return createTracingDecorator(loggers, { message, opts, level }); + } + export function info(message: string) { + const opts = TraceOptions.None; + return createTracingDecorator(loggers, { message, opts }); + } + export function warn(message: string) { + const opts = DEFAULT_OPTS; + const level = LogLevel.Warn; + return createTracingDecorator(loggers, { message, opts, level }); + } +} diff --git a/src/client/logging/index.ts b/src/client/logging/index.ts index 272c8dceffc7..19dc23d0bfa6 100644 --- a/src/client/logging/index.ts +++ b/src/client/logging/index.ts @@ -5,11 +5,10 @@ export { // aliases // (for convenience) - TraceOptions, + logError, + logInfo, + logVerbose, + logWarning, traceDecorators, - traceError, - traceInfo, - traceVerbose, - traceWarning, tracing -} from './_trace'; +} from './_global'; diff --git a/src/client/logging/logger.ts b/src/client/logging/logger.ts index 8a585670d2f2..f6d29e83b9b9 100644 --- a/src/client/logging/logger.ts +++ b/src/client/logging/logger.ts @@ -5,118 +5,92 @@ import * as util from 'util'; import * as winston from 'winston'; import * as Transport from 'winston-transport'; -import { isCI, isTestExecution } from '../common/constants'; -import { monkeypatchConsole } from './_console'; import { getFormatter } from './formatters'; import { LogLevel, resolveLevelName } from './levels'; -import { getConsoleTransport, getFileTransport } from './transports'; +import { getConsoleTransport, getFileTransport, isConsoleTransport } from './transports'; +import { Arguments } from './util'; -// Initialize the loggers as soon as this module is imported. -const consoleLogger = winston.createLogger(); -const fileLogger = winston.createLogger(); -initialize(); +export type LoggerConfig = { + level?: LogLevel; + file?: { + logfile: string; + }; + console?: { + label?: string; + }; +}; -// tslint:disable-next-line:no-any -type Arguments = any[]; +// Create a logger just the way we like it. +export function createLogger(config?: LoggerConfig) { + const logger = winston.createLogger({ + // We would also set "levels" here. + }); + if (config) { + configureLogger(logger, config); + } + return logger; +} -interface ILogger { - transports: unknown[]; - levels: winston.config.AbstractConfigSetLevels; - log(level: string, message: string): void; +interface IConfigurableLogger { + level: string; + add(transport: Transport): void; } -function getLevelName(level: LogLevel, logger: ILogger): string { - const levelName = resolveLevelName(level, logger.levels); - if (levelName) { - return levelName; - } else if (logger === consoleLogger) { - // XXX Hard-coding this is fragile: - return 'silly'; - } else { - return resolveLevelName(LogLevel.Info, logger.levels) || 'info'; +// Set up a logger just the way we like it. +export function configureLogger(logger: IConfigurableLogger, config: LoggerConfig) { + if (config.level) { + const levelName = resolveLevelName(config.level); + if (levelName) { + logger.level = levelName; + } + } + + if (config.file) { + const formatter = getFormatter(); + const transport = getFileTransport(config.file.logfile, formatter); + logger.add(transport); + } + if (config.console) { + const formatter = getFormatter({ label: config.console.label }); + const transport = getConsoleTransport(formatter); + logger.add(transport); } } +export interface ILogger { + transports: unknown[]; + levels: winston.config.AbstractConfigSetLevels; + log(level: string, message: string): void; +} + // Emit a log message derived from the args to all enabled transports. -function log(loggers: ILogger[], logLevel: LogLevel, args: Arguments) { +export function logToAll(loggers: ILogger[], logLevel: LogLevel, args: Arguments) { const message = args.length === 0 ? '' : util.format(args[0], ...args.slice(1)); for (const logger of loggers) { if (logger.transports.length > 0) { - const levelName = getLevelName(logLevel, logger); + const levelName = getLevelName(logLevel, logger.levels, isConsoleLogger(logger)); logger.log(levelName, message); } } } -// Emit a log message derived from the args to all enabled transports. -export function _log(logLevel: LogLevel, ...args: Arguments) { - log([consoleLogger, fileLogger], logLevel, args); -} - -/** - * Initialize the logger. - * - * For console we do two things here: - * - Anything written to the logger will be displayed in the console - * window as well This is the behavior of the extension when running - * it. When running tests on CI, we might not want this behavior, as - * it'll pollute the test output with logging (as mentioned this is - * optional). Messages logged using our logger will be prefixed with - * `Python Extension: ....` for console window. This way, its easy - * to identify messages specific to the python extension. - * - Monkey patch the console.log and similar methods to send messages - * to the file logger. When running UI tests or similar, and we want - * to see everything that was dumped into `console window`, then we - * need to hijack the console logger. To do this we need to monkey - * patch the console methods. This is optional (generally done when - * running tests on CI). - * - * For the logfile: - * - we send all logging output to a log file. We log to the file - * only if a file has been specified as an env variable. Currently - * this is setup on CI servers. - */ -function initialize() { - if (process.env.VSC_PYTHON_LOG_FILE) { - initializeFileLogging(fileLogger, process.env.VSC_PYTHON_LOG_FILE); - if (isCI) { - sendConsoleToLogger(fileLogger); - } - } - - // Do not log to console if running tests and we're not - // asked to do so. - if (!isTestExecution() || process.env.VSC_PYTHON_FORCE_LOGGING) { - if (isCI) { - // In CI there's no need for the label. - initializeConsoleLogging(consoleLogger); - } else { - initializeConsoleLogging(consoleLogger, 'Python Extension:'); +function isConsoleLogger(logger: ILogger): boolean { + for (const transport of logger.transports) { + if (isConsoleTransport(transport)) { + return true; } } + return false; } -interface ILoggerTransports { - add(transport: Transport): void; -} - -function initializeFileLogging(logger: ILoggerTransports, logfile: string) { - const formatter = getFormatter(); - const transport = getFileTransport(logfile, formatter); - logger.add(transport); -} - -function sendConsoleToLogger(...loggers: ILogger[]) { - function logToAll(logLevel: LogLevel, ...args: Arguments) { - log(loggers, logLevel, args); +function getLevelName(level: LogLevel, levels: winston.config.AbstractConfigSetLevels, isConsole?: boolean): string { + const levelName = resolveLevelName(level, levels); + if (levelName) { + return levelName; + } else if (isConsole) { + // XXX Hard-coding this is fragile: + return 'silly'; + } else { + return resolveLevelName(LogLevel.Info, levels) || 'info'; } - monkeypatchConsole(logToAll); -} - -function initializeConsoleLogging(logger: ILoggerTransports, label?: string) { - // When we use our logger, ensure we also log to the console (for end users). - const formatter = getFormatter({ label }); - const transport = getConsoleTransport(formatter); - // tslint:disable-next-line:no-any - logger.add(transport as any); } diff --git a/src/client/logging/_trace.ts b/src/client/logging/trace.ts similarity index 53% rename from src/client/logging/_trace.ts rename to src/client/logging/trace.ts index 97aa41b3d02d..3191df5bd9d7 100644 --- a/src/client/logging/_trace.ts +++ b/src/client/logging/trace.ts @@ -6,7 +6,7 @@ import { CallInfo, trace as traceDecorator } from '../common/utils/decorators'; import { TraceInfo, tracing as _tracing } from '../common/utils/misc'; import { sendTelemetryEvent } from '../telemetry'; import { LogLevel } from './levels'; -import { _log as log } from './logger'; +import { ILogger, logToAll } from './logger'; import { argsToLogString, returnValueToLogString } from './util'; // The information we want to log. @@ -16,58 +16,16 @@ export enum TraceOptions { ReturnValue = 2 } -// tslint:disable-next-line:no-any -export function traceVerbose(...args: any[]) { - log(LogLevel.Info, ...args); -} - -// tslint:disable-next-line:no-any -export function traceError(...args: any[]) { - log(LogLevel.Error, ...args); -} - -// tslint:disable-next-line:no-any -export function traceInfo(...args: any[]) { - log(LogLevel.Info, ...args); -} - -// tslint:disable-next-line:no-any -export function traceWarning(...args: any[]) { - log(LogLevel.Warn, ...args); -} - -export namespace traceDecorators { - const DEFAULT_OPTS: TraceOptions = TraceOptions.Arguments | TraceOptions.ReturnValue; - - function trace(logInfo: LogInfo) { - return traceDecorator((call, traced) => logResult(logInfo, traced, call)); - } - - export function verbose(message: string, opts: TraceOptions = DEFAULT_OPTS) { - return trace({ message, opts }); - } - export function error(message: string) { - const opts = DEFAULT_OPTS; - const level = LogLevel.Error; - return trace({ message, opts, level }); - } - export function info(message: string) { - const opts = TraceOptions.None; - return trace({ message, opts }); - } - export function warn(message: string) { - const opts = DEFAULT_OPTS; - const level = LogLevel.Warn; - return trace({ message, opts, level }); - } +export function createTracingDecorator(loggers: ILogger[], logInfo: LogInfo) { + return traceDecorator((call, traced) => logResult(loggers, logInfo, traced, call)); } // This is like a "context manager" that logs tracing info. -export function tracing(logInfo: LogInfo, run: () => T, call?: CallInfo): T { - return _tracing((traced) => logResult(logInfo, traced, call), run); +export function tracing(loggers: ILogger[], logInfo: LogInfo, run: () => T, call?: CallInfo): T { + return _tracing((traced) => logResult(loggers, logInfo, traced, call), run); } -type LogInfo = { +export type LogInfo = { opts: TraceOptions; message: string; level?: LogLevel; @@ -104,15 +62,15 @@ function formatMessages(info: LogInfo, traced: TraceInfo, call?: CallInfo): stri return messages.join(', '); } -function logResult(info: LogInfo, traced: TraceInfo, call?: CallInfo) { +function logResult(loggers: ILogger[], info: LogInfo, traced: TraceInfo, call?: CallInfo) { const formatted = formatMessages(info, traced, call); if (traced.err === undefined) { // The call did not fail. if (!info.level || info.level > LogLevel.Error) { - log(LogLevel.Info, formatted); + logToAll(loggers, LogLevel.Info, [formatted]); } } else { - log(LogLevel.Error, formatted, traced.err); + logToAll(loggers, LogLevel.Error, [formatted, traced.err]); // tslint:disable-next-line:no-any sendTelemetryEvent('ERROR' as any, undefined, undefined, traced.err); } diff --git a/src/client/logging/transports.ts b/src/client/logging/transports.ts index 098f23bc6b78..1fb45e158d0a 100644 --- a/src/client/logging/transports.ts +++ b/src/client/logging/transports.ts @@ -8,16 +8,21 @@ import * as winston from 'winston'; import * as Transport from 'winston-transport'; import { EXTENSION_ROOT_DIR } from '../constants'; import { LogLevel, resolveLevel } from './levels'; +import { Arguments } from './util'; const formattedMessage = Symbol.for('message'); +export function isConsoleTransport(transport: unknown): boolean { + // tslint:disable-next-line:no-any + return (transport as any).isConsole; +} + // A winston-compatible transport type. // We do not use transports.ConsoleTransport because it cannot // adapt to our custom log levels very well. class ConsoleTransport extends Transport { // tslint:disable:no-console - // tslint:disable-next-line:no-any - private static funcByLevel: { [K in LogLevel]: (...args: any[]) => void } = { + private static funcByLevel: { [K in LogLevel]: (...args: Arguments) => void } = { [LogLevel.Error]: console.error, [LogLevel.Warn]: console.warn, [LogLevel.Info]: console.info, @@ -27,6 +32,9 @@ class ConsoleTransport extends Transport { private static defaultFunc = console.log; // tslint:enable:no-console + // This is used to identify the type. + public readonly isConsole = true; + constructor( // tslint:disable-next-line:no-any options?: any, diff --git a/src/client/logging/util.ts b/src/client/logging/util.ts index 2813870a9a87..79d977136a89 100644 --- a/src/client/logging/util.ts +++ b/src/client/logging/util.ts @@ -3,7 +3,7 @@ 'use strict'; // tslint:disable-next-line:no-any -type Arguments = any[]; +export type Arguments = any[]; function valueToLogString(value: unknown, kind: string): string { if (value === undefined) { From c328595b57cd397862b22c34afdeba8f1285e00d Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Thu, 7 May 2020 19:46:02 -0600 Subject: [PATCH 29/30] Combine the loggers. --- src/client/logging/_global.ts | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/client/logging/_global.ts b/src/client/logging/_global.ts index bd932a143abe..3db67190d25e 100644 --- a/src/client/logging/_global.ts +++ b/src/client/logging/_global.ts @@ -10,8 +10,7 @@ import { configureLogger, createLogger, ILogger, LoggerConfig, logToAll } from ' import { createTracingDecorator, LogInfo, TraceOptions, tracing as _tracing } from './trace'; import { Arguments } from './util'; -const consoleLogger = createLogger(); -const fileLogger = createLogger(); +const globalLogger = createLogger(); initialize(); /** @@ -50,15 +49,13 @@ function initialize() { config.console.label = 'Python Extension:'; } } - configureLogger(consoleLogger, config); - delete config.console; if (process.env.VSC_PYTHON_LOG_FILE) { config.file = { logfile: process.env.VSC_PYTHON_LOG_FILE }; nonConsole = true; } - configureLogger(fileLogger, config); + configureLogger(globalLogger, config); if (isCI && nonConsole) { delete config.console; @@ -71,8 +68,6 @@ function initialize() { } } -const loggers = [consoleLogger, fileLogger]; - function sendConsoleToLogger(logger: ILogger) { function _log(logLevel: LogLevel, ...args: Arguments) { logToAll([logger], logLevel, args); @@ -82,7 +77,7 @@ function sendConsoleToLogger(logger: ILogger) { // Emit a log message derived from the args to all enabled transports. export function log(logLevel: LogLevel, ...args: Arguments) { - logToAll(loggers, logLevel, args); + logToAll([globalLogger], logLevel, args); } // tslint:disable-next-line:no-any @@ -107,27 +102,27 @@ export function logWarning(...args: any[]) { // This is like a "context manager" that logs tracing info. export function tracing(info: LogInfo, run: () => T, call?: CallInfo): T { - return _tracing(loggers, info, run, call); + return _tracing([globalLogger], info, run, call); } export namespace traceDecorators { const DEFAULT_OPTS: TraceOptions = TraceOptions.Arguments | TraceOptions.ReturnValue; export function verbose(message: string, opts: TraceOptions = DEFAULT_OPTS) { - return createTracingDecorator(loggers, { message, opts }); + return createTracingDecorator([globalLogger], { message, opts }); } export function error(message: string) { const opts = DEFAULT_OPTS; const level = LogLevel.Error; - return createTracingDecorator(loggers, { message, opts, level }); + return createTracingDecorator([globalLogger], { message, opts, level }); } export function info(message: string) { const opts = TraceOptions.None; - return createTracingDecorator(loggers, { message, opts }); + return createTracingDecorator([globalLogger], { message, opts }); } export function warn(message: string) { const opts = DEFAULT_OPTS; const level = LogLevel.Warn; - return createTracingDecorator(loggers, { message, opts, level }); + return createTracingDecorator([globalLogger], { message, opts, level }); } } From 875235f1bd7d0fa720bd321d53909fbbfc2590b5 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Thu, 7 May 2020 19:53:46 -0600 Subject: [PATCH 30/30] Fold _console.ts into _global.ts. --- src/client/logging/_console.ts | 44 ------------------------------- src/client/logging/_global.ts | 47 +++++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 53 deletions(-) delete mode 100644 src/client/logging/_console.ts diff --git a/src/client/logging/_console.ts b/src/client/logging/_console.ts deleted file mode 100644 index f37758012112..000000000000 --- a/src/client/logging/_console.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -// tslint:disable:no-console - -import { LogLevel } from './levels'; -// Ensure that the console functions are bound before monkeypatching. -import './transports'; -import { Arguments } from './util'; - -/** - * What we're doing here is monkey patching the console.log so we can - * send everything sent to console window into our logs. This is only - * required when we're directly writing to `console.log` or not using - * our `winston logger`. This is something we'd generally turn on, only - * on CI so we can see everything logged to the console window - * (via the logs). - */ -export function monkeypatchConsole(log: (logLevel: LogLevel, ...args: Arguments) => void) { - // The logging "streams" (methods) of the node console. - const streams = ['log', 'error', 'warn', 'info', 'debug', 'trace']; - const levels: { [key: string]: LogLevel } = { - error: LogLevel.Error, - warn: LogLevel.Warn - }; - // tslint:disable-next-line:no-any - const consoleAny: any = console; - for (const stream of streams) { - // Using symbols guarantee the properties will be unique & prevents - // clashing with names other code/library may create or have created. - // We could use a closure but it's a bit trickier. - const sym = Symbol.for(stream); - consoleAny[sym] = consoleAny[stream]; - // tslint:disable-next-line: no-function-expression - consoleAny[stream] = function () { - const args = Array.prototype.slice.call(arguments); - const fn = consoleAny[sym]; - fn(...args); - const level = levels[stream] || LogLevel.Info; - log(level, ...args); - }; - } -} diff --git a/src/client/logging/_global.ts b/src/client/logging/_global.ts index 3db67190d25e..6577725ef56d 100644 --- a/src/client/logging/_global.ts +++ b/src/client/logging/_global.ts @@ -4,7 +4,6 @@ import { isCI, isTestExecution } from '../common/constants'; import { CallInfo } from '../common/utils/decorators'; -import { monkeypatchConsole } from './_console'; import { LogLevel } from './levels'; import { configureLogger, createLogger, ILogger, LoggerConfig, logToAll } from './logger'; import { createTracingDecorator, LogInfo, TraceOptions, tracing as _tracing } from './trace'; @@ -60,7 +59,7 @@ function initialize() { if (isCI && nonConsole) { delete config.console; // Send console.*() to the non-console loggers. - sendConsoleToLogger( + monkeypatchConsole( // This is a separate logger that matches our config but // does not do any console logging. createLogger(config) @@ -68,13 +67,6 @@ function initialize() { } } -function sendConsoleToLogger(logger: ILogger) { - function _log(logLevel: LogLevel, ...args: Arguments) { - logToAll([logger], logLevel, args); - } - monkeypatchConsole(_log); -} - // Emit a log message derived from the args to all enabled transports. export function log(logLevel: LogLevel, ...args: Arguments) { logToAll([globalLogger], logLevel, args); @@ -126,3 +118,40 @@ export namespace traceDecorators { return createTracingDecorator([globalLogger], { message, opts, level }); } } + +// Ensure that the console functions are bound before monkeypatching. +import './transports'; + +/** + * What we're doing here is monkey patching the console.log so we can + * send everything sent to console window into our logs. This is only + * required when we're directly writing to `console.log` or not using + * our `winston logger`. This is something we'd generally turn on, only + * on CI so we can see everything logged to the console window + * (via the logs). + */ +function monkeypatchConsole(logger: ILogger) { + // The logging "streams" (methods) of the node console. + const streams = ['log', 'error', 'warn', 'info', 'debug', 'trace']; + const levels: { [key: string]: LogLevel } = { + error: LogLevel.Error, + warn: LogLevel.Warn + }; + // tslint:disable-next-line:no-any + const consoleAny: any = console; + for (const stream of streams) { + // Using symbols guarantee the properties will be unique & prevents + // clashing with names other code/library may create or have created. + // We could use a closure but it's a bit trickier. + const sym = Symbol.for(stream); + consoleAny[sym] = consoleAny[stream]; + // tslint:disable-next-line: no-function-expression + consoleAny[stream] = function () { + const args = Array.prototype.slice.call(arguments); + const fn = consoleAny[sym]; + fn(...args); + const level = levels[stream] || LogLevel.Info; + logToAll([logger], level, args); + }; + } +}