Skip to content

Commit 2864cca

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 2864cca

File tree

3 files changed

+76
-75
lines changed

3 files changed

+76
-75
lines changed

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import * as vscode from 'vscode'
7+
import { ToolkitLogger, TopicLogger } from './toolkitLogger'
78

89
export type LogTopic = 'crashReport' | 'notifications' | 'test' | 'unknown'
910

@@ -36,10 +37,6 @@ export abstract class BaseLogger implements Logger {
3637
logFile?: vscode.Uri
3738
topic: LogTopic = 'unknown'
3839

39-
setTopic(topic: LogTopic = 'unknown') {
40-
this.topic = topic
41-
}
42-
4340
debug(message: string | Error, ...meta: any[]): number {
4441
return this.sendToLog('debug', message, ...meta)
4542
}
@@ -131,17 +128,15 @@ export function getLogger(topic?: LogTopic): Logger {
131128
if (!logger) {
132129
return new ConsoleLogger()
133130
}
134-
;(logger as BaseLogger).setTopic?.(topic)
135-
return logger
131+
return new TopicLogger(topic ?? 'unknown', logger as ToolkitLogger)
136132
}
137133

138134
export function getDebugConsoleLogger(topic?: LogTopic): Logger {
139135
const logger = toolkitLoggers['debugConsole']
140136
if (!logger) {
141137
return new ConsoleLogger()
142138
}
143-
;(logger as BaseLogger).setTopic?.(topic)
144-
return logger
139+
return new TopicLogger(topic ?? 'unknown', logger as ToolkitLogger)
145140
}
146141

147142
// jscpd:ignore-start

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

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { normalize } from 'path'
77
import * as vscode from 'vscode'
88
import winston from 'winston'
9-
import { BaseLogger, LogLevel, compareLogLevel } from './logger'
9+
import { BaseLogger, LogLevel, LogTopic, compareLogLevel } from './logger'
1010
import { OutputChannelTransport } from './outputChannelTransport'
1111
import { isSourceMappingAvailable } from '../vscode/env'
1212
import { formatError, ToolkitError, UnknownError } from '../errors'
@@ -21,12 +21,26 @@ class ErrorLog {
2121
) {}
2222
}
2323

24+
/* Format the message with topic header */
25+
function prependTopic(topic: string, message: string | Error): string | ErrorLog {
26+
if (typeof message === 'string') {
27+
// TODO: remove this after all calls are migrated and topic is a required param.
28+
if (topic === 'unknown') {
29+
return message
30+
}
31+
return `${topic}: ` + message
32+
} else if (message instanceof Error) {
33+
return new ErrorLog(topic, message)
34+
}
35+
return message
36+
}
37+
2438
// Need to limit how many logs are actually tracked
2539
// LRU cache would work well, currently it just dumps the least recently added log
2640
const logmapSize: number = 1000
41+
2742
export class ToolkitLogger extends BaseLogger implements vscode.Disposable {
2843
private readonly logger: winston.Logger
29-
/* topic is used for header in log messages, default is 'Unknown' */
3044
private disposed: boolean = false
3145
private idCounter: number = 0
3246
private logMap: { [logID: number]: { [filePath: string]: string } } = {}
@@ -111,20 +125,6 @@ export class ToolkitLogger extends BaseLogger implements vscode.Disposable {
111125
})
112126
}
113127

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-
128128
private mapError(level: LogLevel, err: Error): Error | string {
129129
// Use ToolkitError.trace even if we have source mapping (see below), because:
130130
// 1. it is what users will see, we want visibility into that when debugging
@@ -141,17 +141,16 @@ export class ToolkitLogger extends BaseLogger implements vscode.Disposable {
141141
}
142142

143143
override sendToLog(level: LogLevel, message: string | Error, ...meta: any[]): number {
144-
const messageWithTopic = this.addTopicToMessage(message)
145144
if (this.disposed) {
146145
throw new Error('Cannot write to disposed logger')
147146
}
148147

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

151-
if (messageWithTopic instanceof ErrorLog) {
152-
this.logger.log(level, '%O', messageWithTopic, ...meta, { logID: this.idCounter })
150+
if (message instanceof Error) {
151+
this.logger.log(level, '%O', message, ...meta, { logID: this.idCounter })
153152
} else {
154-
this.logger.log(level, messageWithTopic, ...meta, { logID: this.idCounter })
153+
this.logger.log(level, message, ...meta, { logID: this.idCounter })
155154
}
156155

157156
this.logMap[this.idCounter % logmapSize] = {}
@@ -200,3 +199,44 @@ export class ToolkitLogger extends BaseLogger implements vscode.Disposable {
200199
}
201200
}
202201
}
202+
203+
/**
204+
* Wraps a `ToolkitLogger` and defers to it for everything except `topic`.
205+
*/
206+
export class TopicLogger extends BaseLogger implements vscode.Disposable {
207+
/**
208+
* Wraps a `ToolkitLogger` and defers to it for everything except `topic`.
209+
*/
210+
public constructor(
211+
topic: LogTopic,
212+
private logger: ToolkitLogger
213+
) {
214+
super()
215+
this.topic = topic
216+
}
217+
218+
override setLogLevel(logLevel: LogLevel): void {
219+
this.logger.setLogLevel(logLevel)
220+
}
221+
222+
override logLevelEnabled(logLevel: LogLevel): boolean {
223+
return this.logger.logLevelEnabled(logLevel)
224+
}
225+
226+
override getLogById(logID: number, file: vscode.Uri): string | undefined {
227+
return this.logger.getLogById(logID, file)
228+
}
229+
230+
override enableDebugConsole(): void {
231+
this.logger.enableDebugConsole()
232+
}
233+
234+
override sendToLog(level: LogLevel, message: string | Error, ...meta: any[]): number {
235+
if (typeof message === 'string') {
236+
message = prependTopic(this.topic, message) as string
237+
}
238+
return this.logger.sendToLog(level, message, meta)
239+
}
240+
241+
public async dispose(): Promise<void> {}
242+
}

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) => {

0 commit comments

Comments
 (0)