Skip to content

Refactor logging to use AsyncLocalStorage context instead of prop drilling #307

@acreeger

Description

@acreeger

Note to iloom

Execute in TRIVIAL mode due to plan being specified below:

Problem

When --json flag is used with CLI commands, all logging should go to stderr so that only valid JSON is output to stdout. The current implementation requires passing a custom logger through every layer of the call stack ("prop drilling"):

cli.ts → StartCommand → LoomManager → DatabaseManager → NeonProvider
                     → IssueTrackerFactory → GitHubService
                     → installDependencies()

This approach:

  • Requires modifying constructors and method signatures throughout the codebase
  • Is error-prone (easy to forget to pass the logger somewhere)
  • Makes the code harder to maintain
  • Couples unrelated components to logging concerns

Solution

Use Node.js's built-in AsyncLocalStorage to create a React Context-like pattern for logging. This allows any code in the async call stack to access the correct logger without explicit parameter passing.

Implementation Plan

1. Create logger context module

Create src/utils/logger-context.ts:

import { AsyncLocalStorage } from 'node:async_hooks'
import { logger as defaultLogger, type Logger } from './logger.js'

const loggerStorage = new AsyncLocalStorage<Logger>()

// Get current logger from context, or fall back to default
export function getLogger(): Logger {
  return loggerStorage.getStore() ?? defaultLogger
}

// Run code with a specific logger in context
export function withLogger<T>(logger: Logger, fn: () => T | Promise<T>): T | Promise<T> {
  return loggerStorage.run(logger, fn)
}

2. Update CLI entry point

Wrap command execution in withLogger() when --json flag is used:

// cli.ts
if (options.json) {
  const jsonLogger = createStderrLogger()
  await withLogger(jsonLogger, () => command.execute())
} else {
  await command.execute()
}

3. Update all logging call sites

Replace direct logger imports with getLogger() calls in:

  • src/lib/GitHubService.ts
  • src/lib/IssueTrackerFactory.ts
  • src/lib/LoomManager.ts
  • src/lib/DatabaseManager.ts
  • src/lib/providers/NeonProvider.ts
  • src/utils/package-manager.ts
  • src/utils/neon-helpers.ts
  • src/commands/start.ts
  • src/commands/finish.ts
  • src/commands/cleanup.ts
  • Any other files using the logger

4. Remove prop drilling code

Revert/remove the custom logger parameters added to:

  • Constructor parameters (logger?: Logger)
  • Method parameters
  • Instance properties (private logger: Logger)

5. Update tests

  • Add tests for logger-context.ts
  • Verify that getLogger() returns the correct logger in different contexts
  • Test that withLogger() properly scopes the logger to the async context

Benefits

  1. Cleaner API: No need to pass loggers through constructors/methods
  2. Less error-prone: Can't forget to pass the logger
  3. Easier to maintain: Adding logging to new code just requires getLogger()
  4. Built-in: Uses Node.js native AsyncLocalStorage, no external dependencies
  5. Familiar pattern: Similar to React Context for frontend developers

Files Changed (Current Prop Drilling Approach to Revert)

The following files were modified with the prop drilling approach and should be reverted/refactored:

  • src/cli.ts - passes jsonLogger to IssueTrackerFactory
  • src/commands/finish.ts - passes logger to installDependencies
  • src/commands/start.ts - passes logger to createNeonProviderFromSettings
  • src/lib/GitHubService.ts - added logger constructor param
  • src/lib/IssueTrackerFactory.ts - added logger param to create()
  • src/lib/LoomManager.ts - passes logger to installDependencies
  • src/lib/providers/NeonProvider.ts - added logger constructor param
  • src/utils/neon-helpers.ts - added logger param
  • src/utils/package-manager.ts - added customLogger param

Testing

  1. Run il start <issue> --json and verify only JSON goes to stdout
  2. Run il start <issue> --json 2>/dev/null | jq . to confirm valid JSON
  3. Run without --json and verify normal logging behavior

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions