davianspace_logging 1.0.3 copy "davianspace_logging: ^1.0.3" to clipboard
davianspace_logging: ^1.0.3 copied to clipboard

Logging framework for Dart and Flutter inspired by Microsoft.Extensions.Logging with providers, filtering, scopes, structured logs, and JSON output.

davianspace_logging #

Enterprise-grade structured logging framework for Dart and Flutter. Conceptually equivalent to Microsoft.Extensions.Logging, expressed idiomatically in Dart.

pub.dev License: MIT


Features #

Capability Description
Structured logging Properties stored separately from messages; providers decide formatting
Multiple providers Console, debug, memory, and custom providers run simultaneously
Log-level filtering Global floor, category-prefix rules, and provider-specific rules
Scoped contexts Zone-aware async-safe scopes automatically inject contextual properties
Logger factory Category-based logger creation with lifetime management
Pluggable formatters Simple text and JSON out of the box; implement LogFormatter for custom
Zero allocations Fast-path exits before allocating when log level is disabled
No dependencies Pure Dart; works on all platforms (CLI, Flutter, server)

Quick start #

import 'package:davianspace_logging/davianspace_logging.dart';

void main() {
  final factory = LoggingBuilder()
    .addConsole()
    .setMinimumLevel(LogLevel.info)
    .build();

  final logger = factory.createLogger('MyApp');

  logger.info('Application started');
  logger.info('User logged in', properties: {'userId': 42, 'role': 'admin'});
  logger.warning('Disk space low', properties: {'freeGb': 1.2});
  logger.error('Unexpected failure', error: exception, stackTrace: st);

  factory.dispose();
}

Architecture #

LoggingBuilder
    │
    ▼
LoggerFactory (LoggerFactoryImpl)
    ├── FilterRuleSet         ← routing decisions
    ├── TimestampProvider     ← injectable clock
    └── List<LoggerProvider>
            ├── ConsoleLoggerProvider ── ConsoleLogger (stdout + ANSI color)
            ├── DebugLoggerProvider   ── DebugLogger   (dart:developer)
            └── MemoryLoggerProvider  ── MemoryLogger  (in-memory store)

LoggerFactory.createLogger("Category")
    └── LoggerImpl  ←── active LoggingScope (Zone-propagated)
            │
            ▼
          LogEvent (immutable, shared across providers)
            ├── level, category, message, timestamp
            ├── properties     ← caller-supplied structured data
            └── scopeProperties ← merged from LoggingScope chain

Log levels #

enum LogLevel { trace, debug, info, warning, error, critical, none }

Levels are ordered; none suppresses all output:

logger.isEnabled(LogLevel.debug);   // true / false depending on config

Structured properties #

Properties are kept separate from the message so every provider can decide whether to render them inline (text) or persist them as structured fields (JSON):

logger.info(
  'Order placed',
  properties: {
    'orderId': 1042,
    'amount':  299.99,
    'currency': 'USD',
  },
);

Log filtering #

final factory = LoggingBuilder()
  .addConsole()
  .setMinimumLevel(LogLevel.debug)                // global floor
  .addFilterRule(FilterRule(
      categoryPrefix: 'network',
      minimumLevel:   LogLevel.error,             // noisy subsystem override
  ))
  .addFilterRule(FilterRule(
      providerType:   ConsoleLoggerProvider,
      categoryPrefix: 'metrics',
      minimumLevel:   LogLevel.none,              // silence metrics on console
  ))
  .build();

Rule specificity (highest wins):

  1. Provider type and category prefix
  2. Category prefix only
  3. Provider type only
  4. Global minimum (catch-all)

Scoped logging #

Scopes carry contextual properties through an entire async call chain via Dart's Zone mechanism — no manual threading required:

final scope = logger.beginScope({
  'requestId': request.id,
  'userId':    request.userId,
});

await scope.runAsync(() async {
  logger.info('Processing');          // ← includes requestId + userId
  await callDownstream();
  logger.info('Done');                // ← still includes requestId + userId
});

