Logging Migration Guide
This guide helps you migrate from Brobot's legacy logging systems to the new Unified Logging System.
Overview
The Unified Logging System replaces three separate logging mechanisms:
- SLF4J direct usage
- ActionLogger for structured logs
- ConsoleReporter for console output
All functionality is now available through a single BrobotLogger
interface.
Migration Checklist
- Add BrobotLogger dependency injection
- Replace ConsoleReporter static calls
- Update ActionLogger usage
- Migrate SLF4J logger statements
- Add session scoping to tests
- Update configuration files
- Remove legacy logger imports
Step-by-Step Migration
Step 1: Add BrobotLogger Injection
Replace static logger declarations with injected BrobotLogger:
// Old way - multiple loggers
public class MyAutomation {
private static final Logger log = LoggerFactory.getLogger(MyAutomation.class);
@Autowired
private ActionLogger actionLogger;
// ...
}
// New way - single logger
public class MyAutomation {
@Autowired
private BrobotLogger logger;
// ...
}
Step 2: Migrate ConsoleReporter Calls
ConsoleReporter static methods should be replaced with BrobotLogger instance methods:
Basic Output
// Old way
ConsoleReporter.print("Processing started");
ConsoleReporter.println("Processing complete");
// New way
logger.observation("Processing started");
logger.observation("Processing complete");
Level-based Output
// Old way
ConsoleReporter.print(OutputLevel.HIGH, "Detailed info");
ConsoleReporter.println(OutputLevel.LOW, "Important message");
// New way
logger.observation("Detailed info"); // Respects console level setting
logger.observation("Important message", "WARNING"); // Always shown at LOW
Action Reporting
// Old way
ConsoleReporter.print(match, stateObject, actionOptions);
// New way
logger.action(actionOptions.getAction().toString(), stateObject);
Formatted Output
// Old way
ConsoleReporter.format("Found %d matches in %.2f seconds", count, time);
// New way
logger.observation(String.format("Found %d matches in %.2f seconds", count, time));
// Or with metadata
logger.log()
.observation("Search complete")
.metadata("matchCount", count)
.metadata("duration", time)
.log();
Colored Output
// Old way
ConsoleReporter.println("Success!", AnsiColor.GREEN);
ConsoleReporter.print("Error!", AnsiColor.RED);
// New way
logger.log()
.observation("Success!")
.color(AnsiColor.GREEN)
.log();
logger.log()
.error(new Exception("Error!"))
.color(AnsiColor.RED)
.log();
Step 3: Migrate ActionLogger Usage
Replace ActionLogger method calls with BrobotLogger equivalents:
Action Logging
// Old way
actionLogger.logAction(sessionId, actionResult, objectCollection);
// New way
logger.action("CLICK", stateObject, actionResult);
State Transitions
// Old way
Set<State> from = Set.of(currentState);
Set<State> to = Set.of(targetState);
actionLogger.logStateTransition(sessionId, from, to, from, true, 1500);
// New way
logger.transition(currentState, targetState, true, 1500);
Observations
// Old way
actionLogger.logObservation(sessionId, "UI_CHECK",
"Button is enabled", "INFO");
// New way
logger.observation("Button is enabled", "INFO");
// Or with more context
logger.log()
.observation("Button is enabled")
.metadata("elementType", "UI_CHECK")
.log();
Performance Metrics
// Old way
actionLogger.logPerformanceMetrics(sessionId,
actionDuration, pageLoadTime, totalDuration);
// New way
logger.log()
.performanceLog()
.message("Test execution metrics")
.metadata("actionDuration", actionDuration)
.metadata("pageLoadTime", pageLoadTime)
.metadata("totalDuration", totalDuration)
.log();
// Or use built-in timing
try (var timer = logger.startTimer("TestExecution")) {
// Perform test
} // Duration logged automatically
Error Logging
// Old way
actionLogger.logError(sessionId, errorMessage, screenshotPath);
// New way
logger.log()
.error(exception)
.message(errorMessage)
.screenshot(screenshotPath)
.log();
Step 4: Migrate SLF4J Usage
Replace direct SLF4J logger calls:
Basic Logging
// Old way
log.info("Starting automation");
log.debug("Current state: {}", state);
log.warn("Retry attempt {} of {}", attempt, maxRetries);
log.error("Failed to find element", exception);
// New way
logger.observation("Starting automation");
logger.observation("Current state: " + state);
logger.observation("Retry attempt " + attempt + " of " + maxRetries, "WARNING");
logger.error("Failed to find element", exception);
Conditional Logging
// Old way
if (log.isDebugEnabled()) {
log.debug("Expensive computation: {}", computeDebugInfo());
}
// New way
if (logger.getConsoleLevel() == OutputLevel.HIGH) {
logger.observation("Expensive computation: " + computeDebugInfo());
}
Step 5: Add Session Management
Wrap test and automation code in sessions:
// Old way
public void runTest() {
String sessionId = UUID.randomUUID().toString();
// Pass sessionId to every log call
actionLogger.logAction(sessionId, result, collection);
}
// New way
public void runTest() {
try (var session = logger.session(UUID.randomUUID().toString())) {
// Session ID automatically included in all logs
logger.action("START", testCase);
}
}
Step 6: Update Test Classes
Migrate test class logging:
// Old way
@Test
public class LoginTest {
private static final Logger log = LoggerFactory.getLogger(LoginTest.class);
@Autowired
private ActionLogger actionLogger;
@Test
public void testLogin() {
log.info("Starting login test");
String sessionId = "test-" + System.currentTimeMillis();
// Perform test
ActionResult result = performLogin();
actionLogger.logAction(sessionId, result, null);
if (!result.isSuccess()) {
log.error("Login failed");
}
}
}
// New way
@Test
public class LoginTest {
@Autowired
private BrobotLogger logger;
@Test
public void testLogin() {
try (var session = logger.session("test-" + System.currentTimeMillis())) {
logger.observation("Starting login test");
// Perform test
ActionResult result = performLogin();
logger.action("LOGIN", loginPage, result);
if (!result.isSuccess()) {
logger.error("Login failed", null);
}
}
}
}
Configuration Migration
Application Properties
Update your application.yml or application.properties:
# Old configuration
logging:
level:
io.github.jspinak.brobot: DEBUG
console:
reporter:
level: HIGH
# New configuration
brobot:
logging:
console:
level: HIGH
colors: true
structured:
enabled: true
slf4j:
level: DEBUG
Logback Configuration
The unified logger still uses SLF4J internally, so your logback.xml continues to work:
<!-- This still works with unified logging -->
<configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>logs/brobot.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="io.github.jspinak.brobot" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</configuration>
Common Migration Patterns
Pattern 1: Action with Retry
// Old way
for (int i = 0; i < maxRetries; i++) {
log.debug("Attempt {} of {}", i + 1, maxRetries);
ActionResult result = performAction();
ConsoleReporter.print(match, stateObject, actionOptions);
actionLogger.logAction(sessionId, result, objectCollection);
if (result.isSuccess()) {
log.info("Action succeeded on attempt {}", i + 1);
break;
}
if (i == maxRetries - 1) {
log.error("Action failed after {} attempts", maxRetries);
actionLogger.logError(sessionId, "Max retries exceeded", screenshot);
}
}
// New way
for (int i = 0; i < maxRetries; i++) {
logger.log()
.action("CLICK")
.target(stateObject)
.metadata("attempt", i + 1)
.metadata("maxRetries", maxRetries)
.log();
ActionResult result = performAction();
logger.action("CLICK", stateObject, result);
if (result.isSuccess()) {
logger.observation("Action succeeded on attempt " + (i + 1));
break;
}
if (i == maxRetries - 1) {
logger.log()
.error(new Exception("Max retries exceeded"))
.screenshot(screenshot)
.metadata("attempts", maxRetries)
.log();
}
}
Pattern 2: State Validation
// Old way
log.info("Validating state: {}", expectedState.getName());
Set<State> currentStates = detectStates();
if (currentStates.contains(expectedState)) {
ConsoleReporter.println("State validation passed", AnsiColor.GREEN);
actionLogger.logObservation(sessionId, "STATE_CHECK",
"Correct state detected", "INFO");
} else {
ConsoleReporter.println("State validation failed", AnsiColor.RED);
log.error("Expected state {} but found {}", expectedState, currentStates);
actionLogger.logError(sessionId, "Wrong state detected", screenshot);
}
// New way
logger.observation("Validating state: " + expectedState.getName());
Set<State> currentStates = detectStates();
if (currentStates.contains(expectedState)) {
logger.log()
.observation("State validation passed")
.color(AnsiColor.GREEN)
.metadata("expectedState", expectedState.getName())
.metadata("actualStates", currentStates)
.log();
} else {
logger.log()
.error(new Exception("Wrong state detected"))
.screenshot(screenshot)
.metadata("expected", expectedState)
.metadata("actual", currentStates)
.color(AnsiColor.RED)
.log();
}
Pattern 3: Performance Tracking
// Old way
long startTime = System.currentTimeMillis();
log.info("Starting batch process");
processBatch();
long duration = System.currentTimeMillis() - startTime;
ConsoleReporter.format("Batch completed in %d ms", duration);
actionLogger.logPerformanceMetrics(sessionId, 0, 0, duration);
// New way
try (var timer = logger.startTimer("BatchProcess")) {
logger.observation("Starting batch process");
processBatch();
logger.observation("Batch completed");
} // Duration automatically logged
Gradual Migration Strategy
If you need to migrate gradually:
Phase 1: Add Unified Logger Alongside Existing
public class MyClass {
// Keep existing loggers temporarily
private static final Logger log = LoggerFactory.getLogger(MyClass.class);
@Autowired
private ActionLogger actionLogger;
// Add new logger
@Autowired
private BrobotLogger logger;
// Use both during transition
public void myMethod() {
log.info("Old style logging");
logger.observation("New style logging");
}
}
Phase 2: Create Adapter Methods
public class LoggingAdapter {
@Autowired
private BrobotLogger logger;
// Adapter for ConsoleReporter
public static void print(String message) {
// Get singleton instance and delegate
getInstance().logger.observation(message);
}
// Adapter for ActionLogger
public void logAction(String sessionId, ActionResult result) {
try (var session = logger.session(sessionId)) {
logger.action("LEGACY", null, result);
}
}
}
Phase 3: Replace Usage Incrementally
- Start with new code - use only BrobotLogger
- Migrate test classes one at a time
- Update utility classes
- Migrate core automation classes
- Remove legacy logger declarations
Phase 4: Clean Up
- Remove legacy logger imports
- Delete adapter methods
- Remove old configuration
- Update documentation
Troubleshooting
Issue: Lost Session Context
Symptom: Session ID not appearing in logs
Solution: Ensure code is wrapped in session scope
try (var session = logger.session("test-123")) {
// All logging here includes session ID
}
Issue: No Console Output
Symptom: Logs appear in files but not console
Solution: Check console level setting
logger.setConsoleLevel(OutputLevel.HIGH);
Issue: Lombok Compilation Errors
Symptom: IDE shows errors for getName(), isSuccess() methods
Solution:
- Ensure Lombok plugin is installed
- Enable annotation processing
- Clean and rebuild project
Issue: Missing Metadata
Symptom: Custom metadata not appearing in logs
Solution: Add metadata to context or use builder
// Context metadata (persistent)
logger.getContext().addMetadata("env", "test");
// Per-log metadata
logger.log()
.observation("Test")
.metadata("custom", value)
.log();
Verification
After migration, verify:
- Console Output: Run tests and check console formatting
- Log Files: Verify SLF4J file output still works
- Structured Logs: Check ActionLogger compatibility
- Performance: Ensure no significant overhead
- Thread Safety: Test concurrent execution
Benefits After Migration
- Cleaner Code: Single logger injection
- Better Context: Automatic session tracking
- Richer Logs: Easy metadata addition
- Performance: Built-in timing utilities
- Flexibility: Fluent API for complex scenarios
- Future Proof: Ready for new logging features