Skip to content

Add verbose/debug logging options #12

@BekahHW

Description

@BekahHW

Description

RepoReady currently provides minimal output during operations, making it difficult to troubleshoot issues or understand what the tool is doing behind the scenes. Adding configurable logging levels would help users debug problems and developers understand the tool's behavior.

Current State

  • ❌ No configurable logging levels
  • ❌ Limited debugging information
  • ❌ No verbose output options
  • ❌ Hard to troubleshoot API issues
  • ✅ Some console output with ora spinners
  • ✅ Basic error messages exist

Acceptance Criteria

Logging Levels

  • Implement standard logging levels: error, warn, info, verbose, debug
  • Default to info level for normal operation
  • CLI flags to control verbosity (--verbose, --debug, --quiet)
  • Environment variable support (REPOREADY_LOG_LEVEL)

Logging Content

  • Error level: Only critical errors
  • Warn level: Warnings and recoverable errors
  • Info level: Normal operation messages (current behavior)
  • Verbose level: Detailed progress information
  • Debug level: API calls, timing, internal state

Output Formatting

  • Structured log format with timestamps
  • Color-coded output for different log levels
  • Optional JSON output for machine parsing
  • File output option for persistent logging

Implementation Suggestions

Logger Service

// src/utils/logger.ts
import chalk from 'chalk';
import fs from 'fs';
import path from 'path';

export enum LogLevel {
  ERROR = 0,
  WARN = 1,
  INFO = 2,
  VERBOSE = 3,
  DEBUG = 4
}

export interface LoggerOptions {
  level: LogLevel;
  format: 'text' | 'json';
  outputFile?: string;
  includeTimestamp: boolean;
  useColors: boolean;
}

export class Logger {
  private options: LoggerOptions;
  private fileStream?: fs.WriteStream;

  constructor(options: Partial<LoggerOptions> = {}) {
    this.options = {
      level: this.parseLogLevel(process.env.REPOREADY_LOG_LEVEL) ?? LogLevel.INFO,
      format: 'text',
      includeTimestamp: true,
      useColors: process.stdout.isTTY && !process.env.NO_COLOR,
      ...options
    };

    if (this.options.outputFile) {
      this.fileStream = fs.createWriteStream(this.options.outputFile, { flags: 'a' });
    }
  }

  private parseLogLevel(level?: string): LogLevel | undefined {
    if (!level) return undefined;
    
    const levels: Record<string, LogLevel> = {
      'error': LogLevel.ERROR,
      'warn': LogLevel.WARN,
      'info': LogLevel.INFO,
      'verbose': LogLevel.VERBOSE,
      'debug': LogLevel.DEBUG
    };
    
    return levels[level.toLowerCase()];
  }

  private shouldLog(level: LogLevel): boolean {
    return level <= this.options.level;
  }

  private formatMessage(level: LogLevel, message: string, meta?: any): string {
    const timestamp = this.options.includeTimestamp 
      ? new Date().toISOString()
      : '';
    
    if (this.options.format === 'json') {
      return JSON.stringify({
        timestamp,
        level: LogLevel[level].toLowerCase(),
        message,
        ...meta
      });
    }

    // Text format
    const levelStr = this.colorizeLevel(level, LogLevel[level].padEnd(7));
    const prefix = timestamp ? `${chalk.gray(timestamp)} ` : '';
    const metaStr = meta ? ` ${chalk.gray(JSON.stringify(meta))}` : '';
    
    return `${prefix}${levelStr} ${message}${metaStr}`;
  }

  private colorizeLevel(level: LogLevel, text: string): string {
    if (!this.options.useColors) return text;
    
    switch (level) {
      case LogLevel.ERROR: return chalk.red(text);
      case LogLevel.WARN: return chalk.yellow(text);
      case LogLevel.INFO: return chalk.blue(text);
      case LogLevel.VERBOSE: return chalk.cyan(text);
      case LogLevel.DEBUG: return chalk.gray(text);
      default: return text;
    }
  }

  private write(level: LogLevel, message: string, meta?: any): void {
    if (!this.shouldLog(level)) return;
    
    const formatted = this.formatMessage(level, message, meta);
    
    // Console output
    const output = level === LogLevel.ERROR ? console.error : console.log;
    output(formatted);
    
    // File output
    if (this.fileStream) {
      this.fileStream.write(formatted + '\n');
    }
  }

