Add log buffer and --log-level argument.

This commit is contained in:
Dain Nilsson 2022-03-25 12:23:14 +01:00
parent a9e1ae5560
commit 8a0896b12f
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
7 changed files with 148 additions and 33 deletions

View File

@ -1,9 +1,12 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'app/logging.dart';
import 'app/views/responsive_dialog.dart';
import 'core/state.dart';
import 'desktop/state.dart';
@ -16,36 +19,90 @@ class AboutPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ResponsiveDialog(
title: const Text('About Yubico Authenticator'),
title: const Text('Yubico Authenticator'),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// TODO: Store the version number elsewhere
const Text('Yubico Authenticator version: 6.0.0-alpha.1'),
const Text('Yubico Authenticator: 6.0.0-alpha.1'),
if (isDesktop)
Text('ykman version: ${ref.watch(rpcStateProvider).version}'),
Text('Dart version: ${Platform.version}'),
const SizedBox(height: 8.0),
const Divider(),
if (isDesktop)
TextButton(
const LoggingPanel(),
if (isDesktop) ...[
const Divider(),
OutlinedButton(
onPressed: () async {
_log.info('Running diagnostics...');
final response =
await ref.read(rpcProvider).command('diagnose', []);
_log.info('Response', response['diagnostics']);
final data = response['diagnostics'];
final text = const JsonEncoder.withIndent(' ').convert(data);
await Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Diagnostics done. See log for results...'),
content: Text('Diagnostic data copied to clipboard'),
duration: Duration(seconds: 2),
),
);
},
child: const Text('Run diagnostics...'),
),
]
],
),
);
}
}
class LoggingPanel extends ConsumerWidget {
const LoggingPanel({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Row(
children: [
const Text('Log level:'),
const SizedBox(width: 8.0),
DropdownButton<Level>(
value: ref.watch(logLevelProvider),
isDense: true,
items: [Level.WARNING, Level.INFO, Level.CONFIG, Level.FINE]
.map((e) => DropdownMenuItem(
value: e,
child: Text(e.name.toUpperCase()),
))
.toList(),
onChanged: (level) {
ref.read(logLevelProvider.notifier).setLogLevel(level!);
_log.config('Log level set to $level');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Log level set to $level'),
duration: const Duration(seconds: 2),
),
);
},
),
const SizedBox(width: 8.0),
OutlinedButton(
child: const Text('Copy log'),
onPressed: () async {
_log.info('Copying log to clipboard...');
final logs = LogBuffer.of(context).getLogs().join('\n');
await Clipboard.setData(ClipboardData(text: logs));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Log copied to clipboard'),
duration: Duration(seconds: 2),
),
);
},
),
],
);
}
}

46
lib/app/logging.dart Executable file
View File

@ -0,0 +1,46 @@
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
extension LoggerExt on Logger {
void critical(Object? message, [Object? error, StackTrace? stackTrace]) =>
shout(message, error, stackTrace);
void error(Object? message, [Object? error, StackTrace? stackTrace]) =>
severe(message, error, stackTrace);
void debug(Object? message, [Object? error, StackTrace? stackTrace]) =>
config(message, error, stackTrace);
void traffic(Object? message, [Object? error, StackTrace? stackTrace]) =>
fine(message, error, stackTrace);
}
List<String> initLogBuffer(int maxSize) {
final List<String> _buffer = [];
Logger.root.onRecord.listen((record) {
_buffer.add('[${record.loggerName}] ${record.level}: ${record.message}');
if (record.error != null) {
_buffer.add('${record.error}');
}
while (_buffer.length > maxSize) {
_buffer.removeAt(0);
}
});
return _buffer;
}
class LogBuffer extends InheritedWidget {
final List<String> _buffer;
const LogBuffer(this._buffer, {required Widget child, Key? key})
: super(child: child, key: key);
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
static LogBuffer of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<LogBuffer>();
assert(result != null, 'No LogBuffer found in context');
return result!;
}
List<String> getLogs() {
return List.unmodifiable(_buffer);
}
}

View File

