Skip to content

Commit 2e89077

Browse files
authored
Merge pull request #8 from hbmartin/configurable-logger-channel
require setting logger output channel to use it
2 parents 9cc041d + 15e7253 commit 2e89077

File tree

5 files changed

+97
-56
lines changed

5 files changed

+97
-56
lines changed

src/lib/client/useLogger.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
/* eslint-disable no-console */
21
import { useMemo } from 'react';
32
import type { ILogger } from '../types';
3+
import { createConsoleLogger } from '../utils';
44
import { WebviewLogger } from './WebviewLogger';
55
import type { VsCodeApi } from './types';
66

@@ -15,17 +15,3 @@ export function useLogger(tag: string, vscode?: VsCodeApi): ILogger {
1515
[vscode, tag]
1616
);
1717
}
18-
19-
function createConsoleLogger(tag: string): ILogger {
20-
return {
21-
debug: (message: string, data?: Record<string, unknown>) =>
22-
console.debug(`[${tag}] ${message}`, data),
23-
info: (message: string, data?: Record<string, unknown>) =>
24-
console.info(`[${tag}] ${message}`, data),
25-
warn: (message: string, data?: Record<string, unknown>) =>
26-
console.warn(`[${tag}] ${message}`, data),
27-
error: (message: string, data?: Record<string, unknown>) =>
28-
console.error(`[${tag}] ${message}`, data),
29-
dispose: () => {},
30-
};
31-
}

src/lib/host/logger.ts

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
import * as vscode from 'vscode';
21
import { LogLevel, type ILogger } from '../types';
2+
import { createConsoleLogger } from '../utils';
3+
import type * as vscode from 'vscode';
34

4-
export const disallowedLogKeys = ['password', 'secret', 'token', 'apiKey', 'apiSecret', 'content'];
5+
export const disallowedLogKeys: Set<string> = new Set([
6+
'password',
7+
'secret',
8+
'token',
9+
'apikey',
10+
'apisecret',
11+
'content',
12+
]);
513

