davianspace_logging 1.0.4
davianspace_logging: ^1.0.4 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 |
| Tag-based logging | Lightweight secondary classification via LoggerTagExtension |
| Exception helper | Structured logException with automatic errorType/errorMessage properties |
| HTTP interceptor | Framework-agnostic HttpLogInterceptor for any HTTP client |
| Memory export | MemoryLogStore.exportAsJson() and eventsForTag() for testing and auditing |
| Quick setup | DavianLogger.quick() returns a console logger with zero configuration |
| No dependencies | Pure Dart; works on all platforms (CLI, Flutter, server) |
Quick start #
Option A One-liner with DavianLogger.quick() #
The fastest way to get a working logger without any setup:
import 'package:davianspace_logging/davianspace_logging.dart';
void main() {
final logger = DavianLogger.quick(); // console-backed, debug+
logger.info('Application started');
logger.warning('Config file not found');
logger.error('Unexpected failure', error: exception, stackTrace: st);
DavianLogger.disposeQuickFactory(); // release resources on exit
}
Option B Full LoggingBuilder setup #
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 from least to most severe; none suppresses all output:
| Level | Label | Typical use |
|---|---|---|
trace |
TRCE |
Step-by-step internal flow; disabled in production by default |
debug |
DBUG |
Developer diagnostics; useful during development |
info |
INFO |
Normal application milestones (startup, requests, etc.) |
warning |
WARN |
Recoverable, unexpected situations that deserve attention |
error |
EROR |
Failures preventing the current operation from completing |
critical |
CRIT |
Unrecoverable failures requiring immediate intervention |
none |
Sentinel; disables all output for the target scope |
logger.isEnabled(LogLevel.debug); // true / false depending on config
// isAtLeast helper on the enum itself:
LogLevel.warning.isAtLeast(LogLevel.info); // true
LogLevel.debug.isAtLeast(LogLevel.info); // false
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',
},
);
Avoid string interpolation of domain objects inside the message itself they
belong in properties so downstream consumers (log aggregators, alerting
systems) can query and index them.
Quick setup DavianLogger #
DavianLogger is a static helper for scenarios where a full LoggingBuilder
pipeline is unnecessary. All three entry points share a single internal
ConsoleLoggerProvider-backed factory that is rebuilt automatically if the
minimum level changes.
// Single logger with default category ('App') and level (debug+)
final log = DavianLogger.quick();
// Named category + custom minimum level
final serviceLog = DavianLogger.quick(
category: 'OrderService',
minimumLevel: LogLevel.warning,
);
// Multiple loggers from the shared factory in one step
final factory = DavianLogger.quickFactory(minimumLevel: LogLevel.info);
final authLog = factory.createLogger('Auth');
final dbLog = factory.createLogger('Database');
// Deterministic cleanup (recommended in CLI / server apps at shutdown)
DavianLogger.disposeQuickFactory();
After disposeQuickFactory() the next call to quick() or quickFactory()
automatically rebuilds the factory.
Tag-based logging LoggerTagExtension #
Tags provide a lightweight secondary classification for log entries, orthogonal
to category and level. Tags are stored under the reserved property key 'tag'
and are preserved by every provider.
// Emit at a specific level with a tag
logger.logTagged(LogLevel.info, 'auth', 'User signed in',
properties: {'userId': 42});
// Per-level convenience variants
logger.traceTagged('lifecycle', 'Widget built');
logger.debugTagged('cache', 'Cache miss for key $key');
logger.infoTagged('auth', 'Token refreshed');
logger.warningTagged('payment', 'Retry attempt 1');
logger.errorTagged('payment', 'Charge failed', error: ex, stackTrace: st);
logger.criticalTagged('database', 'Connection pool exhausted');
Query tagged events when using MemoryLogStore:
final authEvents = store.eventsForTag('auth');
The tag key is accessible as LoggerTagExtension.tagKey ('tag') for
consumers that inspect LogEvent.properties directly.
Exception helper LoggerExceptionExtension #
logException reduces the boilerplate of logging caught exceptions by
automatically deriving the message and attaching standard errorType /
errorMessage properties:
try {
await fetchOrder(id);
} catch (e, st) {
logger.logException(e, st,
message: 'Failed to fetch order',
properties: {'orderId': id});
}
Properties always present on every logException call:
| Property | Value |
|---|---|
errorType |
error.runtimeType.toString() |
errorMessage |
error.toString() |
When message is omitted the message is derived automatically:
- If
error.toString()already starts with the type name (e.g.TypeError: ...), it is used as-is. - Otherwise the message becomes
"TypeName: error.toString()".
Additional properties are merged on top and may override the built-in keys.
// Minimal message auto-derived from the error
logger.logException(e, st);
// Custom level
logger.logException(e, st, level: LogLevel.critical);
// Full form
logger.logException(
e, st,
level: LogLevel.error,
message: 'Order processing failed',
properties: {'orderId': 1042, 'stage': 'payment'},
);
HTTP interceptor HttpLogInterceptor #
HttpLogInterceptor wraps a Logger and exposes three callbacks onRequest,
onResponse, and onError that can be wired into any HTTP client interceptor
without a direct dependency on dio, http, or any other network package.
final interceptor = HttpLogInterceptor(
logger,
requestLevel: LogLevel.debug, // default
responseLevel: LogLevel.debug, // default
errorLevel: LogLevel.error, // default
logHeaders: false, // set true to include headers (avoid in prod)
logBody: false, // set true to include bodies (use with caution)
);
// Wire into your HTTP client's lifecycle:
interceptor.onRequest('GET', 'https://api.example.com/orders/1');
interceptor.onResponse(200, 'https://api.example.com/orders/1', durationMs: 42);
interceptor.onError('GET', 'https://api.example.com/orders/1', error, st);
Properties stored per call:
| Method | Property keys |
|---|---|
onRequest |
http.method, http.url, http.request.headers, http.request.body |
onResponse |
http.status, http.url, http.durationMs, http.response.headers, http.response.body |
onError |
http.method, http.url, http.status |
* Only when logHeaders / logBody is true and the value is non-null.
Optional; omitted when not provided.
Dio integration #
import 'package:dio/dio.dart';
import 'package:davianspace_logging/davianspace_logging.dart';
class DioLoggingInterceptor extends Interceptor {
DioLoggingInterceptor(Logger logger)
: _http = HttpLogInterceptor(logger);
final HttpLogInterceptor _http;
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
_http.onRequest(options.method, options.uri.toString(),
headers: Map<String, Object?>.from(options.headers));
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
_http.onResponse(
response.statusCode ?? 0, response.realUri.toString());
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
_http.onError(
err.requestOptions.method, err.requestOptions.uri.toString(),
err, err.stackTrace ?? StackTrace.current);
handler.next(err);
}
}
package:http integration #
import 'package:http/http.dart' as http;
import 'package:davianspace_logging/davianspace_logging.dart';
class LoggingClient extends http.BaseClient {
LoggingClient(Logger logger, http.Client inner)
: _http = HttpLogInterceptor(logger), _inner = inner;
final HttpLogInterceptor _http;
final http.Client _inner;
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
_http.onRequest(request.method, request.url.toString());
try {
final response = await _inner.send(request);
_http.onResponse(response.statusCode, request.url.toString());
return response;
} catch (e, st) {
_http.onError(request.method, request.url.toString(), e, st);
rethrow;
}
}
}
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)
categoryPrefix performs a prefix match 'network' silences both
network.http and network.grpc.
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. Synchronous code is covered too:
scope.run(() {
logger.debug('Synchronous work');
});
Formatters #
Simple (default) #
2026-02-25T14:23:01.123456Z [INFO ] OrderService Order placed {orderId: 1042}
Disable the timestamp for cleaner test output:
.addConsole(formatter: const SimpleFormatter(includeTimestamp: false))
JSON #
{"timestamp":"2026-02-25T14:23:01.123456Z","level":"info","category":"OrderService","message":"Order placed","properties":{"orderId":1042}}
Register on any provider:
LoggingBuilder()
.addConsole(formatter: const JsonFormatter())
.build();
Custom formatter #
Implement LogFormatter to produce any output format:
final class MyFormatter implements LogFormatter {
@override
String format(LogEvent event) =>
'[${event.level.label}] ${event.category}: ${event.message}';
}
Providers #
Console (ConsoleLoggerProvider) #
Writes ANSI-colored output to stdout via dart:io. Available on all platforms
except the web.
LoggingBuilder()
.addConsole()
.addConsole(formatter: const JsonFormatter()) // custom formatter
.build();
Debug (DebugLoggerProvider) #
Writes to dart:developer's log(), visible in Flutter/Dart DevTools.
Works on all platforms including the web.
LoggingBuilder()
.addDebug()
.build();
Memory (MemoryLoggerProvider + MemoryLogStore) #
Stores events in memory; primarily intended for unit and integration tests.
final store = MemoryLogStore(); // unbounded
final store = MemoryLogStore(maxCapacity: 500); // ring-buffer, evicts oldest
Query methods on MemoryLogStore:
store.events; // all events (unmodifiable list)
store.length; // total event count
store.isEmpty; // true when no events
store.isNotEmpty; // true when at least one event
store.eventsAtOrAbove(LogLevel.warning); // warning + error + critical
store.eventsForCategory('OrderService'); // events from one category
store.eventsForTag('auth'); // events with properties['tag'] == 'auth'
store.exportAsJson(); // JSON string of all events
store.clear(); // wipe the store
exportAsJson format
[
{
"timestamp": "2026-02-25T14:23:01.123456Z",
"level": "info",
"category": "OrderService",
"message": "Order placed",
"properties": { "orderId": 1042 },
"scopeProperties": { "requestId": "req-A" },
"error": "Exception: Card declined",
"stackTrace": "..."
}
]
properties, scopeProperties, error, and stackTrace are omitted when
absent/empty. Returns '[]' when the store is empty.
Null (NullLoggerProvider / NullLogger) #
Discards every entry. NullLogger is useful as a no-op default for optional
logger dependencies.
LoggingBuilder().addProvider(NullLoggerProvider()).build();
// Use NullLogger directly for optional dependencies:
class MyService {
MyService({Logger? logger}) : _logger = logger ?? NullLogger();
final Logger _logger;
}
Custom providers #
final class SyslogProvider implements LoggerProvider {
@override
Logger createLogger(String category) =>
SyslogLogger(category: category);
@override
void dispose() { /* close socket */ }
}
LoggingBuilder().addProvider(SyslogProvider()).build();
Implement EventLogger (extends Logger) to receive a fully assembled
LogEvent including scope-merged properties, avoiding double-allocation:
final class SyslogLogger implements EventLogger {
SyslogLogger({required this.category});
@override
final String category;
@override
bool isEnabled(LogLevel level) => level != LogLevel.none;
@override
void write(LogEvent event) => syslog(event.level.label, event.message);
@override
LoggingScope beginScope(Map<String, Object?> properties) =>
LoggingScope(properties);
}
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();
final logger = factory.createLogger('OrderService');
logger.info('Order placed', properties: {'orderId': 1042});
final event = store.events.single;
expect(event.level, equals(LogLevel.info));
expect(event.message, equals('Order placed'));
expect(event.properties['orderId'], equals(1042));
factory.dispose();
});
test('logException captures errorType and errorMessage', () {
final store = MemoryLogStore();
final factory = LoggingBuilder()
.addMemory(store: store)
.setMinimumLevel(LogLevel.trace)
.build();
final logger = factory.createLogger('Service');
final err = FormatException('bad input');
logger.logException(err, StackTrace.current,
message: 'Validation failed',
properties: {'field': 'email'});
final event = store.events.single;
expect(event.properties['errorType'], equals('FormatException'));
expect(event.properties['errorMessage'], equals('bad input'));
expect(event.properties['field'], equals('email'));
factory.dispose();
});
test('eventsForTag returns tagged events only', () {
final store = MemoryLogStore();
final factory = LoggingBuilder()
.addMemory(store: store)
.setMinimumLevel(LogLevel.trace)
.build();
final logger = factory.createLogger('App');
logger.infoTagged('auth', 'Login');
logger.info('Untagged event');
expect(store.eventsForTag('auth'), hasLength(1));
factory.dispose();
});
Performance #
- Zero allocations when a log level is disabled
isEnabledexits 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.
- O(1) eviction in
MemoryLogStoreusesListQueuefor front removal.
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 |
AddDebug() |
.addDebug() on LoggingBuilder |
| Structured logging templates | Plain message + properties map |
IDisposable |
dispose() on factory and providers |
BeginScope chaining |
Nested beginScope calls (child overrides parent keys) |
Key differences #
- Dart has no
IDisposable/using; usescope.run(...)/scope.runAsync(...)for scope lifetime management. - Structured properties use a
Map<String, Object?>instead of C# message templates. - No DI container required
LoggingBuildercovers provider registration and configuration. - Providers register
LoggingBuilderextensions in their own source file rather than viaIServiceCollection. DavianLogger.quick()provides a zero-config console logger equivalent tohost.Services.GetRequiredService<ILogger<T>>().
License #
MIT 2026 Davian Space