@ -109,7 +109,7 @@ class MainPageDrawer extends ConsumerWidget {
},
),
DrawerItem(
titleText: 'About',
titleText: 'Help and feedback',
icon: const Icon(Icons.help_outline),
onTap: () {
final nav = Navigator.of(context);

View File

@ -40,13 +40,7 @@ class _WindowResizeListener extends WindowListener {
}
Future<Widget> initialize() async {
Logger.root.onRecord.listen((record) {
stderr.writeln('[${record.loggerName}] ${record.level}: ${record.message}');
if (record.error != null) {
stderr.writeln(record.error);
}
});
_log.info('Logging initialized, outputting to stderr');
_initLogging();
await windowManager.ensureInitialized();
final prefs = await SharedPreferences.getInstance();
@ -119,3 +113,28 @@ Future<Widget> initialize() async {
)),
);
}
void _initLogging() {
Logger.root.onRecord.listen((record) {
stderr.writeln('[${record.loggerName}] ${record.level}: ${record.message}');
if (record.error != null) {
stderr.writeln(record.error);
}
});
final arguments = Platform.executableArguments;
final logLevelIndex = arguments.indexOf('--log-level');
if (logLevelIndex != -1) {
try {
final levelName = arguments[logLevelIndex + 1];
Level level = Level.LEVELS
.firstWhere((level) => level.name == levelName.toUpperCase());
Logger.root.level = level;
_log.info('Log level initialized from command line argument');
} catch (error) {
_log.severe('Failed to set log level', error);
}
}
_log.info('Logging initialized, outputting to stderr');
}

View File

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:logging/logging.dart';
import 'package:async/async.dart';
import 'package:yubico_authenticator/app/logging.dart';
import '../app/models.dart';
import 'models.dart';
@ -46,6 +47,7 @@ class _Request {
}
const _py2level = {
'TRAFFIC': Level.FINE,
'DEBUG': Level.CONFIG,
'INFO': Level.INFO,
'WARNING': Level.WARNING,
@ -83,7 +85,7 @@ class RpcSession {
//time: DateTime.fromMillisecondsSinceEpoch(event['time'] * 1000),
);
} catch (e) {
_log.severe(e.toString(), event);
_log.error(e.toString(), event);
}
});
@ -122,7 +124,7 @@ class RpcSession {
}
void _send(Map data) {
_log.fine('SEND', jsonEncode(data));
_log.traffic('SEND', jsonEncode(data));
_process.stdin.writeln(jsonEncode(data));
}
@ -137,7 +139,7 @@ class RpcSession {
bool completed = false;
while (!completed) {
final response = await _responses.next;
_log.fine('RECV', jsonEncode(response));
_log.traffic('RECV', jsonEncode(response));
response.map(
signal: (signal) {
final signaler = request.signal;

View File

@ -6,6 +6,7 @@ import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'android/init.dart' as android;
import 'app/logging.dart';
import 'app/app.dart';
import 'core/state.dart';
import 'desktop/init.dart' as desktop;
@ -15,6 +16,7 @@ final _log = Logger('main');
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final logBuffer = initLogBuffer(1000);
try {
final Widget initializedApp;
@ -26,7 +28,10 @@ void main() async {
_initializeDebugLogging();
throw UnimplementedError('Platform not supported');
}
runApp(initializedApp);
runApp(LogBuffer(
logBuffer,
child: initializedApp,
));
} catch (e) {
_log.warning('Platform initialization failed: $e');
runApp(

View File

@ -4,7 +4,6 @@ import 'package:logging/logging.dart';
import 'app/state.dart';
import 'app/views/responsive_dialog.dart';
import 'core/state.dart';
final _log = Logger('settings');
@ -31,20 +30,7 @@ class SettingsPage extends ConsumerWidget {
.toList(),
onChanged: (mode) {
ref.read(themeModeProvider.notifier).setThemeMode(mode!);
},
),
DropdownButtonFormField<Level>(
decoration: const InputDecoration(labelText: 'Logging'),
value: ref.watch(logLevelProvider),
items: [Level.INFO, Level.CONFIG, Level.FINE]
.map((e) => DropdownMenuItem(
value: e,
child: Text(e.name.toUpperCase()),
))
.toList(),
onChanged: (level) {
ref.read(logLevelProvider.notifier).setLogLevel(level!);
_log.config('Log level set to $level');
_log.config('Set theme mode to $mode');
},
),
],