flutter_mxlogger
MXLogger MMAP मेमोरी मैपिंग तकनीक पर आधारित है, iOS, Android और Flutter का समर्थन करता है।
MXLogger एक क्रॉस-प्लेटफ़ॉर्म लॉगिंग लाइब्रेरी है जो mmap मेमोरी मैपिंग तकनीक पर आधारित है, AES CFB 128 बिट एन्क्रिप्शन समर्थन करती है, और iOS, Android और Flutter के लिए समर्थित है। मुख्य कोड C/C++ में लिखा गया है, और Flutter भाग में FFI के माध्यम से कॉल किया जाता है, जिसकी कार्यक्षमता लगभग नेटिव के समान होती है। नीचे की सीरियलाइजेशन Google के ओपन-सोर्स flat_buffers के उपयोग से की जाती है, जो उच्च दक्षता और स्थिरता प्रदान करती है।
यह अंग्रेज़ी मूल स्नैपशॉट है। नवीनतम सामग्री GitHub पर देखें।
iOS Android Flutter Analyzer desktop
license CocoaPods Maven Central pub pub analyzer
MXLogger
MXLogger is a cross-platform logging library built on mmap memory mapping, with AES-CFB-128
encryption. The core is written in C/C++ and serializes records with Google FlatBuffers; the
Flutter side calls straight into it through dart:ffi, so performance is essentially identical to
native. Writing 100,000 records (~134 bytes each) takes about 0.13s on an iPhone 11.
Along with the writers comes mxlogger_analyzer, an analyzer that imports, decrypts and searches
the produced .mx binaries — as a desktop app (macOS/Windows/Linux) or embedded straight into your
iOS/Android Flutter app as a floating ball.
中文说明请点击这里。
MXLogger solves log writing and log analysis. When to upload logs is a business decision —
MXLogger exposes the log directory (diskCachePath), and you upload the files from the native
platform to your own server, then analyze them with mxlogger_analyzer.
.mx files are binary (AES-CFB-128 + flatbuffer), so they need a reader. The analyzer imports them
into sqlite, which backs full-text search, level/time/Tag/Name filtering and a JSON syntax tree view.
| Mode | What it is | How to get it |
|---|---|---|
| Desktop app | macOS/Windows/Linux shell — drop or pick a log file | Releases (built by CI for all three platforms) |
| Embedded in your app | floating ball + bottom sheet inside an iOS/Android Flutter app | mxlogger_analyzer_lib from pub.dev |
Data page — level distribution bar plus level chips, search and time filters, a syntax-colored
JSON tree inside every log card, click-to-filter @name / #tag, and per-record info / share /
fullscreen / copy. The top-right toggle switches to the light theme.
Three-step first-run wizard — ① drop or pick .mx files → ② configure the decryption KEY/IV
(several pairs allowed; the checkbox numbers are the order they are tried in) → ③ import with real
progress.
Fullscreen detail of a single record (Esc closes) — Name / Tags / time / level plus the complete JSON tree, shareable or copyable as a whole.
Fullscreen log detailEmbedded in a host app (phone) — a bottom sheet covering 85% of the screen: tighter padding, icons instead of labels, horizontally scrolling level chips, cards collapsed by default and actions folded into the "⋯" menu.
Embedded bottom sheet
Full documentation for the analyzer: mxlogger_analyzer_lib/README.md.
pod 'MXLogger', '~> 2.0.0'
implementation 'io.github.coder-dongjiayi:mxlogger:latest.release'
dependencies:
flutter_mxlogger: ^2.0.0
mxlogger_analyzer_lib: ^2.0.0 # optional: the in-app log viewer
The native dependencies are pulled in automatically — CocoaPods depends on MXLogger →
MXLoggerCore, Gradle on io.github.coder-dongjiayi:mxlogger.
MXLogger *logger = [MXLogger initializeWithNamespace:@"com.yourdomain.logger"
storagePolicy:MXStoragePolicyYYYYMMDD
fileName:@"mxlog"
fileHeader:@"{\"app_version\":\"2.0.0\"}"
cryptKey:@"abcuioqbsdguijlk" // 16 bytes
iv:@"bccuioqbsdguijiv"];
logger.consoleEnable = YES; // debug only — set NO for benchmarks / release
logger.maxDiskAge = 60 * 60 * 24 * 7; // keep at most 7 days
logger.maxDiskSize = 1024 * 1024 * 10; // use at most 10 MB
logger.level = 0; // 0:debug — write everything to file
[logger debugWithName:@"login" msg:@"token check started" tag:@"login,service"];
[logger infoWithName:@"network" msg:responseJSON tag:@"network,POST,200"];
[logger warnWithName:@"network" msg:@"retry #2" tag:@"network"];
[logger errorWithName:@"flutter" msg:stack tag:@"crash"];
[logger fatalWithName:@"database" msg:@"connection lost" tag:@"db,fatal"];
NSLog(@"logs live in %@", logger.diskCachePath);
MXLogger logger = MXLogger.initialize(
context,
"com.yourdomain.logger", // nameSpace
null, // diskCacheDirectory — null = default
MXStoragePolicyType.YYYY_MM_DD,
"mxlog", // fileName
"{\"app_version\":\"2.0.0\"}", // fileHeader
"abcuioqbsdguijlk", // cryptKey, 16 bytes
"bccuioqbsdguijiv"); // iv
logger.setConsoleEnable(true);
logger.setMaxDiskAge(60 * 60 * 24 * 7);
logger.setMaxDiskSize(1024 * 1024 * 10);
logger.setLevel(0);
// Android takes tag first: debug(tag, name, msg)
logger.debug("login,service", "login", "token check started");
logger.info("network,POST,200", "network", responseJson);
logger.warn("network", "network", "retry #2");
logger.error("crash", "flutter", stack);
logger.fatal("db,fatal", "database", "connection lost");
Log.d("MXLogger", "logs live in " + logger.getDiskCachePath());
final MXLogger logger = await MXLogger.initialize(
nameSpace: "flutter.mxlogger",
storagePolicy: MXStoragePolicyType.yyyy_MM_dd,
fileHeader: jsonEncode(deviceInfo),
consoleEnable: true,
cryptKey: "abcuioqbsdguijlk", // 16 bytes
iv: "bccuioqbsdguijiv", // defaults to cryptKey when omitted
);
logger.setMaxDiskAge(60 * 60 * 24 * 7);
logger.setMaxDiskSize(1024 * 1024 * 10);
logger.setLevel(0);
logger.debug("token check started", name: "login", tag: "login,service");
logger.info(jsonEncode(response), name: "network", tag: "network,POST,200");
logger.warn("retry #2", name: "network", tag: "network");
logger.error(stack, name: "flutter", tag: "crash");
logger.fatal("connection lost", name: "database", tag: "db,fatal");
debugPrint("logs live in ${logger.diskcachePath}");
Common to all three platforms: a logger is identified by nameSpace + diskCacheDirectory, and the underlying C++ instance is deduplicated on that pair. The md5 of the pair is the loggerKey — pass it around a modularized app and write logs through the class methods without holding the logger object.
MXLogger (Objective-C)| API | Notes |
|---|---|
+ initializeWithNamespace: |
default directory Library/com.mxlog.LoggerCache |
+ initializeWithNamespace:fileHeader: |
with a file header |
+ initializeWithNamespace:cryptKey:iv:fileHeader: |
encrypted |
+ initializeWithNamespace:storagePolicy:fileName:fileHeader: |
policy + file name |
+ initializeWithNamespace:storagePolicy:fileName:fileHeader:cryptKey:iv: |
policy + encryption |
+ initializeWithNamespace:diskCacheDirectory:storagePolicy:fileName:fileHeader:cryptKey:iv: |
full parameters |
- initWithNamespace:fileHeader: |
instance initializers, same combinations |
- initWithNamespace:diskCacheDirectory:fileHeader: |
|
- initWithNamespace:cryptKey:iv:fileHeader: |
|
- initWithNamespace:storagePolicy:fileName:fileHeader: |
|
- initWithNamespace:diskCacheDirectory:storagePolicy:fileName:fileHeader:cryptKey:iv: |
designated initializer |
+ destroyWithNamespace: |
release by nameSpace (default directory) |
+ destroyWithNamespace:diskCacheDirectory: |
release by nameSpace + directory |
+ destroyWithLoggerKey: |
release by loggerKey |
+ valueForLoggerKey: |
fetch an existing logger, nil when none |
| Property | Type | Notes |
|---|---|---|
enable |
BOOL |
master switch for writing |
consoleEnable |
BOOL |
console output, off by default; stripped at compile time in Release/Profile (define MXLOGGER_CONSOLE_ENABLED=1 to keep it) |
level |
NSInteger |
minimum level written to disk, 0 debug … 4 fatal |
maxDiskAge |
NSUInteger |
max age in seconds, 0 = unlimited |
maxDiskSize |
NSUInteger |
max total size in bytes, 0 = unlimited |
shouldRemoveExpiredDataWhenEnterBackground |
BOOL |
auto cleanup on entering background, YES by default |
diskCachePath |
NSString (readonly) |
log directory |
logSize |
NSUInteger (readonly) |
bytes currently on disk |
loggerKey |
NSString (readonly) |
md5 of nameSpace + directory |
| API | Notes |
|---|---|
- logWithLevel:name:msg:tag: |
returns 0 on success, -1 expansion / -2 unmap / -3 mmap failed |
- debugWithName:msg:tag: |
level 0 |
- infoWithName:msg:tag: |
level 1 |
- warnWithName:msg:tag: |
level 2 |
- errorWithName:msg:tag: |
level 3 |
- fatalWithName:msg:tag: |
level 4 |
+ debugWithLoggerKey:name:msg:tag: |
write via loggerKey, no logger object needed |
+ infoWithLoggerKey:name:msg:tag: |
|
+ warnWithLoggerKey:name:msg:tag: |
|
+ errorWithLoggerKey:name:msg:tag: |
|
+ fatalWithLoggerKey:name:msg:tag: |
| API | Notes |
|---|---|
- logFiles |
array of dicts: name / size / last_timestamp / create_timestamp |
- errorDesc |
description of the last write failure |
- removeExpireData |
delete expired files, then the oldest ones while over maxDiskSize |
- removeAllData |
delete every log file |
- removeBeforeAllData |
delete every file except the one being written |
+ selectWithDiskCacheFilePath:cryptKey:iv: |
parse a file into entries (newest first): name / msg / tag / level / timestamp / thread_id / is_main_thread / error_code |
MXStoragePolicyTypeMXStoragePolicyYYYYMMDD (daily, default) · MXStoragePolicyYYYYMMDDHH (hourly) ·
MXStoragePolicyYYYYWW (weekly) · MXStoragePolicyYYYYMM (monthly)
com.dongjiayi.mxlogger.MXLogger (Java)| API | Notes |
|---|---|
MXLogger(Context, String nameSpace, String diskCacheDirectory, MXStoragePolicyType, String fileName, String fileHeader, String cryptKey, String iv) |
designated constructor |
MXLogger(Context, String nameSpace, String fileHeader) |
default directory |
MXLogger(Context, String nameSpace, String fileHeader, String cryptKey, String iv) |
encrypted |
MXLogger(Context, String fileHeader, String nameSpace, String diskCacheDirectory) |
custom directory |
static MXLogger initialize(Context, nameSpace, diskCacheDirectory, storagePolicy, fileName, fileHeader, cryptKey, iv) |
full parameters |
static MXLogger initialize(Context, nameSpace, fileHeader) |
|
static MXLogger initialize(Context, nameSpace, fileHeader, cryptKey, iv) |
|
static void destroy(Context, String nameSpace, String diskCacheDirectory) |
release by nameSpace + directory |
static void destroy(String loggerKey) |
release by loggerKey |
Configuration is never cached on the Java side — every getter queries native, the single source of truth (several Java wrappers can share one native instance).
| API | Notes |
|---|---|
void setEnable(boolean) / boolean isEnable() |
master switch for writing |
void setConsoleEnable(boolean) / boolean isConsoleEnable() |
console output |
void setLevel(int) / int getLevel() |
minimum level written to disk, 0 debug … 4 fatal |
void setMaxDiskAge(long) / long getMaxDiskAge() |
max age in seconds, 0 = unlimited |
void setMaxDiskSize(long) / long getMaxDiskSize() |
max total size in bytes, 0 = unlimited |
long getLogSize() |
bytes currently on disk |
String getDiskCachePath() |
log directory |
String getLoggerKey() |
md5 of nameSpace + directory |
String getErrorDesc() |
description of the last write failure |
Note the parameter order — tag comes first on Android.
| API | Notes |
|---|---|
int debug(String tag, String name, String msg) |
level 0 |
int info(String tag, String name, String msg) |
level 1 |
int warn(String tag, String name, String msg) |
level 2 |
int error(String tag, String name, String msg) |
level 3 |
int fatal(String tag, String name, String msg) |
level 4 |
int log(String tag, int level, String name, String msg) |
returns 0 on success, -1 / -2 / -3 on failure |
static int log(String loggerKey, String tag, int level, String name, String msg) |
write via loggerKey |
| API | Notes |
|---|---|
String[] logFiles() |
one JSON string per file: name / size / last_timestamp / create_timestamp |
void removeExpireData() |
delete expired files, then the oldest ones while over maxDiskSize. Android has no lifecycle hook — call it yourself (e.g. in onStop) |
void removeAll() |
delete every log file |
void removeBeforeAllData() |
delete every file except the one being written |
static String[] selectWithFilePath(String diskCacheFilePath, String cryptKey, String iv) |
parse a file, one JSON string per entry (newest first) |
MXStoragePolicyTypeYYYY_MM_DD (daily) · YYYY_MM_DD_HH (hourly) · YYYY_WW (weekly) · YYYY_MM (monthly)
flutter_mxloggerEverything read and written goes through dart:ffi; the MethodChannel is only used to resolve the
platform default directory during initialize.
| API | Notes |
|---|---|
static Future<MXLogger> initialize({required String nameSpace, String? directory, bool consoleEnable = false, MXStoragePolicyType storagePolicy = yyyy_MM_dd, String? fileName, String? fileHeader, String? cryptKey, String? iv}) |
recommended — resolves the platform default directory (iOS Library/com.mxlog.LoggerCache/nameSpace, Android files/com.mxlog.LoggerCache/nameSpace) |
MXLogger({required String nameSpace, required String directory, ...}) |
synchronous constructor, requires an explicit directory |
static void destroy({required String nameSpace, String? directory}) |
release; the matching Dart instances are invalidated first (no use-after-free) |
static void destroyWithLoggerKey(String loggerKey) |
release by loggerKey |
| Getter | Type | Notes |
|---|---|---|
enable |
bool |
whether writing is on |
consoleEnable |
bool |
whether console output is on |
diskcachePath |
String |
log directory (directory + nameSpace) |
diskcacheErrorPath |
String |
path of the local error file error.txt |
loggerKey |
String? |
md5 of nameSpace + directory |
logSize |
int |
bytes currently on disk |
logFiles |
List<MXFileEntity> |
stored log files |
errorDesc |
String? |
last native write error, null when none |
cryptKey / iv |
String? |
the encryption parameters passed at initialization — hand them to the analyzer |
| API | Notes |
|---|---|
void setEnable(bool enable) |
master switch for writing |
void setConsoleEnable(bool enable) |
console output; tree-shaken away in release/profile builds |
void setLevel(int lvl) |
minimum level written to disk, 0 debug … 4 fatal |
void setMaxDiskAge(int age) |
max age in seconds, 0 = unlimited |
void setMaxDiskSize(int size) |
max total size in bytes, 0 = unlimited |
void shouldRemoveExpiredDataWhenEnterBackground(bool should) |
auto cleanup on entering background, true by default |
void removeExpireData() |
delete expired files, then the oldest ones while over the size limit |
void removeAll() |
delete every log file |
void removeBeforeAllData() |
delete every file except the one being written |
int getLogSize() / String getDiskcachePath() / String? getLoggerKey() / List<MXFileEntity> getLogFiles() |
method forms of the getters above |
| API | Notes |
|---|---|
int debug(String msg, {String? name, String? tag}) |
level 0 |
int info(String msg, {String? name, String? tag}) |
level 1 |
int warn(String msg, {String? name, String? tag}) |
level 2 |
int error(String msg, {String? name, String? tag}) |
level 3 |
int fatal(String msg, {String? name, String? tag}) |
level 4 |
int log(int lvl, String msg, {String? name, String? tag}) |
returns 0 on success, -1 expansion / -2 unmap / -3 mmap failed |
static void logLoggerKey(String? loggerKey, int lvl, String msg, {String? name, String? tag}) |
write via loggerKey |
static void debugLog(String? loggerKey, String msg, {String? name, String? tag}) |
and infoLog / warnLog / errorLog / fatalLog, same shape |
| API | Notes |
|---|---|
void writeFail({required int code, required String errorDesc, String? other}) |
call it when log returns non-zero; appends a JSON line to diskcacheErrorPath |
void deleteFailFile() |
delete the local error file |
void closeFailFile() |
close the error file's write stream |
static List<Map<String, dynamic>> selectLogmsg({required String diskcacheFilePath, String? cryptKey, String? iv}) |
parse a file into entries (newest first): name / msg / tag / level / timestamp / thread_id / is_main_thread / error_code; error_code == "1" means that entry failed to decode (wrong cryptKey or iv) |
static List<Map<String, dynamic>> selectLogfiles({required String directory}) |
not implemented — returns an empty list on every platform; use logFiles |
MXFileEntityname (String?) · size (int, bytes) · createTimeStamp / lastTimeStamp (int, seconds) ·
createTime / lastTime (DateTime, derived)
MXStoragePolicyTypeyyyy_MM_dd (daily, default) · yyyy_MM_dd_HH (hourly) · yyyy_ww (weekly) · yyyy_MM (monthly)
mxlogger_analyzer_libMXAnalyzer| API | Notes |
|---|---|
static void initialize({String? databasePath, MXPrefs? prefs}) |
optional pre-configuration: sqlite directory (defaults to getApplicationSupportDirectory()) and a persistence implementation for theme / locale / crypt pairs (memory only when omitted) |
static Future<void> showDebug(OverlayState overlayState, {required String diskcachePath, required List<MxCryptPair> cryptPairs, required MXShareHandler onShare, String? databasePath}) |
show the floating ball (drag to move, tap to open the sheet, double tap to dismiss); calling again while it exists is ignored |
static void dismiss() |
remove the ball and release the database |
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// MaterialApp(navigatorKey: navigatorKey, ...)
await MXAnalyzer.showDebug(
navigatorKey.currentState!.overlay!,
diskcachePath: logger.diskcachePath,
// Pass an empty list for plain logs; several pairs are tried in order,
// per record — so a file whose writer rotated keys still comes out whole.
cryptPairs: [
MxCryptPair(key: logger.cryptKey ?? "", iv: logger.iv ?? ""),
// MxCryptPair(key: "legacy-key", iv: "legacy-iv"),
],
onShare: _shareWithSharePlus, // return false to degrade to the clipboard
);
MXAnalyzer.dismiss();
Opening the sheet parses nothing — the user taps "refresh" to scan the .mx files under
diskcachePath, and every refresh clears the database and re-parses from scratch. Results live in
sqlite, so reopening after an app restart still shows the previous run.
| Type | Notes |
|---|---|
MxCryptPair({required String key, String iv = ""}) |
one decryption pair; an empty iv falls back to key, matching native |
MXShareHandler = Future<bool> Function(MXShareRequest) |
share implementation injected by the host; false degrades to copying to the clipboard |
MXShareRequest |
title / text / fileName (non-null asks for a text-file share) / origin (the trigger's screen rect, needed by the iPad/macOS popover) |
MXHost({required MXPrefs prefs, MXPickLogFiles? pickLogFiles, MXDropTargetBuilder? dropTargetBuilder, MXShareHandler? share}) |
desktop-shell capability injection; a missing capability removes its entry point instead of throwing |
MXPrefs / MXMemoryPrefs |
settings persistence; the in-memory implementation is the default |
MXStore / MXScope / MXState / MXAsyncState |
the stream-based lightweight state container, for custom embeddings |
MXLoggerAnalyzerApp |
the whole analyzer app, for a custom desktop shell |
Storage policy decides how files are rolled over and how they are named:
| Policy | File name | |
|---|---|---|
| daily (default) | 2023-01-11_mxlog.mx |
one file per day |
| hourly | 2023-01-11-15_mxlog.mx |
one file per hour |
| weekly | 2023-01-02w_mxlog.mx |
02w = the 2nd week of the year |
| monthly | 2023-01_mxlog.mx |
one file per month |
Levels are 0:debug 1:info 2:warn 3:error 4:fatal. Setting the level to n means only logs of
level >= n reach the file — with level = 2, debug and info are dropped and only warn / error /
fatal are written. The level only gates disk writes: with console output on, the console still
prints everything.
Console output is meant for development. It costs write performance, so turn it off for benchmarks; in release builds it is compiled/tree-shaken away on all three platforms anyway.
Encryption uses AES-CFB-128 (the assembly implementation from
MMKV, which keeps writes fast). cryptKey
and iv are 16 bytes — longer is truncated, shorter is zero-padded — and iv defaults to cryptKey
when omitted. Keep them: the analyzer needs the same pair to read the file back.
File header (fileHeader) is written once when a file is created. Put the environment there
(app version, platform, device info); the analyzer expands it into a scalar grid plus a JSON tree.
Write benchmark, iOS only (the demo project is here):
| MXLogger | Xlog | Logan | |
|---|---|---|---|
| Time | ~0.13s | ~0.57s | ~14.0s |
| File size | 14008320 B (14M) | 905753 B (0.9M) | 922452 B (0.9M) |
Logan and Xlog compress their data, so their files are much smaller. Log compression is on the MXLogger roadmap.
Library/Caches on
iOS. MXLogger creates the directory at startup only, not on every write — if the system deletes it
while the app is running, nothing crashes and nothing is reported, but nothing gets logged either.nameSpace to keep it unique.maxDiskAge / maxDiskSize are not enforced on every write: the cleanup runs in
removeExpireData (automatic on entering background on iOS and Flutter; call it yourself on
Android).MXLogger is released under the BSD 3-Clause license. See LICENSE.TXT.