changeset 225:fc747d2c2f6a

Add Byteman metrics view Reviewed-by: jkang Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/025171.html
author Andrew Azores <aazores@redhat.com>
date Mon, 25 Sep 2017 10:31:21 -0400
parents 6568fdab115d
children 78c7d1c23616
files mock-api/endpoints/jvm-byteman.endpoint.js package.json src/app/app.module.js src/app/components/jvm-info/byteman/byteman.html src/app/components/jvm-info/byteman/byteman.service.js src/app/components/jvm-info/byteman/byteman.service.spec.js src/app/components/jvm-info/byteman/en.locale.yaml src/app/components/jvm-info/byteman/metrics/byteman-metrics.component.js src/app/components/jvm-info/byteman/metrics/byteman-metrics.controller.js src/app/components/jvm-info/byteman/metrics/byteman-metrics.controller.spec.js src/app/components/jvm-info/byteman/metrics/byteman-metrics.html src/app/components/jvm-info/byteman/metrics/byteman-metrics.routing.js src/app/components/jvm-info/byteman/metrics/byteman-metrics.routing.spec.js src/app/components/jvm-info/byteman/metrics/en.locale.yaml
diffstat 14 files changed, 579 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/mock-api/endpoints/jvm-byteman.endpoint.js	Mon Sep 25 10:31:07 2017 -0400
+++ b/mock-api/endpoints/jvm-byteman.endpoint.js	Mon Sep 25 10:31:21 2017 -0400
@@ -24,6 +24,34 @@
       }
     ));
   });
+  server.app.get('/jvm-byteman/0.0.1/metrics/jvms/:jvmId', function (req, res) {
+    server.logRequest('jvm-byteman', req);
+
+    var jvmId = req.params.jvmId;
+
+    var response = [];
+    response.push({
+      agentId: 'foo-agentId',
+      jvmId: jvmId,
+      timeStamp: { $numberLong: Date.now().toString() },
+      marker: 'foo-marker',
+      payload: '{"action":"ExampleClass.method() called"}'
+    });
+    response.push({
+      agentId: 'foo-agentId',
+      jvmId: jvmId,
+      timeStamp: { $numberLong: Date.now().toString() },
+      marker: 'rand-marker',
+      payload: { doubleKey: Math.random() }
+    });
+
+    res.setHeader('Content-Type', 'application/json');
+    res.send(JSON.stringify(
+      {
+        response: response
+      }
+    ));
+  });
 
   // command channel
   server.init('byteman-command');
--- a/package.json	Mon Sep 25 10:31:07 2017 -0400
+++ b/package.json	Mon Sep 25 10:31:21 2017 -0400
@@ -15,6 +15,7 @@
     "angular-patternfly": "^4.4.1",
     "angular-translate": "^2.15.2",
     "angular-translate-interpolation-messageformat": "^2.15.2",
+    "angularjs-datatables": "^0.5.9",
     "babel-core": "^6.24.0",
     "babel-loader": "^7.0.0",
     "babel-plugin-angularjs-annotate": "^0.7.0",
--- a/src/app/app.module.js	Mon Sep 25 10:31:07 2017 -0400
+++ b/src/app/app.module.js	Mon Sep 25 10:31:21 2017 -0400
@@ -33,10 +33,17 @@
 import 'bootstrap';
 import 'bootstrap-switch';
 
+import 'angular-patternfly/node_modules/patternfly/node_modules/jquery/dist/jquery.js';
+import 'angular-patternfly/node_modules/patternfly/node_modules/datatables.net/js/jquery.dataTables.js';
+import 'angular-patternfly/node_modules/patternfly/node_modules/datatables.net-select/js/dataTables.select.js';
+import 'angularjs-datatables/dist/angular-datatables.min.js';
+import 'angularjs-datatables/dist/plugins/select/angular-datatables.select.min.js';
+
 import {default as authModule, config as authModBootstrap} from 'components/auth/auth.module.js';
 import authInterceptorFactory from './auth-interceptor.factory.js';
 
 require.ensure([], () => {
+  require('angular-patternfly/node_modules/datatables.net-dt/css/jquery.dataTables.css');
   require('angular-patternfly/node_modules/patternfly/dist/css/patternfly.css');
   require('angular-patternfly/node_modules/patternfly/dist/css/patternfly-additions.css');
   require('bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.css');
@@ -50,6 +57,7 @@
       'ui.bootstrap',
       'patternfly',
       'patternfly.navigation',
+      'patternfly.table',
       angularTranslate,
       authModule,
       // non-core modules
--- a/src/app/components/jvm-info/byteman/byteman.html	Mon Sep 25 10:31:07 2017 -0400
+++ b/src/app/components/jvm-info/byteman/byteman.html	Mon Sep 25 10:31:21 2017 -0400
@@ -1,6 +1,7 @@
 <div class="container-fluid" style="margin-top: 2vh;">
   <ul class="nav nav-tabs">
     <li ui-sref-active="active"><a ui-sref="jvmInfo.byteman.rules" translate>byteman.RULES_VIEW</a></li>
+    <li ui-sref-active="active"><a ui-sref="jvmInfo.byteman.metrics" translate>byteman.METRICS_VIEW</a></li>
   </ul>
   <div class="row">
     <ui-view></ui-view>
--- a/src/app/components/jvm-info/byteman/byteman.service.js	Mon Sep 25 10:31:07 2017 -0400
+++ b/src/app/components/jvm-info/byteman/byteman.service.js	Mon Sep 25 10:31:21 2017 -0400
@@ -32,6 +32,7 @@
 const LOAD_RULE_ACTION = 0;
 const UNLOAD_RULE_ACTION = 1;
 const INITIAL_LISTEN_PORT = -1;
+const METRICS_QUERY_LIMIT = 0;
 
 class BytemanService {
 
@@ -64,6 +65,37 @@
     return this._getJvmInfo(systemId, jvmId).then(res => res.mainClass);
   }
 
+  getMetrics (jvmId, oldestLimit) {
+    return this._http.get(urlJoin(this._gatewayUrl, 'jvm-byteman', '0.0.1', 'metrics', 'jvms', jvmId), {
+      params: {
+        query: `timeStamp>=${oldestLimit}`,
+        sort: '-timeStamp',
+        limit: METRICS_QUERY_LIMIT
+      }
+    })
+      .then(res => {
+        let results = [];
+        for (let i = 0; i < res.data.response.length; i++) {
+          let metric = res.data.response[i];
+          let payload;
+          if (typeof metric.payload === 'string') {
+            payload = JSON.parse(metric.payload);
+          } else {
+            payload = metric.payload;
+          }
+          let prop = Object.getOwnPropertyNames(payload)[0];
+
+          results.push({
+            timestamp: metric.timeStamp,
+            marker: metric.marker,
+            name: prop,
+            value: payload[prop]
+          });
+        }
+        return results;
+      });
+  }
+
   _sendCmdChanRequest (systemId, jvmId, action, rule) {
     let defer = this._q.defer();
 
--- a/src/app/components/jvm-info/byteman/byteman.service.spec.js	Mon Sep 25 10:31:07 2017 -0400
+++ b/src/app/components/jvm-info/byteman/byteman.service.spec.js	Mon Sep 25 10:31:21 2017 -0400
@@ -265,5 +265,59 @@
     });
   });
 
+  describe('getMetrics (jvmId, oldestLimit)', () => {
+    it('should resolve mock data when payload is a string type', done => {
+      let response = {
+        response: [
+          {
+            timeStamp: { $numberLong: '5000' },
+            marker: 'foo-marker',
+            payload: '{"action":"foo-method called"}'
+          }
+        ]
+      };
+      httpBackend.when('GET', 'http://example.com:1234/jvm-byteman/0.0.1/metrics/jvms/foo-systemId?limit=0&query=timeStamp%3E%3D6789&sort=-timeStamp')
+        .respond(response);
+      svc.getMetrics('foo-systemId', 6789).then(res => {
+        res.should.deepEqual([{
+          timestamp: { $numberLong: '5000' },
+          marker: 'foo-marker',
+          name: 'action',
+          value: 'foo-method called'
+        }]);
+        done();
+      });
+      httpBackend.expectGET('http://example.com:1234/jvm-byteman/0.0.1/metrics/jvms/foo-systemId?limit=0&query=timeStamp%3E%3D6789&sort=-timeStamp');
+      httpBackend.flush();
+      scope.$apply();
+    });
+
+    it('should resolve mock data when payload is a non-string type', done => {
+      let response = {
+        response: [
+          {
+            timeStamp: { $numberLong: '5000' },
+            marker: 'foo-marker',
+            payload: { doubleKey: 0.125 }
+          }
+        ]
+      };
+      httpBackend.when('GET', 'http://example.com:1234/jvm-byteman/0.0.1/metrics/jvms/foo-systemId?limit=0&query=timeStamp%3E%3D6789&sort=-timeStamp')
+        .respond(response);
+      svc.getMetrics('foo-systemId', 6789).then(res => {
+        res.should.deepEqual([{
+          timestamp: { $numberLong: '5000' },
+          marker: 'foo-marker',
+          name: 'doubleKey',
+          value: 0.125
+        }]);
+        done();
+      });
+      httpBackend.expectGET('http://example.com:1234/jvm-byteman/0.0.1/metrics/jvms/foo-systemId?limit=0&query=timeStamp%3E%3D6789&sort=-timeStamp');
+      httpBackend.flush();
+      scope.$apply();
+    });
+  });
+
 });
 
