Skip to content
Merged
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
16 changes: 1 addition & 15 deletions src/lib/client/useLogger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-console */
import { useMemo } from 'react';
import type { ILogger } from '../types';
import { createConsoleLogger } from '../utils';
import { WebviewLogger } from './WebviewLogger';
import type { VsCodeApi } from './types';

Expand All @@ -15,17 +15,3 @@ export function useLogger(tag: string, vscode?: VsCodeApi): ILogger {
[vscode, tag]
);
}

function createConsoleLogger(tag: string): ILogger {
return {
debug: (message: string, data?: Record<string, unknown>) =>
console.debug(`[${tag}] ${message}`, data),
info: (message: string, data?: Record<string, unknown>) =>
console.info(`[${tag}] ${message}`, data),
warn: (message: string, data?: Record<string, unknown>) =>
console.warn(`[${tag}] ${message}`, data),
error: (message: string, data?: Record<string, unknown>) =>
console.error(`[${tag}] ${message}`, data),
dispose: () => {},
};
}
91 changes: 64 additions & 27 deletions src/lib/host/logger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import * as vscode from 'vscode';
import { LogLevel, type ILogger } from '../types';
import { createConsoleLogger } from '../utils';
import type * as vscode from 'vscode';

export const disallowedLogKeys = ['password', 'secret', 'token', 'apiKey', 'apiSecret', 'content'];
export const disallowedLogKeys: Set<string> = new Set([
'password',
'secret',
'token',
'apikey',
'apisecret',
'content',
]);