Scopes nest; child properties override parent properties with the same key.


Formatters #

Simple (default) #

2026-02-25T14:23:01.123456Z [INFO ] OrderService » Order placed  {orderId: 1042}

JSON #

{"timestamp":"2026-02-25T14:23:01.123456Z","level":"info","category":"OrderService","message":"Order placed","properties":{"orderId":1042}}

Register a formatter on any provider:

LoggingBuilder()
  .addConsole(formatter: JsonFormatter())
  .build();

Custom formatters implement LogFormatter:

final class MyFormatter implements LogFormatter {
  @override
  String format(LogEvent event) =>
      '[${event.level.label}] ${event.message}';
}

Custom providers #

final class SyslogProvider implements LoggerProvider {
  @override
  Logger createLogger(String category) =>
      SyslogLogger(category: category);

  @override
  void dispose() { /* close socket */ }
}

// Register:
LoggingBuilder().addProvider(SyslogProvider()).build();

Implement EventLogger (extends Logger) to receive a pre-built LogEvent including scope-merged properties:

final class SyslogLogger implements EventLogger {
  @override
  void write(LogEvent event) => syslog(event.level.label, event.message);
  // …
}

Testing #

Use MemoryLoggerProvider + MemoryLogStore to assert log output in unit tests:

test('logs order placed at info level', () {
  final store = MemoryLogStore();
  final factory = LoggingBuilder()
      .addMemory(store: store)
      .setMinimumLevel(LogLevel.trace)
      .build();

  OrderService(factory.createLogger('OrderService'))
      .placeOrder(Order(1, 99.99));

  final event = store.events.single;
  expect(event.level, equals(LogLevel.info));
  expect(event.properties['orderId'], equals(1));
  factory.dispose();
});

Performance #

  • Zero allocations when a log level is disabled — isEnabled check exits before any object is created.
  • Single LogEvent allocation per log() call, shared across all providers.
  • Zone-local scope lookup — O(1) read, no contention.
  • No reflection — category names are plain strings; no dart:mirrors.
  • Lazy initialisation — provider caches are populated on first access.

Use the isEnabled guard for expensive message construction:

if (logger.isEnabled(LogLevel.debug)) {
  logger.debug('Snapshot: ${expensiveDump()}');
}

Migration from Microsoft.Extensions.Logging #

MEL (C#) davianspace_logging (Dart)
ILogger Logger (abstract interface)
ILoggerFactory LoggerFactory (abstract interface)
ILoggerProvider LoggerProvider (abstract interface)
ILoggingBuilder LoggingBuilder (concrete builder)
LogLevel enum LogLevel enum (same names, lowercase)
ILogger.BeginScope(state) logger.beginScope(properties)
ILoggerFactory.CreateLogger<T>() factory.createLogger('TypeName')
using var scope = … await scope.runAsync(() async { … })
AddConsole() .addConsole() on LoggingBuilder
Structured logging templates Plain message + properties map
IDisposable dispose() method on factory and providers

Key differences #

  • Dart has no IDisposable / using; use scope.run(...) / scope.runAsync(...) instead of relying on disposal for scope lifetime.
  • Structured properties use a Map<String, Object?> instead of C# message templates.
  • No dependency injection container required — LoggingBuilder covers DI composition.
  • Providers register extensions on LoggingBuilder rather than via IServiceCollection.

License #

MIT © 2026 Davian Space

0
likes
160
points
186
downloads

Documentation

API reference

Publisher

verified publisherdavian.space

Weekly Downloads

Logging framework for Dart and Flutter inspired by Microsoft.Extensions.Logging with providers, filtering, scopes, structured logs, and JSON output.

Repository (GitHub)
View/report issues
Contributing

Topics

#logging #structured-logging #flutter #architecture #observability

License

MIT (license)

Dependencies

davianspace_dependencyinjection

More

Packages that depend on davianspace_logging