diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b8ec8..5642421 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Install: Added automated install and upgrade support for Linux using official setup scripts - Notification: Detect outdated Dev Proxy config files in workspace and show warning when schema versions don't match installed version - Command: `dev-proxy-toolkit.upgrade-configs` - Upgrade config files with Copilot Chat using Dev Proxy MCP tools +- Logging: Added leveled logging to Output panel (`Dev Proxy Toolkit`) across the entire extension covering commands, diagnostics, services, and utilities to help investigate issues - Quick Fixes: Enable local language model fix now adds or updates `languageModel.enabled: true` for supported plugins only - Workspace: Added automatic prompt to recommend extension in `.vscode/extensions.json` when Dev Proxy config files are detected - Command: Added `Add to Workspace Recommendations` to manually add extension to workspace recommendations @@ -23,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed: +- Logging: Fixed log flooding from repeated running state checks by only logging on state changes +- Logging: Fixed log feedback loop caused by Output Channel document events triggering diagnostics + - Diagnostics: Language model diagnostic now correctly targets plugins that can use a local language model (OpenAIMockResponsePlugin, OpenApiSpecGeneratorPlugin, TypeSpecGeneratorPlugin) and shows as an informational hint instead of a warning ## [1.12.0] - 2026-01-29 diff --git a/README.md b/README.md index 3bcab6a..c1fa26f 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,10 @@ This extension includes an MCP server for AI-assisted development. See [Dev Prox ## Troubleshooting +**View debug logs** +- Open the Output panel (`View > Output`) and select **Dev Proxy Toolkit** from the dropdown to see extension logs +- Use the log level setting to control verbosity (trace, debug, info, warning, error) + **Dev Proxy not detected?** - Ensure Dev Proxy is installed and available in your PATH - Check the `dev-proxy-toolkit.version` setting if you have both stable and beta installed diff --git a/src/code-actions.ts b/src/code-actions.ts index f88afc6..f4f21c0 100644 --- a/src/code-actions.ts +++ b/src/code-actions.ts @@ -4,6 +4,7 @@ import parse from 'json-to-ast'; import { getASTNode, getRangeFromASTNode } from './utils'; import { pluginSnippets } from './data'; import snippetsJson from './snippets/json-snippets.json'; +import * as logger from './logger'; /** * Extract the diagnostic code value from the object format. @@ -60,6 +61,7 @@ export const registerCodeActions = (context: vscode.ExtensionContext) => { context.globalState.get('devProxyInstall'); if (!devProxyInstall) { + logger.debug('Dev Proxy install not found, code actions disabled'); return; } @@ -124,8 +126,8 @@ export function extractSchemaFilename(schemaUrl: string): string { if (match) { return match[1]; } - } catch { - // Fall through to default + } catch (error) { + logger.warn('Failed to extract schema filename, using default', { schemaUrl, error }); } return defaultSchema; diff --git a/src/code-lens.ts b/src/code-lens.ts index d1b5ade..f452ea8 100644 --- a/src/code-lens.ts +++ b/src/code-lens.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { isConfigFile, getASTNode, getRangeFromASTNode } from './utils'; import parse from 'json-to-ast'; import { pluginSnippets } from './data'; +import * as logger from './logger'; export const registerCodeLens = (context: vscode.ExtensionContext) => { context.subscriptions.push( @@ -31,44 +32,48 @@ export const pluginLensProvider: vscode.CodeLensProvider = { export const createCodeLensForPluginNodes = (document: vscode.TextDocument) => { const codeLens: vscode.CodeLens[] = []; if (isConfigFile(document)) { - const documentNode = parse(document.getText()) as parse.ObjectNode; - const pluginsNode = getASTNode( - documentNode.children, - 'Identifier', - 'plugins' - ); + try { + const documentNode = parse(document.getText()) as parse.ObjectNode; + const pluginsNode = getASTNode( + documentNode.children, + 'Identifier', + 'plugins' + ); - if ( - pluginsNode && - (pluginsNode.value as parse.ArrayNode).children.length !== 0 - ) { - const pluginNodes = (pluginsNode.value as parse.ArrayNode) - .children as parse.ObjectNode[]; + if ( + pluginsNode && + (pluginsNode.value as parse.ArrayNode).children.length !== 0 + ) { + const pluginNodes = (pluginsNode.value as parse.ArrayNode) + .children as parse.ObjectNode[]; - pluginNodes.forEach((pluginNode: parse.ObjectNode) => { - const pluginNameNode = getASTNode( - pluginNode.children, - 'Identifier', - 'name' - ); - if (!pluginNameNode) { - return; - } - const pluginName = (pluginNameNode?.value as parse.LiteralNode) - .value as string; + pluginNodes.forEach((pluginNode: parse.ObjectNode) => { + const pluginNameNode = getASTNode( + pluginNode.children, + 'Identifier', + 'name' + ); + if (!pluginNameNode) { + return; + } + const pluginName = (pluginNameNode?.value as parse.LiteralNode) + .value as string; - const isValidName = pluginSnippets[pluginName]; + const isValidName = pluginSnippets[pluginName]; - if (isValidName) { - codeLens.push( - new vscode.CodeLens(getRangeFromASTNode(pluginNameNode), { - title: `📄 ${pluginName}`, - command: 'dev-proxy-toolkit.openPluginDoc', - arguments: [pluginName], - }) - ); - } - }); + if (isValidName) { + codeLens.push( + new vscode.CodeLens(getRangeFromASTNode(pluginNameNode), { + title: `📄 ${pluginName}`, + command: 'dev-proxy-toolkit.openPluginDoc', + arguments: [pluginName], + }) + ); + } + }); + } + } catch (error) { + logger.warn('Failed to parse config file for code lens generation', { file: document.fileName, error }); } } diff --git a/src/commands/config.ts b/src/commands/config.ts index 0c7414c..4af5c5d 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -3,6 +3,7 @@ import { Commands } from '../constants'; import { executeCommand } from '../utils/shell'; import { getDevProxyExe } from '../detect'; import { VersionPreference } from '../enums'; +import * as logger from '../logger'; /** * Configuration file commands: open, create new. @@ -25,6 +26,7 @@ export function registerConfigCommands( } async function openConfig(devProxyExe: string): Promise { + logger.debug('Opening Dev Proxy config', { devProxyExe }); await executeCommand(`${devProxyExe} config open`); } @@ -36,15 +38,18 @@ async function createNewConfig(devProxyExe: string): Promise { const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (!workspaceFolder) { + logger.warn('Cannot create config: no workspace folder open'); vscode.window.showErrorMessage('No workspace folder open'); return; } + logger.info('Creating new config file', { fileName, workspaceFolder }); const devProxyFolder = vscode.Uri.file(`${workspaceFolder}/.devproxy`); const configUri = vscode.Uri.file(`${workspaceFolder}/.devproxy/${fileName}`); // Check if file already exists if (await fileExists(configUri)) { + logger.warn('Config file already exists', { path: configUri.fsPath }); vscode.window.showErrorMessage('A file with that name already exists'); return; } @@ -67,9 +72,11 @@ async function createNewConfig(devProxyExe: string): Promise { ); // Open the newly created file + logger.info('Config file created', { path: configUri.fsPath }); const document = await vscode.workspace.openTextDocument(configUri); await vscode.window.showTextDocument(document); } catch (error) { + logger.error('Failed to create new config file', error); vscode.window.showErrorMessage('Failed to create new config file'); } } diff --git a/src/commands/discovery.ts b/src/commands/discovery.ts index e1154c3..6ec8498 100644 --- a/src/commands/discovery.ts +++ b/src/commands/discovery.ts @@ -3,6 +3,7 @@ import { Commands } from '../constants'; import { TerminalService } from '../services/terminal'; import { getDevProxyExe } from '../detect'; import { VersionPreference } from '../enums'; +import * as logger from '../logger'; /** * URL discovery command. @@ -40,10 +41,12 @@ async function discoverUrlsToWatch( // User cancelled if (processNames === undefined) { + logger.debug('URL discovery cancelled by user'); return; } const command = buildDiscoverCommand(devProxyExe, processNames); + logger.info('Starting URL discovery', { processNames: processNames || '(all processes)', command }); terminalService.sendCommand(terminal, command); } diff --git a/src/commands/docs.ts b/src/commands/docs.ts index ca76f84..6923ba5 100644 --- a/src/commands/docs.ts +++ b/src/commands/docs.ts @@ -3,6 +3,7 @@ import { Commands } from '../constants'; import { pluginDocs } from '../data'; import parse from 'json-to-ast'; import { getASTNode, getRangeFromASTNode } from '../utils/ast'; +import * as logger from '../logger'; /** * Documentation and language model configuration commands. @@ -21,8 +22,11 @@ export function registerDocCommands(context: vscode.ExtensionContext): void { function openPluginDocumentation(pluginName: string): void { const doc = pluginDocs[pluginName]; if (doc) { + logger.debug('Opening plugin docs', { pluginName, url: doc.url }); const target = vscode.Uri.parse(doc.url); vscode.env.openExternal(target); + } else { + logger.warn('Plugin docs not found', { pluginName }); } } @@ -42,11 +46,13 @@ async function addLanguageModelConfig(uri: vscode.Uri): Promise { } } catch (error) { // Fallback to simple text-based insertion + logger.debug('Failed to parse document with json-to-ast, using text-based fallback', error); addLanguageModelFallback(document, edit, uri); } await vscode.workspace.applyEdit(edit); await document.save(); + logger.info('Language model configuration added'); vscode.window.showInformationMessage('Language model configuration added'); } diff --git a/src/commands/install.ts b/src/commands/install.ts index 705fc2f..9993295 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -8,6 +8,7 @@ import { openUpgradeDocumentation, } from '../utils/shell'; import { PackageManager, VersionPreference } from '../enums'; +import * as logger from '../logger'; /** * Installation and upgrade commands. @@ -34,6 +35,7 @@ async function installDevProxy( ): Promise { const message = vscode.window.setStatusBarMessage('Installing Dev Proxy...'); const versionPreference = configuration.get('version') as VersionPreference; + logger.info('Installing Dev Proxy', { platform, versionPreference }); try { if (platform === 'win32') { @@ -55,17 +57,21 @@ async function installOnWindows(versionPreference: VersionPreference): Promise try { await executeCommand('brew --version'); } catch { + logger.warn('Homebrew not found on PATH'); vscode.window.showErrorMessage('Homebrew is not installed. Please install brew and try again.'); return; } try { + logger.info('Installing Dev Proxy via Homebrew', { packageId }); await executeCommand('brew tap dotnet/dev-proxy'); await executeCommand(`brew install ${packageId}`); + logger.info('Dev Proxy installed successfully via Homebrew'); const result = await vscode.window.showInformationMessage('Dev Proxy installed.', 'Reload'); if (result === 'Reload') { await vscode.commands.executeCommand('workbench.action.reloadWindow'); } } catch (error) { + logger.error('Failed to install Dev Proxy via Homebrew', error); vscode.window.showErrorMessage(`Failed to install Dev Proxy.\n${error}`); } } @@ -137,6 +147,7 @@ async function upgradeDevProxy(configuration: vscode.WorkspaceConfiguration): Pr const platform = process.platform; const versionPreference = configuration.get('version') as VersionPreference; const isBeta = versionPreference === VersionPreference.Beta; + logger.info('Upgrading Dev Proxy', { platform, versionPreference }); // Linux uses install script to upgrade if (platform === 'linux') { @@ -182,6 +193,7 @@ async function upgradeDevProxy(configuration: vscode.WorkspaceConfiguration): Pr isBeta ); if (!upgraded) { + logger.warn('Upgrade via winget failed, opening upgrade docs'); openUpgradeDocumentation(); } return; @@ -202,11 +214,13 @@ async function upgradeDevProxy(configuration: vscode.WorkspaceConfiguration): Pr isBeta ); if (!upgraded) { + logger.warn('Upgrade via Homebrew failed, opening upgrade docs'); openUpgradeDocumentation(); } return; } // Unknown platform + logger.warn('Unknown platform, opening upgrade docs', { platform }); openUpgradeDocumentation(); } diff --git a/src/commands/jwt.ts b/src/commands/jwt.ts index 973e496..823bfa9 100644 --- a/src/commands/jwt.ts +++ b/src/commands/jwt.ts @@ -3,6 +3,7 @@ import { Commands } from '../constants'; import { executeCommand } from '../utils/shell'; import { getDevProxyExe } from '../detect'; import { VersionPreference } from '../enums'; +import * as logger from '../logger'; /** * JWT (JSON Web Token) generation commands. @@ -36,8 +37,10 @@ interface JwtParams { async function createJwt(devProxyExe: string): Promise { const params = await collectJwtParams(); if (!params) { + logger.debug('JWT creation cancelled by user'); return; // User cancelled } + logger.info('Generating JWT', { name: params.name, issuer: params.issuer, audiences: params.audiences.length, roles: params.roles.length, scopes: params.scopes.length, validFor: params.validFor }); await vscode.window.withProgress( { @@ -50,8 +53,10 @@ async function createJwt(devProxyExe: string): Promise { const command = buildJwtCommand(devProxyExe, params); const result = await executeCommand(command); const token = extractToken(result); + logger.info('JWT token generated successfully'); await presentToken(token, command); } catch (error) { + logger.error('Failed to generate JWT token', error); vscode.window.showErrorMessage(`Failed to generate JWT token: ${error}`); } } diff --git a/src/commands/proxy.ts b/src/commands/proxy.ts index 75ea94d..3c207d8 100644 --- a/src/commands/proxy.ts +++ b/src/commands/proxy.ts @@ -5,6 +5,7 @@ import { TerminalService } from '../services/terminal'; import { isConfigFile } from '../utils'; import { getDevProxyExe } from '../detect'; import { VersionPreference } from '../enums'; +import * as logger from '../logger'; /** * Proxy lifecycle commands: start, stop, restart. @@ -47,6 +48,7 @@ async function startDevProxy( const configFilePath = getActiveConfigFilePath(); const command = configFilePath ? `${devProxyExe} --config-file "${configFilePath}"` : devProxyExe; + logger.debug('Starting Dev Proxy', { configFile: configFilePath ?? 'default' }); terminalService.sendCommand(terminal, command); } @@ -55,14 +57,17 @@ async function stopDevProxy( devProxyExe: string, configuration: vscode.WorkspaceConfiguration ): Promise { + logger.debug('Stopping Dev Proxy'); await apiClient.stop(); const closeTerminal = configuration.get('closeTerminal', true); if (closeTerminal) { + logger.debug('Waiting for Dev Proxy to stop before closing terminal'); await waitForProxyToStop(apiClient); const terminalService = TerminalService.fromConfiguration(); terminalService.disposeDevProxyTerminals(); + logger.debug('Dev Proxy terminals disposed'); } } @@ -71,6 +76,7 @@ async function restartDevProxy( configuration: vscode.WorkspaceConfiguration, devProxyExe: string ): Promise { + logger.debug('Restarting Dev Proxy'); try { await apiClient.stop(); await waitForProxyToStop(apiClient); @@ -84,7 +90,9 @@ async function restartDevProxy( : devProxyExe; terminalService.sendCommand(terminal, command); - } catch { + logger.debug('Dev Proxy restart command sent'); + } catch (error) { + logger.error('Failed to restart Dev Proxy', error); vscode.window.showErrorMessage('Failed to restart Dev Proxy'); } } diff --git a/src/commands/recording.ts b/src/commands/recording.ts index 3afc139..4e9a8a6 100644 --- a/src/commands/recording.ts +++ b/src/commands/recording.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { Commands, ContextKeys } from '../constants'; import { DevProxyApiClient } from '../services/api-client'; +import * as logger from '../logger'; /** * Recording commands: start/stop recording API requests. @@ -25,8 +26,10 @@ export function registerRecordingCommands(context: vscode.ExtensionContext): voi async function startRecording(apiClient: DevProxyApiClient): Promise { try { await apiClient.startRecording(); + logger.info('Recording started'); vscode.commands.executeCommand('setContext', ContextKeys.isRecording, true); - } catch { + } catch (error) { + logger.error('Failed to start recording', error); vscode.window.showErrorMessage('Failed to start recording'); } } @@ -34,13 +37,21 @@ async function startRecording(apiClient: DevProxyApiClient): Promise { async function stopRecording(apiClient: DevProxyApiClient): Promise { try { await apiClient.stopRecording(); + logger.info('Recording stopped'); vscode.commands.executeCommand('setContext', ContextKeys.isRecording, false); - } catch { + } catch (error) { + logger.error('Failed to stop recording', error); vscode.window.showErrorMessage('Failed to stop recording'); } } async function raiseMockRequest(apiClient: DevProxyApiClient): Promise { - await apiClient.raiseMockRequest(); - vscode.window.showInformationMessage('Mock request raised'); + try { + await apiClient.raiseMockRequest(); + logger.info('Mock request raised'); + vscode.window.showInformationMessage('Mock request raised'); + } catch (error) { + logger.error('Failed to raise mock request', error); + vscode.window.showErrorMessage('Failed to raise mock request'); + } } diff --git a/src/detect.ts b/src/detect.ts index c47e8a5..daad28d 100644 --- a/src/detect.ts +++ b/src/detect.ts @@ -3,19 +3,25 @@ import os from 'os'; import { VersionExeName, VersionPreference } from './enums'; import { executeCommand, resolveDevProxyExecutable } from './utils/shell'; import * as vscode from 'vscode'; +import * as logger from './logger'; + +let lastKnownRunningState: boolean | undefined; export const getVersion = async (devProxyExe: string) => { try { const version = await executeCommand(`${devProxyExe} --version`); const versionLines = version.trim().split('\n'); const lastLine = versionLines[versionLines.length - 1]; + logger.info('Dev Proxy version detected', lastLine.trim()); return lastLine.trim(); } catch (error) { + logger.warn('Failed to get Dev Proxy version', error); return ""; } }; export const detectDevProxyInstall = async (versionPreference: VersionPreference): Promise => { + logger.info('Detecting Dev Proxy installation', { versionPreference }); const configuration = vscode.workspace.getConfiguration('dev-proxy-toolkit'); const customPath = configuration.get('devProxyPath'); const exeName = getDevProxyExe(versionPreference); @@ -28,7 +34,7 @@ export const detectDevProxyInstall = async (versionPreference: VersionPreference const isOutdated = isInstalled && outdatedVersion !== ''; const isRunning = await isDevProxyRunning(devProxyExe); vscode.commands.executeCommand('setContext', 'isDevProxyRunning', isRunning); - return { + const devProxyInstall = { version, isInstalled, isBeta, @@ -37,6 +43,8 @@ export const detectDevProxyInstall = async (versionPreference: VersionPreference isOutdated, isRunning }; + logger.info('Dev Proxy installation detected', devProxyInstall); + return devProxyInstall; }; export const extractVersionFromOutput = (output: string): string => { @@ -91,9 +99,18 @@ export const isDevProxyRunning = async (devProxyExe: string): Promise = }); // If we get any response (even an error), Dev Proxy is running - return response.status >= 200 && response.status < 500; + const running = response.status >= 200 && response.status < 500; + if (running !== lastKnownRunningState) { + logger.info('Dev Proxy running state changed', { running, status: response.status }); + lastKnownRunningState = running; + } + return running; } catch (error) { // If the request fails (connection refused, timeout, etc.), Dev Proxy is not running + if (lastKnownRunningState !== false) { + logger.info('Dev Proxy is not running'); + lastKnownRunningState = false; + } return false; } }; diff --git a/src/diagnostics.ts b/src/diagnostics.ts index b4007f6..cc23ea2 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -7,6 +7,15 @@ import { DiagnosticCodes } from './constants'; import { getDiagnosticCode } from './utils'; import * as semver from 'semver'; import { fetchSchema, validateAgainstSchema } from './services'; +import * as logger from './logger'; + +const logDiagnostics = (diagnostics: vscode.Diagnostic[]) => { + for (const d of diagnostics) { + const severity = ['Error', 'Warning', 'Information', 'Hint'][d.severity]; + const line = d.range.start.line + 1; + logger.info(`[${severity}] Line ${line}: ${d.message}`); + } +}; export const updateConfigFileDiagnostics = async ( context: vscode.ExtensionContext, @@ -16,8 +25,10 @@ export const updateConfigFileDiagnostics = async ( const devProxyInstall = context.globalState.get('devProxyInstall'); if (!devProxyInstall) { + logger.warn('No Dev Proxy install found, skipping config diagnostics'); return; } + logger.info('Running config diagnostics', { file: document.fileName }); const diagnostics: vscode.Diagnostic[] = []; const documentNode = getObjectNodeFromDocument(document); const pluginsNode = getPluginsNode(documentNode); @@ -31,10 +42,14 @@ export const updateConfigFileDiagnostics = async ( // Set initial diagnostics synchronously collection.set(document.uri, diagnostics); + logger.info('Config diagnostics complete', { file: document.fileName, count: diagnostics.length }); + logDiagnostics(diagnostics); // Run async schema content validation and update diagnostics const asyncDiagnostics = await validateConfigSectionContents(document, documentNode); if (asyncDiagnostics.length > 0) { + logger.info('Async schema validation found issues', { file: document.fileName, count: asyncDiagnostics.length }); + logDiagnostics(asyncDiagnostics); collection.set(document.uri, [...diagnostics, ...asyncDiagnostics]); } }; @@ -47,8 +62,10 @@ export const updateFileDiagnostics = ( const devProxyInstall = context.globalState.get('devProxyInstall'); if (!devProxyInstall) { + logger.warn('No Dev Proxy install found, skipping file diagnostics'); return; } + logger.info('Running file diagnostics', { file: document.fileName }); const diagnostics: vscode.Diagnostic[] = []; const documentNode = getObjectNodeFromDocument(document); @@ -56,6 +73,8 @@ export const updateFileDiagnostics = ( checkSchemaCompatibility(documentNode, devProxyInstall, diagnostics); collection.set(document.uri, diagnostics); + logger.info('File diagnostics complete', { file: document.fileName, count: diagnostics.length }); + logDiagnostics(diagnostics); }; const checkConfigSection = ( @@ -119,6 +138,7 @@ const checkSchemaCompatibility = ( const devProxyVersion = devProxyInstall.isBeta ? devProxyInstall.version.split('-')[0] : devProxyInstall.version; + logger.debug('Checking schema compatibility', { schema: schemaValue, installedVersion: devProxyVersion }); if (!schemaValue.includes(`${devProxyVersion}`)) { const diagnostic = new vscode.Diagnostic( getRangeFromASTNode(schemaValueNode), @@ -140,6 +160,7 @@ const checkPlugins = ( if (pluginsNode && (pluginsNode.value as parse.ArrayNode)) { const pluginNodes = (pluginsNode.value as parse.ArrayNode) .children as parse.ObjectNode[]; + logger.debug('Checking plugins', { count: pluginNodes.length }); checkAtLeastOneEnabledPlugin(pluginNodes, diagnostics, pluginsNode); warnOnReporterPosition(pluginNodes, diagnostics); @@ -778,7 +799,7 @@ async function validateSingleConfigSection( }); } } catch (error) { - console.warn(`Error validating config section ${configSectionName}:`, error); + logger.warn(`Error validating config section ${configSectionName}`, error); } } diff --git a/src/documents.ts b/src/documents.ts index 9a88d9d..41890e5 100644 --- a/src/documents.ts +++ b/src/documents.ts @@ -1,12 +1,17 @@ import * as vscode from 'vscode'; import { isConfigFile, isProxyFile } from './utils'; import { updateFileDiagnostics, updateConfigFileDiagnostics } from './diagnostics'; +import * as logger from './logger'; export const registerDocumentListeners = (context: vscode.ExtensionContext, collection: vscode.DiagnosticCollection) => { context.subscriptions.push( vscode.workspace.onDidOpenTextDocument(document => { + if (document.uri.scheme !== 'file') { + return; + } try { if (isProxyFile(document)) { + logger.debug('Proxy file opened', { path: document.uri.fsPath }); updateFileDiagnostics(context, document, collection); vscode.commands.executeCommand('setContext', 'isDevProxyConfigFile', false); } @@ -14,33 +19,39 @@ export const registerDocumentListeners = (context: vscode.ExtensionContext, coll vscode.commands.executeCommand('setContext', 'isDevProxyConfigFile', false); return; } else { + logger.debug('Config file opened', { path: document.uri.fsPath }); vscode.commands.executeCommand('setContext', 'isDevProxyConfigFile', true); updateConfigFileDiagnostics(context, document, collection); } } catch (error) { - console.error('Error handling document open:', error); + logger.error('Error handling document open', error); } }) ); context.subscriptions.push( vscode.workspace.onDidChangeTextDocument(event => { + if (event.document.uri.scheme !== 'file') { + return; + } try { if (!isConfigFile(event.document) && !isProxyFile(event.document)) { collection.delete(event.document.uri); return; } if (isConfigFile(event.document)) { + logger.debug('Config file changed', { path: event.document.uri.fsPath }); updateConfigFileDiagnostics(context, event.document, collection); vscode.commands.executeCommand('setContext', 'isDevProxyConfigFile', true); return; } if (isProxyFile(event.document)) { + logger.debug('Proxy file changed', { path: event.document.uri.fsPath }); updateFileDiagnostics(context, event.document, collection); vscode.commands.executeCommand('setContext', 'isDevProxyConfigFile', false); } } catch (error) { - console.error('Error handling document change:', error); + logger.error('Error handling document change', error); } }) ); diff --git a/src/extension.ts b/src/extension.ts index 1cb62a3..8c8806e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,14 +10,21 @@ import { VersionPreference } from './enums'; import { registerMcpServer } from './mcp'; import { registerTaskProvider } from './task-provider'; import { promptForWorkspaceRecommendation } from './utils'; +import * as logger from './logger'; // Global variable to track the interval let statusBarInterval: NodeJS.Timeout | undefined; export const activate = async (context: vscode.ExtensionContext): Promise => { + const logChannel = logger.initializeLogger(); + context.subscriptions.push(logChannel); + + logger.info('Activating Dev Proxy Toolkit extension'); + const configuration = vscode.workspace.getConfiguration('dev-proxy-toolkit'); const versionPreference = configuration.get('version') as VersionPreference; + logger.debug('Configuration loaded', { versionPreference }); const statusBar = createStatusBar(context); await updateGlobalState(context, versionPreference); @@ -32,6 +39,7 @@ export const activate = async (context: vscode.ExtensionContext): Promise { + logger.info('Deactivating Dev Proxy Toolkit extension'); // Clean up the interval if it's still running if (statusBarInterval) { clearInterval(statusBarInterval); diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..d247719 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,70 @@ +import * as vscode from 'vscode'; + +/** + * Centralized logger for the Dev Proxy Toolkit extension. + * + * Uses a VS Code LogOutputChannel to provide leveled logging that + * appears in the Output panel and respects the user's log level settings. + */ + +let logger: vscode.LogOutputChannel | undefined; + +/** + * Initialize the logger. Must be called once during extension activation. + */ +export function initializeLogger(): vscode.LogOutputChannel { + logger = vscode.window.createOutputChannel('Dev Proxy Toolkit', { log: true }); + return logger; +} + +/** + * Get the logger instance. Falls back to a no-op if not initialized. + */ +function getLogger(): vscode.LogOutputChannel | undefined { + return logger; +} + +/** + * Log a trace-level message. + */ +export function trace(message: string, ...args: unknown[]): void { + try { + getLogger()?.trace(message, ...args); + } catch { /* channel may be closed */ } +} + +/** + * Log a debug-level message. + */ +export function debug(message: string, ...args: unknown[]): void { + try { + getLogger()?.debug(message, ...args); + } catch { /* channel may be closed */ } +} + +/** + * Log an info-level message. + */ +export function info(message: string, ...args: unknown[]): void { + try { + getLogger()?.info(message, ...args); + } catch { /* channel may be closed */ } +} + +/** + * Log a warning-level message. + */ +export function warn(message: string, ...args: unknown[]): void { + try { + getLogger()?.warn(message, ...args); + } catch { /* channel may be closed */ } +} + +/** + * Log an error-level message. + */ +export function error(message: string, ...args: unknown[]): void { + try { + getLogger()?.error(message, ...args); + } catch { /* channel may be closed */ } +} diff --git a/src/mcp.ts b/src/mcp.ts index 6f6c8d4..9dab806 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -1,6 +1,8 @@ import * as vscode from 'vscode'; +import * as logger from './logger'; export const registerMcpServer = (context: vscode.ExtensionContext) => { + logger.debug('Registering MCP server definition provider'); const didChangeEmitter = new vscode.EventEmitter(); context.subscriptions.push( diff --git a/src/notifications.ts b/src/notifications.ts index 364055c..4b6c504 100644 --- a/src/notifications.ts +++ b/src/notifications.ts @@ -2,10 +2,12 @@ import * as vscode from 'vscode'; import { DevProxyInstall } from './types'; import { Commands } from './constants'; import { findOutdatedConfigFiles, getNormalizedVersion } from './utils'; +import * as logger from './logger'; export const handleStartNotification = (context: vscode.ExtensionContext) => { const devProxyInstall = context.globalState.get('devProxyInstall'); if (!devProxyInstall) { + logger.warn('Dev Proxy install not found in global state'); return () => { const message = `Dev Proxy is not installed, or not in PATH.`; return { @@ -20,6 +22,7 @@ export const handleStartNotification = (context: vscode.ExtensionContext) => { }; }; if (!devProxyInstall.isInstalled) { + logger.warn('Dev Proxy is not installed'); return () => { const message = `Dev Proxy is not installed, or not in PATH.`; return { @@ -34,6 +37,7 @@ export const handleStartNotification = (context: vscode.ExtensionContext) => { }; }; if (devProxyInstall.isOutdated) { + logger.warn('Dev Proxy is outdated', { current: devProxyInstall.version, available: devProxyInstall.outdatedVersion }); return () => { const message = `New Dev Proxy version ${devProxyInstall.outdatedVersion} is available.`; return { diff --git a/src/services/api-client.ts b/src/services/api-client.ts index 06c65a6..a9544ad 100644 --- a/src/services/api-client.ts +++ b/src/services/api-client.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import * as logger from '../logger'; /** * Client for communicating with the Dev Proxy API. @@ -40,7 +41,8 @@ export class DevProxyApiClient { signal: AbortSignal.timeout(2000), }); return response.status >= 200 && response.status < 500; - } catch { + } catch (error) { + logger.debug('Dev Proxy API unreachable', error); return false; } } @@ -49,6 +51,7 @@ export class DevProxyApiClient { * Stop the proxy. */ async stop(): Promise { + logger.debug('Stopping Dev Proxy'); await this.post('/proxy/stopproxy'); } @@ -56,6 +59,7 @@ export class DevProxyApiClient { * Raise a mock request. */ async raiseMockRequest(): Promise { + logger.debug('Raising mock request'); await this.post('/proxy/mockrequest'); } @@ -63,6 +67,7 @@ export class DevProxyApiClient { * Start recording API requests. */ async startRecording(): Promise { + logger.debug('Starting recording'); await this.post('/proxy', { recording: true }); } @@ -70,6 +75,7 @@ export class DevProxyApiClient { * Stop recording API requests. */ async stopRecording(): Promise { + logger.debug('Stopping recording'); await this.post('/proxy', { recording: false }); } @@ -83,10 +89,12 @@ export class DevProxyApiClient { signal: AbortSignal.timeout(this.timeout), }); if (!response.ok) { + logger.debug('Failed to get proxy status', { status: response.status }); return null; } return (await response.json()) as ProxyStatus; - } catch { + } catch (error) { + logger.debug('Failed to get proxy status', error); return null; } } diff --git a/src/services/schema-validator.ts b/src/services/schema-validator.ts index bb58ef5..0628dd9 100644 --- a/src/services/schema-validator.ts +++ b/src/services/schema-validator.ts @@ -1,4 +1,5 @@ import Ajv, { ErrorObject } from 'ajv'; +import * as logger from '../logger'; /** * Schema cache to avoid fetching the same schema multiple times. @@ -48,13 +49,14 @@ export interface SchemaValidationError { export async function fetchSchema(schemaUrl: string): Promise { // Check cache first if (schemaCache.has(schemaUrl)) { + logger.debug('Schema loaded from cache', { schemaUrl }); return schemaCache.get(schemaUrl); } try { const response = await fetch(schemaUrl); if (!response.ok) { - console.warn(`Failed to fetch schema from ${schemaUrl}: ${response.status}`); + logger.warn(`Failed to fetch schema from ${schemaUrl}: ${response.status}`); return undefined; } @@ -62,7 +64,7 @@ export async function fetchSchema(schemaUrl: string): Promise { if (terminal.name === 'Dev Proxy') { + logger.debug('Disposing Dev Proxy terminal'); terminal.dispose(); } }); @@ -71,6 +76,7 @@ export class TerminalService { * Send a command to a terminal. */ sendCommand(terminal: vscode.Terminal, command: string): void { + logger.debug('Sending command to terminal', { terminal: terminal.name, command }); terminal.sendText(command); } } diff --git a/src/state.ts b/src/state.ts index 55f346f..9f4f367 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,9 +1,12 @@ import * as vscode from 'vscode'; import { detectDevProxyInstall } from './detect'; import { VersionPreference } from './enums'; +import * as logger from './logger'; export const updateGlobalState = async (context: vscode.ExtensionContext, versionPreference: VersionPreference) => { + logger.debug('Updating global state'); const devProxyInstall = await detectDevProxyInstall(versionPreference); vscode.commands.executeCommand('setContext', 'isDevProxyInstalled', devProxyInstall.isInstalled); context.globalState.update('devProxyInstall', devProxyInstall); + logger.info('Global state updated', { isInstalled: devProxyInstall.isInstalled, version: devProxyInstall.version }); }; \ No newline at end of file diff --git a/src/status-bar.ts b/src/status-bar.ts index 42a56e6..a91462f 100644 --- a/src/status-bar.ts +++ b/src/status-bar.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { DevProxyInstall } from './types'; import { getDevProxyExe, isDevProxyRunning } from './detect'; import { VersionPreference } from './enums'; +import * as logger from './logger'; export const createStatusBar = (context: vscode.ExtensionContext): vscode.StatusBarItem => { const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); @@ -64,6 +65,6 @@ export const statusBarLoop = async (context: vscode.ExtensionContext, statusBar: } } catch (error) { // Log but don't throw to prevent extension crashes - console.error('Error in statusBarLoop:', error); + logger.error('Error in statusBarLoop', error); } }; diff --git a/src/task-provider.ts b/src/task-provider.ts index bc36a89..64f9a07 100644 --- a/src/task-provider.ts +++ b/src/task-provider.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { getDevProxyExe } from './detect'; import { VersionPreference } from './enums'; +import * as logger from './logger'; interface DevProxyTaskDefinition extends vscode.TaskDefinition { type: 'devproxy'; @@ -134,6 +135,7 @@ export class DevProxyTaskProvider implements vscode.TaskProvider { } export const registerTaskProvider = (context: vscode.ExtensionContext) => { + logger.debug('Registering task provider'); const provider = new DevProxyTaskProvider(context); context.subscriptions.push( vscode.tasks.registerTaskProvider(DevProxyTaskProvider.DevProxyType, provider) diff --git a/src/test/logger.test.ts b/src/test/logger.test.ts new file mode 100644 index 0000000..a7bee8a --- /dev/null +++ b/src/test/logger.test.ts @@ -0,0 +1,46 @@ +/** + * Logger module tests. + * Verifies the logger initializes and provides leveled logging functions. + */ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import { initializeLogger, debug, info, warn, error, trace } from '../logger'; + +suite('Logger', () => { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('initializeLogger should create a LogOutputChannel', () => { + const channel = initializeLogger(); + assert.ok(channel); + assert.strictEqual(channel.name, 'Dev Proxy Toolkit'); + channel.dispose(); + }); + + test('logging functions should not throw before initialization', () => { + // These should be safe to call even if a different logger instance is active + assert.doesNotThrow(() => trace('test')); + assert.doesNotThrow(() => debug('test')); + assert.doesNotThrow(() => info('test')); + assert.doesNotThrow(() => warn('test')); + assert.doesNotThrow(() => error('test')); + }); + + test('logging functions should not throw after initialization', () => { + const channel = initializeLogger(); + assert.doesNotThrow(() => trace('test trace')); + assert.doesNotThrow(() => debug('test debug')); + assert.doesNotThrow(() => info('test info')); + assert.doesNotThrow(() => warn('test warn')); + assert.doesNotThrow(() => error('test error')); + channel.dispose(); + }); +}); diff --git a/src/utils/config-detection.ts b/src/utils/config-detection.ts index 105ee9e..db91a1f 100644 --- a/src/utils/config-detection.ts +++ b/src/utils/config-detection.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import parse from 'json-to-ast'; +import * as logger from '../logger'; /** * Find a property node by its key type and value. @@ -62,7 +63,8 @@ export function isConfigFile(document: vscode.TextDocument): boolean { } return false; - } catch { + } catch (error) { + logger.debug('Failed to parse document for config file detection', { file: document.fileName, error }); return false; } } @@ -83,7 +85,8 @@ export function isProxyFile(document: vscode.TextDocument): boolean { } return false; - } catch { + } catch (error) { + logger.debug('Failed to parse document for proxy file detection', { file: document.fileName, error }); return false; } } diff --git a/src/utils/shell.ts b/src/utils/shell.ts index bd00e70..398c5f1 100644 --- a/src/utils/shell.ts +++ b/src/utils/shell.ts @@ -9,6 +9,7 @@ import { VersionPreference, WingetPackageIdentifier, } from '../enums'; +import * as logger from '../logger'; /** * Utility functions for shell execution and package management. @@ -81,6 +82,7 @@ export async function upgradeDevProxyWithPackageManager( try { // Check if package manager is available await executeCommand(`${packageManager} --version`); + logger.debug('Package manager available', { packageManager }); // Check if Dev Proxy is installed via package manager const listCommand = @@ -88,6 +90,7 @@ export async function upgradeDevProxyWithPackageManager( const listOutput = await executeCommand(listCommand); if (!listOutput.includes(packageId)) { + logger.warn('Dev Proxy not found in package manager', { packageManager, packageId }); return false; } @@ -111,6 +114,7 @@ export async function upgradeDevProxyWithPackageManager( try { await executeCommand(upgradeCommand); statusMessage.dispose(); + logger.info('Dev Proxy upgraded successfully', { packageManager }); const result = await vscode.window.showInformationMessage( `${versionText} has been successfully upgraded!`, @@ -122,10 +126,12 @@ export async function upgradeDevProxyWithPackageManager( return true; } catch (error) { statusMessage.dispose(); + logger.error('Failed to upgrade Dev Proxy', { packageManager, error }); vscode.window.showErrorMessage(`Failed to upgrade ${versionText}: ${error}`); return false; } } catch { + logger.warn('Package manager not available for upgrade', { packageManager }); return false; } } @@ -164,13 +170,16 @@ export async function resolveDevProxyExecutable( ): Promise { // 1. Use custom path if provided if (customPath && customPath.trim() !== '') { + logger.info('Using custom Dev Proxy path from settings', { customPath: customPath.trim() }); return customPath.trim(); } // 2. Try bare command first if (await canExecute(exeName)) { + logger.info('Dev Proxy found on PATH', { exeName }); return exeName; } + logger.debug('Dev Proxy not found on PATH, trying alternatives', { exeName }); const platform = os.platform(); @@ -178,6 +187,7 @@ export async function resolveDevProxyExecutable( if (platform !== 'win32') { const loginShellPath = await tryLoginShell(exeName); if (loginShellPath) { + logger.info('Dev Proxy found via login shell', { path: loginShellPath }); return loginShellPath; } } @@ -187,11 +197,13 @@ export async function resolveDevProxyExecutable( for (const dir of commonPaths) { const fullPath = `${dir}/${exeName}`; if (fs.existsSync(fullPath)) { + logger.info('Dev Proxy found at common path', { path: fullPath }); return fullPath; } } // Fallback to bare command (will fail gracefully in getVersion) + logger.warn('Dev Proxy executable not found, falling back to bare command', { exeName }); return exeName; } @@ -220,13 +232,14 @@ async function tryLoginShell(exeName: string): Promise { } try { + logger.debug('Trying login shell', { shell, exeName }); const result = await executeCommand(`${shell} -l -c "which ${exeName}"`); const path = result.trim(); if (path && !path.includes('not found')) { return path; } } catch { - // Shell failed, try next + logger.debug('Login shell did not find executable', { shell, exeName }); } }