  error(message: string, meta?: any): void {
    this.write(LogLevel.ERROR, message, meta);
  }

  warn(message: string, meta?: any): void {
    this.write(LogLevel.WARN, message, meta);
  }

  info(message: string, meta?: any): void {
    this.write(LogLevel.INFO, message, meta);
  }

  verbose(message: string, meta?: any): void {
    this.write(LogLevel.VERBOSE, message, meta);
  }

  debug(message: string, meta?: any): void {
    this.write(LogLevel.DEBUG, message, meta);
  }

  // Convenience methods
  apiCall(method: string, endpoint: string, params?: any): void {
    this.debug(`API Call: ${method} ${endpoint}`, { params });
  }

  timing(operation: string, duration: number): void {
    this.verbose(`${operation} completed in ${duration}ms`);
  }

  close(): void {
    if (this.fileStream) {
      this.fileStream.end();
    }
  }
}

// Global logger instance
export const logger = new Logger();

// Update logger configuration from CLI options
export function configureLogger(options: {
  verbose?: boolean;
  debug?: boolean;
  quiet?: boolean;
  logFile?: string;
  jsonLog?: boolean;
}): void {
  let level = LogLevel.INFO;
  
  if (options.quiet) level = LogLevel.WARN;
  if (options.verbose) level = LogLevel.VERBOSE;
  if (options.debug) level = LogLevel.DEBUG;
  
  // Create new logger instance with updated options
  Object.assign(logger, new Logger({
    level,
    outputFile: options.logFile,
    format: options.jsonLog ? 'json' : 'text'
  }));
}

Enhanced GitHub Service with Logging

// src/utils/github.ts - Add logging
import { logger } from './logger';

export class GitHubService {
  // ... existing code ...

  async getRepositoryInfo(owner: string, repo: string): Promise<RepositoryInfo> {
    const startTime = Date.now();
    logger.info(`Starting evaluation of ${owner}/${repo}`);
    logger.verbose(`Fetching repository metadata for ${owner}/${repo}`);
    
    try {
      // API call logging
      logger.apiCall('GET', `/repos/${owner}/${repo}`);
      const repoData = await this.octokit.rest.repos.get({ owner, repo });
      logger.debug('Repository metadata received', { 
        name: repoData.data.name,
        description: repoData.data.description?.substring(0, 100)
      });

      logger.verbose('Checking for community health files...');
      
      // File existence checks with logging
      const hasReadme = await this.checkFileWithLogging(owner, repo, 'README.md');
      const hasContributing = await this.checkFileWithLogging(owner, repo, 'CONTRIBUTING.md');
      // ... other checks
      
      logger.verbose('Checking for issues with specific labels...');
      const goodFirstIssues = await this.getIssuesWithLabel(owner, repo, 'good first issue');
      logger.debug(`Found ${goodFirstIssues.length} good first issues`);
      
      const repositoryInfo: RepositoryInfo = {
        owner,
        repo,
        name: repoData.data.name,
        // ... rest of the implementation
      };
      
      const duration = Date.now() - startTime;
      logger.timing(`Repository info collection for ${owner}/${repo}`, duration);
      logger.info(`Successfully evaluated ${owner}/${repo}`);
      
      return repositoryInfo;
    } catch (error) {
      logger.error(`Failed to get repository info for ${owner}/${repo}`, { 
        error: error.message,
        status: error.status 
      });
      throw error;
    }
  }

  private async checkFileWithLogging(owner: string, repo: string, path: string): Promise<boolean> {
    logger.debug(`Checking for file: ${path}`);
    
    try {
      logger.apiCall('GET', `/repos/${owner}/${repo}/contents/${path}`);
      await this.octokit.rest.repos.getContent({ owner, repo, path });
      logger.verbose(`✅ Found ${path}`);
      return true;
    } catch (error) {
      if (error.status === 404) {
        logger.verbose(`❌ ${path} not found in repository`);
        
        // Check organization-level files
        logger.debug(`Checking for ${path} in organization .github repository`);
        const hasOrgFile = await this.checkOrganizationFile(owner, path);
        
        if (hasOrgFile) {
          logger.verbose(`✅ Found ${path} in organization .github repository`);
          return true;
        }
        
        return false;
      }
      
      logger.warn(`Error checking ${path}:`, { error: error.message });
      return false;
    }
  }
}

CLI Integration

// src/commands/evaluate.ts - Add logging options
import { configureLogger, logger } from '../utils/logger';

export function createEvaluateCommand(): Command {
  const command = new Command('evaluate');
  
  command
    .description('Evaluate a GitHub repository for contributor readiness')
    .argument('<repository>', 'Repository in format owner/repo')
    .option('-t, --token <token>', 'GitHub personal access token')
    .option('-v, --verbose', 'Enable verbose output')
    .option('-d, --debug', 'Enable debug output')
    .option('-q, --quiet', 'Suppress non-error output')
    .option('--log-file <file>', 'Write logs to file')
    .option('--json-log', 'Output logs in JSON format')
    .action(async (repository: string, options) => {
      // Configure logging based on CLI options
      configureLogger({
        verbose: options.verbose,
        debug: options.debug,
        quiet: options.quiet,
        logFile: options.logFile,
        jsonLog: options.jsonLog
      });
      
      logger.info('RepoReady evaluation starting');
      logger.debug('CLI options', { repository, ...options });
      
      const spinner = ora({
        text: 'Fetching repository information...',
        // Only show spinner if not in verbose/debug mode
        isEnabled: !options.verbose && !options.debug
      }).start();
      
      try {
        // ... existing evaluation logic
        logger.info('Evaluation completed successfully');
      } catch (error) {
        logger.error('Evaluation failed', { error: error.message });
        throw error;
      } finally {
        logger.close();
      }
    });

  return command;
}

CLI Usage Examples

# Normal operation (info level)
rr evaluate facebook/react

# Verbose output
rr evaluate facebook/react --verbose

# Debug output with API details
rr evaluate facebook/react --debug

# Quiet mode (only warnings and errors)
rr evaluate facebook/react --quiet

# Log to file
rr evaluate facebook/react --log-file evaluation.log

# JSON log format for processing
rr evaluate facebook/react --json-log > results.jsonl

# Environment variable configuration
REPOREADY_LOG_LEVEL=debug rr evaluate facebook/react

Log Level Examples

Info Level (Default)

2025-10-01T10:30:00.000Z INFO    Starting evaluation of facebook/react
2025-10-01T10:30:02.000Z INFO    Successfully evaluated facebook/react

Verbose Level

2025-10-01T10:30:00.000Z INFO    Starting evaluation of facebook/react
2025-10-01T10:30:00.100Z VERBOSE Fetching repository metadata for facebook/react
2025-10-01T10:30:01.000Z VERBOSE Checking for community health files...
2025-10-01T10:30:01.200Z VERBOSE ✅ Found README.md
2025-10-01T10:30:01.400Z VERBOSE ✅ Found CONTRIBUTING.md
2025-10-01T10:30:01.800Z VERBOSE Repository info collection for facebook/react completed in 1800ms
2025-10-01T10:30:02.000Z INFO    Successfully evaluated facebook/react

Debug Level

2025-10-01T10:30:00.000Z INFO    Starting evaluation of facebook/react
2025-10-01T10:30:00.050Z DEBUG   CLI options {"repository":"facebook/react","debug":true}
2025-10-01T10:30:00.100Z DEBUG   API Call: GET /repos/facebook/react
2025-10-01T10:30:00.800Z DEBUG   Repository metadata received {"name":"react","description":"The library for web and native user interfaces."}
2025-10-01T10:30:01.000Z DEBUG   Checking for file: README.md
2025-10-01T10:30:01.200Z DEBUG   API Call: GET /repos/facebook/react/contents/README.md

Files to Create/Modify

  • src/utils/logger.ts (new) - Logger implementation
  • src/commands/evaluate.ts (modify) - Add logging options
  • src/commands/create.ts (modify) - Add logging options
  • src/utils/github.ts (modify) - Add detailed logging
  • src/evaluator/index.ts (modify) - Add evaluation logging
  • README.md (modify) - Document logging options

Benefits

  • 🔍 Better troubleshooting capabilities
  • 📊 Understand tool performance and behavior
  • 🐛 Easier debugging for contributors
  • ⚙️ Configurable verbosity for different use cases
  • 📝 Persistent logging for analysis
  • 🤖 Machine-readable output for automation

Testing Considerations

  • Test different log levels
  • Test file output functionality
  • Test JSON format parsing
  • Test color output in different terminals
  • Test environment variable configuration

Resources

Estimated Effort

Medium - Requires integration across multiple files but well-defined patterns exist.


Great for contributors who want to improve debugging and user experience! 🔍

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions