240 lines
5.9 KiB
Dart
240 lines
5.9 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:logger/logger.dart';
|
|
|
|
/// Application-wide logger with configurable levels and output
|
|
class AppLogger {
|
|
static final AppLogger _instance = AppLogger._internal();
|
|
factory AppLogger() => _instance;
|
|
|
|
late final Logger _logger;
|
|
bool _isInitialized = false;
|
|
|
|
AppLogger._internal();
|
|
|
|
/// Initialize the logger with appropriate settings
|
|
void initialize({Level? level, LogOutput? output, LogFilter? filter}) {
|
|
if (_isInitialized) return;
|
|
|
|
_logger = Logger(
|
|
level: level ?? _getDefaultLevel(),
|
|
filter: filter ?? ProductionFilter(),
|
|
printer: _createPrinter(),
|
|
output: output ?? _createOutput(),
|
|
);
|
|
|
|
_isInitialized = true;
|
|
}
|
|
|
|
/// Get default log level based on build mode
|
|
Level _getDefaultLevel() {
|
|
if (kDebugMode) {
|
|
return Level.debug;
|
|
} else if (kProfileMode) {
|
|
return Level.info;
|
|
} else {
|
|
return Level.warning;
|
|
}
|
|
}
|
|
|
|
/// Create appropriate printer based on build mode
|
|
LogPrinter _createPrinter() {
|
|
if (kDebugMode) {
|
|
// Detailed printer for development
|
|
return PrettyPrinter(
|
|
methodCount: 2,
|
|
errorMethodCount: 8,
|
|
lineLength: 120,
|
|
colors: true,
|
|
printEmojis: true,
|
|
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
|
|
);
|
|
} else {
|
|
// Simple printer for production
|
|
return SimplePrinter(colors: false);
|
|
}
|
|
}
|
|
|
|
/// Create appropriate output based on build mode
|
|
LogOutput _createOutput() {
|
|
if (kReleaseMode) {
|
|
// In production, you might want to send logs to a file or remote service
|
|
return ConsoleOutput();
|
|
} else {
|
|
return ConsoleOutput();
|
|
}
|
|
}
|
|
|
|
/// Ensure logger is initialized
|
|
void _ensureInitialized() {
|
|
if (!_isInitialized) {
|
|
initialize();
|
|
}
|
|
}
|
|
|
|
/// Log a trace message (most verbose)
|
|
void trace(String message, {Object? error, StackTrace? stackTrace}) {
|
|
_ensureInitialized();
|
|
_logger.t(message, error: error, stackTrace: stackTrace);
|
|
}
|
|
|
|
/// Log a debug message
|
|
void debug(String message, {Object? error, StackTrace? stackTrace}) {
|
|
_ensureInitialized();
|
|
_logger.d(message, error: error, stackTrace: stackTrace);
|
|
}
|
|
|
|
/// Log an info message
|
|
void info(String message, {Object? error, StackTrace? stackTrace}) {
|
|
_ensureInitialized();
|
|
_logger.i(message, error: error, stackTrace: stackTrace);
|
|
}
|
|
|
|
/// Log a warning message
|
|
void warning(String message, {Object? error, StackTrace? stackTrace}) {
|
|
_ensureInitialized();
|
|
_logger.w(message, error: error, stackTrace: stackTrace);
|
|
}
|
|
|
|
/// Log an error message
|
|
void error(String message, {Object? error, StackTrace? stackTrace}) {
|
|
_ensureInitialized();
|
|
_logger.e(message, error: error, stackTrace: stackTrace);
|
|
}
|
|
|
|
/// Log a fatal error message
|
|
void fatal(String message, {Object? error, StackTrace? stackTrace}) {
|
|
_ensureInitialized();
|
|
_logger.f(message, error: error, stackTrace: stackTrace);
|
|
}
|
|
|
|
/// Log network request
|
|
void logRequest(
|
|
String method,
|
|
String url, {
|
|
Map<String, dynamic>? headers,
|
|
dynamic body,
|
|
}) {
|
|
if (!kReleaseMode) {
|
|
_ensureInitialized();
|
|
final buffer = StringBuffer();
|
|
buffer.writeln('→ $method $url');
|
|
|
|
if (headers != null && headers.isNotEmpty) {
|
|
buffer.writeln('Headers: ${_sanitizeHeaders(headers)}');
|
|
}
|
|
|
|
if (body != null) {
|
|
buffer.writeln('Body: ${_sanitizeBody(body)}');
|
|
}
|
|
|
|
_logger.d(buffer.toString());
|
|
}
|
|
}
|
|
|
|
/// Log network response
|
|
void logResponse(
|
|
String method,
|
|
String url,
|
|
int statusCode, {
|
|
dynamic body,
|
|
Duration? duration,
|
|
}) {
|
|
if (!kReleaseMode) {
|
|
_ensureInitialized();
|
|
final buffer = StringBuffer();
|
|
buffer.write('← $method $url [$statusCode]');
|
|
|
|
if (duration != null) {
|
|
buffer.write(' (${duration.inMilliseconds}ms)');
|
|
}
|
|
buffer.writeln();
|
|
|
|
if (body != null) {
|
|
buffer.writeln('Body: ${_sanitizeBody(body)}');
|
|
}
|
|
|
|
if (statusCode >= 200 && statusCode < 300) {
|
|
_logger.d(buffer.toString());
|
|
} else if (statusCode >= 400) {
|
|
_logger.e(buffer.toString());
|
|
} else {
|
|
_logger.w(buffer.toString());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sanitize headers to remove sensitive information
|
|
Map<String, dynamic> _sanitizeHeaders(Map<String, dynamic> headers) {
|
|
final sanitized = Map<String, dynamic>.from(headers);
|
|
|
|
// List of sensitive header keys to redact
|
|
const sensitiveKeys = [
|
|
'authorization',
|
|
'cookie',
|
|
'set-cookie',
|
|
'x-api-key',
|
|
'x-auth-token',
|
|
];
|
|
|
|
for (final key in sensitiveKeys) {
|
|
if (sanitized.containsKey(key.toLowerCase())) {
|
|
sanitized[key] = '***REDACTED***';
|
|
}
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
/// Sanitize body to remove sensitive information
|
|
dynamic _sanitizeBody(dynamic body) {
|
|
if (body is Map) {
|
|
final sanitized = Map<String, dynamic>.from(body);
|
|
|
|
// List of sensitive field names to redact
|
|
const sensitiveFields = [
|
|
'password',
|
|
'pwd',
|
|
'passwd',
|
|
'token',
|
|
'secret',
|
|
'apiKey',
|
|
'api_key',
|
|
];
|
|
|
|
for (final field in sensitiveFields) {
|
|
if (sanitized.containsKey(field)) {
|
|
sanitized[field] = '***REDACTED***';
|
|
}
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
// For string bodies, check if it contains sensitive patterns
|
|
if (body is String) {
|
|
if (body.length > 1000) {
|
|
return '${body.substring(0, 1000)}... (truncated)';
|
|
}
|
|
|
|
// Redact potential passwords in query strings or form data
|
|
return body.replaceAllMapped(
|
|
RegExp(
|
|
r'(password|pwd|passwd|token|secret)=[^&\s]+',
|
|
caseSensitive: false,
|
|
),
|
|
(match) => '${match.group(1)}=***REDACTED***',
|
|
);
|
|
}
|
|
|
|
return body;
|
|
}
|
|
|
|
/// Close the logger and release resources
|
|
void close() {
|
|
if (_isInitialized) {
|
|
_logger.close();
|
|
_isInitialized = false;
|
|
}
|
|
}
|
|
}
|