Beholder is a powerful, fully customizable logging library for Dart and Flutter, built with a focus on performance, structured data, and clean architecture. Unlike other solutions, Beholder avoids global singletons in favor of a strictly instance-based approach.
No global state. Create independent logger instances for different modules, layers, or environments. This makes testing and isolation effortless.
Many transports (databases, files, network clients) require asynchronous setup. Beholder manages their init() and dispose() cycles, protecting your system from using uninitialized resources via State Guards.
Fine-tune which data each transport handles with precision:
- Type Filtering (
allowedTypes/ignoredTypes): Control processing based on the data type (e.g., skipHeartbeator only allowNetworkRequest). - Tag Filtering (
allowedTags/ignoredTags): Filter records based on their string tags. Perfect for isolating "UI" logs from "Network" logs even if they share the same level. - Synchronous Checks: Filtering happens instantly before any expensive record processing (like converter execution) begins.
Beholder uses a lazy, high-performance templating system. You can use built-in placeholders or provide custom ones via ContextPlaceholder.
Standard system placeholders (available in every record):
{logName}: The name of the logger instance.{logLevel}: Numeric representation of the log level (e.g.,100).{logLevelName}: Human-readable level name (e.g.,INFO).{logTags}: String representation of the associated tags.{logDateTime}: Local ISO8601 timestamp.{logDateTimeUtc}: UTC ISO8601 timestamp.{logData}: The main data object, formatted using the matchingLogEntryConverter.{logMessage}: The optional message string from theLogEntry.
All placeholders are resolved lazily. For example, {logData} will only trigger the converter's onConvert function if a transport actually contains this placeholder in its output format.
Programmatically list all available keys: If you are building a custom transport and need to know which keys are available:
@override
Future<String> log(RecordEntry<Object> record) async {
// Print all available placeholders
record.placeholder.help();
// Use the manager to resolve a specific template
return record.placeholder.template('[{logLevelName}] {logData}');
}Beholder v0.9.8 is engineered for high-load applications:
- Lazy Evaluation: Expensive operations, such as formatting complex objects into strings (
logData), are only performed when a transport actually requests them. - O(1) Converter Lookups: Finding the right converter for a data type is a constant-time operation thanks to built-in type caching and "warm-up" during initialization.
- Internal Caching: Formatted data is computed exactly once per record, even if dispatched to multiple transports.
- Zero-Block Dispatch: Utilizes
Future.valueandunawaitedto ensure logging never blocks your app's main execution flow.
Define your log levels, converters, and transports:
final class MyOptions extends BeholderOptions<AppTag> {
@override
int get logLevel => 0;
@override
List<LogLevel> get levels => [
LogLevel(
level: 100,
name: 'info',
transports: [
// Use TransportAdapter for fine-grained control
TransportAdapter(
transport: ConsoleTransport(),
ignoredTypes: [Heartbeat], // Skip technical noise
ignoredTags: ['internal', 'sensitive'], // Skip specific tags
onLog: (record) => '[${record.time}] ${record.description}',
),
],
),
];
@override
List<LogEntryConverter> get converters => [
LogEntryConverter<DateTime>(onConvert: (dt) => dt.toIso8601String()),
LogEntryConverter<User>(onConvert: (u) => 'User(id: ${u.id})'),
];
}base class MyLogger extends Beholder<AppTag> {
MyLogger() : super(name: 'App', settings: MyOptions());
void info(Object data, {List<AppTag>? tags}) =>
log(entry: LogEntry(data), level: 100, tags: tags);
}void main() async {
final logger = MyLogger();
// Mandatory initialization for async transports
await logger.init();
logger.info('Application started');
logger.info(Heartbeat(), tags: [AppTag.system]); // Ignored by console transport
// Graceful shutdown
await logger.dispose();
}Add beholder to your pubspec.yaml:
dependencies:
beholder: ^0.9.8MIT