|
| 1 | +# Logging Adapter Specification |
| 2 | + |
| 3 | +## Purpose & User Problem |
| 4 | + |
| 5 | +The vscode-coder extension currently handles logging through the Storage class, where logging functionality is commingled with storage concerns. The `writeToCoderOutputChannel` method exists in the Storage class alongside file system operations, secret management, and other unrelated functionality. This makes it difficult to: |
| 6 | + |
| 7 | +- **Debug user-reported issues**: When users report bugs, especially client connection issues that don't reproduce locally, there's no consistent way to enable detailed logging to help diagnose the problem |
| 8 | +- **Centralize logging logic**: Logging is tied to the Storage class, which also manages secrets, URLs, CLI binaries, and file operations - a clear violation of single responsibility |
| 9 | +- **Control verbosity**: Users can't easily enable debug-level logging when needed without modifying code |
| 10 | +- **Standardize output**: While `writeToCoderOutputChannel` adds timestamps, other parts use direct `output.appendLine` calls |
| 11 | +- **Expand debugging capabilities**: Adding new diagnostic logging for specific subsystems (like client connections) requires passing the Storage instance around |
| 12 | +- **Maintain separation of concerns**: The Storage class has become a "god object" that handles too many responsibilities |
| 13 | + |
| 14 | +## Success Criteria |
| 15 | + |
| 16 | +1. **Centralized Logging**: All log output goes through a single adapter |
| 17 | +2. **Log Levels**: Support for `debug` and `info` levels |
| 18 | + - Additional levels can be added in future iterations |
| 19 | +3. **Type Safety**: Fully typed TypeScript implementation |
| 20 | + - No `any` types in the logger module or modified files |
| 21 | + - No `@ts-ignore` or `eslint-disable` comments |
| 22 | + - All types explicitly defined or properly inferred |
| 23 | +4. **Testable**: |
| 24 | + - Unit tests use `ArrayAdapter` for fast, isolated testing |
| 25 | + - ArrayAdapter provides immutable snapshots via `getSnapshot()` to prevent test interference |
| 26 | + - Integration tests use `OutputChannelAdapter` to verify VS Code integration |
| 27 | + - Test isolation for concurrent test suites: |
| 28 | + - `setAdapter()` throws if adapter already set (prevents test conflicts) |
| 29 | + - `withAdapter()` provides safe temporary adapter swapping with automatic revert |
| 30 | + - Ideal for Vitest's concurrent test execution |
| 31 | + - `reset()` and `setAdapter()` methods restricted to test environment (NODE_ENV === 'test') |
| 32 | + - `reset()` properly disposes of configuration change listeners to prevent memory leaks |
| 33 | + - Methods throw error if called in production for safety |
| 34 | +5. **Performance**: Minimal overhead for logging operations |
| 35 | + - Measured relative to a no-op adapter baseline |
| 36 | + - Debug calls when disabled: < 10% overhead vs no-op |
| 37 | + - Debug calls when enabled: < 10x overhead vs no-op (including formatting) |
| 38 | + - Info calls: < 5x overhead vs no-op |
| 39 | + - Measurement methodology: |
| 40 | + - Use `performance.now()` from Node's `perf_hooks` |
| 41 | + - Compare against `NoOpAdapter` that does nothing |
| 42 | + - Run 10,000 iterations, discard outliers, use median |
| 43 | + - CI passes if within percentage thresholds (not absolute times) |
| 44 | +6. **Fault Tolerance**: Logger never throws exceptions |
| 45 | + - Silently swallows OutputChannel errors (e.g., disposed channel on reload) |
| 46 | + - Logging failures must not crash the extension |
| 47 | + - No error propagation from the logging subsystem |
| 48 | + - Accepts that OutputChannel writes may interleave under concurrent access |
| 49 | +7. **Live Configuration**: Monitor `coder.verbose` setting changes without requiring extension restart |
| 50 | + - Supports workspace, folder, and global configuration levels |
| 51 | + - Uses the most specific configuration available |
| 52 | + |
| 53 | +## Scope & Constraints |
| 54 | + |
| 55 | +### In Scope |
| 56 | +- Extract logging functionality from the Storage class into a dedicated logging adapter |
| 57 | +- Create a logging adapter/service with support for debug and info levels |
| 58 | +- Convert all existing `writeToCoderOutputChannel` and `output.appendLine` calls to use the adapter |
| 59 | +- Maintain integration with VS Code's OutputChannel ("Coder") |
| 60 | + - OutputChannel is created in `extension.ts` and passed to both Logger and Storage |
| 61 | + - Logger uses it for logging via OutputChannelAdapter |
| 62 | + - Storage continues to use it for progress reporting (e.g., binary downloads) |
| 63 | +- Provide a simple API for logging (e.g., `logger.debug("message")`, `logger.info("message")`) |
| 64 | + - Callers only provide the message content, not formatting |
| 65 | + - Only string messages accepted (no automatic object serialization) |
| 66 | + - Callers must stringify objects/errors before logging (e.g., using template literals) |
| 67 | +- Allow runtime configuration of log levels |
| 68 | +- Handle all formatting (timestamps, level tags, etc.) within the logger |
| 69 | +- Remove only the `writeToCoderOutputChannel` method from Storage class |
| 70 | + |
| 71 | +### Out of Scope |
| 72 | +- External logging services integration (future enhancement) |
| 73 | +- File-based logging (all logs go to VS Code OutputChannel) |
| 74 | +- Log file rotation or persistence |
| 75 | +- Structured logging formats (JSON, etc.) |
| 76 | +- Performance metrics or telemetry |
| 77 | +- Custom UI for viewing logs (uses VS Code's built-in OutputChannel UI) |
| 78 | +- Synchronization of concurrent writes (OutputChannel writes may interleave) |
| 79 | +- Automatic object/error serialization (callers must convert to strings) |
| 80 | + |
| 81 | +## Technical Considerations |
| 82 | + |
| 83 | +### Architecture |
| 84 | +- Singleton pattern for the logger instance |
| 85 | +- Interface-based design with pluggable adapters: |
| 86 | + - `LogAdapter` interface for output handling |
| 87 | + - `OutputChannelAdapter` for VS Code OutputChannel integration |
| 88 | + - `ArrayAdapter` for testing (stores logs in memory) |
| 89 | +- OutputChannel ownership and lifecycle: |
| 90 | + - Created once in `extension.ts` activate method |
| 91 | + - Passed to OutputChannelAdapter constructor |
| 92 | + - Also passed to Storage for non-logging uses (progress reporting) |
| 93 | + - Single channel shared between Logger and Storage |
| 94 | + - Note: VS Code OutputChannel is async; concurrent writes may interleave |
| 95 | + - This is acceptable for debugging/diagnostic purposes |
| 96 | + - In browser/web extensions, OutputChannel maps to in-memory buffer (no file I/O) |
| 97 | +- Configuration through VS Code settings: |
| 98 | + - `coder.verbose`: boolean setting to enable debug logging (default: false) |
| 99 | + - When true: shows debug level logs |
| 100 | + - When false: shows info level and above only |
| 101 | + - Respects workspace folder-specific settings (uses most specific configuration) |
| 102 | +- Configuration monitoring using `vscode.workspace.onDidChangeConfiguration` |
| 103 | + - Only responds to changes in `coder.verbose` specifically |
| 104 | + - Ignores all other configuration changes to avoid unnecessary processing |
| 105 | + - Updates when workspace folders are added/removed or active editor changes |
| 106 | +- **Centralized formatting**: All log formatting (timestamps, level tags, source location) happens within the logger implementation, not at call sites |
| 107 | + |
| 108 | +### API Design |
| 109 | +```typescript |
| 110 | +interface LogAdapter { |
| 111 | + write(message: string): void |
| 112 | + clear(): void |
| 113 | +} |
| 114 | + |
| 115 | +interface Logger { |
| 116 | + log(message: string, severity?: LogLevel): void // Core method, defaults to INFO |
| 117 | + debug(message: string): void // String only - no object serialization |
| 118 | + info(message: string): void // String only - no object serialization |
| 119 | + setLevel(level: LogLevel): void |
| 120 | + setAdapter(adapter: LogAdapter): void // For testing only - throws if adapter already set |
| 121 | + withAdapter<T>(adapter: LogAdapter, fn: () => T): T // Safe temporary adapter swap |
| 122 | + reset(): void // For testing only - throws if NODE_ENV !== 'test', disposes listeners |
| 123 | +} |
| 124 | + |
| 125 | +enum LogLevel { |
| 126 | + DEBUG = 0, |
| 127 | + INFO = 1, |
| 128 | + NONE = 2 // Disables all logging |
| 129 | +} |
| 130 | + |
| 131 | +// Example implementations |
| 132 | +class OutputChannelAdapter implements LogAdapter { |
| 133 | + constructor(private outputChannel: vscode.OutputChannel) {} |
| 134 | + write(message: string): void { |
| 135 | + try { |
| 136 | + this.outputChannel.appendLine(message) |
| 137 | + } catch { |
| 138 | + // Silently ignore - channel may be disposed |
| 139 | + } |
| 140 | + } |
| 141 | + clear(): void { |
| 142 | + try { |
| 143 | + this.outputChannel.clear() |
| 144 | + } catch { |
| 145 | + // Silently ignore - channel may be disposed |
| 146 | + } |
| 147 | + } |
| 148 | +} |
| 149 | + |
| 150 | +class ArrayAdapter implements LogAdapter { |
| 151 | + private logs: string[] = [] |
| 152 | + |
| 153 | + write(message: string): void { |
| 154 | + this.logs.push(message) |
| 155 | + } |
| 156 | + |
| 157 | + clear(): void { |
| 158 | + this.logs = [] |
| 159 | + } |
| 160 | + |
| 161 | + getSnapshot(): readonly string[] { |
| 162 | + return [...this.logs] // Return defensive copy |
| 163 | + } |
| 164 | +} |
| 165 | + |
| 166 | +class NoOpAdapter implements LogAdapter { |
| 167 | + write(message: string): void { |
| 168 | + // Intentionally empty - baseline for performance tests |
| 169 | + } |
| 170 | + |
| 171 | + clear(): void { |
| 172 | + // Intentionally empty - baseline for performance tests |
| 173 | + } |
| 174 | +} |
| 175 | +``` |
| 176 | + |
| 177 | +### Log Format |
| 178 | +- **Standard format**: `[LEVEL] TIMESTAMP MESSAGE` |
| 179 | + - Timestamp in UTC ISO-8601 format (e.g., `2024-01-15T10:30:45.123Z`) |
| 180 | + - Example: `[info] 2024-01-15T10:30:45.123Z Starting extension...` |
| 181 | + - Example: `[debug] 2024-01-15T10:30:45.456Z Processing file: example.ts` |
| 182 | + |
| 183 | +- **Debug mode enhancement**: When `coder.verbose` is true, debug logs include source location: |
| 184 | + ``` |
| 185 | + [debug] 2024-01-15T10:30:45.456Z Processing file: example.ts |
| 186 | + at processFile (src/utils/fileHandler.ts:45) |
| 187 | + ``` |
| 188 | + - Note: In browser/web extensions, `Error().stack` may be empty, disabling source location |
| 189 | + |
| 190 | +### Implementation Plan |
| 191 | + |
| 192 | +1. Create the logger module at `src/logger.ts` with: |
| 193 | + - Singleton pattern implementation |
| 194 | + - LogAdapter interface and implementations (OutputChannelAdapter, ArrayAdapter) |
| 195 | + - Logger initialization accepts OutputChannel (not created internally) |
| 196 | + - Configuration reading from VS Code settings (`coder.verbose`) |
| 197 | + - Use `workspace.getConfiguration()` to respect folder-specific settings |
| 198 | + - Configuration change listener using `workspace.onDidChangeConfiguration` |
| 199 | + - Filter to only handle `coder.verbose` changes using `affectsConfiguration()` |
| 200 | + - Re-read configuration on folder/editor changes to respect local settings |
| 201 | + - Timestamp formatting in UTC ISO-8601 (using `new Date().toISOString()`) and level prefixes |
| 202 | + - Debug mode with source location tracking (file/line info) |
| 203 | + - Gracefully handle empty `Error().stack` in browser environments |
| 204 | + - Test method guards: `reset()` and `setAdapter()` check `process.env.NODE_ENV === 'test'` |
| 205 | + - `setAdapter()` throws if adapter already installed (prevents concurrent test conflicts) |
| 206 | + - `withAdapter()` implementation: |
| 207 | + ```typescript |
| 208 | + withAdapter<T>(adapter: LogAdapter, fn: () => T): T { |
| 209 | + const previous = this.adapter |
| 210 | + this.adapter = adapter |
| 211 | + try { |
| 212 | + return fn() |
| 213 | + } finally { |
| 214 | + this.adapter = previous |
| 215 | + } |
| 216 | + } |
| 217 | + ``` |
| 218 | + - `reset()` implementation must dispose configuration listener to prevent memory leaks |
| 219 | +2. Create comprehensive tests at `src/logger.test.ts`: |
| 220 | + - Unit tests using ArrayAdapter for logic testing |
| 221 | + - Separate integration tests for OutputChannelAdapter |
| 222 | + - Performance benchmarks: |
| 223 | + - Create NoOpAdapter as baseline |
| 224 | + - Measure relative performance using `performance.now()` |
| 225 | + - Ensure overhead stays within specified percentages |
| 226 | + - Test both cold and warm paths |
| 227 | +3. Update `extension.ts`: |
| 228 | + - Create OutputChannel in activate method |
| 229 | + - Initialize Logger with OutputChannel via OutputChannelAdapter |
| 230 | + - Continue passing OutputChannel to Storage (for progress reporting) |
| 231 | +4. Extract and refactor the existing `writeToCoderOutputChannel` logic from Storage class |
| 232 | +5. Remove ONLY the `writeToCoderOutputChannel` method from Storage (keep OutputChannel for other uses) |
| 233 | +6. Systematically replace each `writeToCoderOutputChannel` call with appropriate logger methods |
| 234 | +7. For `output.appendLine` calls in Storage, evaluate each: |
| 235 | + - Logging messages → convert to logger calls |
| 236 | + - Progress/status messages → keep as direct OutputChannel calls |
| 237 | +8. Verify functionality with existing tests |
| 238 | +9. Run linting (`yarn lint`) and ensure code quality |
| 239 | + |
| 240 | +### File Locations |
| 241 | +- Logger implementation: `src/logger.ts` |
| 242 | +- Tests: `src/logger.test.ts` |
| 243 | +- Type definitions included in the logger file |
| 244 | + |
0 commit comments