ADR-007: Logging Abstraction Layer¶
Status¶
Accepted
Context¶
The codebase previously used a global logrus.Logger instance accessed through package-level functions in internal/logging. This approach had several drawbacks:
- Global Mutable State: The global logger made testing difficult and created hidden dependencies
- Tight Coupling: Direct dependency on logrus throughout the codebase made it hard to change logging implementations
- Testing Challenges: Tests couldn't easily mock or verify logging behavior without affecting global state
- Concurrency Issues: Global state can lead to race conditions in concurrent code
Decision¶
We have implemented a logging abstraction layer with the following components:
Logger Interface¶
A logging.Logger interface defines the contract for all logging operations:
type Logger interface {
Debug(msg string, fields ...Field)
Info(msg string, fields ...Field)
Warn(msg string, fields ...Field)
Error(msg string, fields ...Field)
WithError(err error) Logger
WithField(key string, value interface{}) Logger
WithFields(fields ...Field) Logger
}
LogrusAdapter Implementation¶
A LogrusAdapter struct implements the Logger interface by wrapping logrus.Logger:
NewLogrusAdapter(level, format string)creates a new logger with specified configurationNewLogrusAdapterFromLogger(*logrus.Logger)wraps an existing logrus instance- All methods delegate to the underlying logrus logger
Dependency Injection¶
Components receive logger instances through constructors rather than accessing global variables:
// Before (anti-pattern)
func NewParser() *Parser {
return &Parser{
logger: logging.GetLogger(), // Global access
}
}
// After (recommended)
func NewParser(logger logging.Logger) *Parser {
return &Parser{
logger: logger,
}
}
Standardized Field Names¶
Constants in internal/logging/constants.go define standard field names for structured logging:
Consequences¶
Positive¶
- Improved Testability: Tests can inject mock loggers and verify logging behavior
- Decoupling: Application code depends on the interface, not the implementation
- Flexibility: Easy to swap logging implementations (e.g., switch from logrus to zap or slog)
- No Global State: Eliminates global mutable state and associated concurrency issues
- Better Testing: Each test can have its own logger instance without interference
Negative¶
- Migration Effort: Existing code needs to be updated to use dependency injection
- Slightly More Verbose: Constructors need to accept logger parameters
- Backward Compatibility: Deprecated
GetLogger()function maintained for transition period
Neutral¶
- Learning Curve: Developers need to understand the abstraction pattern
- Consistency: Requires discipline to use the interface consistently
Implementation Notes¶
Phase 1 (Complete)¶
- ✅ Created
logging.Loggerinterface - ✅ Implemented
LogrusAdapter - ✅ Added comprehensive unit tests
- ✅ Defined standardized field constants
- ✅ Updated documentation
Phase 2 (Planned)¶
- Migrate parsers to use dependency injection
- Update CLI commands to create and inject loggers
- Remove global logger usage throughout codebase
- Eventually deprecate and remove
GetLogger()function
References¶
- Requirements: 6.1, 6.2, 6.3, 6.4, 6.5
- Tasks: 1, 1.1 in
.kiro/specs/code-quality-refactoring/tasks.md - Related ADRs: ADR-001 (Parser Interface Standardization)
Date¶
2025-11-01