Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(logging): topicLogger for headers #5586

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 114 additions & 21 deletions packages/core/src/shared/logger/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,20 @@

import * as vscode from 'vscode'

/** define log topics */
type LogTopic = 'Unknown' | 'Test'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type LogTopic = 'Unknown' | 'Test'
type LogTopic = 'unknown' | 'test'


const toolkitLoggers: {
main: Logger | undefined
channel: Logger | undefined
debugConsole: Logger | undefined
} = { main: undefined, channel: undefined, debugConsole: undefined }
} = { main: undefined, debugConsole: undefined }

export interface Logger {
debug(message: string, ...meta: any[]): number
debug(error: Error, ...meta: any[]): number
verbose(message: string, ...meta: any[]): number
verbose(error: Error, ...meta: any[]): number
info(message: string, ...meta: any[]): number
info(error: Error, ...meta: any[]): number
warn(message: string, ...meta: any[]): number
warn(error: Error, ...meta: any[]): number
error(message: string, ...meta: any[]): number
error(error: Error, ...meta: any[]): number
log(logLevel: LogLevel, message: string, ...meta: any[]): number
log(logLevel: LogLevel, error: Error, ...meta: any[]): number
debug(message: string | Error, ...meta: any[]): number
verbose(message: string | Error, ...meta: any[]): number
info(message: string | Error, ...meta: any[]): number
warn(message: string | Error, ...meta: any[]): number
error(message: string | Error, ...meta: any[]): number
setLogLevel(logLevel: LogLevel): void
/** Returns true if the given log level is being logged. */
logLevelEnabled(logLevel: LogLevel): boolean
Expand All @@ -32,6 +27,85 @@ export interface Logger {
enableDebugConsole(): void
}

/**
* Base Logger class
* Used as a wrapper around the logger interface for appending messages
* Also more compatible with future change
*/
abstract class baseLogger implements Logger {
Copy link
Contributor

@justinmk3 justinmk3 Sep 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WinstonToolkitLogger is referenced in only a few isolated places--it's already "encapsulated". The logger interface is essentially getLogger(), and already abstracts logging for the codebase.

Our base logger is winstonToolkitLogger.ts, but it has a misleading name. At present it happens to use winston as the "backend", but that can always be changed in the future without any accruing costs / tech debt. Whereas in the meantime, adding extra indirection (in the form of 2 new classes, TopicLogger and baseLogger) is a permanent cost that may never pay off.

To summarize, the near-term steps we can take are:

