Modular Logging System
Overview
The Modular Logging System is a new architectural component that provides format-aware, verbosity-driven action logging. It separates data collection, formatting, and output concerns to create a flexible, maintainable logging solution that produces clean, focused output at different verbosity levels.
Problem Statement
The original logging system had several issues:
- Mixed Concerns: Formatting logic was scattered across multiple components
- Inconsistent Output: Different parts of the system produced different log formats
- Hard to Maintain: Changes to log format required modifications in multiple places
- Limited Flexibility: Difficult to add new output formats or verbosity levels
Architecture
System Components
┌─────────────────────────────────────────────────────────────────┐
│ ActionLifecycleAspect │
│ (Populates ActionResult with execution context) │
├─────────────────────────────────────────────────────────────────┤
│ ActionResult │
│ (Single source of truth with ActionExecutionContext, │
│ ActionMetrics, and EnvironmentSnapshot) │
├─────────────────────────────────────────────────────────────────┤
│ ActionLoggingService │
│ (Routes ActionResult to appropriate formatter) │
├─────────────┬─────────────┬─────────────────────────────────────┤
│ Quiet │ Normal │ Verbose │
│ Formatter │ Formatter │ Formatter │
├─────────────┴─────────────┴─────────────────────────────────────┤
│ Output Destinations │
│ (Console, File, Remote, etc.) │
└─────────────────────────────────────────────────────────────────┘
Core Design Principles
- Single Source of Truth: ActionResult contains all information about an action execution
- Separation of Concerns: Data collection, formatting, and output are separate responsibilities
- Pluggable Formatters: Easy to add new output formats without changing core logic
- Configuration-Driven: Verbosity level controlled by configuration, not code
- Immutable Data Flow: ActionResult is populated once and passed through the system
Components
Enhanced ActionResult
The ActionResult class has been enhanced with inner classes to capture comprehensive execution data:
public class ActionResult {
// Existing fields...
private ActionExecutionContext executionContext;
private ActionMetrics actionMetrics;
private EnvironmentSnapshot environmentSnapshot;
@Data
public static class ActionExecutionContext {
private String actionType;
private List<StateImage> targetImages = new ArrayList<>();
private List<String> targetStrings = new ArrayList<>();
private List<Region> targetRegions = new ArrayList<>();
private String primaryTargetName;
private boolean success;
private Duration executionDuration = Duration.ZERO;
private Instant startTime;
private Instant endTime;
private List<Match> resultMatches = new ArrayList<>();
private Throwable executionError;
private String executingThread;
private String actionId;
}
@Data
public static class ActionMetrics {
private long executionTimeMs;
private int matchCount;
private double bestMatchConfidence = 0.0;
private String threadName;
private String actionId;
private int retryCount = 0;
private Map<String, Long> timingBreakdown = new HashMap<>();
}
@Data
public static class EnvironmentSnapshot {
private List<MonitorInfo> monitors = new ArrayList<>();
private String osName;
private String javaVersion;
private boolean headlessMode;
private Instant captureTime;
@Data
public static class MonitorInfo {
private int index;
private String displayId;
private Rectangle bounds;
private boolean isPrimary;
}
}
}
ActionLogFormatter Interface
The formatter interface defines the contract for all formatters:
public interface ActionLogFormatter {
/**
* Format an ActionResult into a log message string.
*
* @param actionResult the result of an action execution
* @return formatted log message, or null if this action should not be logged
*/
String format(ActionResult actionResult);
/**
* Determine if an action should be logged at this verbosity level.
*
* @param actionResult the result to check
* @return true if this action should produce log output
*/
boolean shouldLog(ActionResult actionResult);
/**
* Get the verbosity level this formatter handles.
*
* @return the verbosity level
*/
VerbosityLevel getVerbosityLevel();
enum VerbosityLevel {
QUIET, // Minimal output: ✓ Find Working.ClaudeIcon • 234ms
NORMAL, // Balanced output: timestamps, key info, success indicators
VERBOSE // Detailed output: full metadata, environment info, timing details
}
}
Formatter Implementations
QuietFormatter
Produces minimal single-line output perfect for monitoring and CI/CD environments:
✓ Find Working.ClaudeIcon • 234ms
✗ Click Button.Submit • 156ms
✓ Type "username" • 89ms
Features:
- Single-line format
- Success/failure symbol (✓/✗)
- Action type (cleaned up)
- Target information (State.Object format)
- Duration in milliseconds
NormalFormatter
Balanced output with timestamps and essential information:
[12:34:56] ✓ Find Working.ClaudeIcon completed in 234ms (1 match)
[12:34:57] ✗ Click Button.Submit failed in 156ms (No matches found)
[12:34:58] ✓ Type TextInput.Username completed in 89ms
Features:
- Timestamp prefix
- Success/failure indication
- Completion status
- Match count information
- Error messages for failures
VerboseFormatter
Comprehensive multi-line output for debugging and analysis:
=== ACTION EXECUTION ===
Started: 2024-01-20 12:34:56.123
Completed: 2024-01-20 12:34:56.357
Action ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Thread: main
--- ACTION DETAILS ---
Type: FIND
Status: SUCCESS ✓
Duration: 234ms
--- TARGETS ---
Images (1):
[1] Working.ClaudeIcon
--- RESULTS ---
Matches: 1
[1] Score: 0.987 Region: R[100,200 300x400]
--- ENVIRONMENT ---
OS: Mac OS X
Java: 21.0.7
Headless: false
Monitors: 1
--- METRICS ---
Execution Time: 234ms
Match Count: 1
Best Match Score: 0.987
========================
ActionLoggingService
The central service that coordinates logging:
@Service
@Slf4j
public class ActionLoggingService {
private final Map<VerbosityLevel, ActionLogFormatter> formatters;
@Value("${brobot.logging.verbosity:QUIET}")
private String verbosityConfig;
public void logAction(ActionResult actionResult) {
if (!isLoggingEnabled() || actionResult == null) {
return;
}
VerbosityLevel level = getEffectiveVerbosityLevel();
ActionLogFormatter formatter = formatters.get(level);
if (formatter != null && formatter.shouldLog(actionResult)) {
String formattedMessage = formatter.format(actionResult);
if (formattedMessage != null) {
outputFormattedMessage(formattedMessage, level);
}
}
}
}
Simplified ActionLifecycleAspect
The aspect now focuses solely on populating ActionResult with execution context:
@Aspect
@Component
public class ActionLifecycleAspect {
@Autowired
private ActionLoggingService actionLoggingService;
@Around("actionPerform()")
public Object manageActionLifecycle(ProceedingJoinPoint joinPoint) throws Throwable {
// Extract action information
ActionResult actionResult = extractActionResult(joinPoint);
// Initialize execution context
populateExecutionContext(actionResult, context, objectCollection);
try {
// Execute the action
Object result = joinPoint.proceed();
// Update with results
updateExecutionContextWithResults(actionResult, context, duration);
// Log the completed action
actionLoggingService.logAction(actionResult);
return result;
} catch (Exception e) {
// Update with error information
updateExecutionContextWithError(actionResult, context, e);
// Log the failed action
actionLoggingService.logAction(actionResult);
throw e;
}
}
}
Configuration
Application Properties
brobot:
logging:
verbosity: QUIET # QUIET, NORMAL, or VERBOSE
console:
enabled: true
file:
enabled: false
path: logs/actions.log
Programmatic Configuration
// Change verbosity at runtime
actionLoggingService.setVerbosityLevel(VerbosityLevel.VERBOSE);
// Log with specific verbosity override
actionLoggingService.logAction(actionResult, VerbosityLevel.NORMAL);
Usage Examples
Basic Usage
The system works automatically through AspectJ integration:
// Actions are automatically logged when executed
Action action = new Action();
action.find(stateImage); // Automatically logged based on verbosity
Custom Logging
For manual logging scenarios:
@Autowired
private ActionLoggingService loggingService;
// Create and populate ActionResult manually
ActionResult result = new ActionResult();
ActionResult.ActionExecutionContext context = new ActionResult.ActionExecutionContext();
context.setActionType("CUSTOM_ACTION");
context.setSuccess(true);
context.setExecutionDuration(Duration.ofMillis(100));
result.setExecutionContext(context);
// Log it
loggingService.logAction(result);
Extension Guide
Adding a New Formatter
- Implement the ActionLogFormatter interface:
@Component
public class JsonFormatter implements ActionLogFormatter {
@Override
public String format(ActionResult actionResult) {
// Convert to JSON format
return objectMapper.writeValueAsString(actionResult);
}
@Override
public boolean shouldLog(ActionResult actionResult) {
return true; // Log all actions
}
@Override
public VerbosityLevel getVerbosityLevel() {
return VerbosityLevel.NORMAL; // Or create a new level
}
}
- The formatter is automatically registered via Spring component scanning.
Adding New Metrics
Extend the ActionMetrics class:
public static class ActionMetrics {
// Existing fields...
// Add new metrics
private long memoryUsageDelta;
private int cpuUsagePercent;
private Map<String, Object> customMetrics = new HashMap<>();
}
Custom Output Destinations
Override the output method in ActionLoggingService:
@Component
public class RemoteLoggingService extends ActionLoggingService {
@Override
protected void outputFormattedMessage(String message, VerbosityLevel level) {
// Send to remote logging service
remoteLogger.send(message, level);
}
}
Benefits
For Developers
- Clean Code: Logging logic separated from business logic
- Easy Maintenance: Changes to log format only require updating formatters
- Extensible: New formats and destinations easily added
- Testable: Each component can be tested in isolation
For Users
- Consistent Output: All actions logged in the same format
- Configurable Verbosity: Choose the right level of detail
- Performance: Minimal overhead in QUIET mode
- Debugging: VERBOSE mode provides comprehensive information
For Operations
- CI/CD Friendly: QUIET mode perfect for automated environments
- Monitoring: Consistent format enables easy parsing
- Troubleshooting: VERBOSE mode captures all relevant context
- Flexible Deployment: Configure verbosity per environment
Performance Considerations
Overhead
- QUIET Mode: < 100 microseconds per action
- NORMAL Mode: < 200 microseconds per action
- VERBOSE Mode: < 500 microseconds per action
Memory Usage
- ActionExecutionContext: ~500 bytes per action
- ActionMetrics: ~200 bytes per action
- EnvironmentSnapshot: ~1KB (cached and reused)
Optimization Tips
- Use QUIET mode in production
- Enable file logging asynchronously
- Implement sampling for high-frequency actions
- Use conditional logging based on action type
Migration from Legacy Logging
Before (Scattered Logging)
// In ActionExecution
System.out.println("✓ " + action + " " + target);
// In ConsoleReporter
ConsoleReporter.log(OutputLevel.HIGH, result.toString());
// In ActionLogger
actionLogger.logAction(sessionId, result, collection);
After (Unified Logging)
// All logging handled by ActionLoggingService
actionLoggingService.logAction(actionResult);
Migration Steps
- Update brobot library to latest version
- Enable modular logging in configuration
- Remove direct console output calls
- Test with different verbosity levels
- Adjust formatters as needed
Troubleshooting
No Output
- Check if logging is enabled:
brobot.logging.console.enabled=true
- Verify verbosity level:
brobot.logging.verbosity=QUIET
- Ensure AspectJ is enabled:
@EnableAspectJAutoProxy
Wrong Format
- Verify correct formatter is being used
- Check verbosity configuration
- Ensure ActionResult is properly populated
Performance Issues
- Switch to QUIET mode for production
- Disable file logging if not needed
- Consider async logging for high-volume scenarios
Future Enhancements
Planned Features
- Structured Logging: JSON/XML output formats
- Remote Logging: Send logs to centralized servers
- Real-time Streaming: WebSocket-based log streaming
- Analytics Integration: Export to analytics platforms
- Custom Filters: Filter logs based on criteria
- Log Aggregation: Combine logs from multiple sources
Integration Opportunities
- OpenTelemetry for distributed tracing
- ELK Stack for log analysis
- Prometheus for metrics export
- Grafana for visualization