Arcade Logger
The arcade_logger
package provides a high-performance, asynchronous logging system for Arcade applications. It runs in a separate isolate to ensure logging operations never block your main application thread.
Installation
Section titled “Installation”Add arcade_logger
to your pubspec.yaml
:
dependencies: arcade_logger: ^<latest-version>
Features
Section titled “Features”- Asynchronous Logging: Runs in separate isolate for non-blocking operations
- Colored Output: ANSI-styled console output for better readability
- Log Levels: Support for debug, info, warning, and error levels
- Named Loggers: Create hierarchical loggers for different components
- Thread-Safe: Safe to use from multiple isolates
- Integration: Works seamlessly with arcade_config
Quick Start
Section titled “Quick Start”import 'package:arcade_logger/arcade_logger.dart';
void main() async { // Initialize the logger await Logger.init();
// Get the root logger final logger = Logger.root;
// Log messages logger.debug('Debug message'); logger.info('Info message'); logger.warning('Warning message'); logger.error('Error message');
// Log with additional data logger.info('User logged in');}
Named Loggers
Section titled “Named Loggers”Create loggers for different parts of your application:
// Create named loggersfinal dbLogger = Logger('Database');final apiLogger = Logger('API');final authLogger = Logger('API.Auth');
// Log with component contextdbLogger.info('Connected to database');apiLogger.debug('Processing request');authLogger.warning('Failed login attempt');
Log Levels
Section titled “Log Levels”Available Levels
Section titled “Available Levels”enum LogLevel { debug, // Detailed diagnostic information info, // General informational messages warning, // Warning messages error, // Error messages none, // Disable logging}
Setting Log Level
Section titled “Setting Log Level”import 'package:arcade_config/arcade_config.dart';
// Set global log levelArcadeConfig.logLevel = LogLevel.warning;
// Only warning and error messages will be loggedlogger.debug('This will not appear');logger.info('This will not appear');logger.warning('This will appear');logger.error('This will appear');
Dynamic Log Level
Section titled “Dynamic Log Level”// Change log level at runtimevoid setLogLevel(String level) { switch (level.toLowerCase()) { case 'debug': ArcadeConfig.logLevel = LogLevel.debug; break; case 'info': ArcadeConfig.logLevel = LogLevel.info; break; case 'warning': ArcadeConfig.logLevel = LogLevel.warning; break; case 'error': ArcadeConfig.logLevel = LogLevel.error; break; }}
Integration with Arcade
Section titled “Integration with Arcade”Request Logging Hooks
Section titled “Request Logging Hooks”class LoggingHooks { final Logger logger = Logger('HTTP');
BeforeHook<RequestContext> createBeforeHook() { return (context) async { final requestId = Uuid().v4(); context.extra['requestId'] = requestId;
logger.info('Request started', { 'id': requestId, 'method': context.request.method, 'path': context.request.uri.path, 'ip': context.request.connectionInfo?.remoteAddress.address, 'userAgent': context.request.headers['user-agent']?.first, });
return context; }; }
AfterHook<RequestContext, dynamic> createAfterHook() { return (context, result) async { final requestId = context.extra['requestId']; final duration = DateTime.now().difference( context.extra['startTime'] as DateTime );
logger.info('Request completed', { 'id': requestId, 'duration': duration.inMilliseconds, 'status': context.response.statusCode, });
return result; }; }}
// Apply to routesfinal logging = LoggingHooks();
route.group('/api') .before(logging.createBeforeHook()) .after(logging.createAfterHook());
Error Logging
Section titled “Error Logging”class ErrorLogger { static final logger = Logger('Error');
static void logError( dynamic error, StackTrace? stackTrace, { Map<String, dynamic>? context, }) { final errorData = { 'error': error.toString(), 'type': error.runtimeType.toString(), if (stackTrace != null) 'stackTrace': stackTrace.toString(), if (context != null) ...context, };
if (error is HttpException) { logger.warning('HTTP Exception', errorData); } else { logger.error('Unhandled error', errorData); } }}
// Global error handlerroute.onError((context, error, stackTrace) async { ErrorLogger.logError(error, stackTrace, context: { 'path': context.request.uri.path, 'method': context.request.method, });
return ErrorResponse( statusCode: 500, message: 'Internal server error', );});
Database Query Logging
Section titled “Database Query Logging”class DatabaseLogger { static final logger = Logger('Database');
static Future<T> logQuery<T>( String query, Future<T> Function() execute, { Map<String, dynamic>? params, }) async { final start = DateTime.now();
logger.debug('Executing query', { 'query': query, if (params != null) 'params': params, });
try { final result = await execute(); final duration = DateTime.now().difference(start);
logger.debug('Query completed', { 'query': query, 'duration': duration.inMilliseconds, });
return result; } catch (error, stackTrace) { logger.error('Query failed', { 'query': query, 'error': error.toString(), 'stackTrace': stackTrace.toString(), }); rethrow; } }}
// Usagefinal users = await DatabaseLogger.logQuery( 'SELECT * FROM users WHERE active = ?', () => db.query('SELECT * FROM users WHERE active = ?', [true]), params: {'active': true},);
Advanced Usage
Section titled “Advanced Usage”Structured Logging
Section titled “Structured Logging”class StructuredLogger { final Logger logger; final Map<String, dynamic> defaultFields;
StructuredLogger(String name, {Map<String, dynamic>? defaults}) : logger = Logger(name), defaultFields = defaults ?? {};
void log( LogLevel level, String message, { Map<String, dynamic>? fields, String? correlationId, }) { final data = { ...defaultFields, if (fields != null) ...fields, 'timestamp': DateTime.now().toIso8601String(), if (correlationId != null) 'correlationId': correlationId, };
switch (level) { case LogLevel.debug: logger.debug(message, data); break; case LogLevel.info: logger.info(message, data); break; case LogLevel.warning: logger.warning(message, data); break; case LogLevel.error: logger.error(message, data); break; case LogLevel.none: break; } }}
// Usagefinal apiLogger = StructuredLogger('API', defaults: { 'service': 'user-service', 'version': '1.0.0', 'environment': 'production',});
apiLogger.log( LogLevel.info, 'User created', correlationId: 'req-123',);
Performance Logging
Section titled “Performance Logging”class PerformanceLogger { static final logger = Logger('Performance');
static Future<T> measure<T>( String operation, Future<T> Function() action, { Map<String, dynamic>? metadata, }) async { final stopwatch = Stopwatch()..start();
try { final result = await action(); stopwatch.stop();
logger.info('Operation completed', { 'operation': operation, 'duration': stopwatch.elapsedMilliseconds, 'success': true, if (metadata != null) ...metadata, });
return result; } catch (error) { stopwatch.stop();
logger.error('Operation failed', { 'operation': operation, 'duration': stopwatch.elapsedMilliseconds, 'success': false, 'error': error.toString(), if (metadata != null) ...metadata, });
rethrow; } }}
// Usagefinal result = await PerformanceLogger.measure( 'fetch_user_profile', () async => await userService.getProfile(userId), metadata: {'userId': userId},);
Audit Logging
Section titled “Audit Logging”class AuditLogger { static final logger = Logger('Audit');
static void logAction({ required String action, required String userId, required String resource, String? resourceId, Map<String, dynamic>? changes, String? ipAddress, }) { logger.info('Audit event', { 'action': action, 'userId': userId, 'resource': resource, if (resourceId != null) 'resourceId': resourceId, if (changes != null) 'changes': changes, if (ipAddress != null) 'ipAddress': ipAddress, 'timestamp': DateTime.now().toIso8601String(), }); }}
// Usage in routesroute.put('/users/:id').handle((context) async { final userId = context.pathParameters['id']!; final updates = await context.jsonMap();
final oldUser = await userService.getUser(userId); final newUser = await userService.updateUser(userId, updates);
AuditLogger.logAction( action: 'update_user', userId: context.currentUser.id, resource: 'user', resourceId: userId, changes: { 'before': oldUser.toJson(), 'after': newUser.toJson(), }, ipAddress: context.request.connectionInfo?.remoteAddress.address, );
return newUser;});
Log Aggregation
Section titled “Log Aggregation”class LogAggregator { static final _metrics = <String, int>{}; static Timer? _reportTimer;
static void init() { _reportTimer = Timer.periodic(Duration(minutes: 1), (_) { _reportMetrics(); }); }
static void increment(String metric) { _metrics[metric] = (_metrics[metric] ?? 0) + 1; }
static void _reportMetrics() { if (_metrics.isEmpty) return;
Logger.root.info('Metrics report', Map.from(_metrics)); _metrics.clear(); }
static void dispose() { _reportTimer?.cancel(); _reportMetrics(); // Final report }}
// Usageroute.before((context) async { LogAggregator.increment('requests.total'); LogAggregator.increment('requests.${context.request.method}'); return context;});
Output Formatting
Section titled “Output Formatting”Console Output
Section titled “Console Output”The logger produces colored console output:
2024-01-15 10:30:45 [DEBUG] MyApp: Debug message2024-01-15 10:30:45 [INFO] MyApp: Info message2024-01-15 10:30:45 [WARNING] MyApp: Warning message2024-01-15 10:30:45 [ERROR] MyApp: Error message
Colors:
- Debug: Gray
- Info: Blue
- Warning: Yellow
- Error: Red
JSON Output
Section titled “JSON Output”For production environments, consider JSON output:
class JsonLogger { static void log(LogRecord record) { final json = jsonEncode({ 'timestamp': record.time.toIso8601String(), 'level': record.level.name, 'logger': record.loggerName, 'message': record.message, 'data': record.data, });
print(json); }}
Best Practices
Section titled “Best Practices”- Use Named Loggers: Create loggers for each component
- Structured Data: Pass data as Map instead of string interpolation
- Appropriate Levels: Use correct log levels for different scenarios
- Avoid Blocking: Don’t perform expensive operations in log calls
- Correlation IDs: Use request/correlation IDs for tracing
- Sensitive Data: Never log passwords, tokens, or PII
- Performance: Check log level before expensive operations
Testing with Logger
Section titled “Testing with Logger”import 'package:test/test.dart';import 'package:arcade_logger/arcade_logger.dart';
void main() { setUpAll(() async { await Logger.init(); ArcadeConfig.logLevel = LogLevel.debug; });
test('logs messages correctly', () async { final logger = Logger('Test');
logger.info('Test message');
// Logger runs asynchronously in isolate await Future.delayed(Duration(milliseconds: 10)); });}
Performance Considerations
Section titled “Performance Considerations”- Logging runs in separate isolate - no main thread blocking
- Excessive logging can still impact performance
- Use appropriate log levels in production
- Consider log sampling for high-traffic endpoints
- Implement log rotation for file outputs
Next Steps
Section titled “Next Steps”- Learn about Configuration for log level management
- Explore Error Handling patterns
- See Hooks for request logging