ghost_logger 1.3.1
ghost_logger: ^1.3.1 copied to clipboard
Can be Unseen but always there. Stealing errors before they steal your users' experience. A flexible logging utility with optional crash reporting integration.
Ghost Logger ๐ป #
Unseen but always there. Stealing errors before they steal your users' experience.
A lightweight, flexible logging utility for Flutter with colored output, emoji indicators, optional crash reporting, and persistent file logging.
Features #
- ๐ป Silent Operation โ Works invisibly in the background
- ๐จ Colored Output โ ANSI-colored log levels in the terminal
- ๐ Emoji Indicators โ Quick visual scanning of log severity
- ๐ฏ Convenience Methods โ
logDebug(),logInfo(),logWarning(),logError() - ๐ Long Message Support โ Automatic chunking prevents
print()from clipping output - ๐ File Logging โ Persist warnings and errors to local files for production monitoring
- ๐ค Log Export โ Retrieve log files per-session or across all sessions on demand
- ๐งน Auto Cleanup โ Configurable log retention with automatic old-file deletion
- ๐ Pluggable Crash Reporting โ Works with any crash service via a simple interface
- ๐ ๏ธ Two Output Mechanisms โ
print(colored) ordeveloper.log(DevTools-friendly) - ๐ Multi-Platform โ iOS, Android, macOS, Windows, Linux (file logging not supported on Web)
Installation #
Add ghost_logger to your pubspec.yaml:
dependencies:
ghost_logger: ^1.3.1
Then run:
flutter pub get
Quick Start #
Basic Setup #
Configure GhostLogger once at app startup, before runApp:
import 'package:ghost_logger/ghost_logger.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await GhostLogger.configure(
loggerType: LoggerType.console,
);
runApp(const MyApp());
}
Logging Messages #
// Convenience methods (recommended)
GhostLogger.logDebug('Variable value: $data', tag: 'DataService');
GhostLogger.logInfo('User logged in successfully', tag: 'Auth');
GhostLogger.logWarning('Deprecated API called', tag: 'Network');
GhostLogger.logError(
'Failed to load data',
tag: 'API',
stackTrace: StackTrace.current,
);
// Or use the main method with explicit level
GhostLogger.log(
message: 'Processing user data',
level: LogLevel.info,
tag: 'DataManager',
);
Log Levels #
| Level | Emoji | Color | Use Case |
|---|---|---|---|
debug |
โ๏ธ | Gray | Variable values, detailed flow |
info |
๐ | Cyan | User actions, important events |
warning |
โ ๏ธ | Yellow | Potential issues, deprecations |
error |
โ | Red | Failures that need attention |
Output Mechanisms #
LoggerType.print (Default) #
Uses Dart's print() with full ANSI color support. Long messages are automatically split into 1,000-character chunks so output is never clipped.
await GhostLogger.configure(
loggerType: LoggerType.print,
withColors: true,
);
Best for: General development, colored terminal output.
LoggerType.console #
Uses dart:developer log() with full DevTools integration and timeline support. Colors are not applied for IDE compatibility. Handles long messages natively without clipping.
await GhostLogger.configure(
loggerType: LoggerType.console,
);
Best for: Flutter DevTools, timeline debugging.
Message Length Control #
Use maxMessageLength to cap how many characters are printed. Messages exceeding the limit are truncated with a ... [truncated] indicator. Stack traces are always printed in full.
await GhostLogger.configure(
maxMessageLength: 300, // Only first 300 chars printed; rest truncated
);
Applies to both
LoggerType.printandLoggerType.console. Omit this parameter to always print the full message.
File Logging #
File logging persists selected log levels to local files. Each app session creates separate timestamped files per level. Files are stored in the application support directory and are accessible across sessions.
Platform note: File logging is not supported on Flutter Web. A debug warning is emitted if attempted on Web.
Enabling File Logging #
await GhostLogger.configure(
storeErrors: true, // Write error logs to file
storeWarnings: true, // Write warning logs to file
storeInfo: false, // Info logs not stored (default)
storeDebug: false, // Debug logs not stored (default)
logRetentionDays: 7, // Keep files for 7 days (default)
autoCleanOldLogs: true, // Clean on startup (default)
);
File logging works in both debug and release builds โ it is not gated by isDebugMode.
Log File Format #
Each line in a log file follows this format:
2026-05-18T07:30:00.123456 | ERROR | AuthService | Login failed: invalid token
2026-05-18T07:30:01.456789 | WARNING | NO-TAG | Deprecated endpoint called
- Timestamp โ ISO 8601 with microseconds
- Level โ padded to 7 characters for alignment
- Tag โ your tag value, or
NO-TAGif none was provided - Message โ the raw log message
File Naming #
ghost_logger_error_2026-05-18_07-30-00.log
ghost_logger_warning_2026-05-18_07-30-00.log
The timestamp in the filename is the session start time, so all logs from one app launch share the same timestamp prefix.
Per-Entry Callback #
Get notified each time a line is written to any log file:
await GhostLogger.configure(
storeErrors: true,
onLogFileUpdated: (File logFile, LogLevel level) async {
// logFile is the dart:io File โ read, upload, or share it however you prefer
// level tells you which severity file was updated
if (level == LogLevel.error) {
// e.g. upload to your server
}
},
);
Important: Do not call
GhostLogger.log*()insideonLogFileUpdatedwith a stored level. The write queue is still active during the callback, so a recursive log call would enqueue a new write while the current one is completing โ this can produce unexpected interleaving in the output file. Console output inside the callback works normally.
Exporting Log Files #
// Current session files only (default)
final List<File>? files = await GhostLogger.exportLogs();
if (files != null) {
for (final file in files) {
// Share, upload, or read the file
final content = await file.readAsString();
}
}
// All sessions on disk (including previous app launches)
final List<File>? allFiles = await GhostLogger.exportLogs(
scope: LogExportScope.allSessions,
);
exportLogs() returns null when no log files have been written yet โ Dart null safety ensures you handle this case.
Diagnostic Log Reporting #
If you are tracking down "silent bugs" (issues where the app logic fails but no actual exception is thrown), you can command GhostLogger to push its current log files directly to your crash reporter.
// Send the current session's log files to your crash service on demand
await GhostLogger.sendDiagnosticLogs();
Note: This relies on your CrashReporter implementation of sendDiagnosticLogs(). By default, it does nothing unless implemented.
Cleaning Log Files #
// Delete files older than logRetentionDays (runs automatically if autoCleanOldLogs is true)
await GhostLogger.cleanOldLogs();
// Delete ALL ghost_logger_*.log files regardless of age
await GhostLogger.cleanOldLogs(includeRecents: true);
Crash Reporting Integration #
GhostLogger integrates with any crash reporting service through the CrashReporter interface.
Firebase Crashlytics #
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:ghost_logger/ghost_logger.dart';
class FirebaseCrashReporter implements CrashReporter {
@override
Future<void> log(String message) async {
await FirebaseCrashlytics.instance.log(message);
}
@override
Future<void> recordError(
dynamic exception,
StackTrace? stackTrace, {
String? reason,
}) async {
await FirebaseCrashlytics.instance.recordError(
exception,
stackTrace,
reason: reason,
fatal: false,
);
}
@override
Future<void> setCollectionEnabled(bool enabled) async {
await FirebaseCrashlytics.instance
.setCrashlyticsCollectionEnabled(enabled);
}
@override
Future<void> sendDiagnosticLogs() async {
// Implement your file reading and diagnostic upload logic here.
// See the package documentation or GitHub repository for a complete example.
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
await GhostLogger.configure(
crashReporter: FirebaseCrashReporter(),
enableCrashReporting: kReleaseMode,
);
runApp(const MyApp());
}
Custom Reporter #
Implement CrashReporter for any service (Sentry, Datadog, etc.):
class CustomCrashReporter implements CrashReporter {
@override
Future<void> log(String message) async { /* your implementation */ }
@override
Future<void> recordError(dynamic exception, StackTrace? stackTrace, {String? reason}) async {
/* your implementation */
}
@override
Future<void> setCollectionEnabled(bool enabled) async { /* your implementation */ }
@override
Future<void> sendDiagnosticLogs() async { /* your implementation */ }
}
Full configure() Reference #
await GhostLogger.configure(
// Console output
isDebugMode: kDebugMode, // Show console logs in debug mode only (default: kDebugMode)
loggerType: LoggerType.print, // Output mechanism: print or console (default: print)
withColors: true, // Enable ANSI colors for LoggerType.print (default: true)
maxMessageLength: null, // Max chars to print; null = unlimited (default: null)
// Crash reporting
crashReporter: null, // Your CrashReporter implementation (default: null)
enableCrashReporting: false, // Enable crash service reporting (default: false)
// File logging
storeDebug: false, // Write debug logs to file (default: false)
storeInfo: false, // Write info logs to file (default: false)
storeWarnings: false, // Write warning logs to file (default: false)
storeErrors: false, // Write error logs to file (default: false)
logRetentionDays: 7, // Days to keep log files (default: 7)
autoCleanOldLogs: true, // Auto-delete old files at startup (default: true)
onLogFileUpdated: null, // Callback per file write: (File, LogLevel) โ void
);
API Reference #
GhostLogger.log() #
await GhostLogger.log(
message: 'Your message', // Required โ any type, toString() is called
level: LogLevel.debug, // Severity (default: debug)
tag: 'ServiceName', // Optional source identifier
stackTrace: StackTrace.current, // Optional stack trace
reportToCrashService: null, // Override crash reporting for this call
);
GhostLogger.exportLogs() #
final List<File>? files = await GhostLogger.exportLogs(
scope: LogExportScope.currentSession, // or LogExportScope.allSessions
);
Returns null if no files exist yet. Also triggers onLogFileUpdated for each returned file if configured.
GhostLogger.cleanOldLogs() #
await GhostLogger.cleanOldLogs(
includeRecents: false, // true = delete ALL log files regardless of age
);
GhostLogger.flush() #
Waits for all pending file writes to complete. Log calls enqueue file writes
asynchronously and return immediately, so there may be writes still in progress
when the app is about to terminate. Call flush() in your dispose or shutdown
handler to ensure no buffered entries are lost.
@override
Future<void> dispose() async {
await GhostLogger.flush();
super.dispose();
}
flush() is a no-op when no writes are pending and is safe to call at any time.
Best Practices #
Production-Ready Setup #
await GhostLogger.configure(
loggerType: LoggerType.console,
withColors: false, // Colors not needed in production
storeErrors: true, // Capture errors to disk
storeWarnings: true, // Capture warnings to disk
logRetentionDays: 14, // Keep 2 weeks of logs
enableCrashReporting: kReleaseMode,
crashReporter: FirebaseCrashReporter(),
onLogFileUpdated: (file, level) async {
if (level == LogLevel.error) {
// Upload error logs to your monitoring server
}
},
);
Always Include Stack Traces for Errors #
try {
await riskyOperation();
} catch (e, stackTrace) {
GhostLogger.logError(
'Operation failed: $e',
tag: 'Service',
stackTrace: stackTrace,
);
}
Use Tags Consistently #
GhostLogger.logInfo('Data loaded', tag: 'DataService');
GhostLogger.logError('Login failed', tag: 'Auth');
GhostLogger.logWarning('Rate limit near', tag: 'API');
Troubleshooting #
Colors not working in Android Studio #
Android Studio has limited ANSI support. Options:
- Disable colors:
withColors: false - Switch to
LoggerType.consolewhich has no color codes - Use VS Code or run from an external terminal for full color support
Long messages still clipping #
This only affects LoggerType.print. Messages are automatically chunked at 1,000 characters. If you want to cap total output, use maxMessageLength. Alternatively, switch to LoggerType.console which handles any length natively.
File logging not writing anything #
- Ensure at least one of
storeDebug,storeInfo,storeWarnings,storeErrorsistrue - File logging does not work on Flutter Web
- The session file is created lazily on first write, so no file exists until at least one matching log is called
exportLogs() returns null #
No matching log files have been written yet this session (or across all sessions for allSessions scope). Write at least one log of a stored level first.
Platform Support #
| Platform | Console Logging | File Logging |
|---|---|---|
| Android | โ | โ |
| iOS | โ | โ |
| macOS | โ | โ |
| Windows | โ | โ |
| Linux | โ | โ |
| Web | โ | โ (not supported) |
Contributing #
Contributions are welcome!
- Open an issue to discuss changes
- Submit a pull request with tests
- Follow the existing code style
Support #
- ๐ Issues: GitHub Issues
- ๐ฌ Discussions: GitHub Discussions
License #
MIT License โ see LICENSE file
Ghost Logger โ Stealing errors before they steal your users' experience. ๐ป