mxlogger_analyzer_lib 2.0.2
mxlogger_analyzer_lib: ^2.0.2 copied to clipboard
MXLogger 2.0 log analyzer. Import, decrypt, parse and search .mx binary log files; embeddable in-app debugger.
English | 简体中文
mxlogger_analyzer_lib #
The MXLogger 2.0 log analyzer core: import, decrypt (AES-CFB-128), parse and search the .mx
mmap binary logs written by MXLogger.
Records land in sqlite, which backs full-text search, level/time/Tag/Name filtering and a JSON
syntax tree view.
Two ways to use it, one main in this repository for each:
| Mode | Entry point | What it is |
|---|---|---|
| Standalone app | lib/main_desktop.dart |
macOS/Windows/Linux desktop shell — drop or pick a log file to analyze |
| Embedded in a host app | lib/main_package.dart |
Inspect on-device logs inside an iOS/Android app via a floating ball + bottom sheet |
Screens #
Data page: level distribution bar + level chips, search and time filters, syntax-colored JSON
tree inside each log card, click-to-filter @name / #tag, and per-record info / context /
share / fullscreen / copy. The top-right toggle switches to the light theme (both token sets are fully
aligned).
Context panel: anchored on one record, it shows the 20 records before and after it in the whole log (current filters are ignored), with load-more at both ends, the anchor outlined in the accent color and the order following the list's sort direction — so after filtering down to a pile of ERRORs you can see what happened around one without clearing the filters.

Three-step first-run wizard: ① drop or pick .mx files → ② configure the decryption KEY/IV
(multiple pairs allowed) → ③ import with real progress.

Fullscreen detail of a single record (Esc closes): Name / Tags / time / type plus the complete JSON tree, shareable or copyable as a whole. An in-record search box highlights every match, shows "N / M", jumps between matches with Enter / Shift+Enter (scrolling to the current one) and auto-expands collapsed JSON nodes that contain it.

Embedded 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.