  • rename winstonToolkitLogger.ts to toolkitLogger.ts.
  • add addTopicToMessage to it.

In the future if we need other logger implementations, those can be added at near-zero cost because callers are already using the getLogger() abstraction.

Copy link
Contributor

@nkomonen-amazon nkomonen-amazon Sep 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this Base class can apply to the existing loggers NullLogger, ConsoleLogger, and the WinstonLogger. Would it make sense to have them all the implement this abstract class? This will allow us to drop each of their duplicated info(), debug(),... methods

Winston logger would still have the addTopicToMessage() method as Justin suggested

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure that makes sense

abstract coreLogger: Logger
debug(message: string | Error, ...meta: any[]): number {
return this.sendToLog('debug', message, meta)
}
verbose(message: string | Error, ...meta: any[]): number {
return this.sendToLog('verbose', message, meta)
}
info(message: string | Error, ...meta: any[]): number {
return this.sendToLog('info', message, meta)
}
warn(message: string | Error, ...meta: any[]): number {
return this.sendToLog('warn', message, meta)
}
error(message: string | Error, ...meta: any[]): number {
return this.sendToLog('error', message, meta)
}
setLogLevel(logLevel: LogLevel): void {
this.coreLogger.setLogLevel(logLevel)
}
logLevelEnabled(logLevel: LogLevel): boolean {
return this.coreLogger.logLevelEnabled(logLevel)
}
getLogById(logID: number, file: vscode.Uri): string | undefined {
return this.coreLogger.getLogById(logID, file)
}
/** HACK: Enables logging to vscode Debug Console. */
enableDebugConsole(): void {
this.coreLogger.enableDebugConsole()
}
abstract sendToLog(
type: 'debug' | 'verbose' | 'info' | 'warn' | 'error',
message: string | Error,
...meta: any[]
): number
}
/**
* Logger with topic headers
*
* @param topic identifies the message topic, appended to the front of message followed by a colen.
* @param coreLogger the actual logger it wraps around, in this case the 'main' logger
*/
export class TopicLogger extends baseLogger {
private topic: LogTopic
override coreLogger: Logger
/** Default topic is 'Unknown' */
constructor(logger: Logger, topic: LogTopic = 'Unknown') {
super()
this.coreLogger = logger
this.topic = topic
}
/** Format the message with topic header */
private addTopicToMessage(message: string | Error): string | Error {
const topicPrefix = `${this.topic}: `
if (typeof message === 'string') {
return topicPrefix + message
} else if (message instanceof Error) {
/** Create a new Error object to avoid modifying the original */
const topicError = new Error(topicPrefix + message.message)
topicError.name = message.name
topicError.stack = message.stack
return topicError
}
return message
}
override sendToLog(
type: 'debug' | 'verbose' | 'info' | 'warn' | 'error',
message: string | Error,
...meta: any[]
): number {
return this.coreLogger[type](this.addTopicToMessage(message), meta)
}
}

/**
* Log levels ordered for comparison.
*
Expand Down Expand Up @@ -85,18 +159,37 @@ export function compareLogLevel(l1: LogLevel, l2: LogLevel): number {
/**
* Gets the logger if it has been initialized
* the logger is of `'main'` or `undefined`: Main logger; default impl: logs to log file and log output channel
* @param topic identifies the message topic, appended to the front of message followed by a colen.
*/
export function getLogger(): Logger {
const logger = toolkitLoggers['main']
if (!logger) {
export function getLogger(topic?: LogTopic): Logger {
const coreLogger = toolkitLoggers['main']
if (!coreLogger) {
return new ConsoleLogger()
}

return logger
/**
* Temperpory check so we don't get a million Unknown logs
* TODO: remove once the majority of log calls are migrated
*/
if (!topic) {
return coreLogger
}
return new TopicLogger(coreLogger, topic)
}

export function getDebugConsoleLogger(): Logger {
return toolkitLoggers['debugConsole'] ?? new ConsoleLogger()
/**
* Gets the logger if it has been initialized
* the logger is of `'debugConsole', logs to IDE debug console channel
* @param topic identifies the message topic, appended to the front of message followed by a colen.
*/
export function getDebugConsoleLogger(topic?: LogTopic): Logger {
const baseLogger = toolkitLoggers['debugConsole']
if (!baseLogger) {
return new ConsoleLogger()
}
if (!topic) {
return baseLogger
}
return new TopicLogger(baseLogger, topic)
}

export class NullLogger implements Logger {
Expand Down
71 changes: 71 additions & 0 deletions packages/core/src/test/shared/logger/topicLogger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import assert from 'assert'
import { stub, SinonStubbedInstance } from 'sinon'
import { TopicLogger, Logger, LogLevel } from '../../../shared/logger/logger'

describe('TopicLogger', () => {
let mockCoreLogger: SinonStubbedInstance<Logger>
let topicLogger: TopicLogger

beforeEach(() => {
mockCoreLogger = {
debug: stub(),
verbose: stub(),
info: stub(),
warn: stub(),
error: stub(),
setLogLevel: stub(),
logLevelEnabled: stub(),
getLogById: stub(),
enableDebugConsole: stub(),
} as SinonStubbedInstance<Logger>
topicLogger = new TopicLogger(mockCoreLogger as Logger, 'Test')
})

it('adds topic prefix to string type messages', () => {
topicLogger.info('Test message')
assert(mockCoreLogger.info.calledWith('Test: Test message'))
})

it('adds topic prefix to Error type messages', () => {
const testError = new Error('Test error')
topicLogger.error(testError)
assert(mockCoreLogger.error.calledOnce)
const calledArg = mockCoreLogger.error.firstCall.args[0]
assert(calledArg instanceof Error)
assert.strictEqual(calledArg.message, 'Test: Test error')
assert.strictEqual(calledArg.name, testError.name)
assert.strictEqual(calledArg.stack, testError.stack)
})

it('topic is "Unknown" when not specified', () => {
const defaultTopicLogger = new TopicLogger(mockCoreLogger as Logger)
defaultTopicLogger.debug('Verbose message')
assert(mockCoreLogger.debug.calledWith('Unknown: Verbose message'))
})

it('delegates setLogLevel to core logger', () => {
topicLogger.setLogLevel('debug' as LogLevel)
assert(mockCoreLogger.setLogLevel.calledWith('debug'))
})

it('delegates logLevelEnabled to core logger', () => {
topicLogger.logLevelEnabled('info' as LogLevel)
assert(mockCoreLogger.logLevelEnabled.calledWith('info'))
})

it('delegates getLogById to core logger', () => {
const mockUri = {} as any
topicLogger.getLogById(123, mockUri)
assert(mockCoreLogger.getLogById.calledWith(123, mockUri))
})

it('delegates enableDebugConsole to core logger', () => {
topicLogger.enableDebugConsole()
assert(mockCoreLogger.enableDebugConsole.called)
})
})
Loading