function removePromptsFromData<T>(dictionary: T | undefined | null): T | undefined {
if (dictionary === null || dictionary === undefined) {
Expand All @@ -15,12 +23,10 @@ function removePromptsFromData<T>(dictionary: T | undefined | null): T | undefin
return dictionary;
}

const clone = structuredClone(dictionary) as Record<string, unknown>;

try {
for (const key in clone) {
const value = clone[key];
if (disallowedLogKeys.includes(key)) {
const clone = structuredClone(dictionary) as Record<string, unknown>;
for (const [key, value] of Object.entries(clone)) {
if (disallowedLogKeys.has(key.toLowerCase())) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete clone[key];
continue;
Expand All @@ -32,21 +38,21 @@ function removePromptsFromData<T>(dictionary: T | undefined | null): T | undefin
clone[key] = removePromptsFromData(value as Record<string, unknown>) as unknown;
}
}
return clone as unknown as T;
} catch (error) {
console.error('Error processing log data:', error);
return {} as T;
}

return clone as unknown as T;
}

/**
* Static logger class for extension-wide logging
*/
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
class LoggerImpl {
private static readonly outputChannel: vscode.OutputChannel =
vscode.window.createOutputChannel('IPC');
// eslint-disable-next-line sonarjs/public-static-readonly
static outputChannel: vscode.OutputChannel | undefined = undefined;
private static readonly consoleLogger = createConsoleLogger('RVW');

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

public static dispose() {
this.outputChannel.dispose();
this.outputChannel?.dispose();
this.outputChannel = undefined;
}

private static log(level: LogLevel, message: string, data: Record<string, unknown> | undefined) {
const timestamp = new Date().toISOString().split('T')[1];
const levelStr = LogLevel[level] || 'UNKNOWN';
const cleanedData = removePromptsFromData(data);
const line = `[${timestamp}] [${levelStr}] ${message}`;
if (cleanedData === undefined) {
this.outputChannel.appendLine(line);
if (this.outputChannel === undefined) {
const methodName = LogLevel[level].toLowerCase() as
| undefined
| keyof Omit<ILogger, 'dispose'>;
if (methodName !== undefined && typeof this.consoleLogger[methodName] === 'function') {
if (cleanedData === undefined) {
this.consoleLogger[methodName](`[${timestamp}] ${message}`);
} else {
this.consoleLogger[methodName](`[${timestamp}] ${message}`, cleanedData);
}
}
} else {
try {
this.outputChannel.appendLine(`${line} : ${JSON.stringify(cleanedData)}`);
} catch {
this.outputChannel.appendLine(`${line} : unserializable data`);
const levelStr = LogLevel[level] || 'UNKNOWN';
const line = `[${timestamp}] [${levelStr}] ${message}`;

if (cleanedData === undefined) {
this.outputChannel.appendLine(line);
Comment on lines +96 to +97
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): Catching all errors when serializing cleanedData may mask unexpected issues.

Consider logging the specific error when serialization fails for reasons other than circular references to improve debuggability.

Suggested change
if (cleanedData === undefined) {
this.outputChannel.appendLine(line);
if (cleanedData === undefined) {
this.outputChannel.appendLine(line);
} else {
try {
this.outputChannel.appendLine(`${line} : ${JSON.stringify(cleanedData)}`);
} catch (err) {
this.outputChannel.appendLine(`${line} : unserializable data (${err instanceof Error ? err.message : String(err)})`);
}
}

} else {
try {
this.outputChannel.appendLine(`${line} : ${JSON.stringify(cleanedData)}`);
} catch {
this.outputChannel.appendLine(`${line} : unserializable data`);
}
}
}
}
}

export const Logger: ILogger = {
debug: (message: string, data?: Record<string, unknown>) => LoggerImpl.debug(message, data),
info: (message: string, data?: Record<string, unknown>) => LoggerImpl.info(message, data),
warn: (message: string, data?: Record<string, unknown>) => LoggerImpl.warn(message, data),
error: (message: string, data?: Record<string, unknown>) => LoggerImpl.error(message, data),
dispose: () => LoggerImpl.dispose(),
};
export const Logger: ILogger & {
setOutputChannel: (outputChannel: vscode.OutputChannel | undefined) => void;
} =
/**
* Sets the VS Code output channel for the logger.
* The logger takes ownership of the channel and will dispose it
* when `Logger.dispose()` is called or when a new channel is set.
* @param outputChannel The output channel to use for logging.
*/
{
setOutputChannel: (outputChannel: vscode.OutputChannel | undefined) => {
if (LoggerImpl.outputChannel === outputChannel) {
return;
}
LoggerImpl.dispose();
LoggerImpl.outputChannel = outputChannel;
},
debug: (message: string, data?: Record<string, unknown>) => LoggerImpl.debug(message, data),
info: (message: string, data?: Record<string, unknown>) => LoggerImpl.info(message, data),
warn: (message: string, data?: Record<string, unknown>) => LoggerImpl.warn(message, data),
error: (message: string, data?: Record<string, unknown>) => LoggerImpl.error(message, data),
dispose: () => LoggerImpl.dispose(),
};

export const getLogger = (tag: string): ILogger => ({
debug: (message: string, data?: Record<string, unknown>) =>
Expand Down
16 changes: 16 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/* eslint-disable no-console */
import type { ILogger } from './types';

/**
* Generate a unique ID for requests
*/
Expand All @@ -8,3 +11,16 @@ export function generateId(prefix: string): string {
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
export function createConsoleLogger(tag: string): ILogger {
return {
debug: (message: string, data?: Record<string, unknown>) =>
console.debug(`[${tag}] ${message}`, data),
info: (message: string, data?: Record<string, unknown>) =>
console.info(`[${tag}] ${message}`, data),
warn: (message: string, data?: Record<string, unknown>) =>
console.warn(`[${tag}] ${message}`, data),
error: (message: string, data?: Record<string, unknown>) =>
console.error(`[${tag}] ${message}`, data),
dispose: () => {},
};
}
13 changes: 7 additions & 6 deletions tests/lib/host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ describe('host module exports', () => {

expect(typeof Logger).toBe('object');
expect(typeof getLogger).toBe('function');
expect(Array.isArray(disallowedLogKeys)).toBe(true);
// Recent change: disallowedLogKeys is now a Set
expect(disallowedLogKeys instanceof Set).toBe(true);
});

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

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

expect(disallowedLogKeys.length).toBeGreaterThan(0);
expect(disallowedLogKeys).toContain('password');
expect(disallowedLogKeys).toContain('token');
expect(disallowedLogKeys).toContain('secret');
expect(disallowedLogKeys.size).toBeGreaterThan(0);
expect(disallowedLogKeys.has('password')).toBe(true);
expect(disallowedLogKeys.has('token')).toBe(true);
expect(disallowedLogKeys.has('secret')).toBe(true);
});

it('should have working isViewApiRequest function', async () => {
Expand Down
17 changes: 9 additions & 8 deletions tests/lib/host/logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ describe('host/logger', () => {
mockOutputChannel.appendLine.mockClear();
mockOutputChannel.dispose.mockClear();

// Route Logger output to the mocked VS Code output channel
Logger.setOutputChannel(mockOutputChannel);

// Mock date for consistent timestamps
originalDateNow = Date.prototype.toISOString;
Date.prototype.toISOString = vi.fn(() => '2024-01-01T12:00:00.000Z');
Expand All @@ -19,6 +22,8 @@ describe('host/logger', () => {
afterEach(() => {
Date.prototype.toISOString = originalDateNow;
vi.clearAllMocks();
// Reset output channel between tests
Logger.setOutputChannel(undefined);
});

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

describe('disallowedLogKeys', () => {
it('should export correct disallowed keys', () => {
expect(disallowedLogKeys).toEqual([
'password',
'secret',
'token',
'apiKey',
'apiSecret',
'content',
]);
// Recent change: exported as Set with lowercase API keys
expect(disallowedLogKeys).toEqual(
new Set(['password', 'secret', 'token', 'apikey', 'apisecret', 'content'])
);
});
});

Expand Down
Loading