logd 0.8.5
logd: ^0.8.5 copied to clipboard
Modular hierarchical logging for Dart and Flutter. Features modular pipeline processing, O(1) configuration resolution, and low‑overhead performance.
logd

A modular hierarchical logger for Dart and Flutter. Build structured logs, control output destinations, and keep overhead minimal.
Why logd? #
- Hierarchical configuration – Loggers are named with dot‑separated paths (
app.network.http). Settings propagate from parents to children unless overridden. - Zero‑boilerplate – Simple
Logger.get('app')gives a fully‑configured logger. - Performance‑first – Lazy resolution, aggressive caching, and optional inheritance freezing keep the cost of a disabled logger essentially zero.
- Flexible output – Choose between console, file, network, HTML, or any custom sink; format logs as text, structured JSON, HTML, Markdown or LLM‑optimized TOON.
- Layout Sovereignty – A centralized engine guarantees structural integrity (e.g., perfect boxes) across all terminal widths.
- Platform‑agnostic styling – Decouple visual intent from representation using the semantic
LogThemesystem. - Web & Desktop Parity – Built-in platform-aware stack trace parsers for Chrome (V8), Firefox, Safari, and the Dart VM, preserving column numbers for high-fidelity source map resolution.
Getting Started #
Add logd to your project:
dependencies:
logd: ^latest_version
Then run:
dart pub get # or flutter pub get
Quick Example #
import 'package:logd/logd.dart';
void main() {
final logger = Logger.get('app');
logger.info('Application started');
logger.debug('Debug message');
logger.warning('Low disk space');
logger.error('Connection failed',
error: exception,
stackTrace: stack,
);
}
Typical console output
[app][INFO] 2025-01-23 05:30:12.456
--example/main.dart:12 (main)
----Application started
Tip: Use
Logger.configureto set global log‑levels, handlers, or timestamps.logduses Deep Equality to ensure that re-configuring with identical values results in zero performance overhead.
Core Concepts #
Hierarchical Loggers #
Loggers inherit configuration from their ancestors.
// Configure the entire app
Logger.configure('app', logLevel: LogLevel.warning);
// Override a subsystem
Logger.configure('app.network', logLevel: LogLevel.debug);
// Create a logger deep in the tree
final uiLogger = Logger.get('app.ui.button'); // inherits WARNING
final httpLogger = Logger.get('app.network.http'); // inherits DEBUG
Bulk Configuration #
For complex or large applications, configuring multiple loggers individually can lead to boilerplate and redundant cache invalidation traversals. You can use the bulk configuration API to apply multiple updates at once:
Logger.configureMultiple({
'global': const LoggerConfig(logLevel: LogLevel.warning),
'app.network': const LoggerConfig(logLevel: LogLevel.debug),
'app.ui': const LoggerConfig(enabled: false),
});
- Atomic Validation: Input validation occurs across the entire configuration map before any updates are written. If a single configuration fails (e.g., negative stack trace count), the entire update is cleanly rejected.
- Single-Pass Cache Invalidation: The cache is invalidated in a single optimized pass for all changed loggers and their descendants, eliminating redundant tree-walks and reducing GC pressure.
Pattern-Based Configuration #
When you want to apply configurations to loggers that don't share a strict parent-child relationship or to match loggers dynamically across your application using wildcards, you can use configurePattern. It supports standard glob wildcards (* matches zero or more characters, ? matches a single character).
// Configure all database loggers to DEBUG
Logger.configurePattern('*.database', logLevel: LogLevel.debug);
// Disable all third-party package loggers under a prefix
Logger.configurePattern('vendor.*', enabled: false);
// Wildcard matches are evaluated dynamically on cache resolution,
// with newer pattern rules overriding older ones if they both match.
Logger.configurePattern('app.services.*', logLevel: LogLevel.info);
Log levels #
| Level | Description |
|---|---|
trace |
Diagnostic noise |
debug |
Developer debugging |
info |
Informational |
warning |
Potential issue |
error |
Failure |
Advanced Features #
Custom Handlers #
Create complex pipelines of formatters and sinks:
final jsonHandler = Handler(
formatter: const JsonFormatter(
metadata: {LogMetadata.timestamp, LogMetadata.logger},
),
sink: FileSink(
'logs/app.log',
fileRotation: TimeRotation(
interval: Duration(days: 1),
timestamp: Timestamp(formatter: 'yyyy-MM-dd'),
backupCount: 7,
compress: true,
),
),
filters: [LevelFilter(LogLevel.info)],
);
Logger.configure('app', handlers: [jsonHandler]);
Result: JSON logs written to logs/app.log, rotated daily, keeping 7 compressed backups.
Note
Modern formatters (v0.6.1+) automatically include mandatory data like level, message, error, and stackTrace. The metadata parameter is used only for additional context like timestamps or logger names.
Atomic multi‑line logs #
Prevent interleaving in concurrent environments:
final buffer = logger.infoBuffer;
buffer?.writeln('=== User Session ===');
buffer?.writeln('User ID: ${user.id}');
buffer?.writeln('Login time: ${DateTime.now()}');
buffer?.writeln('IP: ${request.ip}');
buffer?.sink(); // writes atomically
Multiple Outputs #
You can either use multiple handlers:
final consoleHandler = Handler(
formatter: const StructuredFormatter(),
decorators: const [
BoxDecorator(),
StyleDecorator(),
SuffixDecorator(
label: '[v1.0.2]',
align: true,
),
],
sink: const ConsoleSink(lineLength: 80),
);
final fileHandler = Handler(
formatter: const PlainFormatter(),
sink: FileSink('logs/app.log'),
);
Logger.configure('global', handlers: [consoleHandler, fileHandler]);
Or use a multi-sink in a handler:
final multiSinkHandler = Handler(
formatter: PlainFormatter(),
sink: MultiSink(sinks: [
ConsoleSink(),
FileSink('logs/app.log'),
],
),
);
Filtering #
Control which logs reach which handlers:
// Level-based filtering
final errorHandler = Handler(
formatter: JsonFormatter(),
sink: FileSink('logs/errors.log'),
filters: [LevelFilter(LogLevel.error)], // Errors only
);
// Regex-based filtering (exclude sensitive data)
final publicHandler = Handler(
formatter: PlainFormatter(),
sink: FileSink('logs/public.log'),
filters: [
RegexFilter(r'password|secret|token', exclude: true),
],
);
Timezone & Timestamp #
final timestamp = Timestamp(
formatter: 'yyyy-MM-dd HH:mm:ss.SSS Z',
timezone: Timezone.named('America/New_York'),
);
Logger.configure('app', timestamp: timestamp);
For high-frequency or daily aggregate logs where sub-second precision is not needed, you can use date-only formatting to bypass sub-second calculations entirely:
final dateOnly = Timestamp.dateOnly('yyyy-MM-dd');
Logger.configure('app.audit', timestamp: dateOnly);
File Rotation #
| Strategy | Example |
|---|---|
| Size | FileSink('logs/app.log', fileRotation: SizeRotation(maxSize: '10 MB', backupCount: 5, compress: true)) |
| Time | FileSink('logs/app.log', fileRotation: TimeRotation(interval: Duration(hours: 1), timestamp: Timestamp(formatter: 'yyyy-MM-dd_HH'), backupCount: 24)) |
High-Performance Execution (v0.8.0+) #
logd features a modular engine architecture to match your performance requirements:
- StandardEngine (Default): A reliable, platform-agnostic engine running on the Dart GC heap. Fully compatible with Web, Desktop, Mobile, and VM.
- ArenaEngine: Uses isolate-local LIFO object pooling to eliminate GC pressure. Ideal for complex logs with many decorators.
- NativeEngine (Opt-in): Leverages
dart:ffiand a Binary IR (B-IR) instruction stream for native VM platforms. Fully stabilized in v0.8.1 with 100% verified layout parity.
// StandardEngine is used by default.
// You can explicitly swap to ArenaEngine or NativeEngine in your Handler setup.
// To optimize performance further, you can freeze inheritance:
Logger.get('app').freezeInheritance();
Inheritance Control & Diagnostics (v0.8.2+) #
logd features a mature, production-ready logger inheritance management system for hot paths and troubleshooting:
- Bake & Force Update:
logger.freezeInheritance()bakes configuration down the hierarchy to bypass tree-walks. It returns the number of fields written. To re-snapshot currently frozen configurations after ancestry changes, calllogger.freezeInheritance(force: true). - Selective Unfreeze: Restore dynamic propagation to the parent hierarchy on specific fields, or restrict the restoration strictly to descendants:
logger.unfreezeInheritance( fields: {'logLevel'}, includeSelf: false, // only unfreeze descendants ); - Tree Visualization: Visualize the active registry tree, with annotations for explicit/frozen settings and actual effective values:
final textTree = Logger.formatHierarchy(); Logger.printHierarchy(sink: print); // or custom sink - JSON-serializable Export:
Logger.exportHierarchy()returns a map of all registered loggers, including effective resolved values and ghost-node detection ('implicit': truefor implicit loggers). - Subtree & Global Reset: Reset the entire registry or a specific namespace subtree to default unresolved settings:
Logger.reset('app.ui'); // Resets only 'app.ui' and its descendants Logger.reset(); // Global reset, clears the entire registry - Hierarchy Depth Warning (v0.8.5+): Accessing loggers with abnormally deep hierarchies (e.g. >10 levels) prints an
InternalLoggerwarning on first access to protect against stack overflows and resolution performance issues. This threshold is customizable:
This can be disabled by settingLogger.maxHierarchyDepth = 12; // Customize safety limitLogger.maxHierarchyDepth <= 0.
Isolate Configuration Transport (v0.8.4+) #
To share configurations across isolates, serialize the configuration registry to a plain JSON-compatible map and import it in a worker isolate:
// In primary isolate:
final configMap = Logger.exportConfig();
// In background isolate:
Logger.importConfig(configMap);
Non-serializable fields (like custom sinks or formatters) are mapped via the LoggerSerializationRegistry.register API to allow seamless reconstructive routing.
Observability & Metrics (v0.8.4+) #
Monitor cache efficiency, pipeline handler failures, and memory buffer allocations or GC/leak warnings:
final metricsJson = LoggerMetrics.toJson();
print('Cache hits: ${LoggerMetrics.cacheHits}');
print('Buffer leaks: ${LoggerMetrics.bufferLeaks}');
Graceful Fallback & Degradation (v0.8.4+) #
If all configured handlers throw exceptions, logd gracefully falls back to console output so that critical diagnostics are never lost. You can customize this behavior (or disable it) using Logger.fallbackHandler:
Logger.fallbackHandler = (final entry, final error, final stackTrace) {
stderr.writeln('ALERT: All log handlers failed for: ${entry.message}');
};
Testing Utilities (v0.8.4+) #
Import package:logd/testing.dart to assert against logs inside your test suites:
import 'package:logd/testing.dart';
void main() {
test('verify logic logs warning', () async {
final logger = TestLogger.get('app');
final capture = CaptureSink();
logger.configure(handlers: [
Handler(formatter: const PlainFormatter(), sink: capture),
]);
performAction(logger);
expect(capture, hasLog(
message: contains('action failed'),
level: LogLevel.warning,
));
});
}
For a detailed walkthrough of each execution engine, see the Execution Engines Guide.
Use Cases #
Development Console #
Logger.configure('global', handlers: [
const Handler(
formatter: StructuredFormatter(),
decorators: [
HierarchyDepthPrefixDecorator(indent: '│ '),
BoxDecorator(borderStyle: BorderStyle.rounded),
StyleDecorator(theme: LogTheme(colorScheme: LogColorScheme.darkScheme)),
],
sink: const ConsoleSink(lineLength: 80),
),
]);
Production JSON #
Logger.configure('global', handlers: [
Handler(
formatter: JsonFormatter(),
sink: FileSink('logs/production.log'),
),
]);
LLM-Native Logging (TOON) #
Optimize logs for consumption by AI agents by using the Token-Oriented Object Notation:
Logger.configure('ai.agent', handlers: [
Handler(
formatter: const ToonFormatter(
arrayName: 'context',
metadata: {LogMetadata.timestamp},
),
sink: FileSink('logs/ai_feed.toon'),
),
]);
Result: A highly token-efficient, flat format that LLMs can parse with minimal overhead.
Schema Maturity
v0.7.1+ introduces Explicit Schemas for TOON. By setting explicitSchema: true, the formatter generates a typed, aligned header block that describes every column (including enum values for levels):
logs[]{
timestamp : iso8601;
level : enum(TRACE,DEBUG,INFO,WARNING,ERROR);
message : markdown;
}:
This provides zero-shot precision for machine-consumption without prior out-of-band configuration.
Network Logging #
Ship logs to remote servers with built-in resilience:
const httpSink = HttpSink(
url: 'https://logs.api.com',
batchSize: 50,
flushInterval: Duration(seconds: 10),
dropPolicy: DropPolicy.discardOldest,
);
Logger.configure('app', handlers: [
Handler(formatter: JsonFormatter(), sink: httpSink),
]);
Supported sinks: HttpSink (batching & retries), SocketSink (real-time streaming).
// For real-time streaming to a WebSocket server:
const socketSink = SocketSink(
url: 'wss://monitor.example.com/logs',
);
Microservice Logging #
Logger.configure('api', handlers: [
Handler(formatter: JsonFormatter(), sink: FileSink('logs/api.log')),
]);
Logger.configure('database', handlers: [
Handler(formatter: JsonFormatter(), sink: FileSink('logs/db.log')),
]);
Logger.configure('auth', handlers: [
Handler(
formatter: JsonFormatter(),
sink: FileSink('logs/security.log'),
filters: [LevelFilter(LogLevel.warning)],
),
]);
Flutter integration #
Capture framework errors and async errors:
void main() {
// Listen to all uncaught Flutter errors
FlutterError.onError = (final details) {
Logger.get('app.crash').error(
'Flutter error',
error: details.exception,
stackTrace: details.stack,
);
};
runZonedGuarded(
() => runApp(MyApp()),
(error, stack) {
Logger.get('app.crash').error(
'Uncaught error',
error: error,
stackTrace: stack,
);
},
);
}
Documentation #
- Documentation Index - Overview and navigation
- Logger Philosophy - Design principles and rationale
- Logger Architecture - Implementation details
- Handler Guide - Pipeline and sink customization
- Execution Engines Guide - Standard, Arena, and Native engines guide
- Engine Stability Report - Engine profiling & memory lifecycle report
- Migration Guide - Upgrading from legacy components
- Decorator Composition - Execution priority and flow
- Time Module - Timestamp and timezone handling
- Roadmap - Planned features and vision
Contributing #
- Report bugs or suggest features via GitHub Issues.
- Share ideas in Discussions.
All contributions should follow the guidelines in CONTRIBUTING.md and, for docs, doc/CONTRIBUTING_DOCS.md.
License #
This project is licensed under the BSD 3-Clause License. See the LICENSE file for details.
Support #
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Package page: logd on pub.dev