Logging page

- Per isolate logging page.
- Drop down to control log level threshold.
- Console area where messages are displayed.
- UI test.

R=rmacnak@google.com

Review URL: https://codereview.chromium.org//1250853006 .
This commit is contained in:
John McCutchan 2015-07-22 14:33:13 -07:00
parent ea65b867d3
commit d3154f4247
11 changed files with 347 additions and 0 deletions

View file

@ -36,6 +36,7 @@ export 'package:observatory/src/elements/isolate_view.dart';
export 'package:observatory/src/elements/json_view.dart';
export 'package:observatory/src/elements/library_ref.dart';
export 'package:observatory/src/elements/library_view.dart';
export 'package:observatory/src/elements/logging.dart';
export 'package:observatory/src/elements/metrics.dart';
export 'package:observatory/src/elements/nav_bar.dart';
export 'package:observatory/src/elements/object_common.dart';

View file

@ -29,6 +29,7 @@
<link rel="import" href="src/elements/json_view.html">
<link rel="import" href="src/elements/library_ref.html">
<link rel="import" href="src/elements/library_view.html">
<link rel="import" href="src/elements/logging.html">
<link rel="import" href="src/elements/metrics.html">
<link rel="import" href="src/elements/nav_bar.html">
<link rel="import" href="src/elements/object_common.html">

View file

