Siglat
flutter_logging_service
Production-minded logging for Flutter — persistent files, crash capture,
sessions, masking, live streaming, and an in-app viewer.
Install · Quick start · Features · Config · Example · العربية
The name siglat comes from Arabic سجلات (sijillāt): records / registers / logs — plural of سجل (sijill).
Why
Most Flutter apps start with print / debugPrint, then need:
- logs that survive process death
- a separate crash trail
- PII redaction before anything leaves the device
- a way to view / export / attach logs to a bug report
Siglat covers that path without pulling in a separate console package. Console output is built in (ANSI-colored); remote sinks are left to your app. File I/O runs off the UI isolate.
Package name on disk: flutter_logging_service · Repo: Zyzto/Siglat
Features at a glance
| Area | What you get |
|---|---|
| Files | Rotating main log + dedicated crash file, size + age retention |
| Console | Internal ANSI printer; muted in release by default |
| APIs | Log, Loggable mixin, or LoggingService directly |
| Sessions | sid= on file lines without breaking [LEVEL] markers |
| Privacy | Opt-in default masks + custom MaskingRules |
| Live | LoggingService.stream, ring buffer, LogViewer widget |
| Export | Text dump, zip, keyword search, GitHub issue body helper |
| Perf | Optional isolate writer, write queue, backpressure |
Platforms: Android, iOS, Linux, macOS, Windows (full). Web: console + memory buffer (no file isolate).
Install
dependencies:
flutter_logging_service:
git:
url: https://github.com/Zyzto/Siglat.git
ref: main # prefer a tag (e.g. v0.2.0) once published
import 'package:flutter_logging_service/flutter_logging_service.dart';
Quick start
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await LoggingService.init(
LoggingConfig(
appName: 'MyApp',
enableDefaultMasking: true,
sessionStartExtra: '1.2.3',
),
);
runApp(const MyApp());
}
Log.info('User signed in');
Log.warning('Slow network', error: e, stackTrace: st);
Log.severe('Uncaught failure', error: e, stackTrace: st);
await LoggingService.exportLogsZip();
Initialize before other services that log.
Ways to log
Log — static helper
Auto-detects *Service / *Dao / *Repository from the stack in debug/profile. In release, stack parsing is skipped (no component); use Loggable if you need a name.
Log.debug('cache miss');
Log.info('synced 12 rows');
Log.error('upload failed', error: e, stackTrace: st);
Loggable — mixin
Component name from runtimeType (cheap in release).
class ExpenseRepository with Loggable {
Future<void> save() async {
logInfo('saving expense');
try {
// …
} catch (e, st) {
logError('save failed', error: e, stackTrace: st);
}
}
}
LoggingService — explicit component
LoggingService.info('route restored', component: 'Nav');
LoggingService.severe(
'Flutter framework error',
component: 'CrashHandler',
error: details.exception,
stackTrace: details.stack,
);
Levels
| Level | Typical use | Notes |
|---|---|---|
DEBUG |
Dev noise | Skipped entirely in release |
INFO |
Normal events | Default min level in release |
WARNING |
Recoverable issues | |
ERROR |
Handled failures | Never aggregated |
SEVERE |
Crashes / fatal | Also written to the crash file |
Configuration
Only appName is required. Sensible defaults cover most apps.
await LoggingService.init(
LoggingConfig(
appName: 'MyApp',
// Files
logFileName: 'myapp.log',
crashLogFileName: 'myapp_crashes.log',
logDirectory: null, // → documents/logs
maxLogFileSize: 5 * 1024 * 1024,
maxLogFiles: 5,
maxTotalLogSize: 50 * 1024 * 1024,
logsRetention: const Duration(days: 14),
// Behavior
enableAggregation: true,
aggregationTimeout: const Duration(milliseconds: 100),
minLogLevel: null, // DEBUG in debug, INFO in release
bufferLimit: 2000,
startNewSession: true,
sessionStartExtra: '1.2.3',
enableDefaultMasking: false,
// Console
enableConsole: true,
enableConsoleInRelease: false,
// Perf (IO platforms)
useIsolateWriter: true,
writeQueueCapacity: 500,
perLevelQuotas: null,
quotaWindow: const Duration(seconds: 60),
),
);
| Flag | Default | Meaning |
|---|---|---|
enableConsole |
true |
ANSI console output |
enableConsoleInRelease |
false |
Allow console in release (info+ only; debug stays skipped) |
useIsolateWriter |
true |
File append/rotate/zip on a dedicated isolate |
enableDefaultMasking |
false |
Built-in email / bearer / JWT / password / api-key patterns |
startNewSession |
true |
New sid on each init |
Sessions
final sid = LoggingService.startSession(extra: 'resumed');
// LoggingService.sessionId
File lines keep a stable shape for UI colorizers:
[2026-08-06T12:00:00.000Z] [INFO] [MyService] hello sid=s1_abc123
[LEVEL] is never replaced by the session id.
Masking
Applied before console, stream, aggregation, and file write.
await LoggingService.init(
LoggingConfig(appName: 'MyApp', enableDefaultMasking: true),
);
LoggingService.addMaskingRule(
MaskingRule('super-secret', replacement: '[HIDDEN]'),
);
LoggingService.addMaskingRule(
MaskingRule(r'sk_live_[A-Za-z0-9]+', isRegExp: true, replacement: '[KEY]'),
);
formatLogsForGitHub and exports use masked content.
Live stream & LogViewer
LoggingService.stream.listen((LogRecord r) {
// r.maskedMessage, r.levelName, r.component, r.sessionId, …
});
// In-app viewer (i18n-agnostic labels; override via LogViewerLabels)
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const LogViewer()),
);
Web and memory-only mode feed the viewer from the ring buffer when files are unavailable.
Files, export, search
await LoggingService.flush(); // aggregation + write queue
final text = await LoggingService.getLogContent(maxLines: 500);
final path = await LoggingService.exportLogs(); // .txt bundle
final zip = await LoggingService.exportLogsZip(type: ExportType.all);
final hits = await LoggingService.searchLogs(keywords: ['ERROR', 'timeout']);
await LoggingService.clearLogs();
await LoggingService.clearLogsBefore(const Duration(days: 7));
final issueBody = await LoggingService.formatLogsForGitHub(
'Steps to reproduce…',
);
getLogContent, searchLogs, and exports flush for you. If you read raw files yourself after heavy logging, call flush() first.
Console behavior
- Format:
[appName] [LEVEL] message(ANSI: debug gray, info green, warning blue, error red) - Debug / profile: on when
enableConsoleis true - Release: off unless
enableConsoleInRelease: true enableConsoleInReleasedoes not bring backDEBUG(hard skip + default min level INFO)SEVERE→ file[SEVERE], console[ERROR](intentional parity with the old printer)
No easy_logger dependency — printing is internal.
Aggregation
Similar method(params) messages (same component + level) are batched for the file. Unique messages still hit the console immediately. Errors and severe logs are never aggregated.
[INFO] [AGGREGATED] fetch() called 12 times (id: 1, 2, 3 …)
Isolate writer & backpressure
On IO platforms with useIsolateWriter: true (default), append / rotate / clear / zip run on a dedicated isolate so the UI isolate only enqueues work.
When the write queue is saturated:
debug/infomay be droppederror/severeare never dropped
Optional perLevelQuotas + quotaWindow add soft rate limits.
Lifecycle
LoggingService.setMinLogLevel(LogLevel.warning);
LoggingService.setAggregationEnabled(false);
await LoggingService.flush();
await LoggingService.dispose(); // then init() may be called again
Testing
setUp(() async {
await LoggingService.init(
const LoggingConfig(
appName: 'Test',
startNewSession: false,
enableAggregation: false,
enableConsole: false,
),
store: MemoryLogFileStore(),
);
});
tearDown(() async {
await LoggingService.dispose();
});
LoggingService.disableFileLogging() also forces the memory store. Prefer injecting MemoryLogFileStore for deterministic tests.
Example
cd example
flutter pub get
flutter run
The example covers sessions, masking, zip export, and the live LogViewer.
License
MPL-2.0 — weak copyleft, commercial use allowed. Modified package files stay under MPL; your app can remain closed-source.
Libraries
- flutter_logging_service
- A comprehensive logging service for Flutter apps.