Browse Source

feat(dashboard/stats): stats page

Ed Rooth 11 years ago
parent
commit
ef99b9ac49

+ 2 - 1
mod/dashboard/Gruntfile.js

@@ -121,7 +121,8 @@ module.exports = function(grunt) {
         files: {
           src: [
             '<%= config.appPath %>/*.js',
-            '<%= config.appPath %>/{module,page}**/*.js'
+            '<%= config.appPath %>/{module,page}**/*.js',
+            '!<%= config.appPath %>/vega.js'
           ]
         }
       }

+ 1 - 1
mod/dashboard/app/etcd-dashboard.js

@@ -91,7 +91,7 @@ etcdDashboard.config(function($routeProvider, $locationProvider, $httpProvider,
 
   // Show toast for any non-suppressed http response errors.
   $rootScope.$on(CORE_EVENT.RESP_ERROR, function(e, rejection) {
-    var errorMsg = 'An error occurred';
+    var errorMsg = 'Request Error';
     if (rejection.data && rejection.data.message) {
       errorMsg = rejection.data.message;
     }

+ 1 - 0
mod/dashboard/app/main.scss

@@ -16,6 +16,7 @@
 
 // UI Modules
 @import "ui/breadcrumb";
+@import "ui/latency-graph";
 
 // Pages
 @import "page/browser/browser";

+ 43 - 13
mod/dashboard/app/module/node.js → mod/dashboard/app/module/etcd-api.js

@@ -6,7 +6,7 @@
 'use strict';
 
 angular.module('etcd.module')
-.factory('nodeSvc', function($http, $q, $, _, pathSvc, toastSvc) {
+.factory('etcdApiSvc', function($http, $q, $, _, pathSvc) {
 
   function createNode(node) {
     var payload  = {
@@ -78,22 +78,48 @@ angular.module('etcd.module')
   }
 
   function fetchStat(name) {
-    return $http.get(pathSvc.getStatFullKeyPath(name));
+    return $http.get(pathSvc.getStatFullKeyPath(name), {
+      supressNotifications: true
+    });
+  }
+
+  function getPeerUri(peerName) {
+    return fetchNode('/_etcd/machines/' + peerName)
+    .then(function(peerInfo) {
+      var data = decodeURIComponent(peerInfo.value);
+      data = data.replace(/&/g, '\",\"').replace(/=/g,'\":\"');
+      data = JSON.parse('{"' + data + '"}');
+      return data.etcd;
+    });
   }
 
   function getLeaderUri() {
-    return fetchStat('leader')
-    .then(function(resp) {
-      return fetchNode('/_etcd/machines/' + resp.data.leader)
-      .then(function(leaderNode) {
-        var data = decodeURIComponent(leaderNode.value);
-        data = data.replace(/&/g, '\",\"').replace(/=/g,'\":\"');
-        data = JSON.parse('{"' + data + '"}');
-        return data.etcd;
+    return fetchLeaderStats()
+    .then(function(stats) {
+      return getPeerUri(stats.leaderName);
+    });
+  }
+
+  function fetchPeerDetailStats(peerName) {
+    return getPeerUri(peerName).then(function(peerUri) {
+      return $http.get(peerUri + pathSvc.getStatFullKeyPath('self'))
+      .then(function(resp) {
+        return resp.data;
       });
-    })
-    .catch(function() {
-      toastSvc.error('Error fetching leader.');
+    });
+  }
+
+  function fetchLeaderStats() {
+    return fetchStat('leader').then(function(resp) {
+      var result = {
+        followers: [],
+        leaderName: resp.data.leader
+      };
+      _.each(resp.data.followers, function(value, key) {
+        value.name = key;
+        result.followers.push(value);
+      });
+      return result;
     });
   }
 
@@ -102,6 +128,10 @@ angular.module('etcd.module')
 
     fetchStat: fetchStat,
 
+    fetchLeaderStats: fetchLeaderStats,
+
+    fetchPeerDetailStats: fetchPeerDetailStats,
+
     getLeaderUri: getLeaderUri,
 
     create: createNode,

+ 2 - 2
mod/dashboard/app/page/browser/browser-ctrl.js

@@ -1,7 +1,7 @@
 'use strict';
 
 angular.module('etcd.page')
-.controller('BrowserCtrl', function($scope, $modal, nodeSvc, pathSvc,
+.controller('BrowserCtrl', function($scope, $modal, etcdApiSvc, pathSvc,
       ETCD_EVENT, d3, pollerSvc) {
 
   $scope.currPath = '/';
@@ -60,7 +60,7 @@ angular.module('etcd.page')
   };
 
   $scope.fetchNode = function() {
-    return nodeSvc.fetch($scope.currPath)
+    return etcdApiSvc.fetch($scope.currPath)
     .then(function(node) {
       $scope.currNode = node;
     });

+ 2 - 2
mod/dashboard/app/page/browser/create-node-ctrl.js

@@ -2,7 +2,7 @@
 
 angular.module('etcd.page')
 .controller('CreateNodeCtrl', function($scope, $rootScope, $modalInstance, _,
-      ETCD_EVENT, nodeSvc, pathSvc, key) {
+      ETCD_EVENT, etcdApiSvc, pathSvc, key) {
 
   $scope.key = key;
   if (key === '/') {
@@ -12,7 +12,7 @@ angular.module('etcd.page')
   }
 
   $scope.save = function(node) {
-    $scope.requestPromise = nodeSvc.create(node)
+    $scope.requestPromise = etcdApiSvc.create(node)
     .then(function() {
       $rootScope.$broadcast(ETCD_EVENT.NODE_CHANGED, node);
       $modalInstance.close(node);

+ 2 - 2
mod/dashboard/app/page/browser/edit-node-ctrl.js

@@ -2,14 +2,14 @@
 
 angular.module('etcd.page')
 .controller('EditNodeCtrl', function($scope, $rootScope, $modalInstance, _,
-      ETCD_EVENT, nodeSvc, pathSvc, node) {
+      ETCD_EVENT, etcdApiSvc, pathSvc, node) {
 
   $scope.node = node;
 
   $scope.displayKey = pathSvc.truncate(node.key, 50) + '/'
 
   $scope.save = function(node) {
-    $scope.requestPromise = nodeSvc.save(node)
+    $scope.requestPromise = etcdApiSvc.save(node)
     .then(function() {
       $rootScope.$broadcast(ETCD_EVENT.NODE_CHANGED, node);
       $modalInstance.close(node);

+ 2 - 2
mod/dashboard/app/page/browser/edit-ttl-ctrl.js

@@ -2,13 +2,13 @@
 
 angular.module('etcd.page')
 .controller('EditTtlCtrl', function($scope, $rootScope, $modalInstance, _,
-      ETCD_EVENT, nodeSvc, node) {
+      ETCD_EVENT, etcdApiSvc, node) {
 
   $scope.node = node;
 
   $scope.save = function(ttl) {
     node.ttl = ttl;
-    $scope.requestPromise = nodeSvc.save(node)
+    $scope.requestPromise = etcdApiSvc.save(node)
     .then(function() {
       $rootScope.$broadcast(ETCD_EVENT.NODE_CHANGED, node);
       $modalInstance.close(node);

+ 30 - 0
mod/dashboard/app/page/stats/_stats.scss

@@ -0,0 +1,30 @@
+.ed-p-stats {
+  .panel-body {
+    padding: 30px;
+  }
+}
+
+.ed-m-square-status {
+  height: 10px;
+  width: 10px;
+  display: inline-block;
+  margin-right: 5px;
+
+  &.ed-m-square-status--green {
+    background-color: #00DB24;
+  }
+
+  &.ed-m-square-status--orange {
+    background-color: #FFC000;
+  }
+
+  &.ed-m-square-status--red {
+    background-color: #c40022;
+  }
+}
+
+.ed-p-stats__leader-container {
+  padding-bottom: 20px;
+  margin-bottom: 20px;
+  border-bottom: 1px solid #999;
+}

+ 59 - 3
mod/dashboard/app/page/stats/stats-ctrl.js

@@ -1,7 +1,63 @@
 'use strict';
 
 angular.module('etcd.page')
-.controller('StatsCtrl', function($scope) {
-  $scope.hi = 'hello';
-});
+.controller('StatsCtrl', function($scope, $modal, etcdApiSvc, pollerSvc) {
+
+  $scope.followers = null;
+  $scope.leader = null;
+  $scope.leaderName = null;
+
+  $scope.parseLatencyStats = function(stats) {
+    $scope.followers = stats.followers;
+    $scope.leaderName = stats.leaderName;
+  };
+
+  $scope.fetchLeaderDetails = function() {
+    return etcdApiSvc.fetchPeerDetailStats($scope.leaderName)
+    .then(function(leaderSelfStats) {
+      $scope.leader = {
+        name: $scope.leaderName,
+        uptime: leaderSelfStats.leaderInfo.uptime
+      };
+    });
+  };
+
+  $scope.$watch('leaderName', function(leaderName) {
+    if (leaderName) {
+      pollerSvc.kill('leaderDetailsPoller');
+      pollerSvc.register('leaderDetailsPoller', {
+        fn: $scope.fetchLeaderDetails,
+        scope: $scope,
+        interval: 5000
+      });
+    }
+  });
 
+  $scope.openDetailModal = function(peerName) {
+    $modal.open({
+      templateUrl: '/page/stats/stats-detail.html',
+      controller: 'StatsDetailCtrl',
+      resolve: {
+        peerName: d3.functor(peerName),
+      }
+    });
+  };
+
+  $scope.getSquareStatusClass = function(follower) {
+    if (follower.latency.current < 25) {
+      return 'ed-m-square-status--green';
+    }
+    if (follower.latency.current < 60) {
+      return 'ed-m-square-status--orange';
+    }
+    return 'ed-m-square-status--red';
+  };
+
+  pollerSvc.register('statsPoller', {
+    fn: etcdApiSvc.fetchLeaderStats,
+    then: $scope.parseLatencyStats,
+    scope: $scope,
+    interval: 500
+  });
+
+});

+ 19 - 0
mod/dashboard/app/page/stats/stats-detail-ctrl.js

@@ -0,0 +1,19 @@
+'use strict';
+
+angular.module('etcd.page')
+.controller('StatsDetailCtrl', function($scope, $modalInstance, _, etcdApiSvc,
+      peerName) {
+
+  etcdApiSvc.fetchPeerDetailStats(peerName)
+  .then(function(stats) {
+    $scope.stats = stats;
+    $scope.objectKeys = _.without(_.keys($scope.stats), '$$hashKey');
+  });
+
+  $scope.identityFn = _.identity;
+
+  $scope.close = function() {
+    $modalInstance.dismiss('close');
+  };
+
+});

+ 28 - 0
mod/dashboard/app/page/stats/stats-detail.html

@@ -0,0 +1,28 @@
+<div class="ed-p-stats-info">
+
+  <div class="modal-header">
+    <h4 class="modal-title">Peer Stats</h4>
+  </div>
+
+  <div class="modal-body">
+    <table id="ed-m-property-table" class="table">
+      <thead>
+        <tr>
+          <th>property</th>
+          <th>value</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr ng-repeat="key in objectKeys | orderBy:identityFn">
+          <td>{{key}}</td>
+          <td>{{stats[key]}}</td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+
+  <div class="modal-footer">
+    <button type="button" ng-click="close()" class="btn btn-primary">Close</button>
+  </div>
+
+</div>

+ 64 - 1
mod/dashboard/app/page/stats/stats.html

@@ -1 +1,64 @@
-stats
+<div class="ed-p-stats">
+  <co-nav-title title="Stats"></co-nav-title>
+  <div class="row">
+
+
+    <div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
+      <div class="panel co-m-panel co-fx-box-shadow-heavy">
+        <div class="panel-body">
+          <h2>Peer Latency</h2>
+          <co-latency-graph peer-data="followers"></co-latency-graph>
+        </div>
+      </div>
+    </div>
+
+
+
+    <div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
+      <div class="panel co-m-panel co-fx-box-shadow-heavy">
+        <div class="panel-body">
+          <div class="ed-p-stats__leader-container">
+            <h2>Leader</h2>
+            <dl>
+              <dt>Name</dt>
+              <dd><a ng-bind="leader.name" ng-click="openDetailModal(leader.name)" href="#"></a></dl>
+              <dt>Uptime</dt>
+              <dd ng-bind="leader.uptime"></dd>
+            </dl>
+          </div>
+
+          <h2>Followers</h2>
+          <table class="table co-m-table">
+            <thead>
+              <tr>
+                <th>Name</th>
+                <th>Latency</th>
+                <th>Raft Requests</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr ng-repeat="follower in followers | orderBy:'name' track by follower.name">
+                <td>
+                  <a href="#" ng-click="openDetailModal(follower.name)" ng-bind="follower.name"></a>
+                </td>
+                <td>
+                  <div class="ed-m-square-status" ng-class="getSquareStatusClass(follower)"></div>
+                  <span>{{follower.latency.current | number:1 }} ms</span>
+                </td>
+                <td>
+                  <span ng-highlight="follower.counts.fail">{{follower.counts.fail}} failures</span>,
+                  <span>{{follower.counts.success}} successes</span>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+
+        </div>
+      </div>
+    </div>
+
+
+
+
+  </div>
+</div>

+ 4 - 0
mod/dashboard/app/ui/_latency-graph.scss

@@ -0,0 +1,4 @@
+.ed-m-latency-graph {
+  min-height: 200px;
+  overflow: hidden;
+}

+ 93 - 0
mod/dashboard/app/ui/graph-config.js

@@ -0,0 +1,93 @@
+'use strict';
+
+angular.module('etcd.ui').value('graphConfig', {
+
+  'padding': {'top': 10, 'left': 5, 'bottom': 40, 'right': 10},
+  'data': [
+    {
+      'name': 'stats'
+    },
+    {
+      'name': 'thresholds',
+      'values': [50, 100]
+    }
+  ],
+  'scales': [
+    {
+      'name': 'y',
+      'type': 'ordinal',
+      'range': 'height',
+      'domain': {'data': 'stats', 'field': 'index'}
+    },
+    {
+      'name': 'x',
+      'range': 'width',
+      'domainMin': 0,
+      'domainMax': 100,
+      'nice': true,
+      'zero': true,
+      'domain': {'data': 'stats', 'field': 'data.latency.current'}
+    },
+    {
+      'name': 'color',
+      'type': 'linear',
+      'domain': [10, 50, 100, 1000000000],
+      'range': ['#00DB24', '#FFC000', '#c40022', '#c40022']
+    }
+  ],
+  'axes': [
+    {
+      'type': 'x',
+      'scale': 'x',
+      'ticks': 6,
+      'name': 'Latency (ms)'
+    },
+    {
+      'type': 'y',
+      'scale': 'y',
+      'properties': {
+        'ticks': {
+          'stroke': {'value': 'transparent'}
+        },
+        'majorTicks': {
+          'stroke': {'value': 'transparent'}
+        },
+        'labels': {
+          'fill': {'value': 'transparent'}
+        },
+        'axis': {
+          'stroke': {'value': '#333'},
+          'strokeWidth': {'value': 1}
+        }
+      }
+    }
+  ],
+  'marks': [
+    {
+      'type': 'rect',
+      'from': {'data': 'stats'},
+      'properties': {
+        'enter': {
+          'x': {'scale': 'x', 'value': 0},
+          'x2': {'scale': 'x', 'field': 'data.latency.current'},
+          'y': {'scale': 'y', 'field': 'index', 'offset': -1},
+          'height': {'value': 3},
+          'fill': {'scale':'color', 'field':'data.latency.current'}
+        }
+      }
+    },
+    {
+        'type': 'symbol',
+        'from': {'data': 'stats'},
+        'properties': {
+          'enter': {
+            'x': {'scale': 'x', 'field': 'data.latency.current'},
+            'y': {'scale': 'y', 'field': 'index'},
+            'size': {'value': 50},
+            'fill': {'value': '#000'}
+          }
+        }
+      }
+    ]
+
+});

+ 1 - 0
mod/dashboard/app/ui/latency-graph.html

@@ -0,0 +1 @@
+<div class="ed-m-latency-graph"></div>

+ 45 - 0
mod/dashboard/app/ui/latency-graph.js

@@ -0,0 +1,45 @@
+'use strict';
+
+angular.module('etcd.ui').directive('coLatencyGraph', function(etcdApiSvc,
+      $, d3, _, graphConfig) {
+
+  return {
+    templateUrl: '/ui/latency-graph.html',
+    restrict: 'E',
+    replace: true,
+    scope: {
+      peerData: '='
+    },
+    link: function postLink(scope, elem, attrs) {
+      var padding = 60;
+
+      function updateGraph() {
+        var width = elem.width() - padding,
+            height = 300;
+
+        vg.parse.spec(graphConfig, function(chart) {
+          chart({
+            el: elem[0],
+            data: {
+              'stats': scope.peerData
+            }
+          })
+          .width(width)
+          .height(height)
+          .update();
+        });
+      }
+
+      scope.$watch('peerData', function(peerData) {
+        if (!_.isEmpty(peerData)) {
+          updateGraph();
+        }
+      });
+
+      elem.on('$destroy', function() {
+      });
+
+    }
+  };
+
+});

+ 2 - 2
mod/dashboard/app/ui/node-cog.js

@@ -1,7 +1,7 @@
 'use strict';
 
 angular.module('etcd.ui')
-.directive('edNodeCog', function($modal, $rootScope, nodeSvc, toastSvc,
+.directive('edNodeCog', function($modal, $rootScope, etcdApiSvc, toastSvc,
       ETCD_EVENT) {
 
   return {
@@ -48,7 +48,7 @@ angular.module('etcd.ui')
                 btnText: d3.functor('Delete'),
                 errorFormatter: d3.functor('etcdApi'),
                 executeFn: _.identity.bind(null, function() {
-                  return nodeSvc.delete($scope.node)
+                  return etcdApiSvc.delete($scope.node)
                   .then(function() {
                     $rootScope.$broadcast(ETCD_EVENT.NODE_DELETED, $scope.node);
                   });