Merge remote-tracking branch 'origin/master' into better-logging
diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart
index af521bb..67667e6 100644
--- a/packages/devtools_app/lib/src/app.dart
+++ b/packages/devtools_app/lib/src/app.dart
@@ -46,6 +46,7 @@
 import 'shared/console/primitives/simple_items.dart';
 import 'shared/dialogs.dart';
 import 'shared/globals.dart';
+import 'shared/log_storage.dart';
 import 'shared/offline_screen.dart';
 import 'shared/primitives/auto_dispose.dart';
 import 'shared/primitives/utils.dart';
@@ -515,6 +516,47 @@
             toggle: preferences.toggleVmDeveloperMode,
             gaItem: gac.vmDeveloperMode,
           ),
+          Column(
+            children: [
+              Row(
+                children: [
+                  CheckboxSetting(
+                    label: const Text(
+                      'Enable verbose logging',
+                    ),
+                    listenable: preferences.verboseLoggingEnabled,
+                    toggle: (enable) => preferences.setVerboseLogging(enable),
+                    gaItem: gac.verboseLogging,
+                  ),
+                  CopyToClipboardControl(
+                    dataProvider: () => LogStorage.root.toString(),
+                    tooltip: 'Copy Logs',
+                  ),
+                  ClearButton(
+                    label: 'Clear Logs',
+                    minScreenWidthForTextBeforeScaling:
+                        double.infinity, // Forces Icon only mode for button
+                    tooltip: 'Clear Logs',
+                    onPressed: () => LogStorage.root.clear(),
+                  ),
+                ],
+              ),
+              const Row(
+                children: [
+                  SizedBox(
+                    width: defaultSpacing,
+                  ),
+                  Icon(Icons.warning),
+                  SizedBox(
+                    width: defaultSpacing,
+                  ),
+                  Text(
+                    'Logs may contain sensitive information. Always check their contents before sharing.',
+                  )
+                ],
+              ),
+            ],
+          ),
         ],
       ),
       actions: const [
diff --git a/packages/devtools_app/lib/src/framework/framework_core.dart b/packages/devtools_app/lib/src/framework/framework_core.dart
index 897235d..afd4501 100644
--- a/packages/devtools_app/lib/src/framework/framework_core.dart
+++ b/packages/devtools_app/lib/src/framework/framework_core.dart
@@ -39,7 +39,7 @@
 
   static void init() {
     // Print the version number at startup.
-    log('DevTools version ${devtools.version}.');
+    log('zzzDevTools version ${devtools.version}.');
   }
 
   /// Returns true if we're able to connect to a device and false otherwise.
diff --git a/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart b/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart
index cdf6286..7b1f357 100644
--- a/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart
@@ -4,6 +4,7 @@
 
 import 'package:flutter/material.dart';
 import 'package:image/image.dart' as image;
+import 'package:logging/logging.dart';
 
 import '../../shared/common_widgets.dart';
 import '../../shared/http/http.dart';
@@ -14,6 +15,8 @@
 import '../../shared/ui/colors.dart';
 import 'network_model.dart';
 
+final _log = Logger('network_request_inspector_views');
+
 // Approximately double the indent of the expandable tile's title.
 const double _rowIndentPadding = 30;
 
@@ -172,6 +175,7 @@
 
   @override
   Widget build(BuildContext context) {
+    _log.info('HttpResponseView: building');
     return ValueListenableBuilder(
       valueListenable: data.requestUpdatedNotifier,
       builder: (context, __, ___) {
@@ -184,17 +188,23 @@
         final responseBody = data.responseBody!;
         final isLoading = data.isFetchingFullData;
         if (isLoading) {
+          _log.info('HttpResponseView: loading');
           return CenteredCircularProgressIndicator(
             size: mediumProgressSize,
           );
         }
+        _log.info('HttpResponseView: DONE loading');
+
         if (contentType != null && contentType.contains('image')) {
+          _log.info('HttpResponseView: showing an image response');
           child = ImageResponseView(data);
         } else if (contentType != null &&
             contentType.contains('json') &&
             responseBody.isNotEmpty) {
+          _log.info('HttpResponseView: showing JSONVIewer');
           child = JsonViewer(encodedJson: responseBody);
         } else {
+          _log.info('HttpResponseView: showing text');
           child = Text(
             responseBody,
             style: theme.fixedFontStyle,
diff --git a/packages/devtools_app/lib/src/service/vm_service_wrapper.dart b/packages/devtools_app/lib/src/service/vm_service_wrapper.dart
index fd23717..b126780 100644
--- a/packages/devtools_app/lib/src/service/vm_service_wrapper.dart
+++ b/packages/devtools_app/lib/src/service/vm_service_wrapper.dart
@@ -7,10 +7,12 @@
 library vm_service_wrapper;
 
 import 'dart:async';
+import 'dart:math';
 
 import 'package:collection/collection.dart' show IterableExtension;
 import 'package:dds_service_extensions/dds_service_extensions.dart';
 import 'package:flutter/foundation.dart';
+import 'package:logging/logging.dart';
 import 'package:vm_service/vm_service.dart';
 
 import '../screens/vm_developer/vm_service_private_extensions.dart';
@@ -18,6 +20,8 @@
 import '../shared/primitives/utils.dart';
 import 'json_to_service_cache.dart';
 
+final _log = Logger('vm_service_wrapper');
+
 class VmServiceWrapper implements VmService {
   VmServiceWrapper(
     this._vmService,
@@ -955,13 +959,34 @@
 
   @visibleForTesting
   Future<T> trackFuture<T>(String name, Future<T> future) {
+    Future<T> loggedFuture = future;
+
+    if (_log.isLoggable(Level.INFO)) {
+      final futureLogId = Random().nextInt(1 << 30);
+      _log.info('[$futureLogId]-trackFuture($name,...): Started');
+      loggedFuture = loggedFuture.then(
+        (value) {
+          _log.info(
+            '[$futureLogId]-trackFuture($name,...): Succeeded',
+          );
+          return value;
+        },
+        onError: (error) {
+          _log.info(
+            '[$futureLogId]-trackFuture($name,...): Failed with: $error',
+          );
+          throw error;
+        },
+      );
+    }
+
     if (!trackFutures) {
-      return future;
+      return loggedFuture;
     }
     vmServiceCallCount++;
     vmServiceCalls.add(name);
 
-    final trackedFuture = TrackedFuture(name, future as Future<Object>);
+    final trackedFuture = TrackedFuture(name, loggedFuture as Future<Object>);
     if (_allFuturesCompleter.isCompleted) {
       _allFuturesCompleter = Completer<bool>();
     }
@@ -974,11 +999,11 @@
       }
     }
 
-    future.then(
+    loggedFuture.then(
       (value) => futureComplete(),
       onError: (error) => futureComplete(),
     );
-    return future;
+    return loggedFuture;
   }
 
   /// Adds support for private VM RPCs that can only be used when VM developer
diff --git a/packages/devtools_app/lib/src/shared/analytics/constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants.dart
index 53d4ac9..675664b 100644
--- a/packages/devtools_app/lib/src/shared/analytics/constants.dart
+++ b/packages/devtools_app/lib/src/shared/analytics/constants.dart
@@ -111,6 +111,7 @@
 const denseMode = 'denseMode';
 const analytics = 'analytics';
 const vmDeveloperMode = 'vmDeveloperMode';
+const verboseLogging = 'verboseLogging';
 const inspectorHoverEvalMode = 'inspectorHoverEvalMode';
 
 // Object explorer:
diff --git a/packages/devtools_app/lib/src/shared/common_widgets.dart b/packages/devtools_app/lib/src/shared/common_widgets.dart
index 395f012..5b07445 100644
--- a/packages/devtools_app/lib/src/shared/common_widgets.dart
+++ b/packages/devtools_app/lib/src/shared/common_widgets.dart
@@ -332,11 +332,12 @@
     double? minScreenWidthForTextBeforeScaling,
     String tooltip = 'Clear',
     bool outlined = true,
+    String label = 'Clear',
     required VoidCallback? onPressed,
   }) : super(
           key: key,
           icon: Icons.block,
-          label: 'Clear',
+          label: label,
           tooltip: tooltip,
           outlined: outlined,
           minScreenWidthForTextBeforeScaling:
diff --git a/packages/devtools_app/lib/src/shared/config_specific/logger/logger_default.dart b/packages/devtools_app/lib/src/shared/config_specific/logger/logger_default.dart
index dbe7b24..3c27f10 100644
--- a/packages/devtools_app/lib/src/shared/config_specific/logger/logger_default.dart
+++ b/packages/devtools_app/lib/src/shared/config_specific/logger/logger_default.dart
@@ -2,17 +2,24 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:logging/logging.dart';
+
 import 'logger.dart';
 
+final _log = Logger('main_log');
+
 void log(Object message, [LogLevel level = LogLevel.debug]) {
   switch (level) {
     case LogLevel.debug:
       print(message);
+      _log.info(message);
       break;
     case LogLevel.warning:
       print('[WARNING]: $message');
+      _log.warning(message);
       break;
     case LogLevel.error:
       print('[ERROR]: $message');
+      _log.shout(message);
   }
 }
diff --git a/packages/devtools_app/lib/src/shared/config_specific/logger/logger_html.dart b/packages/devtools_app/lib/src/shared/config_specific/logger/logger_html.dart
index 7692a06..3485952 100644
--- a/packages/devtools_app/lib/src/shared/config_specific/logger/logger_html.dart
+++ b/packages/devtools_app/lib/src/shared/config_specific/logger/logger_html.dart
@@ -4,17 +4,24 @@
 
 import 'dart:html';
 
+import 'package:logging/logging.dart';
+
 import 'logger.dart';
 
+final _log = Logger('main_log');
+
 void log(Object message, [LogLevel level = LogLevel.debug]) {
   switch (level) {
     case LogLevel.debug:
       window.console.log(message);
+      _log.info(message);
       break;
     case LogLevel.warning:
       window.console.warn(message);
+      _log.warning(message);
       break;
     case LogLevel.error:
       window.console.error(message);
+      _log.shout(message);
   }
 }
diff --git a/packages/devtools_app/lib/src/shared/config_specific/logger/logger_io.dart b/packages/devtools_app/lib/src/shared/config_specific/logger/logger_io.dart
index 83e3af0..461ce40 100644
--- a/packages/devtools_app/lib/src/shared/config_specific/logger/logger_io.dart
+++ b/packages/devtools_app/lib/src/shared/config_specific/logger/logger_io.dart
@@ -2,17 +2,24 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:logging/logging.dart';
+
 import 'logger.dart';
 
+final _log = Logger('main_log');
+
 void log(Object message, [LogLevel level = LogLevel.debug]) {
   switch (level) {
     case LogLevel.debug:
       print(message);
+      _log.info(message);
       break;
     case LogLevel.warning:
       print('[WARNING]: $message');
+      _log.warning(message);
       break;
     case LogLevel.error:
       print('[ERROR]: $message');
+      _log.shout(message);
   }
 }
diff --git a/packages/devtools_app/lib/src/shared/log_storage.dart b/packages/devtools_app/lib/src/shared/log_storage.dart
new file mode 100644
index 0000000..ad7f6ec
--- /dev/null
+++ b/packages/devtools_app/lib/src/shared/log_storage.dart
@@ -0,0 +1,27 @@
+import 'dart:collection';
+
+/// TODO Dart Doc
+class LogStorage {
+  static const int maxLogEntries = 3000;
+
+  final Queue<String> _logs = Queue<String>();
+
+  void addLog(String message) {
+    _logs.add(message);
+    if (_logs.length > maxLogEntries) {
+      _logs.removeFirst();
+    }
+  }
+
+  void clear() {
+    _logs.clear();
+  }
+
+  @override
+  String toString() {
+    return _logs.join('\n');
+  }
+
+  // Static instance for storing the app's logs.
+  static final LogStorage root = LogStorage();
+}
diff --git a/packages/devtools_app/lib/src/shared/preferences.dart b/packages/devtools_app/lib/src/shared/preferences.dart
index ead7ca7..4ef42b3 100644
--- a/packages/devtools_app/lib/src/shared/preferences.dart
+++ b/packages/devtools_app/lib/src/shared/preferences.dart
@@ -6,15 +6,19 @@
 import 'dart:convert';
 
 import 'package:flutter/foundation.dart';
+import 'package:logging/logging.dart';
 
 import '../service/vm_service_wrapper.dart';
 import 'analytics/analytics.dart' as ga;
 import 'analytics/constants.dart' as gac;
 import 'diagnostics/inspector_service.dart';
 import 'globals.dart';
+import 'log_storage.dart';
 import 'primitives/auto_dispose.dart';
 import 'primitives/utils.dart';
 
+final _log = Logger('Preferences');
+
 /// A controller for global application preferences.
 class PreferencesController extends DisposableController
     with AutoDisposeControllerMixin {
@@ -25,6 +29,9 @@
   ValueListenable<bool> get vmDeveloperModeEnabled => _vmDeveloperMode;
   final _vmDeveloperMode = ValueNotifier<bool>(false);
 
+  ValueListenable<bool> get verboseLoggingEnabled => _verboseLogging;
+  final _verboseLogging = ValueNotifier<bool>(Logger.root.level == Level.INFO);
+
   ValueListenable<bool> get denseModeEnabled => _denseMode;
   final _denseMode = ValueNotifier<bool>(false);
 
@@ -63,6 +70,29 @@
       storage.setValue('ui.denseMode', '${_denseMode.value}');
     });
 
+    final String? verboseLoggingEnabledValue =
+        await storage.getValue('verboseLogging');
+
+    setVerboseLogging(verboseLoggingEnabledValue == 'true');
+
+    addAutoDisposeListener(_verboseLogging, () {
+      storage.setValue('verboseLogging', _verboseLogging.value.toString());
+
+      if (_verboseLogging.value) {
+        Logger.root.level = Level.INFO;
+        _log.warning('verboseLogging enabled');
+      } else {
+        Logger.root.level = Level.WARNING;
+        _log.warning('verboseLogging disabled');
+      }
+    });
+
+    Logger.root.onRecord.listen((record) {
+      LogStorage.root.addLog(
+        '[${record.loggerName}-${record.level.name}]: ${record.time.toUtc()}: ${record.message}',
+      );
+    });
+
     await inspector.init();
     await memory.init();
     await performance.init();
@@ -91,6 +121,10 @@
     VmServiceWrapper.enablePrivateRpcs = enableVmDeveloperMode;
   }
 
+  void setVerboseLogging(bool enableVerboseLogging) {
+    _verboseLogging.value = enableVerboseLogging;
+  }
+
   /// Change the value for the dense mode setting.
   void toggleDenseMode(bool enableDenseMode) {
     _denseMode.value = enableDenseMode;
@@ -111,6 +145,7 @@
   final _customPubRootDirectories = ListValueNotifier<String>([]);
   final _customPubRootDirectoriesAreBusy = ValueNotifier<bool>(false);
   final _busyCounter = ValueNotifier<int>(0);
+  static const _verboseLoggingStorageId = 'verboseLogging';
   static const _hoverEvalModeStorageId = 'inspector.hoverEvalMode';
   static const _customPubRootDirectoriesStoragePrefix =
       'inspector.customPubRootDirectories';
diff --git a/packages/devtools_app/pubspec.yaml b/packages/devtools_app/pubspec.yaml
index 6bbe129..4cd0f57 100644
--- a/packages/devtools_app/pubspec.yaml
+++ b/packages/devtools_app/pubspec.yaml
@@ -44,6 +44,7 @@
   intl: '>=0.16.1 <0.18.0'
   js: ^0.6.1+1
   leak_tracker: 2.0.1
+  logging: ^1.1.1
   mime: ^1.0.0
   path: ^1.8.0
   perfetto_compiled: