Skip to content

Instantly share code, notes, and snippets.

@Nidal-Bakir
Last active March 9, 2025 13:36
Show Gist options
  • Select an option

  • Save Nidal-Bakir/6a4720951fafa97f09db250d8a7a4099 to your computer and use it in GitHub Desktop.

Select an option

Save Nidal-Bakir/6a4720951fafa97f09db250d8a7a4099 to your computer and use it in GitHub Desktop.
Flutter console logger with colors
// MIT License
//
// Copyright (c) 2025 Nidal Bakir
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'dart:async';
// ignore: unused_import
import 'dart:developer' as dev;
import 'dart:io';
import 'dart:isolate';
import 'package:equatable/equatable.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
// ignore: unused_shown_name
import 'package:flutter/foundation.dart' show debugPrint, kDebugMode;
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:stack_trace/stack_trace.dart';
import '../../../config/env/env.dart';
part 'ansi_color.dart';
part 'file_logger.dart';
part 'types.dart';
int _sequenceNumber = 0;
class Logger {
Logger._();
/// The current logging level of the app. verbose as default.
///
/// All logs with levels above this level will be omitted.
///
/// Levels:
///
/// * 0: off
/// * 1: fatal
/// * 2: error
/// * 3: warning
/// * 4: info
/// * 5: debug
/// * 6: verbose
static LoggingLevel loggingLevel = LoggingLevel.verbose;
static Future<void> wormUpFileLoggerIsolate() async {
if (Env.isStagingInReleaseMode()) {
return _FileLogger().init();
}
}
static void log(
LoggingLevel level,
String message, {
Object? error,
StackTrace? stackTrace,
String? tag,
bool shouldReportToCrashlytics = false,
bool forMultilineLog = false,
}) {
assert(!(forMultilineLog && shouldReportToCrashlytics), "You can not report to Crashlytics with multi-line log ! ");
++_sequenceNumber;
final logEvent = _LogEvent(level, message, error, stackTrace, tag, _sequenceNumber);
final crashlytics = FirebaseCrashlytics.instance;
if (crashlytics.isCrashlyticsCollectionEnabled) {
unawaited(crashlytics.log(logEvent.toStringForPureTextLog()));
}
if (Env.isStagingInReleaseMode()) {
unawaited(
_FileLogger().writeLog(
forMultilineLog //
? logEvent.toStringForMultilineLog()
: logEvent.toStringForPureTextLog(),
),
);
}
if (shouldReportToCrashlytics) {
unawaited(_reportToCrashlytics(logEvent));
}
if (_shouldPrintLog(logEvent)) {
debugPrint(
forMultilineLog //
? logEvent.toStringForMultilineLog()
: logEvent.toStringNoStackTraceFormat(withColors: true),
);
// dev.log(
// forMultilineLog //
// ? logEvent.toStringForMultilineLog()
// : logEvent.toStringNoStackTraceFormat(withColors: true),
// time: DateTime.now(),
// sequenceNumber: _sequenceNumber,
// );
}
}
static bool _shouldPrintLog(_LogEvent logEvent) {
return kDebugMode && logEvent.level.index <= Logger.loggingLevel.index;
}
static Future<void> _reportToCrashlytics(_LogEvent logEvent) async {
final crashlytics = FirebaseCrashlytics.instance;
if (crashlytics.isCrashlyticsCollectionEnabled) {
return crashlytics.recordError(
logEvent.error,
logEvent.stackTrace,
fatal: logEvent.level == LoggingLevel.fatal,
reason: logEvent.message,
printDetails: false,
information: [
'level: ${logEvent.level.name}',
if (logEvent.tag != null) 'tag: ${logEvent.tag}',
'NO #$_sequenceNumber',
],
);
}
}
/// Log a message at level [LoggingLevel.fatal].
static void f(
String message, {
Object? error,
StackTrace? stackTrace,
String? tag,
bool shouldReportToCrashlytics = false,
bool forMultilineLog = false,
}) {
log(
LoggingLevel.fatal,
message,
error: error,
stackTrace: stackTrace,
tag: tag,
shouldReportToCrashlytics: shouldReportToCrashlytics,
forMultilineLog: forMultilineLog,
);
}
/// Log a message at level [LoggingLevel.error].
static void e(
String message, {
Object? error,
StackTrace? stackTrace,
String? tag,
bool shouldReportToCrashlytics = false,
bool forMultilineLog = false,
}) {
log(
LoggingLevel.error,
message,
error: error,
stackTrace: stackTrace,
tag: tag,
shouldReportToCrashlytics: shouldReportToCrashlytics,
forMultilineLog: forMultilineLog,
);
}
/// Log a message at level [LoggingLevel.warning].
static void w(String message, {Object? error, StackTrace? stackTrace, String? tag, bool forMultilineLog = false}) {
log(
LoggingLevel.warning,
message,
error: error,
stackTrace: stackTrace,
tag: tag,
forMultilineLog: forMultilineLog,
);
}
/// Log a message at level [LoggingLevel.info].
static void i(String message, {Object? error, StackTrace? stackTrace, String? tag, bool forMultilineLog = false}) {
log(LoggingLevel.info, message, error: error, stackTrace: stackTrace, tag: tag, forMultilineLog: forMultilineLog);
}
/// Log a message at level [LoggingLevel.debug].
static void d(String message, {Object? error, StackTrace? stackTrace, String? tag, bool forMultilineLog = false}) {
log(LoggingLevel.debug, message, error: error, stackTrace: stackTrace, tag: tag, forMultilineLog: forMultilineLog);
}
/// Log a message at level [LoggingLevel.verbose].
static void v(String message, {Object? error, StackTrace? stackTrace, String? tag, bool forMultilineLog = false}) {
log(
LoggingLevel.verbose,
message,
error: error,
stackTrace: stackTrace,
tag: tag,
forMultilineLog: forMultilineLog,
);
}
}
// MIT License
//
// Copyright (c) 2025 Nidal Bakir
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
part of 'logger.dart';
/// This class handles colorizing of terminal output.
class _AnsiColor {
_AnsiColor.none() : fg = null, bg = null, color = false;
_AnsiColor.fg(this.fg) : bg = null, color = true;
_AnsiColor.bg(this.bg) : fg = null, color = true;
/// ANSI Control Sequence Introducer, signals the terminal for new settings.
static const ansiEsc = '\x1B[';
/// Reset all colors and options for current SGRs to terminal defaults.
static const ansiDefault = '${ansiEsc}0m';
final int? fg;
final int? bg;
final bool color;
@override
String toString() {
if (fg != null) {
return '${ansiEsc}38;5;${fg}m';
} else if (bg != null) {
return '${ansiEsc}48;5;${bg}m';
} else {
return '';
}
}
String call(String msg) {
if (color) {
return msg.split('\n').map((e) => '$this$e').join('\n') + ansiDefault;
} else {
return msg;
}
}
_AnsiColor toFg() => _AnsiColor.fg(bg);
_AnsiColor toBg() => _AnsiColor.bg(fg);
/// Defaults the terminal's foreground color without altering the background.
String get resetForeground => color ? '${ansiEsc}39m' : '';
/// Defaults the terminal's background color without altering the foreground.
String get resetBackground => color ? '${ansiEsc}49m' : '';
static int grey(double level) => 232 + (level.clamp(0.0, 1.0) * 23).round();
}
// MIT License
//
// Copyright (c) 2025 Nidal Bakir
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
part of 'logger.dart';
const _bufferDuration = Duration(seconds: 2);
class _FileLogger {
_FileLogger._() {
init();
}
static _FileLogger? _i;
factory _FileLogger() {
return _i ??= _FileLogger._();
}
Isolate? _isolate;
SendPort? _isolateSendPort;
var _didInit = false;
var _inStartUpMode = false;
var _disposing = false;
Future<void>? _initFuture;
Future<void> init() async {
return _initFuture ??= _init();
}
Future<void> _init() async {
if (_didInit || _disposing || _inStartUpMode) {
return;
}
_inStartUpMode = true;
_didInit = true;
final receivePort = ReceivePort();
_isolate = await Isolate.spawn(_fileLoggerIsolateEntryPoint, receivePort.sendPort);
_isolateSendPort = (await receivePort.first) as SendPort;
final dir = (await path_provider.getExternalStorageDirectory())?.path;
if (dir == null) {
await dispose(killNow: true);
return;
}
_isolate?.errors.listen((e) => Logger.e('Error from the file logger isolate:', error: e, tag: 'FileLogger'));
final logFilePath = p.join(dir, 'log.log');
_isolateSendPort?.send(logFilePath);
_inStartUpMode = false;
}
Future<void> writeLog(String log) async {
if (_disposing || _inStartUpMode) {
return;
}
assert(_isolate != null && _isolateSendPort != null);
_isolateSendPort?.send(log);
}
Future<void> dispose({bool killNow = false}) async {
_disposing = true;
void resetValues() {
_isolateSendPort = null;
_isolate = null;
_didInit = false;
_disposing = false;
_inStartUpMode = false;
}
if (killNow) {
_isolate?.kill();
resetValues();
return;
}
await writeLog('kill');
await Future<void>.delayed(_bufferDuration + const Duration(milliseconds: 250));
_isolate?.kill();
resetValues();
}
}
void _fileLoggerIsolateEntryPoint(SendPort s) async {
final receivePort = ReceivePort();
s.send(receivePort.sendPort);
RandomAccessFile openFile(String path) {
final logFile = File(path);
if (!logFile.existsSync()) {
logFile.createSync(recursive: true);
}
final sizeInMb = logFile.lengthSync() / (1024 * 1024);
if (sizeInMb >= 50) {
logFile.deleteSync();
logFile.createSync(recursive: true);
}
return logFile.openSync(mode: FileMode.writeOnlyAppend);
}
var didOpenLogFile = false;
RandomAccessFile? randomAccessFile;
final completer = Completer<void>();
void close() {
randomAccessFile?.flushSync();
randomAccessFile?.closeSync();
completer.complete();
}
receivePort.listen(
(event) {
if (!didOpenLogFile) {
didOpenLogFile = true;
randomAccessFile = openFile(event as String);
return;
}
randomAccessFile?.writeStringSync(event as String);
if (event == 'kill') {
close();
}
},
onDone: () {
close();
},
onError: (e) {
close();
},
);
await completer.future;
}
// MIT License
//
// Copyright (c) 2025 Nidal Bakir
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
part of 'logger.dart';
enum LoggingLevel {
off,
fatal,
error,
warning,
info,
debug,
verbose;
bool isDebugOrHigher() => index >= debug.index;
}
final _levelColors = {
LoggingLevel.verbose: _AnsiColor.fg(_AnsiColor.grey(0.5)),
LoggingLevel.debug: _AnsiColor.none(),
LoggingLevel.info: _AnsiColor.fg(12),
LoggingLevel.warning: _AnsiColor.fg(190),
LoggingLevel.error: _AnsiColor.fg(199),
LoggingLevel.fatal: _AnsiColor.fg(196),
};
final _levelPrefixes = {
LoggingLevel.verbose: '[V]',
LoggingLevel.debug: '[D]',
LoggingLevel.info: '[I]',
LoggingLevel.warning: '[W]',
LoggingLevel.error: '[E]',
LoggingLevel.fatal: '[F]',
};
class _LogEvent extends Equatable {
_LogEvent(this.level, this.message, this.error, this.stackTrace, this.tag, this.sequenceNumber) {
if (stackTrace != null) {
stringFormattedStackTrace = Trace.format(stackTrace!);
}
}
final LoggingLevel level;
final String message;
final Object? error;
final StackTrace? stackTrace;
final String? tag;
final int sequenceNumber;
late final String? stringFormattedStackTrace;
String toStringFormatted({required bool withColors}) {
return _toString(formatTrace: true, withColors: withColors, addUnicodeBoxLines: true, forMultilineLog: false);
}
String toStringNoStackTraceFormat({required bool withColors}) {
return _toString(formatTrace: false, withColors: withColors, addUnicodeBoxLines: true, forMultilineLog: false);
}
String toStringForPureTextLog() {
return _toString(formatTrace: false, withColors: false, addUnicodeBoxLines: false, forMultilineLog: false);
}
String toStringForMultilineLog() {
return _toString(formatTrace: false, withColors: false, addUnicodeBoxLines: false, forMultilineLog: true);
}
String _toString({
required bool formatTrace,
required bool withColors,
required bool addUnicodeBoxLines,
required bool forMultilineLog,
}) {
final levelPrefix = _levelPrefixes[level]!;
var messageLog = '';
if (addUnicodeBoxLines) {
messageLog =
'╔═══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗\n';
}
if (forMultilineLog) {
messageLog += '$levelPrefix ';
} else {
messageLog += '[log: #$sequenceNumber] $levelPrefix (${DateTime.now().toUtc()})\n';
}
if (tag != null) {
messageLog += '($tag) ';
}
if (forMultilineLog) {
messageLog += message;
} else {
messageLog += 'Message: $message';
}
if (error != null) {
messageLog += '\nError:\n$error';
}
if (stackTrace != null) {
if (formatTrace) {
messageLog += '\nStackTrace:\n$stringFormattedStackTrace';
} else {
messageLog += '\nStackTrace:\n$stackTrace';
}
}
if (stackTrace == null && !forMultilineLog) {
messageLog += '\n';
}
if (addUnicodeBoxLines) {
messageLog +=
'╚═══════════════════════════════════════════════════════════════════════════════════════════════════════════════╝';
}
final color = _levelColors[level]!;
return withColors ? color(messageLog) : messageLog;
}
@override
List<Object?> get props => [level, tag, message, error, stackTrace];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment