-
Notifications
You must be signed in to change notification settings - Fork 7
Open
Description
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
infolevel 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/reactLog 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 implementationsrc/commands/evaluate.ts(modify) - Add logging optionssrc/commands/create.ts(modify) - Add logging optionssrc/utils/github.ts(modify) - Add detailed loggingsrc/evaluator/index.ts(modify) - Add evaluation loggingREADME.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
Labels
No labels