logd

logd Hero

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 LogTheme system.
  • 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.configure to set global log‑levels, handlers, or timestamps. logd uses 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:ffi and 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, call logger.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': true for 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 InternalLogger warning on first access to protect against stack overflows and resolution performance issues. This threshold is customizable:
    Logger.maxHierarchyDepth = 12; // Customize safety limit
    
    This can be disabled by setting Logger.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


Contributing

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

Libraries

logd
testing