Skip to content

Commit aff1842

Browse files
committed
feat(logging): support logger instances
Problem: `getLogger('foo')` returns a global singleton, so the "current topic" only is stored until the next `getLogger` call. Solution: Modify `getLogger('foo')` to return a wrapper which stores its topic. This allows modules to declare their own module-local shared logger: const logger = getLogger('foo')
1 parent 98d8324 commit aff1842

File tree

5 files changed

+97
-89
lines changed

5 files changed

+97
-89
lines changed

packages/core/src/shared/logger/logger.ts

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ import * as vscode from 'vscode'
77

88
export type LogTopic = 'crashReport' | 'notifications' | 'test' | 'unknown'
99

10+
class ErrorLog {
11+
constructor(
12+
public topic: string,
13+
public error: Error
14+
) {}
15+
}
16+
1017
const toolkitLoggers: {
1118
main: Logger | undefined
1219
debugConsole: Logger | undefined
@@ -30,16 +37,17 @@ export interface Logger {
3037
getLogById(logID: number, file: vscode.Uri): string | undefined
3138
/** HACK: Enables logging to vscode Debug Console. */
3239
enableDebugConsole(): void
40+
sendToLog(
41+
logLevel: 'debug' | 'verbose' | 'info' | 'warn' | 'error',
42+
message: string | Error,
43+
...meta: any[]
44+
): number
3345
}
3446

3547
export abstract class BaseLogger implements Logger {
3648
logFile?: vscode.Uri
3749
topic: LogTopic = 'unknown'
3850

39-
setTopic(topic: LogTopic = 'unknown') {
40-
this.topic = topic
41-
}
42-
4351
debug(message: string | Error, ...meta: any[]): number {
4452
return this.sendToLog('debug', message, ...meta)
4553
}
@@ -58,17 +66,16 @@ export abstract class BaseLogger implements Logger {
5866
log(logLevel: LogLevel, message: string | Error, ...meta: any[]): number {
5967
return this.sendToLog(logLevel, message, ...meta)
6068
}
61-
abstract setLogLevel(logLevel: LogLevel): void
62-
abstract logLevelEnabled(logLevel: LogLevel): boolean
63-
abstract getLogById(logID: number, file: vscode.Uri): string | undefined
64-
/** HACK: Enables logging to vscode Debug Console. */
65-
abstract enableDebugConsole(): void
66-
6769
abstract sendToLog(
6870
logLevel: 'debug' | 'verbose' | 'info' | 'warn' | 'error',
6971
message: string | Error,
7072
...meta: any[]
7173
): number
74+
abstract setLogLevel(logLevel: LogLevel): void
75+
abstract logLevelEnabled(logLevel: LogLevel): boolean
76+
abstract getLogById(logID: number, file: vscode.Uri): string | undefined
77+
/** HACK: Enables logging to vscode Debug Console. */
78+
abstract enableDebugConsole(): void
7279
}
7380

7481
/**
@@ -121,6 +128,20 @@ export function compareLogLevel(l1: LogLevel, l2: LogLevel): number {
121128
return logLevels.get(l1)! - logLevels.get(l2)!
122129
}
123130

131+
/* Format the message with topic header */
132+
function prependTopic(topic: string, message: string | Error): string | ErrorLog {
133+
if (typeof message === 'string') {
134+
// TODO: remove this after all calls are migrated and topic is a required param.
135+
if (topic === 'unknown') {
136+
return message
137+
}
138+
return `${topic}: ` + message
139+
} else if (message instanceof Error) {
140+
return new ErrorLog(topic, message)
141+
}
142+
return message
143+
}
144+
124145
/**
125146
* Gets the logger if it has been initialized
126147
* the logger is of `'main'` or `undefined`: Main logger; default impl: logs to log file and log output channel
@@ -131,17 +152,15 @@ export function getLogger(topic?: LogTopic): Logger {
131152
if (!logger) {
132153
return new ConsoleLogger()
133154
}
134-
;(logger as BaseLogger).setTopic?.(topic)
135-
return logger
155+
return new TopicLogger(topic ?? 'unknown', logger)
136156
}
137157

138158
export function getDebugConsoleLogger(topic?: LogTopic): Logger {
139159
const logger = toolkitLoggers['debugConsole']
140160
if (!logger) {
141161
return new ConsoleLogger()
142162
}
143-
;(logger as BaseLogger).setTopic?.(topic)
144-
return logger
163+
return new TopicLogger(topic ?? 'unknown', logger)
145164
}
146165

147166
// jscpd:ignore-start
@@ -194,6 +213,46 @@ export class ConsoleLogger extends BaseLogger {
194213
return 0
195214
}
196215
}
216+
217+
/**
218+
* Wraps a `ToolkitLogger` and defers to it for everything except `topic`.
219+
*/
220+
export class TopicLogger extends BaseLogger implements vscode.Disposable {
221+
/**
222+
* Wraps a `ToolkitLogger` and defers to it for everything except `topic`.
223+
*/
224+
public constructor(
225+
public override topic: LogTopic,
226+
public readonly logger: Logger
227+
) {
228+
super()
229+
}
230+
231+
override setLogLevel(logLevel: LogLevel): void {
232+
this.logger.setLogLevel(logLevel)
233+
}
234+
235+
override logLevelEnabled(logLevel: LogLevel): boolean {
236+
return this.logger.logLevelEnabled(logLevel)
237+
}
238+
239+
override getLogById(logID: number, file: vscode.Uri): string | undefined {
240+
return this.logger.getLogById(logID, file)
241+
}
242+
243+
override enableDebugConsole(): void {
244+
this.logger.enableDebugConsole()
245+
}
246+
247+
override sendToLog(level: LogLevel, message: string | Error, ...meta: any[]): number {
248+
if (typeof message === 'string') {
249+
message = prependTopic(this.topic, message) as string
250+
}
251+
return this.logger.sendToLog(level, message, meta)
252+
}
253+
254+
public async dispose(): Promise<void> {}
255+
}
197256
// jscpd:ignore-end
198257

199258
export function getNullLogger(type?: 'debugConsole' | 'main'): Logger {

packages/core/src/shared/logger/toolkitLogger.ts

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,12 @@ import { SharedFileTransport } from './sharedFileTransport'
1414
import { ConsoleLogTransport } from './consoleLogTransport'
1515
import { isWeb } from '../extensionGlobals'
1616

17-
class ErrorLog {
18-
constructor(
19-
public topic: string,
20-
public error: Error
21-
) {}
22-
}
23-
2417
// Need to limit how many logs are actually tracked
2518
// LRU cache would work well, currently it just dumps the least recently added log
2619
const logmapSize: number = 1000
20+
2721
export class ToolkitLogger extends BaseLogger implements vscode.Disposable {
2822
private readonly logger: winston.Logger
29-
/* topic is used for header in log messages, default is 'Unknown' */
3023
private disposed: boolean = false
3124
private idCounter: number = 0
3225
private logMap: { [logID: number]: { [filePath: string]: string } } = {}
@@ -111,20 +104,6 @@ export class ToolkitLogger extends BaseLogger implements vscode.Disposable {
111104
})
112105
}
113106

114-
/* Format the message with topic header */
115-
private addTopicToMessage(message: string | Error): string | ErrorLog {
116-
if (typeof message === 'string') {
117-
// TODO: remove this after all calls are migrated and topic is a required param.
118-
if (this.topic === 'unknown') {
119-
return message
120-
}
121-
return `${this.topic}: ` + message
122-
} else if (message instanceof Error) {
123-
return new ErrorLog(this.topic, message)
124-
}
125-
return message
126-
}
127-
128107
private mapError(level: LogLevel, err: Error): Error | string {
129108
// Use ToolkitError.trace even if we have source mapping (see below), because:
130109
// 1. it is what users will see, we want visibility into that when debugging
@@ -141,17 +120,16 @@ export class ToolkitLogger extends BaseLogger implements vscode.Disposable {
141120
}
142121

143122
override sendToLog(level: LogLevel, message: string | Error, ...meta: any[]): number {
144-
const messageWithTopic = this.addTopicToMessage(message)
145123
if (this.disposed) {
146124
throw new Error('Cannot write to disposed logger')
147125
}
148126

149127
meta = meta.map((o) => (o instanceof Error ? this.mapError(level, o) : o))
150128

151-
if (messageWithTopic instanceof ErrorLog) {
152-
this.logger.log(level, '%O', messageWithTopic, ...meta, { logID: this.idCounter })
129+
if (message instanceof Error) {
130+
this.logger.log(level, '%O', message, ...meta, { logID: this.idCounter })
153131
} else {
154-
this.logger.log(level, messageWithTopic, ...meta, { logID: this.idCounter })
132+
this.logger.log(level, message, ...meta, { logID: this.idCounter })
155133
}
156134

157135
this.logMap[this.idCounter % logmapSize] = {}

packages/core/src/test/globalSetup.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import globals from '../shared/extensionGlobals'
1414
import { CodelensRootRegistry } from '../shared/fs/codelensRootRegistry'
1515
import { CloudFormationTemplateRegistry } from '../shared/fs/templateRegistry'
1616
import { getLogger, LogLevel } from '../shared/logger'
17-
import { setLogger } from '../shared/logger/logger'
17+
import { setLogger, TopicLogger } from '../shared/logger/logger'
1818
import { FakeExtensionContext } from './fakeExtensionContext'
1919
import { TestLogger } from './testLogger'
2020
import * as testUtil from './testUtil'
@@ -136,7 +136,7 @@ export const mochaHooks = {
136136
* Verifies that the TestLogger instance is still the one set as the toolkit's logger.
137137
*/
138138
export function getTestLogger(): TestLogger {
139-
const logger = getLogger()
139+
const logger = getLogger() instanceof TopicLogger ? (getLogger() as TopicLogger).logger : getLogger()
140140
assert.strictEqual(logger, testLogger, 'The expected test logger is not the current logger')
141141
assert.ok(testLogger, 'TestLogger was expected to exist')
142142

packages/core/src/test/shared/logger/toolkitLogger.test.ts

Lines changed: 13 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import * as vscode from 'vscode'
1010
import { ToolkitLogger } from '../../../shared/logger/toolkitLogger'
1111
import { MockOutputChannel } from '../../mockOutputChannel'
1212
import { sleep, waitUntil } from '../../../shared/utilities/timeoutUtils'
13-
import { ToolkitError } from '../../../shared/errors'
13+
import { getLogger } from '../../../shared/logger'
14+
import { assertLogsContain } from '../../globalSetup.test'
1415

1516
/**
1617
* Disposes the logger then waits for the write streams to flush. The `expected` and `unexpected` arrays just look
@@ -240,55 +241,20 @@ describe('ToolkitLogger', function () {
240241
})
241242

242243
it('prepends topic to message', async function () {
243-
testLogger = new ToolkitLogger('info')
244-
testLogger.logToOutputChannel(outputChannel, false)
245-
testLogger.setTopic('test')
246-
testLogger.setLogLevel('verbose')
247-
248-
const testMessageWithHeader = 'test: This is a test message'
249-
testLogger.verbose('This is a test message')
250-
assert.ok(
251-
(await waitForLoggedTextByContents(testMessageWithHeader)).includes(testMessageWithHeader),
252-
'Expected header added'
253-
)
254-
255-
const msg = "topic: 'test'"
256-
testLogger.verbose(new ToolkitError('root error', { code: 'something went wrong' }))
257-
assert.ok((await waitForLoggedTextByContents(msg)).includes(msg), 'Expected header added')
258-
259-
const msg2 = "topic: 'test'"
260-
testLogger.verbose(new ToolkitError('root error', { code: 'something went wrong' }))
261-
assert.ok((await waitForLoggedTextByContents(msg2)).includes(msg2), 'Expected header added')
262-
})
263-
264-
it('unknown topic header ignored in message', async function () {
265-
const testMessage = 'This is a test message'
266-
const unknowntestMessage = 'unknown: This is a test message'
244+
const logger = getLogger('notifications')
245+
logger.setLogLevel('verbose')
267246

268-
testLogger = new ToolkitLogger('info')
269-
testLogger.logToOutputChannel(outputChannel, false)
270-
testLogger.setTopic('unknown')
271-
testLogger.setLogLevel('verbose')
272-
testLogger.verbose(testMessage)
273-
274-
const waitForMessage = waitForLoggedTextByContents(testMessage)
275-
assert.ok((await waitForMessage).includes(testMessage), 'Expected message logged')
276-
assert.ok(!(await waitForMessage).includes(unknowntestMessage), 'unexpected header in log')
277-
})
247+
logger.verbose('This is a test message')
248+
assertLogsContain('notifications: This is a test message', true, 'verbose')
278249

279-
it('switch topic on same logger', async function () {
280-
const testMessage = 'This is a test message'
281-
const testMessageWithHeader = 'test: This is a test message'
282-
283-
testLogger = new ToolkitLogger('info')
284-
testLogger.logToOutputChannel(outputChannel, false)
285-
testLogger.setTopic('unknown')
286-
testLogger.setTopic('test')
287-
testLogger.setLogLevel('verbose')
288-
testLogger.verbose(testMessage)
250+
// const msg2 = "topic: 'test'"
251+
// logger.verbose(new ToolkitError('root error', { code: 'something went wrong' }))
252+
// assertLogsContain("topic: 'something went wrong'", true, 'verbose')
289253

290-
const waitForMessage = waitForLoggedTextByContents(testMessageWithHeader)
291-
assert.ok((await waitForMessage).includes(testMessageWithHeader), 'Expected header added')
254+
// 'unknown' is not prepended
255+
const logger3 = getLogger('unknown')
256+
logger3.verbose('This is a test message')
257+
assertLogsContain('This is a test message', true, 'verbose')
292258
})
293259

294260
happyLogScenarios.forEach((scenario) => {

packages/core/src/test/testLogger.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export class TestLogger implements Logger {
1717
}[] = []
1818
private count: number = 0
1919
public constructor(private logLevel: LogLevel = 'debug') {}
20+
logFile?: Uri | undefined
2021

2122
public enableDebugConsole(): void {}
2223

@@ -50,6 +51,10 @@ export class TestLogger implements Logger {
5051
.map((loggedEntry) => loggedEntry.entry)
5152
}
5253

54+
public sendToLog(logLevel: LogLevel, msg: string, entries: Loggable[]): number {
55+
return this.addLoggedEntries(logLevel, [msg, ...entries])
56+
}
57+
5358
private addLoggedEntries(logLevel: LogLevel, entries: Loggable[]): number {
5459
entries.forEach((entry) => {
5560
this.loggedEntries.push({

0 commit comments

Comments
 (0)