
x_logger
Configurable logging for Flutter: named loggers, levels, pretty / JSON / colored output, filtering, and async export of the log history to a file your users can share โ all configured once and reused across your app.
09:12:03.114 ๐ก [INFO] (QuranReader) ayah rendered {surah=2, ayah=255}
09:12:03.330 โ ๏ธ [WARN] (AudioPlayer) buffering {bufferMs=1200}
09:12:03.440 โ [ERROR] (AudioPlayer) playback stopped | error: Bad state
Contents
- Features
- Architecture
- Install
- Quick start
- Zero config
- Configure once, label with children
- Customizing the style ยท JSON ยท Color
- Filtering
- Outputs
- Writing to a file ยท Exporting / sharing
- Example app ยท Platform notes
Features
- Named instances โ one logger per part of your app (
QuranReader,AudioPlayer,Database, โฆ). - Levels โ
debug,info,warning,error. - Pluggable style (printers) โ
SimplePrinter,PrettyPrinter(boxed + emoji), orCallbackPrinterfor a fully bespoke format. - Color โ ANSI 256-color output with a per-level scheme; auto-enabled when the terminal supports it, off for files.
- Customizable header โ toggle timestamp / level / name / fields, choose a
timestamp format (
iso,clock,dateTime, or your own), UTC or local. - Filtering โ by level, name, or any predicate; combine with
allOf/anyOf; apply globally and/or per output. - Structured fields โ attach
{key: value}context to any call. - Multiple destinations โ
ConsoleOutput, asyncFileOutput,StreamOutput(for in-app log views),MemoryOutput(ring buffer),MultiOutput(fan-out). Each owns its own printer + filter. - Export API โ get the backing
Fileor read the whole history as a string.
Architecture
A record flows through three independent, swappable stages:
log() โ LogFilter (global) โ for each LogOutput: LogFilter (local) โ LogPrinter โ write
LogFilterdecides whether to log.LogPrinterdecides how it looks (style, color).LogOutputdecides where it goes.
Install
dependencies:
x_logger: ^0.2.0
flutter pub get
import 'package:x_logger/x_logger.dart';
Quick start
final log = XLogger(name: 'QuranReader');
log.debug('Opening surah');
log.info('Ayah rendered', fields: {'surah': 2, 'ayah': 255});
log.warning('Slow font load');
log.error('Render failed', error: e, stackTrace: s);
2026-05-24T09:12:03.114 [INFO] (QuranReader) Ayah rendered {surah=2, ayah=255}
Zero config (no customization)
Don't want to configure anything? XLogger.standard() wires up a colorized
console and an on-device file in one line, so export works immediately:
final log = XLogger.standard(name: 'QuranReader');
log.info('ready');
log.error('failed', error: e, stackTrace: s);
// Already exportable โ the file lives in the app documents directory.
final file = await log.getLogFile();
final text = await log.exportLogsAsString();
XLogger.standard accepts optional name, fileName, and minLevel; that's
all you need to touch.
Recommended: configure once, label with children
Set the logger up one time at app startup, then create cheap child()
loggers wherever you want to tag a subsystem. Children share the parent's
config, filter, and outputs โ the same single file, console, and buffers. They
cost almost nothing (just a name), so this does not duplicate I/O or memory.
// lib/logging.dart โ created once, reused everywhere.
final appLog = XLogger(
name: 'WACYI',
outputs: [ConsoleOutput(), FileOutput(fileName: 'app.log')],
);
// Anywhere in the app โ these reuse appLog's file & console:
final quranLog = appLog.child('QuranReader');
final audioLog = appLog.child('AudioPlayer');
quranLog.info('ayah rendered'); // (QuranReader) ...
audioLog.warning('buffering'); // (AudioPlayer) ... โ same app.log
The label after child(...) shows up in (parentheses) so you can tell which
part of the app wrote each line โ handy when you read the exported file.
Create the logger once (a top-level variable, a DI singleton, etc.) and reuse it. The only thing to avoid is constructing a new
XLogger/FileOutputrepeatedly (e.g. insidebuild()), which would open redundant file handles.
If you don't care about per-subsystem labels at all, just use one logger
everywhere and skip child() entirely.
Customizing the style
LoggerConfig drives the built-in printers:
final log = XLogger(
name: 'Database',
config: const LoggerConfig(
showTimestamp: true,
showLevel: true,
showName: false, // hide "(Database)"
showFields: true,
showEmoji: true, // prepend ๐ ๐ก โ ๏ธ โ
useUtc: true,
colorize: true,
timeFormatter: TimeFormats.clock, // 09:12:03.114
),
);
With showEmoji, each level is tagged with its glyph:
09:12:03.114 ๐ [DEBUG] (Database) cache warmed
09:12:03.221 ๐ก [INFO] (Database) row inserted
09:12:03.330 โ ๏ธ [WARN] (Database) slow query
09:12:03.440 โ [ERROR] (Database) connection lost
Use the boxed PrettyPrinter on a watched console:
final log = XLogger(
name: 'AudioPlayer',
outputs: [ConsoleOutput(printer: const PrettyPrinter())],
);
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ ๏ธ WARN (AudioPlayer) 09:12:03.114
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Buffering
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Or take full control of the format with CallbackPrinter:
ConsoleOutput(
printer: CallbackPrinter(
(e) => ['#${e.sequence} ${e.level.label.padRight(5)} ${e.message}'],
),
);
JSON output (pretty-printed)
Use JsonPrinter to render records as JSON โ great for structured logs and for
inspecting payloads. It pretty-prints by default; pass pretty: false for a
single compact line. Values that aren't JSON-encodable fall back to toString(),
so it never throws.
final log = XLogger(
name: 'QuranReader',
outputs: [ConsoleOutput(printer: const JsonPrinter())],
);
log.info('search request', fields: {
'query': 'mercy',
'filters': {'surah': [1, 2, 18], 'exact': false},
});
{
"time": "2026-05-24T09:12:03.114",
"level": "INFO",
"logger": "QuranReader",
"seq": 0,
"message": "search request",
"fields": {
"query": "mercy",
"filters": {
"surah": [1, 2, 18],
"exact": false
}
}
}
Custom colors
const LoggerConfig(
colorize: true,
levelColors: {
LogLevel.debug: AnsiColor.gray,
LogLevel.info: AnsiColor.cyan,
LogLevel.warning: AnsiColor.orange,
LogLevel.error: AnsiColor.magenta,
},
);
Filtering
// Global: only warnings and above.
XLogger(name: 'A', filter: const LevelFilter(LogLevel.warning));
// By predicate (inspect level, name, message, or fields).
PredicateFilter((e) => e.fields['important'] == true);
// By logger name.
const NameFilter(allow: {'QuranReader', 'AudioPlayer'});
// Combine.
LogFilter.allOf([
const LevelFilter(LogLevel.info),
NotFilter(const NameFilter(deny: {'Noisy'})),
]);
Filters also attach per output, so each sink can capture a different slice:
XLogger(
name: 'QuranReader',
filter: const LevelFilter(LogLevel.debug), // file gets everything
outputs: [
FileOutput(fileName: 'app.log'),
ConsoleOutput(filter: const LevelFilter(LogLevel.warning)), // console: warns+
],
);
Outputs
XLogger(
name: 'QuranReader',
outputs: [
ConsoleOutput(), // colorized; errors โ stderr
FileOutput(fileName: 'app.log'), // async, non-blocking
StreamOutput(), // for an in-app log view
MemoryOutput(capacity: 500), // recent-lines ring buffer
],
);
Subscribe to a StreamOutput to render logs live in the UI:
log.stream?.listen((entry) {
setState(() => visibleLines.addAll(entry.lines));
});
Writing to a file
By default FileOutput writes into the app's documents directory โ resolved
automatically via path_provider on Android, iOS, macOS, Windows and Linux, no
setup required:
FileOutput(fileName: 'agent.log'); // app documents dir, all platforms
Override the location when you need to โ a custom directory or an explicit path:
FileOutput(
fileName: 'agent.log',
directoryResolver: () async =>
(await getApplicationSupportDirectory()).path,
);
FileOutput(filePath: '/some/writable/dir/agent.log');
Writes are buffered into an async IOSink, so logging never blocks the UI
thread. Set flushEveryWrite: true for crash-safety at the cost of more I/O.
Exporting / sharing the log
await log.flush(); // ensure buffered writes landed
final file = await log.getLogFile(); // the on-device log file
final history = await log.exportLogsAsString();
final recent = log.memoryDump; // from a MemoryOutput, if attached
await log.dispose(); // flush + close all outputs
To let the user share the file with anyone, hand its path to a share plugin:
final file = await log.getLogFile();
if (file != null) {
await Share.shareXFiles([XFile(file.path)]); // package:share_plus
}
Example app
A runnable Flutter app lives in example/. It has a button for
every level (๐ Debug, ๐ก Info, โ ๏ธ Warning, โ Error) plus a JSON-payload
button, a Text โ Pretty JSON toggle that re-renders the captured records
live, and an Export & share button that writes the log file and opens the
native share sheet โ so you can verify export on every platform.
cd example
flutter pub get
flutter run -d android # Android device/emulator
flutter run -d ios # iOS device/simulator
flutter run -d macos # macOS desktop
flutter run -d windows # Windows desktop
flutter run -d linux # Linux desktop
Platform folders for all five targets are checked in. Export behavior:
| Platform | Export to file | Native share sheet |
|---|---|---|
| Android | โ | โ |
| iOS | โ | โ |
| macOS | โ | โ |
| Windows | โ | โ |
| Linux | โ | falls back to showing the file path |
On Linux, share_plus does not implement file sharing, so the example reports
the on-device file path instead โ the export itself still succeeds.
Platform notes
- This is a Flutter package. File / console output use
dart:io, so they run on iOS, Android, and desktop โ not Flutter web (useConsoleOutputonly, orStreamOutput/MemoryOutputthere). FileOutputwrites on-device; the user exports/shares the file from the app. In unit tests (no platform plugin), prefer an explicitfilePath.
License
See LICENSE.
Libraries
- x_logger
- x_logger โ a configurable logger for tracking application workflows with customizable styles, color, filtering, and asynchronous export of log history to local files.