logd   pub version pub points BSD-3-Clause

logd Hero

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.run scopes 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),
]);

SQLite Persistence (logd_sqlite)

Store structured logs locally with WAL-mode concurrency and rich query engine using package:logd_sqlite:

import 'package:logd/logd.dart';
import 'package:logd_sqlite/logd_sqlite.dart';

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]);

// Query stored logs & level breakdown
final errors = sqliteHandler.queryLogs(
  minLevel: LogLevel.warning,
  search: 'Payment',
  limit: 50,
);
final counts = sqliteHandler.fetchLevelCounts();

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)],
  ),
]);

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

License

BSD 3-Clause. See LICENSE.

Libraries

advanced
Advanced orchestration, execution engines, and isolate internals for logd.
logd
testing