diff --git a/packages/devtools_app/lib/src/screens/logging/logging_controller.dart b/packages/devtools_app/lib/src/screens/logging/logging_controller.dart index ccf6f1a6e46..179faeb8e18 100644 --- a/packages/devtools_app/lib/src/screens/logging/logging_controller.dart +++ b/packages/devtools_app/lib/src/screens/logging/logging_controller.dart @@ -122,6 +122,17 @@ class LoggingController extends DisposableController @visibleForTesting static final settingFilters = >[ + SettingFilter( + name: 'Hide logs below the minimum log level', + includeCallback: (LogData element, Level currentFilterValue) => + element.level >= currentFilterValue.value, + enabledCallback: (Level filterValue) => filterValue >= Level.FINEST, + possibleValues: Level.LEVELS + // Omit Level.OFF and Level.ALL from the possible minimum levels. + .where((level) => level != Level.OFF && level != Level.ALL) + .toList(), + defaultValue: Level.INFO, + ), if (serviceConnection.serviceManager.connectedApp?.isFlutterAppNow ?? true) ...[ ToggleFilter( diff --git a/packages/devtools_app/lib/src/shared/ui/filter.dart b/packages/devtools_app/lib/src/shared/ui/filter.dart index 799a42302ae..fd067085588 100644 --- a/packages/devtools_app/lib/src/shared/ui/filter.dart +++ b/packages/devtools_app/lib/src/shared/ui/filter.dart @@ -221,10 +221,9 @@ class _FilterDialogState extends State> ], for (final filter in widget.controller._settingFilters) ...[ if (filter is ToggleFilter) - ToggleFilterElement(filter: filter) + _ToggleFilterElement(filter: filter) else - // TODO(kenz): add a SettingFilterElement widget. - const SizedBox.shrink(), + _SettingFilterElement(filter: filter), ], ], ), @@ -255,8 +254,8 @@ class _FilterDialogState extends State> } } -class ToggleFilterElement extends StatelessWidget { - const ToggleFilterElement({super.key, required this.filter}); +class _ToggleFilterElement extends StatelessWidget { + const _ToggleFilterElement({required this.filter}); final ToggleFilter filter; @@ -281,6 +280,53 @@ class ToggleFilterElement extends StatelessWidget { } } +class _SettingFilterElement extends StatelessWidget { + const _SettingFilterElement({required this.filter}); + + final SettingFilter filter; + + static const _leadingInset = 6.0; + + @override + Widget build(BuildContext context) { + Widget content = Padding( + // This padding is required to left-align [_SettingFilterElement]s with + // [_ToggleFilterElement] checkboxes in the dialog. + padding: const EdgeInsets.only(left: _leadingInset), + child: Row( + children: [ + Text(filter.name), + const BulletSpacer(), + ValueListenableBuilder( + valueListenable: filter.setting, + builder: (context, value, _) { + return RoundedDropDownButton( + value: value, + items: filter.possibleValues + .map( + (value) => DropdownMenuItem( + value: value, + child: Text('$value'), + ), + ) + .toList(), + onChanged: (value) => filter.setting.value = value!, + ); + }, + ), + ], + ), + ); + if (filter.tooltip != null) { + content = DevToolsTooltip( + message: filter.tooltip, + child: content, + ); + } + return content; + } +} + class Filter { Filter({required this.queryFilter, required this.settingFilters}); diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 8bb481649e9..6f889bbaa16 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -49,6 +49,9 @@ severity. [#8419](https://github.com/flutter/devtools/pull/8419) [#8427](https://github.com/flutter/devtools/pull/8427) ![Logging filter](images/log_filter.png "Logging filter") +* Added support for filtering by log severity / levels. - []() + ![Log level filter](images/log_level_filter.png "Log level filter") + * Fix a bug where logs would get out of order after midnight. - [#8420](https://github.com/flutter/devtools/pull/8420) diff --git a/packages/devtools_app/release_notes/images/log_level_filter.png b/packages/devtools_app/release_notes/images/log_level_filter.png new file mode 100644 index 00000000000..13815a47a23 Binary files /dev/null and b/packages/devtools_app/release_notes/images/log_level_filter.png differ diff --git a/packages/devtools_app/test/logging/logging_controller_test.dart b/packages/devtools_app/test/logging/logging_controller_test.dart index 580a73ec7ef..479f80ddfac 100644 --- a/packages/devtools_app/test/logging/logging_controller_test.dart +++ b/packages/devtools_app/test/logging/logging_controller_test.dart @@ -14,6 +14,7 @@ import 'package:devtools_app_shared/utils.dart'; import 'package:devtools_test/devtools_test.dart'; import 'package:devtools_test/helpers.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:logging/logging.dart'; void main() { group('LoggingController', () { @@ -27,6 +28,7 @@ void main() { jsonEncode({'kind': 'stdout', 'message': message}), 0, summary: message, + level: Level.INFO.value, ), ); } @@ -38,12 +40,21 @@ void main() { jsonEncode({'kind': 'gc', 'message': message}), 0, summary: message, + level: Level.INFO.value, ), ); } - void addLogWithKind(String kind) { - controller.log(LogData(kind, jsonEncode({'foo': 'test_data'}), 0)); + void addLog({required String kind, Level? level, bool isError = false}) { + controller.log( + LogData( + kind, + jsonEncode({'foo': 'test_data'}), + 0, + level: level?.value, + isError: isError, + ), + ); } setUp(() { @@ -88,8 +99,8 @@ void main() { addStdoutData('abc'); addStdoutData('def'); addStdoutData('abc ghi'); - addLogWithKind('Flutter.Navigation'); - addLogWithKind('Flutter.Error'); + addLog(kind: 'Flutter.Navigation'); + addLog(kind: 'Flutter.Error', isError: true); addGcData('gc1'); addGcData('gc2'); @@ -112,8 +123,8 @@ void main() { addStdoutData('abc'); addStdoutData('def'); addStdoutData('abc ghi'); - addLogWithKind('Flutter.Navigation'); - addLogWithKind('Flutter.Error'); + addLog(kind: 'Flutter.Navigation'); + addLog(kind: 'Flutter.Error', isError: true); addGcData('gc1'); addGcData('gc2'); @@ -133,24 +144,27 @@ void main() { addStdoutData('abc'); addStdoutData('def'); addStdoutData('abc ghi'); - addLogWithKind('Flutter.Navigation'); - addLogWithKind('Flutter.Error'); + addLog(kind: 'Flutter.Navigation'); + addLog(kind: 'Flutter.Error', isError: true); // The following logs should all be filtered by default. addGcData('gc1'); addGcData('gc2'); - addLogWithKind('Flutter.FirstFrame'); - addLogWithKind('Flutter.FrameworkInitialization'); - addLogWithKind('Flutter.Frame'); - addLogWithKind('Flutter.ImageSizesForFrame'); - addLogWithKind('Flutter.ServiceExtensionStateChanged'); + addLog(kind: 'Flutter.FirstFrame'); + addLog(kind: 'Flutter.FrameworkInitialization'); + addLog(kind: 'Flutter.Frame'); + addLog(kind: 'Flutter.ImageSizesForFrame'); + addLog(kind: 'Flutter.ServiceExtensionStateChanged'); // At this point data is filtered by the default toggle filter values. expect(controller.data, hasLength(12)); expect(controller.filteredData.value, hasLength(5)); - // Test query filters assuming default toggle filters are all enabled. - for (final filter in controller.activeFilter.value.settingFilters) { + // Test query filters assuming default setting filters are all enabled. + controller.activeFilter.value.settingFilters.first.setting.value = + Level.INFO; + for (final filter + in controller.activeFilter.value.settingFilters.sublist(1)) { filter.setting.value = true; } @@ -186,12 +200,14 @@ void main() { expect(controller.data, hasLength(12)); expect(controller.filteredData.value, hasLength(5)); - // Test toggle filters. - final verboseFlutterFrameworkFilter = + // Test setting filters. + final minimumLogLevelFilter = controller.activeFilter.value.settingFilters[0]; - final verboseFlutterServiceFilter = + final verboseFlutterFrameworkFilter = controller.activeFilter.value.settingFilters[1]; - final gcFilter = controller.activeFilter.value.settingFilters[2]; + final verboseFlutterServiceFilter = + controller.activeFilter.value.settingFilters[2]; + final gcFilter = controller.activeFilter.value.settingFilters[3]; verboseFlutterFrameworkFilter.setting.value = false; controller.setActiveFilter(); @@ -207,6 +223,11 @@ void main() { controller.setActiveFilter(); expect(controller.data, hasLength(12)); expect(controller.filteredData.value, hasLength(12)); + + minimumLogLevelFilter.setting.value = Level.SEVERE; + controller.setActiveFilter(); + expect(controller.data, hasLength(12)); + expect(controller.filteredData.value, hasLength(1)); }); });