@ -138,6 +138,7 @@ class ObservatoryApplication extends Observable {
_pageRegistry.add(new ErrorViewPage(this));
_pageRegistry.add(new MetricsPage(this));
_pageRegistry.add(new PortsPage(this));
_pageRegistry.add(new LoggingPage(this));
// Note that ErrorPage must be the last entry in the list as it is
// the catch all.
_pageRegistry.add(new ErrorPage(this));

View file

@ -294,6 +294,22 @@ class HeapSnapshotPage extends SimplePage {
}
}
class LoggingPage extends SimplePage {
LoggingPage(app) : super('logging', 'logging-page', app);
void _visit(Uri uri) {
super._visit(uri);
getIsolate(uri).then((isolate) {
if (element != null) {
/// Update the page.
LoggingPageElement page = element;
page.isolate = isolate;
}
});
}
}
class ErrorViewPage extends Page {
ErrorViewPage(app) : super(app);

View file

@ -191,6 +191,11 @@
See <a on-click="{{ goto }}" _href="{{ gotoLink('/ports', isolate) }}">ports</a>
</div>
</div>
<div class="memberItem">
<div class="memberValue">
See <a on-click="{{ goto }}" _href="{{ gotoLink('/logging', isolate) }}">logging</a>
</div>
</div>
<!-- Temporarily disabled until UI for dart:io is acceptable.
<template if="{{ isolate.ioEnabled }}">
<div class="memberItem">

View file

@ -0,0 +1,202 @@
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
library logging_page;
import 'dart:async';
import 'dart:html';
import 'observatory_element.dart';
import 'package:logging/logging.dart';
import 'package:observatory/service.dart';
import 'package:observatory/app.dart';
import 'package:observatory/elements.dart';
import 'package:observatory/utils.dart';
import 'package:polymer/polymer.dart';
@CustomTag('logging-page')
class LoggingPageElement extends ObservatoryElement {
static const _kPageSelector = '#page';
static const _kLogSelector = '#log';
static const _kSeverityLevelSelector = '#severityLevelSelector';
LoggingPageElement.created() : super.created();
domReady() {
super.domReady();
_insertLevels();
}
attached() {
super.attached();
_sub();
_resizeSubscription = window.onResize.listen((_) => _updatePageHeight());
_updatePageHeight();
// Turn on the periodic poll timer for this page.
pollPeriod = const Duration(milliseconds:100);
}
detached() {
super.detached();
if (_resizeSubscription != null) {
_resizeSubscription.cancel();
_resizeSubscription = null;
}
_unsub();
}
void onPoll() {
_flushPendingLogs();
}
_updatePageHeight() {
HtmlElement e = shadowRoot.querySelector(_kPageSelector);
final totalHeight = window.innerHeight;
final top = e.offset.top;
final bottomMargin = 32;
final mainHeight = totalHeight - top - bottomMargin;
e.style.setProperty('height', '${mainHeight}px');
}
_insertLevels() {
SelectElement severityLevelSelector =
shadowRoot.querySelector(_kSeverityLevelSelector);
severityLevelSelector.children.clear();
_maxLevelLabelLength = 0;
for (var level in Level.LEVELS) {
var option = new OptionElement();
option.value = level.value.toString();
option.label = level.name;
if (level.name.length > _maxLevelLabelLength) {
_maxLevelLabelLength = level.name.length;
}
severityLevelSelector.children.add(option);
}
severityLevelSelector.selectedIndex = 0;
severityLevel = Level.ALL.value.toString();
}
_reset() {
logRecords.clear();
_unsub();
_sub();
_renderFull();
}
_unsub() {
cancelFutureSubscription(_loggingSubscriptionFuture);
_loggingSubscriptionFuture = null;
}
_sub() {
if (_loggingSubscriptionFuture != null) {
// Already subscribed.
return;
}
_loggingSubscriptionFuture =
app.vm.listenEventStream(Isolate.kLoggingStream, _onEvent);
}
_append(Map logRecord) {
logRecords.add(logRecord);
if (_shouldDisplay(logRecord)) {
// Queue for display.
pendingLogRecords.add(logRecord);
}
}
Element _renderAppend(Map logRecord) {
DivElement logContainer = shadowRoot.querySelector(_kLogSelector);
var element = new DivElement();
element.classes.add('logItem');
element.classes.add(logRecord['level'].name);
element.appendText(
'${logRecord["level"].name.padLeft(_maxLevelLabelLength)} '
'${Utils.formatDateTime(logRecord["time"])} '
'${logRecord["message"].valueAsString}\n');
logContainer.children.add(element);
return element;
}
_renderFull() {
DivElement logContainer = shadowRoot.querySelector(_kLogSelector);
logContainer.children.clear();
pendingLogRecords.clear();
for (var logRecord in logRecords) {
if (_shouldDisplay(logRecord)) {
_renderAppend(logRecord);
}
}
_scrollToBottom(logContainer);
}
/// Is [container] scrolled to the within [threshold] pixels of the bottom?
static bool _isScrolledToBottom(DivElement container, [int threshold = 2]) {
if (container == null) {
return false;
}
// scrollHeight -> complete height of element including scrollable area.
// clientHeight -> height of element on page.
// scrollTop -> how far is an element scrolled (from 0 to scrollHeight).
final distanceFromBottom =
container.scrollHeight - container.clientHeight - container.scrollTop;
const threshold = 2; // 2 pixel slop.
return distanceFromBottom <= threshold;
}
/// Scroll [container] so the bottom content is visible.
static _scrollToBottom(DivElement container) {
if (container == null) {
return;
}
// Adjust scroll so that the bottom of the content is visible.
container.scrollTop = container.scrollHeight - container.clientHeight;
}
_flushPendingLogs() {
DivElement logContainer = shadowRoot.querySelector(_kLogSelector);
bool autoScroll = _isScrolledToBottom(logContainer);
var lastElement;
for (var logRecord in pendingLogRecords) {
lastElement = _renderAppend(logRecord);
}
if (autoScroll) {
_scrollToBottom(logContainer);
}
pendingLogRecords.clear();
}
_onEvent(ServiceEvent event) {
assert(event.kind == Isolate.kLoggingStream);
_append(event.logRecord);
}
void isolateChanged(oldValue) {
_reset();
}
void severityLevelChanged(oldValue) {
_severityLevelValue = int.parse(severityLevel);
_renderFull();
}
Future clear() {
logRecords.clear();
pendingLogRecords.clear();
_renderFull();
return new Future.value(null);
}
bool _shouldDisplay(Map logRecord) {
return logRecord['level'].value >= _severityLevelValue;
}
@observable Isolate isolate;
@observable String severityLevel;
int _severityLevelValue = 0;
int _maxLevelLabelLength = 0;
Future<StreamSubscription> _loggingSubscriptionFuture;
StreamSubscription _resizeSubscription;
final List<Map> logRecords = new List<Map>();
final List<Map> pendingLogRecords = new List<Map>();
}

View file

@ -0,0 +1,77 @@
<link rel="import" href="../../../../packages/polymer/polymer.html">
<link rel="import" href="code_ref.html">
<link rel="import" href="function_ref.html">
<link rel="import" href="nav_bar.html">
<link rel="import" href="observatory_element.html">
<link rel="import" href="sliding_checkbox.html">
<link rel="import" href="view_footer.html">
<polymer-element name="logging-page" extends="observatory-element">
<template>
<link rel="stylesheet" href="css/shared.css">
<style>
.outlined {
-webkit-box-shadow: 0px 0px 2px 1px rgba(0,0,0,0.75);
-moz-box-shadow: 0px 0px 2px 1px rgba(0,0,0,0.75);
box-shadow: 0px 0px 2px 1px rgba(0,0,0,0.75);
margin: 4px;
}
.logItem {
font: normal 14px consolas, courier, monospace;
white-space: pre;
line-height: 125%;
width: 100%;
}
.FINEST {
background-color: #FAFAFA;
}
.FINER {
background-color: #ECEFF1;
}
.FINE {
background-color: #EFEBE9;
}
.CONFIG {
background-color: #FFF3E0;
}
.INFO {
background-color: #F1F8E9;
}
.WARNING {
background-color: #FFE0B2;
}
.SEVERE {
background-color: #FFCCBC;
}
.SHOUT {
background-color: #FFCDD2;
}
#log {
height: 100%;
width: 100%;
overflow-y: auto;
padding: 8px;
}
.fill {
height: 80%;
width: 100%;
}
</style>
<nav-bar>
<top-nav-menu></top-nav-menu>
<vm-nav-menu vm="{{ isolate.vm }}"></vm-nav-menu>
<isolate-nav-menu isolate="{{ isolate }}"></isolate-nav-menu>
<nav-menu link="{{ makeLink('/logging', isolate) }}" anchor="logging" last="{{ true }}"></nav-menu>
<nav-refresh callback="{{ clear }}" label="Clear"></nav-refresh>
</nav-bar>
<div id="page" class="content-centered-big">
<span>Show messages with severity <select id="severityLevelSelector" value="{{ severityLevel }}"></select> and higher</span>
<hr>
<div class="flex-row fill">
<div id="log" class="outlined"></div>
</div>
</div>
</template>
</polymer-element>
<script type="application/dart" src="logging.dart"></script>

View file

@ -135,6 +135,13 @@ class Utils {
return '${seconds}s';
}
static String formatDateTime(DateTime now) {
return '${now.year}-${now.month}-${now.day} '
'${now.hour.toString().padLeft(2)}:'
'${now.minute.toString().padLeft(2)}:'
'${now.second.toString().padLeft(2)}';
}
static String formatSeconds(double x) {
return x.toStringAsFixed(2);
}

View file

@ -103,6 +103,8 @@
'lib/src/elements/library_ref.html',
'lib/src/elements/library_view.dart',
'lib/src/elements/library_view.html',
'lib/src/elements/logging.dart',
'lib/src/elements/logging.html',
'lib/src/elements/metrics.dart',
'lib/src/elements/metrics.html',
'lib/src/elements/nav_bar.dart',

View file

@ -0,0 +1,29 @@
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// VMOptions=--compile_all --error_on_bad_type --error_on_bad_override
import 'dart:async';
import 'dart:developer' as developer;
import 'package:logging/logging.dart';
main() {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((logRecord) {
developer.log(
sequenceNumber: logRecord.sequenceNumber,
millisecondsSinceEpoch: logRecord.time.millisecondsSinceEpoch,
level: logRecord.level.value,
name: logRecord.loggerName,
message: logRecord.message,
zone: null,
error: logRecord.error,
stackTrace: logRecord.stackTrace);
});
new Timer.periodic(new Duration(seconds: 1), (t) {
Logger.root.info('INFO MESSAGE');
});
new Timer.periodic(new Duration(seconds: 1), (t) {
Logger.root.fine('FINE MESSAGE');
});
}

View file

@ -0,0 +1,6 @@
0. Run dart --observe log.dart
1. Visit main isolate's logging page.
2. You should see 'INFO MESSAGE' and 'FINE MESSAGE'.
3. Adjust the level to be 'INFO' and see that 'FINE' messages are hidden.
4. Adjust the level to be 'FINE' and see that all messages are displayed.
5. Adjust the level to be 'SHOUT' and see that no messages are displayed.