Logging in Juice
Juice provides a flexible, context-aware logging system that helps you track what’s happening in your application. This guide covers everything you need to know about logging in Juice.
Default Logging
Out of the box, Juice uses a default logger that provides reasonable logging capabilities:
class MyUseCase extends BlocUseCase<MyBloc, MyEvent> {
@override
Future<void> execute(MyEvent event) async {
// Simple message logging
log('Processing event'); // Outputs: [MyUseCase] Processing event
try {
// Do some work...
} catch (e, stack) {
// Error logging with stack trace
logError('Failed to process event', e, stack);
// Outputs: [MyUseCase] Exception: Failed to process event
// With full error details and stack trace
}
}
}
Key features of the default logger:
- Automatic prefixing with use case name
- Error stack trace formatting
- Color coding in development
- Timestamp tracking
Using Context
The logging system supports rich context to help you track detailed information:
class OrderUseCase extends BlocUseCase<OrderBloc, ProcessOrderEvent> {
@override
Future<void> execute(ProcessOrderEvent event) async {
// Log with context
log('Processing order', context: {
'orderId': event.orderId,
'amount': event.amount,
'userId': bloc.state.userId
});
// Outputs: [OrderUseCase] Processing order | Context: {orderId: 123, amount: 50.0, userId: abc}
try {
await processOrder(event.orderId);
} catch (e, stack) {
// Error logging with context
logError(
'Order processing failed',
e,
stack,
context: {
'orderId': event.orderId,
'state': bloc.state.status,
'retryCount': retryCount
}
);
}
}
}
Context is particularly useful for:
- Debugging production issues
- Tracking user flows
- Monitoring system health
- Performance analysis
Custom Logger Implementation
You can implement your own logging system by creating a class that implements the JuiceLogger
interface:
class CustomLogger implements JuiceLogger {
@override
void log(String message, {
Level level = Level.info,
Map<String, dynamic>? context
}) {
// Your custom logging implementation
final timestamp = DateTime.now().toIso8601String();
final contextStr = context != null ? ' | $context' : '';
print('[$timestamp][$level] $message$contextStr');
}
@override
void logError(
String message,
Object error,
StackTrace stackTrace, {
Map<String, dynamic>? context
}) {
// Your custom error logging implementation
final timestamp = DateTime.now().toIso8601String();
final contextStr = context != null ? ' | $context' : '';
print('[$timestamp][ERROR] $message$contextStr');
print('Error: $error');
print('Stack trace:\n$stackTrace');
}
}
Common use cases for custom loggers:
- Integration with logging services (LogRocket, Sentry, etc.)
- Structured logging for analytics
- Environment-specific logging behavior
- Custom formatting or filtering
Configuring Your Logger
To use your custom logger:
void main() {
// Configure global logger
JuiceLoggerConfig.configureLogger(CustomLogger());
// Or configure per-bloc logger
final bloc = MyBloc(customLogger: CustomLogger());
runApp(MyApp());
}
Best Practices
- Use Context Effectively ```dart // ❌ Poor context usage log(‘Error occurred’, context: {‘error’: ‘Failed’});
// ✅ Rich, useful context log(‘Payment processing failed’, context: { ‘transactionId’: tx.id, ‘amount’: payment.amount, ‘provider’: paymentProvider.name, ‘customerType’: customer.type, ‘retryCount’: attempts });
2. **Log Appropriate Levels**
```dart
// Log important business events
log('Order placed successfully', level: Level.info);
// Log debugging information
log('Cache miss, fetching from network', level: Level.debug);
// Log warnings
log('Rate limit approaching', level: Level.warning);
// Log errors
logError('Payment processing failed', error, stack);
- Structure Context Data ```dart // ❌ Unstructured context log(‘User action’, context: { ‘data’: ‘user 123 clicked button’ });
// ✅ Structured context log(‘User action’, context: { ‘userId’: ‘123’, ‘action’: ‘button_click’, ‘component’: ‘checkout_form’, ‘timestamp’: DateTime.now().toIso8601String() });
4. **Include Relevant State**
```dart
class CheckoutUseCase extends BlocUseCase<CheckoutBloc, CheckoutEvent> {
@override
Future<void> execute(CheckoutEvent event) async {
log('Processing checkout', context: {
'cart': {
'itemCount': bloc.state.cart.items.length,
'total': bloc.state.cart.total,
'currency': bloc.state.currency
},
'customer': {
'id': bloc.state.customerId,
'type': bloc.state.customerType
},
'session': {
'id': bloc.state.sessionId,
'duration': bloc.state.sessionDuration
}
});
}
}
Production Considerations
- Sensitive Data
- Never log passwords, tokens, or sensitive user data
- Mask or truncate potentially sensitive information
- Be careful with user identifiable information (PII)
- Performance
- Consider logging volume in production
- Use sampling for high-frequency events
- Implement log rotation or cleanup
- Be mindful of string concatenation
- Error Reporting
- Always include stack traces with errors
- Provide enough context to reproduce issues
- Consider error grouping/categorization
- Track error frequencies and patterns
- Monitoring
- Use structured logging for metrics
- Track performance indicators
- Monitor error rates
- Set up alerts for critical issues
Example Production Logger
Here’s an example of a production-ready logger:
class ProductionLogger implements JuiceLogger {
final LoggingService _loggingService;
final ErrorReporting _errorReporting;
final bool _isProd;
ProductionLogger({
required LoggingService loggingService,
required ErrorReporting errorReporting,
required bool isProd,
}) : _loggingService = loggingService,
_errorReporting = errorReporting,
_isProd = isProd;
@override
void log(String message, {
Level level = Level.info,
Map<String, dynamic>? context
}) async {
// Clean and validate context
final safeContext = _sanitizeContext(context);
// Add standard fields
final enrichedContext = {
'timestamp': DateTime.now().toIso8601String(),
'level': level.toString(),
'environment': _isProd ? 'prod' : 'dev',
...safeContext ?? {},
};
// Send to logging service
await _loggingService.log(
message,
level: level,
context: enrichedContext
);
}
@override
void logError(
String message,
Object error,
StackTrace stackTrace, {
Map<String, dynamic>? context
}) async {
// Clean and validate context
final safeContext = _sanitizeContext(context);
// Report error
await _errorReporting.captureException(
error,
stackTrace: stackTrace,
context: safeContext
);
// Log error details
await log(
message,
level: Level.error,
context: {
'error': error.toString(),
'stackTrace': stackTrace.toString(),
...safeContext ?? {},
}
);
}
Map<String, dynamic>? _sanitizeContext(Map<String, dynamic>? context) {
if (context == null) return null;
return context.map((key, value) {
// Mask sensitive fields
if (_isSensitive(key)) {
return MapEntry(key, '***');
}
// Clean values
if (value is String) {
return MapEntry(key, _sanitizeString(value));
}
return MapEntry(key, value);
});
}
bool _isSensitive(String key) {
return key.toLowerCase().contains(RegExp(
r'password|token|secret|key|credential|auth'
));
}
String _sanitizeString(String value) {
// Remove common sensitive patterns
return value.replaceAll(RegExp(
r'([0-9]{4})[0-9]*([0-9]{4})'
), r'$1****$2');
}
}
Debugging Tips
- Temporary Debug Logging
// Add detailed logging for debugging log('Cache state', context: { 'entries': cache.length, 'size': cache.size, 'hits': cache.hits, 'misses': cache.misses, 'oldestEntry': cache.oldest?.timestamp, 'newestEntry': cache.newest?.timestamp, });
- State Transitions ```dart @override void emitUpdate({ BlocState? newState, Set
? groupsToRebuild }) { log('State transition', context: { 'oldState': bloc.state.toString(), 'newState': newState.toString(), 'groups': groupsToRebuild?.toList(), 'timestamp': DateTime.now().toIso8601String() });
super.emitUpdate( newState: newState, groupsToRebuild: groupsToRebuild ); }
3. **Performance Tracking**
```dart
Future<void> complexOperation() async {
final stopwatch = Stopwatch()..start();
try {
await doWork();
} finally {
stopwatch.stop();
log('Operation completed', context: {
'duration': stopwatch.elapsedMilliseconds,
'operation': 'complexOperation'
});
}
}
Remember that logging is a crucial tool for understanding and maintaining your application. Take time to implement good logging practices early in your development process.