614
function removePromptsFromData<T>(dictionary: T | undefined | null): T | undefined {
715
if (dictionary === null || dictionary === undefined) {
@@ -15,12 +23,10 @@ function removePromptsFromData<T>(dictionary: T | undefined | null): T | undefin
1523
return dictionary;
1624
}
1725

18-
const clone = structuredClone(dictionary) as Record<string, unknown>;
19-
2026
try {
21-
for (const key in clone) {
22-
const value = clone[key];
23-
if (disallowedLogKeys.includes(key)) {
27+
const clone = structuredClone(dictionary) as Record<string, unknown>;
28+
for (const [key, value] of Object.entries(clone)) {
29+
if (disallowedLogKeys.has(key.toLowerCase())) {
2430
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
2531
delete clone[key];
2632
continue;
@@ -32,21 +38,21 @@ function removePromptsFromData<T>(dictionary: T | undefined | null): T | undefin
3238
clone[key] = removePromptsFromData(value as Record<string, unknown>) as unknown;
3339
}
3440
}
41+
return clone as unknown as T;
3542
} catch (error) {
3643
console.error('Error processing log data:', error);
3744
return {} as T;
3845
}
39-
40-
return clone as unknown as T;
4146
}
4247

4348
/**
4449
* Static logger class for extension-wide logging
4550
*/
4651
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
4752
class LoggerImpl {
48-
private static readonly outputChannel: vscode.OutputChannel =
49-
vscode.window.createOutputChannel('IPC');
53+
// eslint-disable-next-line sonarjs/public-static-readonly
54+
static outputChannel: vscode.OutputChannel | undefined = undefined;
55+
private static readonly consoleLogger = createConsoleLogger('RVW');
5056

5157
public static debug(message: string, data: Record<string, unknown> | undefined = undefined) {
5258
this.log(LogLevel.DEBUG, message, data);
@@ -65,33 +71,64 @@ class LoggerImpl {
6571
}
6672

6773
public static dispose() {
68-
this.outputChannel.dispose();
74+
this.outputChannel?.dispose();
75+
this.outputChannel = undefined;
6976
}
7077

7178
private static log(level: LogLevel, message: string, data: Record<string, unknown> | undefined) {
7279
const timestamp = new Date().toISOString().split('T')[1];
73-
const levelStr = LogLevel[level] || 'UNKNOWN';
7480
const cleanedData = removePromptsFromData(data);
75-
const line = `[${timestamp}] [${levelStr}] ${message}`;
76-
if (cleanedData === undefined) {
77-
this.outputChannel.appendLine(line);
81+
if (this.outputChannel === undefined) {
82+
const methodName = LogLevel[level].toLowerCase() as
83+
| undefined
84+
| keyof Omit<ILogger, 'dispose'>;
85+
if (methodName !== undefined && typeof this.consoleLogger[methodName] === 'function') {
86+
if (cleanedData === undefined) {
87+
this.consoleLogger[methodName](`[${timestamp}] ${message}`);
88+
} else {
89+
this.consoleLogger[methodName](`[${timestamp}] ${message}`, cleanedData);
90+
}
91+
}
7892
} else {
79-
try {
80-
this.outputChannel.appendLine(`${line} : ${JSON.stringify(cleanedData)}`);
81-
} catch {
82-
this.outputChannel.appendLine(`${line} : unserializable data`);
93+
const levelStr = LogLevel[level] || 'UNKNOWN';
94+
const line = `[${timestamp}] [${levelStr}] ${message}`;
95+
96+
if (cleanedData === undefined) {
97+
this.outputChannel.appendLine(line);
98+
} else {
99+
try {
100+
this.outputChannel.appendLine(`${line} : ${JSON.stringify(cleanedData)}`);
101+
} catch {
102+
this.outputChannel.appendLine(`${line} : unserializable data`);
103+
}
83104
}
84105
}
85106
}
86107
}
87108

88-
export const Logger: ILogger = {
89-
debug: (message: string, data?: Record<string, unknown>) => LoggerImpl.debug(message, data),
90-
info: (message: string, data?: Record<string, unknown>) => LoggerImpl.info(message, data),
91-
warn: (message: string, data?: Record<string, unknown>) => LoggerImpl.warn(message, data),
92-
error: (message: string, data?: Record<string, unknown>) => LoggerImpl.error(message, data),
93-
dispose: () => LoggerImpl.dispose(),
94-
};
109+
export const Logger: ILogger & {
110+
setOutputChannel: (outputChannel: vscode.OutputChannel | undefined) => void;
111+
} =
112+
/**
113+
* Sets the VS Code output channel for the logger.
114+
* The logger takes ownership of the channel and will dispose it
115+
* when `Logger.dispose()` is called or when a new channel is set.
116+
* @param outputChannel The output channel to use for logging.
117+
*/
118+
{
119+
setOutputChannel: (outputChannel: vscode.OutputChannel | undefined) => {
120+
if (LoggerImpl.outputChannel === outputChannel) {
121+
return;
122+
}
123+
LoggerImpl.dispose();
124+
LoggerImpl.outputChannel = outputChannel;
125+
},
126+
debug: (message: string, data?: Record<string, unknown>) => LoggerImpl.debug(message, data),
127+
info: (message: string, data?: Record<string, unknown>) => LoggerImpl.info(message, data),
128+
warn: (message: string, data?: Record<string, unknown>) => LoggerImpl.warn(message, data),
129+
error: (message: string, data?: Record<string, unknown>) => LoggerImpl.error(message, data),
130+
dispose: () => LoggerImpl.dispose(),
131+
};
95132

96133
export const getLogger = (tag: string): ILogger => ({
97134
debug: (message: string, data?: Record<string, unknown>) =>

src/lib/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/* eslint-disable no-console */
2+
import type { ILogger } from './types';
3+
14
/**
25
* Generate a unique ID for requests
36
*/
@@ -8,3 +11,16 @@ export function generateId(prefix: string): string {
811
export function getErrorMessage(error: unknown): string {
912
return error instanceof Error ? error.message : String(error);
1013
}
14+
export function createConsoleLogger(tag: string): ILogger {
15+
return {
16+
debug: (message: string, data?: Record<string, unknown>) =>
17+
console.debug(`[${tag}] ${message}`, data),
18+
info: (message: string, data?: Record<string, unknown>) =>
19+
console.info(`[${tag}] ${message}`, data),
20+
warn: (message: string, data?: Record<string, unknown>) =>
21+
console.warn(`[${tag}] ${message}`, data),
22+
error: (message: string, data?: Record<string, unknown>) =>
23+
console.error(`[${tag}] ${message}`, data),
24+
dispose: () => {},
25+
};
26+
}

tests/lib/host.test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ describe('host module exports', () => {
1010

1111
expect(typeof Logger).toBe('object');
1212
expect(typeof getLogger).toBe('function');
13-
expect(Array.isArray(disallowedLogKeys)).toBe(true);
13+
// Recent change: disallowedLogKeys is now a Set
14+
expect(disallowedLogKeys instanceof Set).toBe(true);
1415
});
1516

1617
it('should export WebviewApiProvider', async () => {
@@ -52,13 +53,13 @@ describe('host module exports', () => {
5253
expect(typeof logger.debug).toBe('function');
5354
});
5455

55-
it('should have disallowedLogKeys array with expected values', async () => {
56+
it('should have disallowedLogKeys set with expected values', async () => {
5657
const { disallowedLogKeys } = await import('../../src/lib/host');
5758

58-
expect(disallowedLogKeys.length).toBeGreaterThan(0);
59-
expect(disallowedLogKeys).toContain('password');
60-
expect(disallowedLogKeys).toContain('token');
61-
expect(disallowedLogKeys).toContain('secret');
59+
expect(disallowedLogKeys.size).toBeGreaterThan(0);
60+
expect(disallowedLogKeys.has('password')).toBe(true);
61+
expect(disallowedLogKeys.has('token')).toBe(true);
62+
expect(disallowedLogKeys.has('secret')).toBe(true);
6263
});
6364

6465
it('should have working isViewApiRequest function', async () => {

tests/lib/host/logger.test.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ describe('host/logger', () => {
1111
mockOutputChannel.appendLine.mockClear();
1212
mockOutputChannel.dispose.mockClear();
1313

14+
// Route Logger output to the mocked VS Code output channel
15+
Logger.setOutputChannel(mockOutputChannel);
16+
1417
// Mock date for consistent timestamps
1518
originalDateNow = Date.prototype.toISOString;
1619
Date.prototype.toISOString = vi.fn(() => '2024-01-01T12:00:00.000Z');
@@ -19,6 +22,8 @@ describe('host/logger', () => {
1922
afterEach(() => {
2023
Date.prototype.toISOString = originalDateNow;
2124
vi.clearAllMocks();
25+
// Reset output channel between tests
26+
Logger.setOutputChannel(undefined);
2227
});
2328

2429
describe('Logger static methods', () => {
@@ -320,14 +325,10 @@ describe('host/logger', () => {
320325

321326
describe('disallowedLogKeys', () => {
322327
it('should export correct disallowed keys', () => {
323-
expect(disallowedLogKeys).toEqual([
324-
'password',
325-
'secret',
326-
'token',
327-
'apiKey',
328-
'apiSecret',
329-
'content',
330-
]);
328+
// Recent change: exported as Set with lowercase API keys
329+
expect(disallowedLogKeys).toEqual(
330+
new Set(['password', 'secret', 'token', 'apikey', 'apisecret', 'content'])
331+
);
331332
});
332333
});
333334

0 commit comments

Comments
 (0)