logd 0.9.5
logd: ^0.9.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 structured, hierarchical logging engine for Dart and Flutter.
Most Dart logging libraries handle the simple case well. logd handles the rest: multiple isolates, structured context propagation, background I/O offloading, and a full output pipeline — from styled ANSI terminals to a live browser dashboard — without ever breaking the simple case.
Quick Start #
dependencies:
logd: ^latest_version
import 'package:logd/logd.dart';
void main() {
final logger = Logger.get('app');
logger.info('Server started');
logger.warning('Disk space low', context: {'freeGb': 1.2});
logger.error('Connection failed', error: e, stackTrace: st);
}
That's it for most use cases. Everything below is opt-in.
Why logd? #
The problems it solves #
Logging across isolates is broken in most libraries.
When you offload work to a background isolate, your logger either silently drops entries or forces you to wire up ports manually. logd's .async() constructors offload the entire pipeline — formatting, decoration, I/O — to a worker isolate, returning to your code in ~15µs.
Structured context disappears in async code.
Attaching a requestId to every log entry in a request handler means threading a Map through every function signature. logd's zone-based MDC (LogContext.run) binds context once and propagates it automatically through every await, microtask, and nested call — with zero allocations when no context is present.
Output pipelines are all-or-nothing. Most libraries give you console or file. logd gives you a clean pipeline: Formatter → Decorator → Encoder → Sink. Swap any stage independently. Run the same formatter to a styled ANSI terminal, a JSON file, an HTML report, and a live browser dashboard — simultaneously.
Pre-Wired Handlers #
Seven core ready-to-use handlers cover common destinations out of the box (with network telemetry provided by logd_network). No pipeline wiring required:
// Styled terminal output (dark or light theme)
ConsoleHandler(theme: const LogTheme.dark())
// Structured JSON to file (pretty or compact)
JsonFileHandler('logs/api.json', pretty: true)
// Self-contained HTML report with search and level filters
HtmlFileHandler('logs/session.html')
// Token-efficient format for LLM / AI agent consumption
ToonFileHandler('logs/telemetry.toon')
// Plain text to file
PlainFileHandler('logs/app.log')
// GitHub-Flavored Markdown for CI summaries
MarkdownFileHandler('logs/ci.md')
// In-memory ring buffer for tests and debug panels
MemoryHandler(capacity: 200)
// Live browser dashboard via HTTP + WebSocket server (from package:logd_network)
HttpDashboardHandler(port: 8080)
Add .async() to any output-bound handler to offload its pipeline to a background isolate:
Logger.configure('app', handlers: [
ConsoleHandler.async(),
JsonFileHandler.async('logs/production.json'),
HtmlFileHandler.async('logs/report.html'),
]);
Tip
MemoryHandler and HttpDashboardHandler intentionally stay synchronous — they need direct heap access and deterministic port binding respectively.
Core Concepts #
Log Levels #
| Level | Description |
|---|---|
trace |
Verbose diagnostic noise |
debug |
Developer debugging |
info |
Informational milestones |
warning |
Potential issues |
error |
Failures |
Hierarchical Logger Tree #
Loggers are named with dot-separated paths. Configuration propagates from parents to children unless overridden — the same model as Python's logging or Java's log4j:
// Set the baseline for the whole app
Logger.configure('app', logLevel: LogLevel.warning);
// Override a noisy subsystem
Logger.configure('app.network', logLevel: LogLevel.debug);
// Deep loggers inherit from the nearest configured ancestor
final httpLogger = Logger.get('app.network.http'); // → DEBUG
final uiLogger = Logger.get('app.ui.button'); // → WARNING
Bulk Configuration #
Configure multiple loggers atomically — validated and cache-invalidated in a single pass:
Logger.configureMultiple({
'global': const LoggerConfig(logLevel: LogLevel.warning),
'app.network': const LoggerConfig(logLevel: LogLevel.debug),
'app.ui': const LoggerConfig(enabled: false),
});
- Atomic Validation: the entire map is validated before any updates are written.
- Single-Pass Invalidation: cache is invalidated in one optimized pass for all changed loggers and descendants.
Pattern-Based Configuration #
Match loggers dynamically with glob patterns:
Logger.configurePattern('*.database', logLevel: LogLevel.debug);
Logger.configurePattern('vendor.*', enabled: false);
Logger.configurePattern('app.services.*', logLevel: LogLevel.info);
Ambient Structured Context (MDC) #
Bind context once. It flows automatically through every await, sync call, and microtask — no threading required:
Future<void> handleCheckout(String requestId, int userId) async {
await LogContext.run({'requestId': requestId, 'userId': userId}, () async {
logger.info('Validating cart'); // → {requestId: req-9812, userId: 42}
await chargePaymentGateway(); // downstream async calls inherit context
logger.info('Order completed'); // → {requestId: req-9812, userId: 42}
});
}
Future<void> chargePaymentGateway() async {
// No context parameter. No logger parameter. Zero boilerplate.
Logger.get('app.payment').info('Connecting to Stripe');
// Output includes: {requestId: req-9812, userId: 42}
}
- Scope nesting: inner
LogContext.runscopes inherit outer keys; conflicting keys override without mutation. - Call-site overrides:
logger.info('msg', context: {'key': 'val'})overrides specific keys for one entry. - Concurrent isolation: parallel tasks (
Future.wait) maintain separate contexts with zero race conditions. - Zero-cost fast path: when no context is present, dispatch performs 0 heap allocations.
Output Formats #
| Format | Handler / Formatter | Best for |
|---|---|---|
| Styled ANSI | ConsoleHandler |
Development terminal |
| Structured JSON | JsonFileHandler / JsonFormatter |
Log aggregators (Loki, ELK) |
| HTML report | HtmlFileHandler / HtmlEncoder |
Shareable session logs |
| Markdown | MarkdownFileHandler |
CI/CD job summaries |
| TOON | ToonFileHandler / ToonFormatter |
LLM / AI agent consumption |
| Plain text | PlainFileHandler / PlainFormatter |
Simple file logs |
| SQLite | logd_sqlite |
Queryable persistent storage |
TOON: Logs Optimized for LLMs #
Token-Oriented Object Notation emits the column schema once, then tab-delimited rows — 30–50% fewer tokens than JSON for the same data:
Logger.configure('ai.agent', handlers: [
ToonFileHandler('logs/agent.toon'),
]);
The strict dialect adds parser headers for direct ingestion into DuckDB, Loki, or awk:
Handler(
formatter: const ToonFormatter(
dialect: ToonDialect.strict,
sortKeys: true, // deterministic key ordering
maxDepth: 3, // bounded map recursion
),
sink: FileSink('logs/pipeline.toon'),
)
When slicing a TOON file for an LLM sliding-window prompt:
final schema = ToonEncoder.extractPreamble(bytes) ?? '';
final window = sliceLines(bytes, last: 50);
final prompt = '$schema\n${window.join('\n')}';
Advanced Features #
Custom Pipeline Handlers #
When pre-wired handlers aren't enough, compose your own:
final handler = Handler(
formatter: const JsonFormatter(
metadata: {LogMetadata.timestamp, LogMetadata.logger},
),
decorators: const [StyleDecorator(DarkTheme())],
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: [handler]);
Note
Modern formatters automatically include level, message, error, and stackTrace. The metadata parameter controls optional context like timestamps or logger names.
Multiple Outputs #
Attach multiple handlers to route the same logs to different destinations:
Logger.configure('global', handlers: [
Handler(
formatter: const StructuredFormatter(),
decorators: const [
BoxDecorator(borderStyle: BorderStyle.rounded),
StyleDecorator(DarkTheme()),
],
sink: const ConsoleSink(lineLength: 80),
),
Handler(
formatter: const PlainFormatter(),
sink: FileSink('logs/app.log'),
),
]);
Or fan out a single formatter to multiple sinks:
Handler(
formatter: const PlainFormatter(),
sink: MultiSink(sinks: [
ConsoleSink(),
FileSink('logs/app.log'),
]),
)
Atomic Multi-Line Logs #
Prevent line 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(); // flushed as a single atomic LogEntry
Filtering #
// Level filter — errors only
Handler(filters: [LevelFilter(LogLevel.error)], ...)
// Regex filter — exclude sensitive fields
Handler(filters: [RegexFilter(r'password|secret|token', exclude: true)], ...)
// Context filter — match on MDC key presence or value
Handler(filters: [ContextFilter(key: 'tenantId', value: 'acme')], ...)
Timestamp & Timezone #
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, bypass sub-second calculations entirely with date-only formatting:
Logger.configure('app.audit', timestamp: Timestamp.dateOnly('yyyy-MM-dd'));
File Rotation #
// Size-based
FileSink('logs/app.log', fileRotation: SizeRotation(
maxSize: '10 MB', backupCount: 5, compress: true,
))
// Time-based
FileSink('logs/app.log', fileRotation: TimeRotation(
interval: Duration(hours: 1),
timestamp: Timestamp(formatter: 'yyyy-MM-dd_HH'),
backupCount: 24,
))
Network Logging (logd_network) #
Ship logs to remote servers with built-in resilience using package:logd_network:
import 'package:logd/logd.dart';
import 'package:logd_network/logd_network.dart';
// HTTP batching with retry and drop policy
final httpSink = HttpSink(
url: 'https://logs.api.com',
batchSize: 50,
flushInterval: const Duration(seconds: 10),
dropPolicy: DropPolicy.discardOldest,
);
// Real-time WebSocket streaming
final socketSink = SocketSink(
url: 'wss://monitor.example.com/logs',
);
Logger.configure('app', handlers: [
Handler(formatter: const JsonFormatter(), sink: httpSink),
]);
Performance #
Hot-Path Dispatch Bypass #
Capturing caller origin (file, line, method) requires a StackTrace.current VM call on every log entry. When you don't need it, turn it off:
Logger.configure('app.analytics', includeOrigin: false);
Result: ~5x dispatch throughput gain (~2.38µs vs ~11.76µs per entry). Explicit stackTrace: parameters and stackMethodCount overrides are still respected.
Execution Engines #
| Engine | Allocation | Platform | When to use |
|---|---|---|---|
StandardEngine |
GC heap | All | Default — always correct |
ArenaEngine |
LIFO pool | VM / Native | Complex logs, many decorators |
NativeEngine |
C heap via FFI | VM / Native | Narrow-width wrapping, 1.5x speedup |
Inheritance Freezing #
Eliminate hierarchy tree-walks on hot paths by baking resolved configuration into descendant loggers:
// Bake current resolved config into all descendants (O(1) lookup after)
Logger.get('app').freezeInheritance();
// Re-snapshot after ancestry changes, preserving explicit overrides
Logger.get('app').freezeInheritance(force: true);
// Restore dynamic resolution for specific fields only
Logger.get('app').unfreezeInheritance(
fields: {'logLevel'},
includeSelf: false, // descendants only
);
Inheritance Control & Diagnostics #
// Visualize the live logger hierarchy with effective values
Logger.printHierarchy();
final tree = Logger.formatHierarchy();
// Full JSON export — effective values, frozen state, ghost-node detection
final snapshot = Logger.exportHierarchy();
// Reset a namespace subtree (useful in tests)
Logger.reset('app.ui'); // resets 'app.ui' and its descendants
Logger.reset(); // full registry reset
// Hierarchy safety limit (warns at >10 levels by default)
Logger.maxHierarchyDepth = 12;
Isolate Configuration Transport #
Export and import the full logger registry across isolate boundaries:
// Primary isolate
final config = Logger.exportConfig();
// Worker isolate
Logger.importConfig(config);
Custom formatters and sinks are mapped via LoggerSerializationRegistry.register.
Observability & Metrics #
// Cache efficiency, handler failures, buffer allocations and leak warnings
print('Cache hits: ${LoggerMetrics.cacheHits}');
print('Buffer leaks: ${LoggerMetrics.bufferLeaks}');
print('Handler fails: ${LoggerMetrics.handlerFailures}');
final metricsJson = LoggerMetrics.toJson();
Graceful Fallback & Degradation #
If all configured handlers throw, logd automatically falls back to console output — critical logs are never silently lost. Customize or disable this behavior:
Logger.fallbackHandler = (entry, error, stackTrace) {
stderr.writeln('ALERT: all handlers failed for: ${entry.message}');
};
Use Cases #
Development Console #
Logger.configure('global', handlers: [
Handler(
formatter: const StructuredFormatter(),
decorators: const [
HierarchyDepthPrefixDecorator(indent: '│ '),
BoxDecorator(borderStyle: BorderStyle.rounded),
StyleDecorator(DarkTheme()),
],
sink: const ConsoleSink(lineLength: 80),
),
]);
Production JSON #
Logger.configure('global', handlers: [
Handler(
formatter: const JsonFormatter(),
sink: FileSink('logs/production.log'),
),
]);
Microservice — Per-Subsystem Routing #
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)],
),
]);
SQLite Persistence (logd_sqlite) #
dependencies:
logd: ^0.9.4
logd_sqlite: ^0.1.0
import 'package:logd/logd.dart';
import 'package:logd_sqlite/logd_sqlite.dart';
void main() async {
final sqliteHandler = SqliteHandler(
dbPath: 'app_logs.db',
maxEntries: 10000,
maxAge: const Duration(days: 7),
batchSize: 50,
flushInterval: const Duration(seconds: 2),
walMode: true,
);
Logger.configure('app', handlers: [sqliteHandler]);
final logger = Logger.get('app.payment');
logger.info(
'Payment processed',
context: {'transactionId': 'TX-9042', 'amount': 150.00},
);
// Query stored logs
final errors = sqliteHandler.queryLogs(
minLevel: LogLevel.warning,
search: 'Payment',
limit: 50,
);
// Level breakdown
final counts = sqliteHandler.fetchLevelCounts();
print('Warnings: ${counts[LogLevel.warning]}');
await sqliteHandler.dispose();
}
Key features: WAL-mode batch commits, auto-pruning retention (maxEntries, maxAge), rich query engine (queryLogs, fetchLevelCounts, fetchDistinctLoggerNames).
In-Memory Ring Buffer #
For testing, debug panels, or transient error dumps:
final memoryHandler = MemoryHandler(capacity: 100);
Logger.configure('global', handlers: [memoryHandler]);
// Inspect recent entries programmatically
print(memoryHandler.entries.length);
Flutter Integration #
void main() {
FlutterError.onError = (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,
),
);
}
Testing #
import 'package:logd/testing.dart';
test('logs warning on failure', () 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,
));
});
Satellite Packages #
| Package | Purpose |
|---|---|
logd_sqlite |
WAL-mode SQLite persistence, auto-pruning, rich query engine |
logd_network |
HTTP batching, WebSocket streaming, embedded viewer dashboard |
logd_linters |
Custom lint rules for arena lifecycle and formatter purity |
Documentation #
| Document | Description |
|---|---|
| Logger Philosophy | 13 design principles with rationale |
| Handler Architecture | Pipeline internals |
| Execution Engines Guide | Standard, Arena, Native |
| TOON Specification | Format spec + DuckDB ingestion |
| Isolates Guide | Cross-isolate configuration |
| Migration Guide | Upgrading from legacy components |
| Architecture Decisions | ADR-001 through ADR-006 |
| Roadmap | Planned features |
Contributing #
- Report bugs or suggest features via GitHub Issues
- Share ideas in GitHub Discussions
- Read CONTRIBUTING.md before sending a PR
License #
BSD 3-Clause. See LICENSE.