Standalone app: the desktop shell entry #
The core does not depend on shared_preferences / file_picker / desktop_drop / share_plus directly.
The shell implements MXHost (settings persistence / file picking / file dropping / system share)
with those plugins and injects it, which keeps the entry point thin — the whole of
lib/main_desktop.dart:
import 'package:flutter/material.dart';
import 'package:mxlogger_analyzer_lib/mxlogger_analyzer_lib.dart';
import 'package:mxlogger_analyzer/src/host/desktop_host.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// The desktop shell injects platform capabilities (shared_preferences /
// file_picker / desktop_drop); the core itself depends on none of them.
final MXHost host = await createDesktopHost();
runApp(MXScope(
store: MXStore(host: host),
child: const MXLoggerAnalyzerApp(),
));
}
The MXHost returned by createDesktopHost() (see
lib/src/host/desktop_host.dart) carries four capabilities.
Leaving one out removes the matching entry point instead of throwing:
| Capability | Desktop shell implementation | When absent |
|---|---|---|
prefs |
shared_preferences | falls back to MXMemoryPrefs, settings live in memory only |
pickLogFiles |
file_picker | the "pick a file" entry is not shown |
dropTargetBuilder |
desktop_drop | pages render as-is, no drop target wrapper |
share |
share_plus | "share" degrades to copying to the clipboard |
Run it:
flutter run -t lib/main_desktop.dart -d macos # or windows / linux
Embedded in a host app: the on-device entry #
lib/main_package.dart closes the loop on a real device:
flutter_mxlogger writes genuine encrypted .mx files locally, then the floating ball opens the
analyzer against that same directory.
flutter run -t lib/main_package.dart -d <iOS/Android device>
1. The host writes logs first #
fileHeader carries the device environment (device_info_plus); the analyzer's Header dialog
expands it into a compact scalar grid plus a JSON tree:
final MXLogger logger = await MXLogger.initialize(
nameSpace: "flutter.mxlogger",
storagePolicy: MXStoragePolicyType.yyyy_MM_dd,
fileHeader: jsonEncode(header), // writer-side environment info
consoleEnable: true,
cryptKey: "bnijioijuojiuoju", // 16 bytes
iv: "njkoiuhjbjuiasdh",
);
logger.debug("token check started", name: "login", tag: "login,service");
logger.info(jsonEncode(response), name: "network", tag: "network,POST,200");
logger.error(flutterErrorStack, name: "flutter", tag: "flutter,crash");
logger.fatal("database connection lost", name: "database", tag: "db,fatal");
2. The floating ball opens the analyzer #
MXAnalyzer mounts on the host app's Overlay (drag to move, single tap to open the sheet,
double tap to dismiss) and stays visible across route pushes:
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// MaterialApp(navigatorKey: navigatorKey, ...)
await MXAnalyzer.showDebug(
navigatorKey.currentState!.overlay!,
diskcachePath: logger.diskcachePath, // MXLogger's log directory
// The host passes every decryption pair (empty list when logs are plain);
// multiple pairs are tried in the given order.
cryptPairs: [
MxCryptPair(key: logger.cryptKey ?? "", iv: logger.iv ?? ""),
// MxCryptPair(key: "legacy-key", iv: "legacy-iv"),
],
onShare: _shareWithSharePlus,
);
MXAnalyzer.dismiss(); // removes the ball and releases the database
3. Sharing is the host's job (with a fallback) #
The core depends on no share plugin: the host wires up share_plus itself and injects it through
onShare. Returning false (unavailable in the current environment) makes the core degrade to
copying to the clipboard. A non-null MXShareRequest.fileName asks for a text-file share
(exporting logs); origin is the screen rect of the triggering widget, which the popover-style
share sheet on iPad/macOS needs:
Future<bool> _shareWithSharePlus(MXShareRequest request) async {
final ShareResult result;
if (request.fileName != null) {
result = await SharePlus.instance.share(ShareParams(
title: request.title,
files: [
XFile.fromData(
Uint8List.fromList(utf8.encode(request.text)),
name: request.fileName,
mimeType: "text/plain",
),
],
fileNameOverrides: [request.fileName!],
sharePositionOrigin: request.origin,
));
} else {
result = await SharePlus.instance.share(ShareParams(
title: request.title,
text: request.text,
sharePositionOrigin: request.origin,
));
}
return result.status != ShareResultStatus.unavailable;
}
4. Optional pre-configuration #
MXAnalyzer.initialize(
databasePath: dir.path, // sqlite directory, defaults to getApplicationSupportDirectory()
prefs: myPrefs, // implement MXPrefs to persist theme/locale/user-added crypt pairs
);
How the embedded mode behaves #
- The sheet hosts a nested MaterialApp with its own Navigator, theme and zh/en localizations.
The host is not required to provide a state container or
AppLocalizations— only aMaterialApp(for the Overlay). - Pairs passed via
cryptPairsare moved to the top of the settings table and checked (an already-present identical pair is only checked). Decryption is attempted per record in the order shown by the checkbox numbers, moving on when a pair fails — so records encrypted with different Key/IV inside one file (the writer rotated keys) all come out. - Opening the sheet parses nothing. Parsing only starts when the user taps "refresh" (parsing is slow for large logs and must not block opening the sheet). Whatever the database already holds is shown right away; with nothing stored you get an empty page plus a "refresh logs" button. The refresh entry sits at the top-right of the header (replacing the desktop "change file").
- Results are persisted in sqlite, so reopening the sheet after an app restart still shows the previous run — tap refresh for the latest logs.
- Every refresh clears the database and re-parses the
.mxfiles underdiskcachePathfrom scratch; results are never merged with the previous run, so what you see is exactly this scan. - Deliberately few dependencies: the embedded mode pulls in no KV-storage or file plugins such
as shared_preferences / file_picker / desktop_drop — Key/IV arrive with each
showDebugcall and settings default to memory only (they survive closing and reopening the ball, and reset when the process dies). File picking / dropping / persisted settings are host capabilities (MXHost), provided by the desktop shell.
Phone layout #
One UI serves both desktop and phone, switching on available width (not Platform) at two
breakpoints defined in lib/src/global/util/mx_responsive.dart
(context.isMobileLayout / isNarrowLayout):
- ≤720: side padding 20→14; the brand subtitle and toolbar button labels are hidden (icons only); level chips scroll horizontally on a single line up to the screen edge; the search field takes a full row; Key/IV and time-range inputs stack full-width; action buttons grow (30→38 / 28→36 / floating 36→44); padding around the log area is squeezed to a minimum (list sides 2, card left 6, fold button 16, element gap 5 — on a narrow screen every margin eats body width); log cards are collapsed by default (a single preview line) with the row actions folded into "⋯" that opens a bottom panel; the log detail, context and Header dialogs go fullscreen (no rounded corners, footer buttons split evenly); input font size is unified at 16.
- ≤480: the Header scalar grid becomes a single column.
Timestamps always show the full y-M-d H:m:s.SSS. The design's rule of hiding the date on narrow
screens was not adopted — hours/minutes/seconds alone are not enough when hunting a bug; the width
comes from folding the actions into "⋯" instead.
The filter area (distribution bar + level chips, search field, time range/fold buttons, time
panel) takes up nearly half a phone screen, so a floating button in the bottom-left corner
collapses and expands it manually (the brand row and top-right actions always stay).
Collapsing uses MXCollapsible (heightFactor animated to 0 plus clipping) rather than
conditional building: the subtree stays mounted, so the search text and focus are not lost.
There is no auto-collapse on scroll — the UI jumping as soon as a finger moves is worse.
Safe areas: the embedded sheet starts at 15% of the screen height, so MXAnalyzer removes the top
padding; the bottom home indicator is avoided individually by the list's bottom padding, the
floating buttons, dialog footers and toasts. The regression guard is
test/mobile_layout_test.dart (rendered at iPhone sizes — any overflow fails the test).
Development #
-
Localization: strings live in
lib/src/app/l10n/app_zh.arb/app_en.arb; runflutter gen-l10nin the package directory after editing (generated files are committed underlib/src/app/l10n/gen/). -
Tests:
flutter test(parser / database / pagination / dialog and page widget tests). -
The screenshots above are generated from code with fixed sizes and fake data, so one rerun refreshes the whole set. They run inside a real macOS app because the headless
flutter testenvironment only has placeholder fonts:cd .. # the desktop shell project flutter test integration_test/generate_screenshots_test.dart -d macos \ --dart-define=OUT_DIR=$PWD/mxlogger_analyzer_lib/screenshotsA sandboxed app may not be able to write into the repository directory; it then falls back to the application support directory and prints the path — copy the PNGs back into
mxlogger_analyzer_lib/screenshots/from there.