--- a/src/app/components/jvm-info/byteman/en.locale.yaml	Mon Sep 25 10:31:07 2017 -0400
+++ b/src/app/components/jvm-info/byteman/en.locale.yaml	Mon Sep 25 10:31:21 2017 -0400
@@ -1,2 +1,3 @@
 byteman:
   RULES_VIEW: Rules
+  METRICS_VIEW: Metrics
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/metrics/byteman-metrics.component.js	Mon Sep 25 10:31:21 2017 -0400
@@ -0,0 +1,40 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import BytemanMetricsController from './byteman-metrics.controller.js';
+
+export default angular
+  .module('byteman.metrics', [
+    BytemanMetricsController,
+    'patternfly',
+    'patternfly.table'
+  ])
+  .component('bytemanMetrics', {
+    controller: 'BytemanMetricsController',
+    template: require('./byteman-metrics.html')
+  })
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/metrics/byteman-metrics.controller.js	Mon Sep 25 10:31:21 2017 -0400
@@ -0,0 +1,104 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import service from '../byteman.service.js';
+import _ from 'lodash';
+
+const REFRESH_RATE = 5000; // five seconds
+const MAX_DATA_AGE = 600000; // ten minutes;
+
+class BytemanMetricsController {
+  constructor ($stateParams, $translate, $interval, bytemanService,
+    metricToNumberFilter, timestampToDateFilter) {
+    'ngInject';
+    this.jvmId = $stateParams.jvmId;
+    this._translate = $translate;
+    this._interval = $interval;
+    this._svc = bytemanService;
+    this._metricToNumber = metricToNumberFilter;
+
+    this.config = {
+      selectionMatchProp: 'timestamp',
+      showCheckBoxes: false,
+      itemsAvailable: false
+    };
+
+    this.items = [];
+
+    this._dataAgeLimit = MAX_DATA_AGE;
+
+    this._translate([
+      'byteman.metrics.TIMESTAMP_COL_HEADER',
+      'byteman.metrics.MARKER_COL_HEADER',
+      'byteman.metrics.NAME_COL_HEADER',
+      'byteman.metrics.VALUE_COL_HEADER'
+    ]).then(translations => {
+      this.columns = [
+        {
+          itemField: 'timestamp',
+          header: translations['byteman.metrics.TIMESTAMP_COL_HEADER'],
+          templateFn: timestampToDateFilter
+        },
+        { itemField: 'marker', header: translations['byteman.metrics.MARKER_COL_HEADER'] },
+        { itemField: 'name', header: translations['byteman.metrics.NAME_COL_HEADER'] },
+        { itemField: 'value', header: translations['byteman.metrics.VALUE_COL_HEADER'] }
+      ];
+    });
+  }
+
+  $onInit () {
+    this._update();
+    this._refresh = this._interval(() => this._update(), REFRESH_RATE);
+  }
+
+  $onDestroy () {
+    if (angular.isDefined(this._refresh)) {
+      this._interval.cancel(this._refresh);
+    }
+  }
+
+  set dataAgeLimit (val) {
+    this._dataAgeLimit = val;
+    this._update();
+  }
+
+  get dataAgeLimit () {
+    return this._dataAgeLimit.toString();
+  }
+
+  _update () {
+    this._svc.getMetrics(this.jvmId, Date.now() - this._dataAgeLimit).then(res => {
+      this.items = res;
+      this.config.itemsAvailable = true;
+    });
+  }
+}
+
+export default angular
+  .module('byteman.metrics.controller', [service])
+  .controller('BytemanMetricsController', BytemanMetricsController)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/metrics/byteman-metrics.controller.spec.js	Mon Sep 25 10:31:21 2017 -0400
@@ -0,0 +1,149 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import controllerModule from './byteman-metrics.controller.js';
+
+describe('BytemanMetricsController', () => {
+
+  let ctrl, translate, interval, svc, metricToNumber, timestampToDate;
+  beforeEach(() => {
+    angular.mock.module(controllerModule);
+
+    translate = sinon.stub().returns({
+      then: sinon.stub().yields({
+        'byteman.metrics.TIMESTAMP_COL_HEADER': 'Time Stamp',
+        'byteman.metrics.MARKER_COL_HEADER': 'Marker',
+        'byteman.metrics.NAME_COL_HEADER': 'Name',
+        'byteman.metrics.VALUE_COL_HEADER': 'Value'
+      })
+    });
+
+    interval = sinon.stub().returns('interval-mock');
+    interval.cancel = sinon.spy();
+
+    svc = {};
+    svc.then = sinon.stub();
+    svc.getMetrics = sinon.stub().returns({ then: svc.then });
+
+    metricToNumber = sinon.stub().returns(123);
+
+    timestampToDate = sinon.stub().returns('mock date');
+
+    let timestamp = Date.now();
+    sinon.stub(Date, 'now').returns(timestamp);
+
+    angular.mock.inject($controller => {
+      'ngInject';
+      ctrl = $controller('BytemanMetricsController', {
+        $stateParams: { jvmId: 'foo-jvmId' },
+        $translate: translate,
+        $interval: interval,
+        bytemanService: svc,
+        metricToNumberFilter: metricToNumber,
+        timestampToDateFilter: timestampToDate
+      });
+    });
+  });
+
+  afterEach(() => {
+    Date.now.restore();
+  });
+
+  describe('$onInit ()', () => {
+    it('should start updating', () => {
+      interval.should.not.be.called();
+      svc.getMetrics.should.not.be.called();
+
+      ctrl.$onInit();
+
+      interval.should.be.calledOnce();
+      interval.should.be.calledWith(sinon.match.func, 5000);
+      ctrl.should.have.ownProperty('_refresh');
+      ctrl._refresh.should.equal('interval-mock');
+      svc.getMetrics.should.be.calledOnce();
+
+      let updateFn = interval.firstCall.args[0];
+      updateFn();
+      svc.getMetrics.should.be.calledTwice();
+    });
+  });
+
+  describe('$onDestroy ()', () => {
+    it('should cancel refresh if started', () => {
+      interval.cancel.should.not.be.called();
+      ctrl.$onInit();
+      interval.cancel.should.not.be.called();
+      ctrl.$onDestroy();
+      interval.cancel.should.be.calledOnce();
+      interval.cancel.should.be.calledWith('interval-mock');
+    });
+
+    it('should do nothing if not started', () => {
+      interval.cancel.should.not.be.called();
+      ctrl.$onDestroy();
+      interval.cancel.should.not.be.called();
+    });
+  });
+
+  describe('dataAgeLimit', () => {
+    it('should trigger update with new limit', () => {
+      svc.getMetrics.should.not.be.called();
+      Date.now.returns(100000);
+      ctrl.dataAgeLimit = '30000';
+      svc.getMetrics.should.be.calledWith('foo-jvmId', 100000 - 30000);
+    });
+
+    it('should reflect in getter', () => {
+      ctrl.dataAgeLimit = '30000';
+      ctrl.dataAgeLimit.should.equal('30000');
+    });
+  });
+
+  describe('_update ()', () => {
+    it('should use jvmId and current time minus age limit', () => {
+      Date.now.returns(100000);
+      ctrl._dataAgeLimit = 30000;
+      ctrl._update();
+      svc.getMetrics.should.be.calledOnce();
+      svc.getMetrics.should.be.calledWith('foo-jvmId', 100000 - 30000);
+    });
+
+    it('should set items from service', () => {
+      let items = [{
+        timestamp: 72000,
+        marker: 'foo-marker',
+        name: 'action',
+        value: 'foo-method called'
+      }];
+      svc.then.yields(items);
+      ctrl._update();
+      ctrl.items.should.deepEqual(items);
+      ctrl.config.itemsAvailable.should.be.true();
+    });
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/metrics/byteman-metrics.html	Mon Sep 25 10:31:21 2017 -0400
@@ -0,0 +1,16 @@
+<div class="col-xs-12 col-md-3">
+  <label for="dataAgeCombo" class="label label-info" translate>byteman.metrics.MAX_DATA_AGE_LABEL</label>
+  <select name="dataAgeCombo" class="combobox form-control" ng-model="$ctrl.dataAgeLimit">
+    <option value="10000" translate="byteman.metrics.dataAge.SECONDS" translate-values="{ SECONDS: 10 }" translate-interpolation="messageformat"></option>
+    <option value="30000" translate="byteman.metrics.dataAge.SECONDS" translate-values="{ SECONDS: 30 }" translate-interpolation="messageformat"></option>
+    <option value="60000" translate="byteman.metrics.dataAge.MINUTES" translate-values="{ MINUTES: 1 }" translate-interpolation="messageformat"></option>
+    <option value="600000" selected translate="byteman.metrics.dataAge.MINUTES" translate-values="{ MINUTES: 10, DEFAULT: true }" translate-interpolation="messageformat"></option>
+    <option value="900000" translate="byteman.metrics.dataAge.MINUTES" translate-values="{ MINUTES: 15 }" translate-interpolation="messageformat"></option>
+    <option value="1800000" translate="byteman.metrics.dataAge.MINUTES" translate-values="{ MINUTES: 30 }" translate-interpolation="messageformat"></option>
+  </select>
+</div>
+
+<pf-table-view
+                                                                                                config="$ctrl.config"
+                                                                                                items="$ctrl.items"
+                                                                                                columns="$ctrl.columns"></pf-table-view>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/metrics/byteman-metrics.routing.js	Mon Sep 25 10:31:21 2017 -0400
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+function config ($stateProvider) {
+  'ngInject';
+
+  $stateProvider.state('jvmInfo.byteman.metrics', {
+    url: '/metrics',
+    component: 'bytemanMetrics',
+    resolve: {
+      lazyLoad: ($q, $ocLazyLoad) => {
+        'ngInject';
+        return $q(resolve => {
+          require.ensure(['./byteman-metrics.component.js'], () => {
+            let module = require('./byteman-metrics.component.js');
+            $ocLazyLoad.load({ name: module.default });
+            resolve(module);
+          });
+        });
+      }
+    }
+  });
+}
+
+export { config };
+
+export default angular
+  .module('byteman.metrics.routing', [
+    'ui.router',
+    'oc.lazyLoad'
+  ])
+  .config(config)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/metrics/byteman-metrics.routing.spec.js	Mon Sep 25 10:31:21 2017 -0400
@@ -0,0 +1,77 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+describe('BytemanMetricsRouting', () => {
+
+  let module = require('./byteman-metrics.routing.js');
+  let stateProvider, args, q, ocLazyLoad;
+  beforeEach(() => {
+    angular.mock.module(module.default);
+    stateProvider = {
+      state: sinon.spy()
+    };
+    module.config(stateProvider);
+    args = stateProvider.state.args[0];
+    q = sinon.spy();
+    ocLazyLoad = {
+      load: sinon.spy()
+    };
+  });
+
+  describe('stateProvider', () => {
+    it('should call $stateProvider.state', () => {
+      stateProvider.state.should.be.calledOnce();
+    });
+
+    it('should define a \'jvmInfo.byteman.metrics\' state', () => {
+      args[0].should.equal('jvmInfo.byteman.metrics');
+    });
+
+    it('should map to /metrics', () => {
+      args[1].url.should.equal('/metrics');
+    });
+
+    it('resolve should load byteman-metrics component', done => {
+      let resolveFn = args[1].resolve.lazyLoad[2];
+      resolveFn.should.be.a.Function();
+      resolveFn(q, ocLazyLoad);
+      q.should.be.calledOnce();
+
+      let deferred = q.args[0][0];
+      deferred.should.be.a.Function();
+
+      let resolve = sinon.stub().callsFake(val => {
+        let mod = require('./byteman-metrics.component.js');
+        ocLazyLoad.load.should.be.calledWith({ name: mod.default });
+        val.should.equal(mod);
+        done();
+      });
+      deferred(resolve);
+    });
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/jvm-info/byteman/metrics/en.locale.yaml	Mon Sep 25 10:31:21 2017 -0400
@@ -0,0 +1,11 @@
+byteman:
+  metrics:
+    TIMESTAMP_COL_HEADER: Time Stamp
+    MARKER_COL_HEADER: Marker
+    NAME_COL_HEADER: Name
+    VALUE_COL_HEADER: Value
+
+    MAX_DATA_AGE_LABEL: Max Data Age
+    dataAge:
+      SECONDS: '{SECONDS, plural, =0{0 Seconds} one{1 Second} other{# Seconds}}{DEFAULT, select, true{ (Default)} other{}}'
+      MINUTES: '{MINUTES, plural, =0{0 Minutes} one{1 Minute} other{# Minutes}}{DEFAULT, select, true{ (Default)} other{}}'