Logging System
Mosaic includes a sophisticated, multi-tag logging system designed for modular applications. It provides tag-based categorization, multiple output dispatchers, and production-ready features for debugging and monitoring.
Quick Start
Basic Setup
import 'package:mosaic/mosaic.dart';
void main() async {
// Initialize logger with tags
await logger.init(
tags: ['app', 'network', 'ui', 'auth', 'analytics'],
dispatchers: [
ConsoleDispatcher(),
FileLoggerDispatcher(path: 'logs'),
],
);
runApp(MyApp());
}
Basic Logging
class HomeModule extends Module {
@override
Future<void> onInit() async {
// Log with tags for categorization
logger.info('Home module initialized', ['app', 'ui']);
logger.debug('Loading user preferences', ['app']);
logger.warning('Cache miss for user data', ['network']);
}
void handleUserAction() {
try {
// Some risky operation
performOperation();
logger.info('Operation completed successfully', ['app']);
} catch (error) {
logger.error('Operation failed: $error', ['app'], error);
}
}
}
Logger API
Log Levels
// Available log levels (ordered by severity)
logger.trace('Detailed debugging info', ['debug']); // Most verbose
logger.debug('General debugging info', ['app']); // Development
logger.info('General information', ['app']); // Production info
logger.warning('Something unexpected happened', ['app']); // Warnings
logger.error('An error occurred', ['app'], exception); // Errors
logger.fatal('Critical system error', ['app'], exception); // Critical
Multi-Tag Logging
class ApiService {
Future<User> getUser(String id) async {
logger.info('Fetching user: $id', ['network', 'api', 'user']);
try {
final response = await http.get('/users/$id');
logger.debug('API response: ${response.statusCode}', ['network', 'api']);
if (response.statusCode == 200) {
logger.info('User fetched successfully', ['network', 'user']);
return User.fromJson(response.data);
} else {
logger.warning('API returned ${response.statusCode}', ['network', 'api']);
throw ApiException('User not found');
}
} catch (error) {
logger.error('Failed to fetch user', ['network', 'api', 'user'], error);
rethrow;
}
}
}
Dispatchers
Console Dispatcher
class ConsoleDispatcher extends LogDispatcher {
@override
void dispatch(LogEntry entry) {
final timestamp = entry.timestamp.toIso8601String();
final level = entry.level.name.toUpperCase();
final tags = entry.tags.join(', ');
print('[$timestamp] $level [$tags] ${entry.message}');
if (entry.exception != null) {
print('Exception: ${entry.exception}');
if (entry.stackTrace != null) {
print('StackTrace: ${entry.stackTrace}');
}
}
}
}
File Logger Dispatcher
class FileLoggerDispatcher extends LogDispatcher {
final String path;
final String Function(String tag)? fileNameRole;
FileLoggerDispatcher({
required this.path,
this.fileNameRole,
});
@override
void dispatch(LogEntry entry) {
// Create separate files per tag
for (final tag in entry.tags) {
final fileName = fileNameRole?.call(tag) ??
'${tag}_${DateTime.now().millisecondsSinceEpoch}.log';
final file = File('$path/$fileName');
final logLine = formatLogEntry(entry);
file.writeAsStringSync('$logLine\n', mode: FileMode.append);
}
}
}
Custom Dispatchers
// Remote logging dispatcher
class RemoteLogDispatcher extends LogDispatcher {
final String endpoint;
final String apiKey;
RemoteLogDispatcher({required this.endpoint, required this.apiKey});
@override
void dispatch(LogEntry entry) async {
if (entry.level.severity >= LogLevel.warning.severity) {
try {
await http.post(
endpoint,
headers: {'Authorization': 'Bearer $apiKey'},
body: jsonEncode({
'level': entry.level.name,
'message': entry.message,
'tags': entry.tags,
'timestamp': entry.timestamp.toIso8601String(),
'exception': entry.exception?.toString(),
'stackTrace': entry.stackTrace?.toString(),
}),
);
} catch (error) {
// Fallback to console if remote logging fails
print('Failed to send log to remote: $error');
}
}
}
}
// Analytics dispatcher
class AnalyticsLogDispatcher extends LogDispatcher {
@override
void dispatch(LogEntry entry) {
if (entry.tags.contains('analytics')) {
// Send to analytics service
Analytics.track(entry.message, {
'level': entry.level.name,
'tags': entry.tags,
'timestamp': entry.timestamp.millisecondsSinceEpoch,
});
}
}
}
Tag-Based Organization
Organizing by Feature
class TagConstants {
// Core system tags
static const app = 'app';
static const system = 'system';
static const performance = 'performance';
// Feature tags
static const auth = 'auth';
static const user = 'user';
static const payment = 'payment';
static const notification = 'notification';
// Technical tags
static const network = 'network';
static const database = 'database';
static const cache = 'cache';
static const ui = 'ui';
// Environment tags
static const debug = 'debug';
static const analytics = 'analytics';
static const security = 'security';
}
// Usage in modules
class AuthModule extends Module {
@override
Future<void> onInit() async {
logger.info('Auth module starting', [TagConstants.app, TagConstants.auth]);
}
Future<void> login(String email, String password) async {
logger.info('Login attempt for: $email', [TagConstants.auth, TagConstants.security]);
try {
final result = await authService.login(email, password);
logger.info('Login successful', [TagConstants.auth, TagConstants.analytics]);
events.emit<User>('auth/login_success', result.user);
} catch (error) {
logger.error('Login failed', [TagConstants.auth, TagConstants.security], error);
events.emit<String>('auth/login_failure', error.toString());
}
}
}
Log Filtering
class FilteredConsoleDispatcher extends LogDispatcher {
final List<String> allowedTags;
final LogLevel minimumLevel;
FilteredConsoleDispatcher({
this.allowedTags = const [],
this.minimumLevel = LogLevel.info,
});
@override
void dispatch(LogEntry entry) {
// Filter by level
if (entry.level.severity < minimumLevel.severity) {
return;
}
// Filter by tags
if (allowedTags.isNotEmpty) {
final hasAllowedTag = entry.tags.any((tag) => allowedTags.contains(tag));
if (!hasAllowedTag) {
return;
}
}
// Dispatch if passes filters
print('${entry.level.name.toUpperCase()}: ${entry.message}');
}
}
// Setup with filters
await logger.init(
tags: ['app', 'network', 'auth', 'debug'],
dispatchers: [
FilteredConsoleDispatcher(
allowedTags: ['app', 'auth'], // Only show app and auth logs
minimumLevel: LogLevel.info, // Skip debug/trace in production
),
FileLoggerDispatcher(path: 'logs'), // Log everything to files
],
);
Contextual Logging
Request/Session Context
class LogContext {
static String? _sessionId;
static String? _userId;
static String? _requestId;
static void setSession(String sessionId, String userId) {
_sessionId = sessionId;
_userId = userId;
}
static void setRequest(String requestId) {
_requestId = requestId;
}
static Map<String, String> get context => {
if (_sessionId != null) 'sessionId': _sessionId!,
if (_userId != null) 'userId': _userId!,
if (_requestId != null) 'requestId': _requestId!,
};
}
// Enhanced logger with context
extension ContextualLogger on Logger {
void infoWithContext(String message, List<String> tags) {
final contextData = LogContext.context;
final enrichedMessage = contextData.isNotEmpty
? '$message ${jsonEncode(contextData)}'
: message;
info(enrichedMessage, tags);
}
}
// Usage
class ApiService {
Future<void> processRequest(String requestId) async {
LogContext.setRequest(requestId);
logger.infoWithContext('Processing request', ['api', 'network']);
// Output: "Processing request {"sessionId":"sess_123","userId":"user_456","requestId":"req_789"}"
}
}
Structured Logging
class StructuredLogEntry {
final String message;
final Map<String, dynamic> data;
final List<String> tags;
StructuredLogEntry({
required this.message,
required this.data,
required this.tags,
});
String toJson() => jsonEncode({
'message': message,
'data': data,
'tags': tags,
'timestamp': DateTime.now().toIso8601String(),
});
}
extension StructuredLogger on Logger {
void logStructured(StructuredLogEntry entry, LogLevel level) {
switch (level) {
case LogLevel.info:
info(entry.toJson(), entry.tags);
break;
case LogLevel.warning:
warning(entry.toJson(), entry.tags);
break;
case LogLevel.error:
error(entry.toJson(), entry.tags);
break;
// Add other levels as needed
}
}
}
// Usage
logger.logStructured(
StructuredLogEntry(
message: 'User action performed',
data: {
'action': 'button_click',
'button_id': 'submit_form',
'form_data': {'name': 'John', 'email': 'john@example.com'},
'user_agent': 'Mobile App v2.1.0',
'performance': {'response_time_ms': 145},
},
tags: ['ui', 'analytics', 'performance'],
),
LogLevel.info,
);
Production Configuration
Environment-Based Setup
class LoggerConfig {
static Future<void> init() async {
if (kReleaseMode) {
await _initProductionLogger();
} else if (kProfileMode) {
await _initProfileLogger();
} else {
await _initDevelopmentLogger();
}
}
static Future<void> _initDevelopmentLogger() async {
await logger.init(
tags: ['app', 'network', 'ui', 'auth', 'debug', 'analytics'],
dispatchers: [
ConsoleDispatcher(),
FileLoggerDispatcher(
path: 'logs/dev',
fileNameRole: (tag) => '${tag}_${DateTime.now().day}.log',
),
],
);
}
static Future<void> _initProductionLogger() async {
await logger.init(
tags: ['app', 'network', 'auth', 'analytics', 'security'],
dispatchers: [
FilteredConsoleDispatcher(
allowedTags: ['security'], // Only security logs to console
minimumLevel: LogLevel.warning,
),
FileLoggerDispatcher(
path: '/var/log/app',
fileNameRole: (tag) => '${tag}_${DateTime.now().toIso8601String().split('T')[0]}.log',
),
RemoteLogDispatcher(
endpoint: 'https://logs.myapp.com/api/logs',
apiKey: Environment.logApiKey,
),
],
);
}
static Future<void> _initProfileLogger() async {
await logger.init(
tags: ['app', 'performance', 'network', 'ui'],
dispatchers: [
ConsoleDispatcher(),
PerformanceLogDispatcher(), // Custom dispatcher for profiling
],
);
}
}
Log Rotation
class RotatingFileDispatcher extends LogDispatcher {
final String basePath;
final int maxFileSize; // in bytes
final int maxFiles;
RotatingFileDispatcher({
required this.basePath,
this.maxFileSize = 10 * 1024 * 1024, // 10MB
this.maxFiles = 5,
});
@override
void dispatch(LogEntry entry) {
for (final tag in entry.tags) {
final file = File('$basePath/${tag}.log');
// Check if rotation is needed
if (file.existsSync() && file.lengthSync() > maxFileSize) {
_rotateFile(tag);
}
// Write log entry
file.writeAsStringSync(
'${formatLogEntry(entry)}\n',
mode: FileMode.append,
);
}
}
void _rotateFile(String tag) {
// Move current files: app.log -> app.1.log -> app.2.log etc.
for (int i = maxFiles - 1; i >= 1; i--) {
final oldFile = File('$basePath/$tag.${i}.log');
final newFile = File('$basePath/$tag.${i + 1}.log');
if (oldFile.existsSync()) {
if (i == maxFiles - 1) {
oldFile.deleteSync(); // Delete oldest
} else {
oldFile.renameSync(newFile.path);
}
}
}
// Move current log to .1
final currentFile = File('$basePath/$tag.log');
if (currentFile.existsSync()) {
currentFile.renameSync('$basePath/$tag.1.log');
}
}
}
Performance Optimization
Async Logging
class AsyncLogDispatcher extends LogDispatcher {
final LogDispatcher _innerDispatcher;
final StreamController<LogEntry> _logStream = StreamController<LogEntry>();
AsyncLogDispatcher(this._innerDispatcher) {
// Process logs in background
_logStream.stream.listen((entry) async {
try {
_innerDispatcher.dispatch(entry);
} catch (error) {
print('Log dispatch error: $error');
}
});
}
@override
void dispatch(LogEntry entry) {
// Queue log entry for background processing
_logStream.add(entry);
}
void dispose() {
_logStream.close();
}
}
Batched Remote Logging
class BatchedRemoteDispatcher extends LogDispatcher {
final String endpoint;
final int batchSize;
final Duration flushInterval;
final List<LogEntry> _buffer = [];
Timer? _flushTimer;
BatchedRemoteDispatcher({
required this.endpoint,
this.batchSize = 50,
this.flushInterval = const Duration(seconds: 30),
}) {
_startFlushTimer();
}
@override
void dispatch(LogEntry entry) {
_buffer.add(entry);
if (_buffer.length >= batchSize) {
_flush();
}
}
void _startFlushTimer() {
_flushTimer = Timer.periodic(flushInterval, (_) => _flush());
}
Future<void> _flush() async {
if (_buffer.isEmpty) return;
final batch = List<LogEntry>.from(_buffer);
_buffer.clear();
try {
await http.post(
endpoint,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'logs': batch.map((entry) => {
'level': entry.level.name,
'message': entry.message,
'tags': entry.tags,
'timestamp': entry.timestamp.toIso8601String(),
'exception': entry.exception?.toString(),
}).toList(),
}),
);
} catch (error) {
print('Failed to flush log batch: $error');
// Could implement retry logic here
}
}
void dispose() {
_flushTimer?.cancel();
_flush(); // Flush remaining logs
}
}
Testing and Debugging
Log Testing
class TestLogDispatcher extends LogDispatcher {
final List<LogEntry> capturedLogs = [];
@override
void dispatch(LogEntry entry) {
capturedLogs.add(entry);
}
List<LogEntry> getLogsWithTag(String tag) {
return capturedLogs.where((log) => log.tags.contains(tag)).toList();
}
List<LogEntry> getLogsWithLevel(LogLevel level) {
return capturedLogs.where((log) => log.level == level).toList();
}
void clear() {
capturedLogs.clear();
}
}
// Test usage
void main() {
group('Logger Tests', () {
late TestLogDispatcher testDispatcher;
setUp(() async {
testDispatcher = TestLogDispatcher();
await logger.init(
tags: ['test', 'app'],
dispatchers: [testDispatcher],
);
});
test('should log with correct tags', () {
// Act
logger.info('Test message', ['test', 'app']);
// Assert
final logs = testDispatcher.getLogsWithTag('test');
expect(logs.length, 1);
expect(logs.first.message, 'Test message');
expect(logs.first.tags, contains('test'));
expect(logs.first.tags, contains('app'));
});
test('should capture errors with exceptions', () {
// Arrange
final exception = Exception('Test error');
// Act
logger.error('Error occurred', ['test'], exception);
// Assert
final errorLogs = testDispatcher.getLogsWithLevel(LogLevel.error);
expect(errorLogs.length, 1);
expect(errorLogs.first.exception, exception);
});
});
}
Debug Log Viewer
class DebugLogViewer extends StatefulWidget {
@override
_DebugLogViewerState createState() => _DebugLogViewerState();
}
class _DebugLogViewerState extends State<DebugLogViewer> {
final DebugLogDispatcher _debugDispatcher = DebugLogDispatcher();
List<String> _selectedTags = [];
LogLevel _minimumLevel = LogLevel.debug;
@override
void initState() {
super.initState();
// Add debug dispatcher to capture logs
logger.addDispatcher(_debugDispatcher);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Debug Logs'),
actions: [
IconButton(
icon: Icon(Icons.clear),
onPressed: () => _debugDispatcher.clear(),
),
],
),
body: Column(
children: [
// Filters
_buildFilters(),
// Log list
Expanded(
child: ListView.builder(
itemCount: _debugDispatcher.filteredLogs(_selectedTags, _minimumLevel).length,
itemBuilder: (context, index) {
final log = _debugDispatcher.filteredLogs(_selectedTags, _minimumLevel)[index];
return _buildLogEntry(log);
},
),
),
],
),
);
}
Widget _buildLogEntry(LogEntry log) {
final color = _getLogLevelColor(log.level);
return Card(
margin: EdgeInsets.all(4),
child: ExpansionTile(
leading: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
title: Text(
log.message,
style: TextStyle(fontSize: 14),
),
subtitle: Text(
'${log.level.name.toUpperCase()} • ${log.tags.join(', ')} • ${log.timestamp.toString()}',
style: TextStyle(fontSize: 12),
),
children: [
if (log.exception != null)
Padding(
padding: EdgeInsets.all(16),
child: Text(
'Exception: ${log.exception}\n\nStack Trace:\n${log.stackTrace}',
style: TextStyle(fontFamily: 'monospace', fontSize: 12),
),
),
],
),
);
}
Color _getLogLevelColor(LogLevel level) {
switch (level) {
case LogLevel.trace:
return Colors.grey;
case LogLevel.debug:
return Colors.blue;
case LogLevel.info:
return Colors.green;
case LogLevel.warning:
return Colors.orange;
case LogLevel.error:
return Colors.red;
case LogLevel.fatal:
return Colors.purple;
}
}
Widget _buildFilters() {
return Container(
padding: EdgeInsets.all(16),
child: Column(
children: [
// Tag filter
Wrap(
children: ['app', 'network', 'ui', 'auth', 'debug'].map((tag) {
return FilterChip(
label: Text(tag),
selected: _selectedTags.contains(tag),
onSelected: (selected) {
setState(() {
if (selected) {
_selectedTags.add(tag);
} else {
_selectedTags.remove(tag);
}
});
},
);
}).toList(),
),
// Level filter
DropdownButton<LogLevel>(
value: _minimumLevel,
items: LogLevel.values.map((level) {
return DropdownMenuItem(
value: level,
child: Text(level.name.toUpperCase()),
);
}).toList(),
onChanged: (level) {
if (level != null) {
setState(() {
_minimumLevel = level;
});
}
},
),
],
),
);
}
}
class DebugLogDispatcher extends LogDispatcher {
final List<LogEntry> _logs = [];
@override
void dispatch(LogEntry entry) {
_logs.add(entry);
// Keep only last 1000 logs
if (_logs.length > 1000) {
_logs.removeAt(0);
}
}
List<LogEntry> filteredLogs(List<String> tags, LogLevel minimumLevel) {
return _logs.where((log) {
// Filter by level
if (log.level.severity < minimumLevel.severity) {
return false;
}
// Filter by tags
if (tags.isNotEmpty) {
return log.tags.any((tag) => tags.contains(tag));
}
return true;
}).toList();
}
void clear() {
_logs.clear();
}
}
Advanced Patterns
Hierarchical Logging
class HierarchicalLogger {
final String namespace;
final Logger _baseLogger;
HierarchicalLogger(this.namespace, this._baseLogger);
HierarchicalLogger child(String childNamespace) {
return HierarchicalLogger('$namespace.$childNamespace', _baseLogger);
}
void info(String message, [List<String> additionalTags = const []]) {
final tags = [namespace, ...additionalTags];
_baseLogger.info(message, tags);
}
void error(String message, [List<String> additionalTags = const [], Object? exception]) {
final tags = [namespace, ...additionalTags];
_baseLogger.error(message, tags, exception);
}
}
// Usage
class UserModule extends Module {
late final HierarchicalLogger _logger;
@override
Future<void> onInit() async {
_logger = HierarchicalLogger('user', logger);
_logger.info('User module initialized'); // Tags: ['user']
_setupApiClient();
_setupAuthHandler();
}
void _setupApiClient() {
final apiLogger = _logger.child('api');
apiLogger.info('API client configured'); // Tags: ['user.api']
}
void _setupAuthHandler() {
final authLogger = _logger.child('auth');
authLogger.info('Auth handler ready'); // Tags: ['user.auth']
}
}
Metric Logging
class MetricLogger {
static void timing(String operation, Duration duration, [List<String> tags = const []]) {
logger.info(
'METRIC: $operation completed in ${duration.inMilliseconds}ms',
['metrics', 'timing', ...tags],
);
}
static void counter(String metric, int value, [List<String> tags = const []]) {
logger.info(
'METRIC: $metric = $value',
['metrics', 'counter', ...tags],
);
}
static void gauge(String metric, double value, [List<String> tags = const []]) {
logger.info(
'METRIC: $metric = $value',
['metrics', 'gauge', ...tags],
);
}
}
// Usage with performance monitoring
class ApiService {
Future<User> fetchUser(String id) async {
final stopwatch = Stopwatch()..start();
try {
final user = await _performFetch(id);
MetricLogger.timing('api.fetch_user', stopwatch.elapsed, ['api', 'user']);
MetricLogger.counter('api.fetch_user.success', 1, ['api', 'user']);
return user;
} catch (error) {
MetricLogger.timing('api.fetch_user', stopwatch.elapsed, ['api', 'user', 'error']);
MetricLogger.counter('api.fetch_user.failure', 1, ['api', 'user']);
rethrow;
}
}
}
Best Practices
1. Tag Organization
// ✅ Good - Hierarchical tag structure
class Tags {
// Domain tags
static const auth = 'auth';
static const user = 'user';
static const payment = 'payment';
// Technical tags
static const network = 'network';
static const database = 'database';
static const cache = 'cache';
// Operational tags
static const performance = 'performance';
static const security = 'security';
static const analytics = 'analytics';
}
// ❌ Avoid - Inconsistent tag naming
logger.info('User logged in', ['Auth', 'LOGIN', 'user_management']);
2. Message Formatting
// ✅ Good - Structured, searchable messages
logger.info('User login successful', ['auth'], {
'userId': user.id,
'loginMethod': 'email',
'duration': loginDuration.inMilliseconds,
});
// ❌ Avoid - Unstructured messages
logger.info('User ${user.name} logged in using email after ${loginDuration}ms');
3. Error Logging
// ✅ Good - Comprehensive error logging
try {
await criticalOperation();
} catch (error, stackTrace) {
logger.error(
'Critical operation failed',
['operation', 'critical', 'error'],
error,
stackTrace,
);
// Also emit event for error handling
events.emit<String>('system/critical_error', error.toString());
}
// ❌ Avoid - Silent failures
try {
await criticalOperation();
} catch (error) {
// Silent failure - no logging
}
4. Production Considerations
// ✅ Good - Environment-aware logging
class ProductionLogger {
static bool get shouldLogDebug => !kReleaseMode;
static bool get shouldLogTrace => kDebugMode;
static void debug(String message, List<String> tags) {
if (shouldLogDebug) {
logger.debug(message, tags);
}
}
static void trace(String message, List<String> tags) {
if (shouldLogTrace) {
logger.trace(message, tags);
}
}
}
Troubleshooting
Common Issues
Logs not appearing:
// Check if logger is initialized
await logger.init(tags: ['app'], dispatchers: [ConsoleDispatcher()]);
// Verify dispatcher is added
logger.addDispatcher(ConsoleDispatcher());
Performance issues:
// Use async dispatcher for heavy logging
logger.addDispatcher(AsyncLogDispatcher(FileLoggerDispatcher()));
// Limit log levels in production
FilteredConsoleDispatcher(minimumLevel: LogLevel.warning);
File logging not working:
// Ensure directory exists
Directory('logs').createSync(recursive: true);
// Check permissions
final file = File('logs/app.log');
if (!file.parent.existsSync()) {
file.parent.createSync(recursive: true);
}
Next Steps
- Thread Safety - Safe concurrent logging
- Auto Queue - Retry mechanisms for remote logging
- Testing - Testing with logs
- Best Practices - Production logging strategies
The logging system in Mosaic provides comprehensive debugging and monitoring capabilities that scale from development to production environments.