Contextual
A structured logging library for Dart

Features
- 🪵 Multiple Log Levels - From debug to emergency following RFC 5424
- 🎨 Flexible Formatting - JSON, plain text, and colored output
- 📊 Rich Context Support - Add structured data to your logs
- 🔄 Middleware - Transform and filter log messages
- 📤 Multiple Outputs - Console, files, webhooks, and more
- 🎯 Type-specific Formatting - Custom formatting for your data types
- âš¡ Async & Batching - Efficient log processing
- 🔀 Per-Channel Formatters - Customize formatting for each channel
Installation
Add contextual to your pubspec.yaml:
dependencies:
contextual: ^1.0.0
Quick Start
import 'package:contextual/contextual.dart';
void main() {
// Create a logger
final logger = Logger()
..withContext({'app': 'MyApp'});
// Basic logging
logger.info('Application started');
logger.debug('Configuration loaded');
logger.error('Something went wrong');
// Logging with context
logger.warning(
'Database connection failed',
Context({
'host': 'localhost',
'port': 5432,
'attempts': 3
})
);
}
See the example folder for detailed examples.
Configuration
Load configuration from JSON:
final config = LogConfig(
level: 'debug',
environment: 'development',
channels: const [
ConsoleChannel(ConsoleOptions(), name: 'console'),
DailyFileChannel(DailyFileOptions(path: 'logs/app', retentionDays: 7), name: 'file'),
WebhookChannel(WebhookOptions(url: Uri.parse('https://hooks.slack.com/...')), name: 'slack'),
],
);
final logger = await Logger.create(config: config);
If no configuration is provided, a default configuration will be used. The default configuration logs using the console driver and formats logs using the plain formatter. To disable the the default console logger, at initialization, set the defaultChannelEnabled value to false in the constructor of your Logger instance.
final logger = await Logger.create(config: const LogConfig(
channels: [ConsoleChannel(ConsoleOptions(), name: 'console')],
),
);
Logging Patterns
Contextual supports two patterns for handling log output:
-
Direct driver dispatch (Default) Simple and predictable: logs are sent directly to drivers.
You can opt into centralized batching for driver dispatch with one line using the provided extension:
final logger = Logger() ..addChannel('console', ConsoleLogDriver()); await logger.batched(LogSinkConfig( batchSize: 50, // Number of logs to batch before flushing flushInterval: Duration(seconds: 5), // Time interval for automatic flushing )); // ... your logging code ... // Ensure all logs are delivered before shutting down await logger.shutdown();To disable batching again:
await logger.unbatched(); -
Listener Pattern
For simpler use cases, you can use a listener similar to the logging package:
final logger = Logger(); // Set up a simple listener logger.setListener((logEntry) { print('[${logEntry.record.time}] ${logEntry.record.level}: $logEntry.message'); }); // All logs now go to the listener logger.info('This goes to the listener');The listener pattern is simpler but doesn't support:
- Multiple output destinations
- Channel-specific middleware
- Asynchronous batching
Choose the pattern that best fits your needs:
- Use the sink pattern for production systems needing multiple outputs.
Runtime Channels
A channel is represented by a Channel<T extends LogDriver> which bundles the channel name, driver, optional formatter, and optional middlewares.
final logger = await Logger.create()
..addChannel('console', ConsoleLogDriver(), formatter: PrettyLogFormatter());
final ch = logger.getChannel('console');
if (ch != null) {
final updated = ch.copyWith(formatter: JsonLogFormatter());
logger.addChannel(
updated.name,
updated.driver,
formatter: updated.formatter,
middlewares: updated.middlewares,
);
}
// Target drivers by type
logger.forDriver<ConsoleLogDriver>().info('Only console drivers');
- Use the listener pattern for simple logging.
Log Levels
Supports standard RFC 5424 severity levels:
logger.emergency('System is unusable');
logger.alert('Action must be taken immediately');
logger.critical('Critical conditions');
logger.error('Error conditions');
logger.warning('Warning conditions');
logger.notice('Normal but significant condition');
logger.info('Informational messages');
logger.debug('Debug-level messages');
Formatters
Choose from built-in formatters or create your own. You can set formatters per channel to customize the output for each logging destination.
Setting Formatters Per Channel
final logger = Logger()
..environment('development')
..withContext({'app': 'MyApp'})
// Console channel with PrettyLogFormatter
..addChannel(
'console',
ConsoleLogDriver(),
formatter: PrettyLogFormatter(),
)
// File channel with JsonLogFormatter
..addChannel(
'file',
DailyFileLogDriver('logs/app.log', retentionDays: 7),
formatter: JsonLogFormatter(),
);
// Logs sent to 'console' will be pretty-printed
// Logs sent to 'file' will be in JSON format
Output Destinations
Configure multiple output destinations and target them fluently:
final logger = Logger()
..addChannel(
'console',
ConsoleLogDriver(),
formatter: PrettyLogFormatter(),
)
..addChannel(
'file',
DailyFileLogDriver('logs/app.log', retentionDays: 7),
formatter: JsonLogFormatter(),
);
// Target a single channel (fluent):
logger['console'].info('Only console');
// Or with a method:
logger.channel('console').warning('Only console');
// Or multiple channels:
logger.channels(['console', 'file']).error('Both console and file');
If no formatter is specified for a channel, the logger's default formatter is used.
Using Built-in Formatters
// Set the default formatter for the logger
logger.formatter(PlainTextLogFormatter());
// Choose from the built-in formatters
logger.formatter(PrettyLogFormatter()); // Colorful, human-readable
logger.formatter(JsonLogFormatter()); // JSON format
logger.formatter(RawLogFormatter()); // No formatting
// Set formatter per channel in configuration
final config = LogConfig(
channels: const [
ConsoleChannel(ConsoleOptions(), name: 'console'),
DailyFileChannel(DailyFileOptions(path: 'logs/app', retentionDays: 7), name: 'file'),
],
);
// Apply custom formatter per channel directly when adding channels programmatically,
// or use a builder API if you add one later.
Custom Formatters
Create custom formatters to have full control over log output:
class MyCustomFormatter extends LogMessageFormatter {
@override
String format(Level level, String message, Context context) {
// Your custom formatting logic
return '[Custom] $level: $message';
}
}
// Register the custom formatter
logger.registerFormatter('custom', () => MyCustomFormatter());
// Assign the custom formatter to a channel
logger.addChannel(
'myChannel',
ConsoleLogDriver(),
formatter: MyCustomFormatter(),
);
Type-specific Formatting
Create custom formatters for your data types:
class User {
final String name;
final String email;
User(this.name, this.email);
}
class UserFormatter extends LogTypeFormatter<User> {
@override
String format(Level level, User user, Context context) {
return '{"name": "${user.name}", "email": "${user.email}"}';
}
}
// Register the formatter
logger.addTypeFormatter(UserFormatter());
// Now User objects will be formatted automatically
final user = User('John Doe', 'john@example.com');
logger.info(user);
Output Destinations
Configure multiple output destinations:
logger
// Console output with pretty formatting
.addChannel(
'console',
ConsoleLogDriver(),
formatter: PrettyLogFormatter(),
)
// Daily rotating file with JSON formatting
.addChannel(
'file',
DailyFileLogDriver('logs/app.log', retentionDays: 7),
formatter: JsonLogFormatter(),
)
// Webhook (e.g., Slack) with plain text formatting
.addChannel(
'slack',
WebhookLogDriver(Uri.parse('https://hooks.slack.com/...')),
formatter: PlainTextLogFormatter(),
);
// Log to specific destinations
logger['console'].info('Console only');
logger['file'].info('File only');
logger.forDriver<ConsoleLogDriver>().info('Console by type');
Context
Add structured data to your logs:
// Global context for all logs
logger.withContext({
'environment': 'production',
'version': '1.0.0'
});
// Per-log context
logger.info(
'User logged in',
Context({
'userId': '123',
'ipAddress': '192.168.1.1'
})
);
Middleware
Transform or filter logs:
// Add sensitive data filter
logger.addLogMiddleware(SensitiveDataMiddleware());
// Add request ID to all logs
logger.addMiddleware(() => {
'requestId': generateRequestId()
});
// Channel-specific middleware
// Driver-type specific middleware (applies to all ConsoleLogDrivers)
logger.addDriverMiddleware<ConsoleLogDriver>(ErrorOnlyMiddleware());
Advanced Usage
Batch Processing
Configure batching behavior:
final logger = Logger(
sinkConfig: LogSinkConfig(
batchSize: 50, // Flush after 50 logs
flushInterval: Duration(seconds: 5), // Or every 5 seconds
maxRetries: 3, // Retry failed operations
autoFlush: true // Enable automatic flushing
)
);
Custom Drivers
Implement your own log destinations:
class CustomLogDriver implements LogDriver {
@override
Future<void> log(String formattedMessage) async {
// Your custom logging logic
}
}
Channels
Channels are named logging destinations that can be configured independently. Each channel represents a different way to handle log messages:
// Configure multiple channels with per-channel formatters
final logger = await Logger.create(
config: LogConfig(
channels: [
ConsoleChannel(ConsoleOptions(), name: 'console'),
DailyFileChannel(
DailyFileOptions(path: 'logs/app.log', retentionDays: 7),
name: 'daily',
),
WebhookChannel(
WebhookOptions(url: Uri.parse('https://hooks.slack.com/...')),
name: 'slack',
),
],
),
);
// Log to specific channels
logger['console'].info('Regular log message');
logger['daily'].info('Regular log message');
logger['slack'].critical('Critical system failure!');
// Default behavior logs to all channels for the current environment
logger.error('This goes to all active channels'
);
Environment-based Channel Selection
Channels can be configured to be active only in specific environments:
final logger = Logger(environment: 'production')
..addChannel(
'console',
ConsoleLogDriver(),
formatter: PrettyLogFormatter(),
) // No env specified, always active
..addChannel(
'daily',
DailyFileLogDriver('logs/app.log'),
formatter: JsonLogFormatter(),
) // Production only
..addChannel(
'debug',
ConsoleLogDriver(),
formatter: PrettyLogFormatter(),
); // Development only
// In production: logs to 'console' and 'daily'
// In development: logs to 'console' and 'debug'
Driver Configuration Options
Below are the configuration options for each available driver:
-
Console Driver
- Driver Name:
console - Configuration: No additional configuration required.
- Driver Name:
-
Daily File Driver
- Driver Name:
daily - Configuration Options:
path: The file path where logs will be stored. Default islogs/default.log.days: Number of days to retain log files. Default is14.
- Driver Name:
-
Webhook Driver
- Driver Name:
webhook - Configuration Options:
url: The webhook URL to send logs to.
- Driver Name:
-
Sampling Driver
- Driver Name:
sampling - Configuration Options:
sample_rates: A map of log levels to sampling rates.wrapped_driver: Configuration for the driver that will be wrapped by the sampling driver. Must include adriverkey.
- Driver Name:
-
Stack Driver
- Driver Name:
stack - Configuration Options:
channels: A list of channel names to forward logs to.ignore_exceptions: Boolean to ignore exceptions during logging. Default isfalse.
- Driver Name:
Example Configuration
final config = LogConfig(
'channels': {
'console': {
'driver': 'console',
},
'daily': {
'driver': 'daily',
'config': {
'path': 'logs/app.log',
'days': 30,
},
},
},
);
final logger = await Logger.create(config: config);
'webhook': {
'driver': 'webhook',
'config': {
'url': 'https://hooks.slack.com/...',
},
},
'sampling': {
'driver': 'sampling',
'config': {
'sample_rates': {
'info': 0.1,
'error': 1.0,
},
'wrapped_driver': {
'driver': 'console',
},
},
},
'stack': {
'driver': 'stack',
'config': {
'channels': ['console', 'daily'],
'ignore_exceptions': true,
},
},
},
});
Stack Channels
Stack channels allow you to create a single channel that forwards logs to multiple other channels. This is useful when you want to send the same logs to multiple destinations with different formatting and filtering:
final config = LogConfig(
'channels': {
// Individual channels with their own formatters
'file': {
'driver': 'daily',
'config': {
'path': 'logs/app.log',
}
'formatter': 'json'
},
'slack': {
'driver': 'webhook',
'config': {
'url': 'https://hooks.slack.com/services/...',
}
},
},
);
final logger = await Logger.create(config: config);
// Stack channel that combines both
'production': {
'driver': 'stack',
'config': {
'channels': ['file', 'slack'], // Will forward to both channels
'ignore_exceptions': true
}
}
}
});
// Now you can log to both channels with one call
logger['production'].error('Critical failure');
Stack channels are particularly useful for:
- Sending critical logs to multiple destinations
- Applying different formatting per destination
- Creating backup logging channels
- Setting up monitoring and notification systems
Each channel in a stack maintains its own middleware chain and formatter, allowing for independent processing of logs for each destination.
Understanding Middleware
Contextual uses a two-stage middleware system to provide flexible log processing:
Context Middleware
Context middleware runs first and can add or modify the context data before any formatting happens:
// Add request ID and timestamp to all logs
logger.addMiddleware(() => {
'requestId': generateRequestId(),
'timestamp': DateTime.now().toIso8601String()
});
// Add dynamic user info when available
logger.addMiddleware(() {
if (currentUser != null) {
return {'userId': currentUser.id};
}
return {};
});
// Now these fields are automatically added
logger.info('User action'); // Includes requestId, timestamp, and userId
Driver and Channel Middleware
Driver middleware processes log entries before they reach specific drivers. Channel middleware works similarly but is associated with channels.
// Example of a middleware that filters sensitive data
class SensitiveDataMiddleware implements DriverMiddleware {
@override
FutureOr<DriverMiddlewareResult> handle(
LogEntry entry,
) async {
var message = entry.message;
message = message.replaceAll(RegExp(r'password=[\w\d]+'), 'password=***');
return DriverMiddlewareResult.modify(entry.copyWith(message: message));
}
}
// Example of a middleware that only allows error-level logs
class ErrorOnlyMiddleware implements DriverMiddleware {
@override
FutureOr<DriverMiddlewareResult> handle(
LogEntry entry,
) async {
final errorLevels = ['emergency', 'alert', 'critical', 'error'];
if (!errorLevels.contains(entry.record.level.name.toLowerCase())) {
return DriverMiddlewareResult.stop();
}
return DriverMiddlewareResult.proceed();
}
}
// Add middlewares
logger
// Global middleware applied to all drivers
.addLogMiddleware(SensitiveDataMiddleware())
// Channel-specific middleware only applied to the 'slack' channel
.addDriverMiddleware<WebhookLogDriver>(ErrorOnlyMiddleware());
// Middleware execution flow:
// 1. Context middleware runs first
// 2. Log is formatted
// 3. Global driver middlewares process the log
// 4. Channel-specific middlewares process the log
// 5. Log is sent to the driver
Asynchronous Logging and Shutdown
The logger uses asynchronous processing and batching to improve performance. This means you must properly shut down the logger to ensure all logs are delivered:
void main() async {
final logger = Logger(
sinkConfig: LogSinkConfig(
batchSize: 50, // Buffer up to 50 logs
flushInterval: Duration(seconds: 5), // Or flush every 5 seconds
)
);
try {
// Your application code
logger.info('Application starting');
await runApp();
logger.info('Application shutting down');
} finally {
// Ensure all logs are delivered before exiting
await logger.shutdown();
}
}
// In a web application
class MyApp {
final Logger logger;
Future<void> stop() async {
logger.notice('Stopping application');
await stopServices();
await logger.shutdown();
}
}
The shutdown() method:
- Flushes any buffered log messages
- Waits for all async log operations to complete
- Ensures webhook requests are sent
- Closes file handles and other resources
Always call shutdown() before your application exits to prevent log message loss.
Conclusion
Contextual provides a flexible and powerful logging solution for Dart applications. By allowing per-channel formatters, you can customize the output of each logging destination to suit your needs. Whether you're developing a simple application or a complex system with multiple output channels, Contextual offers the tools you need to implement efficient and informative logging.
Contributing
Contributions are welcome! Feel free to open an issue or submit a pull request on GitHub.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Libraries
- contextual
- A structured logging library with support for formatting, context, and multiple outputs.