feat: log viewer
This commit is contained in:
@@ -12,6 +12,7 @@ import 'services/server_connection_service.dart';
|
||||
import 'services/macos_titlebar_service.dart';
|
||||
import 'services/fullscreen_state_manager.dart';
|
||||
import 'services/update_service.dart';
|
||||
import 'services/settings_service.dart';
|
||||
import 'providers/user_profile_provider.dart';
|
||||
import 'providers/plex_client_provider.dart';
|
||||
import 'providers/theme_provider.dart';
|
||||
@@ -48,6 +49,11 @@ void main() async {
|
||||
// Initialize language codes for track selection
|
||||
await LanguageCodes.initialize();
|
||||
|
||||
// Initialize logger level based on debug setting
|
||||
final settings = await SettingsService.getInstance();
|
||||
final debugEnabled = settings.getEnableDebugLogging();
|
||||
setLoggerLevel(debugEnabled);
|
||||
|
||||
// Start global fullscreen state monitoring
|
||||
FullscreenStateManager().startMonitoring();
|
||||
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../widgets/desktop_app_bar.dart';
|
||||
|
||||
class LogsScreen extends StatefulWidget {
|
||||
const LogsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<LogsScreen> createState() => _LogsScreenState();
|
||||
}
|
||||
|
||||
class _LogsScreenState extends State<LogsScreen> {
|
||||
List<LogEntry> _logs = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadLogs();
|
||||
}
|
||||
|
||||
void _loadLogs() {
|
||||
setState(() {
|
||||
_logs = MemoryLogOutput.getLogs();
|
||||
});
|
||||
}
|
||||
|
||||
String _formatTime(DateTime time) {
|
||||
final hour = time.hour.toString().padLeft(2, '0');
|
||||
final minute = time.minute.toString().padLeft(2, '0');
|
||||
final second = time.second.toString().padLeft(2, '0');
|
||||
final millisecond = time.millisecond.toString().padLeft(3, '0');
|
||||
return '$hour:$minute:$second.$millisecond';
|
||||
}
|
||||
|
||||
void _clearLogs() {
|
||||
setState(() {
|
||||
MemoryLogOutput.clearLogs();
|
||||
_logs = [];
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Logs cleared')),
|
||||
);
|
||||
}
|
||||
|
||||
void _copyAllLogs() {
|
||||
final buffer = StringBuffer();
|
||||
bool isFirst = true;
|
||||
for (final log in _logs.reversed) {
|
||||
if (!isFirst) {
|
||||
buffer.write('\n');
|
||||
}
|
||||
isFirst = false;
|
||||
|
||||
buffer.write(
|
||||
'[${_formatTime(log.timestamp)}] [${log.level.name.toUpperCase()}] ${log.message}');
|
||||
if (log.error != null) {
|
||||
buffer.write('\nError: ${log.error}');
|
||||
}
|
||||
if (log.stackTrace != null) {
|
||||
buffer.write('\nStack trace:\n${log.stackTrace}');
|
||||
}
|
||||
}
|
||||
Clipboard.setData(ClipboardData(text: buffer.toString()));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Logs copied to clipboard')),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getLevelColor(Level level) {
|
||||
switch (level) {
|
||||
case Level.error:
|
||||
case Level.fatal:
|
||||
return Colors.red;
|
||||
case Level.warning:
|
||||
return Colors.orange;
|
||||
case Level.info:
|
||||
return Colors.blue;
|
||||
case Level.debug:
|
||||
case Level.trace:
|
||||
return Colors.grey;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getLevelIcon(Level level) {
|
||||
switch (level) {
|
||||
case Level.error:
|
||||
case Level.fatal:
|
||||
return Icons.error;
|
||||
case Level.warning:
|
||||
return Icons.warning;
|
||||
case Level.info:
|
||||
return Icons.info;
|
||||
case Level.debug:
|
||||
case Level.trace:
|
||||
return Icons.bug_report;
|
||||
default:
|
||||
return Icons.circle;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
CustomAppBar(
|
||||
title: const Text('Logs'),
|
||||
pinned: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _loadLogs,
|
||||
tooltip: 'Refresh',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy),
|
||||
onPressed: _logs.isNotEmpty ? _copyAllLogs : null,
|
||||
tooltip: 'Copy All',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
onPressed: _logs.isNotEmpty ? _clearLogs : null,
|
||||
tooltip: 'Clear Logs',
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_logs.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Text('No logs available'),
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final log = _logs[index];
|
||||
return _LogEntryCard(
|
||||
log: log,
|
||||
formatTime: _formatTime,
|
||||
levelColor: _getLevelColor(log.level),
|
||||
levelIcon: _getLevelIcon(log.level),
|
||||
);
|
||||
},
|
||||
childCount: _logs.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LogEntryCard extends StatefulWidget {
|
||||
final LogEntry log;
|
||||
final String Function(DateTime) formatTime;
|
||||
final Color levelColor;
|
||||
final IconData levelIcon;
|
||||
|
||||
const _LogEntryCard({
|
||||
required this.log,
|
||||
required this.formatTime,
|
||||
required this.levelColor,
|
||||
required this.levelIcon,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_LogEntryCard> createState() => _LogEntryCardState();
|
||||
}
|
||||
|
||||
class _LogEntryCardState extends State<_LogEntryCard> {
|
||||
bool _isExpanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasErrorOrStackTrace =
|
||||
widget.log.error != null || widget.log.stackTrace != null;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 0),
|
||||
child: InkWell(
|
||||
onTap: hasErrorOrStackTrace
|
||||
? () => setState(() => _isExpanded = !_isExpanded)
|
||||
: null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
widget.levelIcon,
|
||||
color: widget.levelColor,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.log.level.name.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: widget.levelColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.formatTime(widget.log.timestamp),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.color
|
||||
?.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.log.message,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (hasErrorOrStackTrace)
|
||||
Icon(
|
||||
_isExpanded
|
||||
? Icons.expand_less
|
||||
: Icons.expand_more,
|
||||
color: Theme.of(context)
|
||||
.iconTheme
|
||||
.color
|
||||
?.withValues(alpha: 0.6),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_isExpanded && hasErrorOrStackTrace) ...[
|
||||
const SizedBox(height: 12),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
if (widget.log.error != null) ...[
|
||||
Text(
|
||||
'Error:',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: widget.levelColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.grey[900]
|
||||
: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: SelectableText(
|
||||
widget.log.error.toString(),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.log.stackTrace != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Stack Trace:',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: widget.levelColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.grey[900]
|
||||
: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: SelectableText(
|
||||
widget.log.stackTrace.toString(),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import '../services/update_service.dart';
|
||||
import '../widgets/desktop_app_bar.dart';
|
||||
import '../widgets/hotkey_recorder_widget.dart';
|
||||
import 'about_screen.dart';
|
||||
import 'logs_screen.dart';
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
@@ -264,6 +265,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
await _settingsService.setEnableDebugLogging(value);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.article),
|
||||
title: const Text('View Logs'),
|
||||
subtitle: const Text('View application logs'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const LogsScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.cleaning_services),
|
||||
title: const Text('Clear Cache'),
|
||||
|
||||
@@ -50,6 +50,8 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
PlexClientProvider? _cachedClientProvider;
|
||||
bool _isPhone = false;
|
||||
List<PlexMediaVersion> _availableVersions = [];
|
||||
StreamSubscription<PlayerLog>? _logSubscription;
|
||||
StreamSubscription<String>? _errorSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -105,6 +107,7 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
final bufferSizeBytes = bufferSizeMB * 1024 * 1024;
|
||||
final enableHardwareDecoding = settingsService
|
||||
.getEnableHardwareDecoding();
|
||||
final debugLoggingEnabled = settingsService.getEnableDebugLogging();
|
||||
|
||||
// Create player with configuration
|
||||
player = Player(
|
||||
@@ -113,6 +116,7 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
libassAndroidFont: 'assets/droid-sans.ttf',
|
||||
libassAndroidFontName: 'Droid Sans Fallback',
|
||||
bufferSize: bufferSizeBytes,
|
||||
logLevel: debugLoggingEnabled ? MPVLogLevel.debug : MPVLogLevel.error,
|
||||
),
|
||||
);
|
||||
controller = VideoController(
|
||||
@@ -151,6 +155,12 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
// Listen to completion
|
||||
player!.stream.completed.listen(_onVideoCompleted);
|
||||
|
||||
// Listen to MPV logs
|
||||
_logSubscription = player!.stream.log.listen(_onPlayerLog);
|
||||
|
||||
// Listen to MPV errors
|
||||
_errorSubscription = player!.stream.error.listen(_onPlayerError);
|
||||
|
||||
// Start periodic progress updates
|
||||
_startProgressTracking();
|
||||
|
||||
@@ -282,6 +292,10 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
// Stop progress tracking
|
||||
_progressTimer?.cancel();
|
||||
|
||||
// Cancel stream subscriptions
|
||||
_logSubscription?.cancel();
|
||||
_errorSubscription?.cancel();
|
||||
|
||||
// Send final stopped state
|
||||
_sendProgress('stopped');
|
||||
|
||||
@@ -907,6 +921,33 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _onPlayerLog(PlayerLog log) {
|
||||
// Map MPV log levels to app logger levels
|
||||
switch (log.level.toLowerCase()) {
|
||||
case 'fatal':
|
||||
case 'error':
|
||||
appLogger.e('[MPV:${log.prefix}] ${log.text}');
|
||||
break;
|
||||
case 'warn':
|
||||
appLogger.w('[MPV:${log.prefix}] ${log.text}');
|
||||
break;
|
||||
case 'info':
|
||||
appLogger.i('[MPV:${log.prefix}] ${log.text}');
|
||||
break;
|
||||
case 'debug':
|
||||
case 'trace':
|
||||
case 'v':
|
||||
appLogger.d('[MPV:${log.prefix}] ${log.text}');
|
||||
break;
|
||||
default:
|
||||
appLogger.d('[MPV:${log.prefix}:${log.level}] ${log.text}');
|
||||
}
|
||||
}
|
||||
|
||||
void _onPlayerError(String error) {
|
||||
appLogger.e('[MPV ERROR] $error');
|
||||
}
|
||||
|
||||
Future<void> _playNext() async {
|
||||
if (_nextEpisode == null || _isLoadingNext) return;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:plezy/utils/app_logger.dart';
|
||||
|
||||
enum ThemeMode { system, light, dark }
|
||||
|
||||
@@ -56,6 +57,8 @@ class SettingsService {
|
||||
// Debug Logging
|
||||
Future<void> setEnableDebugLogging(bool enabled) async {
|
||||
await _prefs.setBool(_keyEnableDebugLogging, enabled);
|
||||
// Update logger level immediately when setting changes
|
||||
setLoggerLevel(enabled);
|
||||
}
|
||||
|
||||
bool getEnableDebugLogging() {
|
||||
|
||||
+224
-1
@@ -1,5 +1,204 @@
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
/// Redacts sensitive information from log messages
|
||||
String _redactSensitiveData(String message) {
|
||||
String redacted = message;
|
||||
|
||||
// Redact Plex tokens (alphanumeric strings typically 20+ characters)
|
||||
// Pattern: X-Plex-Token=... or token=... or accessToken=... or similar
|
||||
redacted = redacted.replaceAllMapped(
|
||||
RegExp(r'([Tt]oken[=:]\s*)([A-Za-z0-9_-]{10,})', caseSensitive: false),
|
||||
(match) => '${match.group(1)}[REDACTED]',
|
||||
);
|
||||
|
||||
// Redact authorization headers
|
||||
redacted = redacted.replaceAllMapped(
|
||||
RegExp(r'([Aa]uthorization[=:]\s*)([A-Za-z0-9_\-\.]+)', caseSensitive: false),
|
||||
(match) => '${match.group(1)}[REDACTED]',
|
||||
);
|
||||
|
||||
// Redact API keys
|
||||
redacted = redacted.replaceAllMapped(
|
||||
RegExp(r'([Aa]pi[Kk]ey[=:]\s*)([A-Za-z0-9_-]{10,})', caseSensitive: false),
|
||||
(match) => '${match.group(1)}[REDACTED]',
|
||||
);
|
||||
|
||||
// Redact passwords
|
||||
redacted = redacted.replaceAllMapped(
|
||||
RegExp(r'([Pp]assword[=:]\s*)([^\s&,;]+)', caseSensitive: false),
|
||||
(match) => '${match.group(1)}[REDACTED]',
|
||||
);
|
||||
|
||||
// Redact full URLs with tokens in query parameters
|
||||
redacted = redacted.replaceAllMapped(
|
||||
RegExp(r'(https?://[^\s]*[?&])([Xx]-[Pp]lex-[Tt]oken|token)=([A-Za-z0-9_-]+)'),
|
||||
(match) => '${match.group(1)}${match.group(2)}=[REDACTED]',
|
||||
);
|
||||
|
||||
// Redact IP addresses in dot notation (e.g., 192.168.1.100)
|
||||
redacted = redacted.replaceAllMapped(
|
||||
RegExp(r'\b(\d{1,3}\.)(\d{1,3}\.)(\d{1,3}\.)(\d{1,3})\b'),
|
||||
(match) => '${match.group(1)}***.***.${match.group(4)}',
|
||||
);
|
||||
|
||||
// Redact IP addresses in dash notation (e.g., 192-168-1-11)
|
||||
redacted = redacted.replaceAllMapped(
|
||||
RegExp(r'\b(\d{1,3}-)(\d{1,3}-)(\d{1,3}-)(\d{1,3})\b'),
|
||||
(match) => '${match.group(1)}***-***-${match.group(4)}',
|
||||
);
|
||||
|
||||
// Redact standalone token-like strings (20+ alphanumeric characters)
|
||||
// Only if they appear in common token contexts
|
||||
redacted = redacted.replaceAllMapped(
|
||||
RegExp(r'\b([A-Za-z0-9_-]{20,})\b'),
|
||||
(match) {
|
||||
final token = match.group(1)!;
|
||||
// Only redact if it looks like a token (mixed case or contains hyphens/underscores)
|
||||
if (token.contains(RegExp(r'[A-Z]')) && token.contains(RegExp(r'[a-z]')) ||
|
||||
token.contains('_') || token.contains('-')) {
|
||||
return '[REDACTED_TOKEN]';
|
||||
}
|
||||
return token;
|
||||
},
|
||||
);
|
||||
|
||||
return redacted;
|
||||
}
|
||||
|
||||
/// Represents a single log entry stored in memory
|
||||
class LogEntry {
|
||||
final DateTime timestamp;
|
||||
final Level level;
|
||||
final String message;
|
||||
final Object? error;
|
||||
final StackTrace? stackTrace;
|
||||
|
||||
LogEntry({
|
||||
required this.timestamp,
|
||||
required this.level,
|
||||
required this.message,
|
||||
this.error,
|
||||
this.stackTrace,
|
||||
});
|
||||
|
||||
/// Estimate the memory size of this log entry in bytes
|
||||
int get estimatedSize {
|
||||
int size = 0;
|
||||
// DateTime: ~8 bytes
|
||||
size += 8;
|
||||
// Level enum: ~4 bytes
|
||||
size += 4;
|
||||
// Message string: 2 bytes per character (UTF-16)
|
||||
size += message.length * 2;
|
||||
// Error string: 2 bytes per character if present
|
||||
if (error != null) {
|
||||
size += error.toString().length * 2;
|
||||
}
|
||||
// Stack trace string: 2 bytes per character if present
|
||||
if (stackTrace != null) {
|
||||
size += stackTrace.toString().length * 2;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom log output that stores logs in memory with a circular buffer
|
||||
class MemoryLogOutput extends LogOutput {
|
||||
static const int maxLogSizeBytes = 5 * 1024 * 1024; // 5 MB
|
||||
static final List<LogEntry> _logs = [];
|
||||
static int _currentSize = 0;
|
||||
|
||||
/// Get all stored logs (newest first)
|
||||
static List<LogEntry> getLogs() => List.unmodifiable(_logs.reversed);
|
||||
|
||||
/// Clear all stored logs
|
||||
static void clearLogs() {
|
||||
_logs.clear();
|
||||
_currentSize = 0;
|
||||
}
|
||||
|
||||
/// Get current log buffer size in bytes
|
||||
static int getCurrentSize() => _currentSize;
|
||||
|
||||
/// Get current log buffer size in MB
|
||||
static double getCurrentSizeMB() => _currentSize / (1024 * 1024);
|
||||
|
||||
@override
|
||||
void output(OutputEvent event) {
|
||||
// Extract relevant information from the log event
|
||||
for (var line in event.lines) {
|
||||
final logEntry = LogEntry(
|
||||
timestamp: DateTime.now(),
|
||||
level: event.level,
|
||||
message: _redactSensitiveData(line),
|
||||
);
|
||||
|
||||
_logs.add(logEntry);
|
||||
_currentSize += logEntry.estimatedSize;
|
||||
|
||||
// Maintain buffer size limit (remove oldest entries)
|
||||
while (_currentSize > maxLogSizeBytes && _logs.isNotEmpty) {
|
||||
final removed = _logs.removeAt(0);
|
||||
_currentSize -= removed.estimatedSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom log printer that also stores error and stack trace information
|
||||
class MemoryAwareLogPrinter extends LogPrinter {
|
||||
final LogPrinter _wrappedPrinter;
|
||||
|
||||
MemoryAwareLogPrinter(this._wrappedPrinter);
|
||||
|
||||
@override
|
||||
List<String> log(LogEvent event) {
|
||||
// Store the log with error and stack trace if available
|
||||
final message = _redactSensitiveData(event.message.toString());
|
||||
final error = event.error != null
|
||||
? _redactSensitiveData(event.error.toString())
|
||||
: null;
|
||||
|
||||
final logEntry = LogEntry(
|
||||
timestamp: DateTime.now(),
|
||||
level: event.level,
|
||||
message: message,
|
||||
error: error,
|
||||
stackTrace: event.stackTrace,
|
||||
);
|
||||
|
||||
MemoryLogOutput._logs.add(logEntry);
|
||||
MemoryLogOutput._currentSize += logEntry.estimatedSize;
|
||||
|
||||
// Maintain buffer size limit (remove oldest entries)
|
||||
while (MemoryLogOutput._currentSize > MemoryLogOutput.maxLogSizeBytes &&
|
||||
MemoryLogOutput._logs.isNotEmpty) {
|
||||
final removed = MemoryLogOutput._logs.removeAt(0);
|
||||
MemoryLogOutput._currentSize -= removed.estimatedSize;
|
||||
}
|
||||
|
||||
// Delegate to wrapped printer for console output
|
||||
return _wrappedPrinter.log(event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom production filter that respects our level setting even in release mode
|
||||
class ProductionFilter extends LogFilter {
|
||||
Level _currentLevel = Level.debug;
|
||||
|
||||
void setLevel(Level level) {
|
||||
_currentLevel = level;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldLog(LogEvent event) {
|
||||
return event.level.value >= _currentLevel.value;
|
||||
}
|
||||
}
|
||||
|
||||
/// Global filter instance
|
||||
final _productionFilter = ProductionFilter();
|
||||
|
||||
/// Centralized logger instance for the application.
|
||||
///
|
||||
/// Usage:
|
||||
@@ -11,4 +210,28 @@ import 'package:logger/logger.dart';
|
||||
/// appLogger.w('Warning message');
|
||||
/// appLogger.e('Error message', error: e, stackTrace: stackTrace);
|
||||
/// ```
|
||||
final appLogger = Logger(printer: SimplePrinter(), level: Level.debug);
|
||||
Logger appLogger = Logger(
|
||||
printer: MemoryAwareLogPrinter(SimplePrinter()),
|
||||
filter: _productionFilter,
|
||||
level: Level.debug,
|
||||
);
|
||||
|
||||
/// Update the logger's level dynamically based on debug setting
|
||||
/// Recreates the logger instance to ensure it works in release mode
|
||||
void setLoggerLevel(bool debugEnabled) {
|
||||
final newLevel = debugEnabled ? Level.debug : Level.info;
|
||||
|
||||
// Update the filter level
|
||||
_productionFilter.setLevel(newLevel);
|
||||
|
||||
// Recreate the logger instance with the new level
|
||||
// This ensures it works in release mode where Logger.level might be optimized away
|
||||
appLogger = Logger(
|
||||
printer: MemoryAwareLogPrinter(SimplePrinter()),
|
||||
filter: _productionFilter,
|
||||
level: newLevel,
|
||||
);
|
||||
|
||||
// Also set the static level for consistency
|
||||
Logger.level = newLevel;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user