davianspace_logging 1.0.0
davianspace_logging: ^1.0.0 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.
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):
- Provider type and category prefix
- Category prefix only
- Provider type only
- 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 —
isEnabledcheck exits before any object is created. - Single
LogEventallocation perlog()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; usescope.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 —
LoggingBuildercovers DI composition. - Providers register extensions on
LoggingBuilderrather than viaIServiceCollection.
License #
MIT © 2026 Davian Space