This key describes the metrics corresponding to the graph tooltip labels in more detail.
+
+ {{#each-in data.labels as |label desc| }}
+
{{label}}
+
{{{desc}}}
+ {{/each-in}}
+
+ {{#unless data.labels}}
+ No metrics loaded.
+ {{/unless}}
+
+
+
+
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui-v2/app/components/topology-metrics/series/index.js b/ui-v2/app/components/topology-metrics/series/index.js
index 7558ff5651..0e7e8ed6c2 100644
--- a/ui-v2/app/components/topology-metrics/series/index.js
+++ b/ui-v2/app/components/topology-metrics/series/index.js
@@ -27,14 +27,15 @@ export default Component.extend({
this.drawGraphs();
},
change: function(evt) {
- this.data = evt.data;
+ this.set('data', evt.data.series);
this.element.querySelector('.sparkline-loader').style.display = 'none';
this.drawGraphs();
+ this.rerender();
},
},
drawGraphs: function() {
- if (!this.data.series) {
+ if (!this.data) {
return;
}
@@ -50,13 +51,13 @@ export default Component.extend({
// To be safe, filter any series that actually have no data points. This can
// happen thanks to our current provider contract allowing empty arrays for
// series data if there is no value.
- //
- // TODO(banks): switch series provider data to be a single array with series
- // values as properties as we need below to enforce sensible alignment of
- // timestamps and explicit summing expectations.
- let series = ((this.data || {}).series || []).filter(s => s.data.length > 0);
+ let maybeData = this.data || {};
+ let series = maybeData.data || [];
+ let labels = maybeData.labels || {};
+ let unitSuffix = maybeData.unitSuffix || '';
+ let keys = Object.keys(labels).filter(l => l != 'Total');
- if (series.length == 0) {
+ if (series.length == 0 || keys.length == 0) {
// Put the graph in an error state that might get fixed if metrics show up
// on next poll.
let loader = this.element.querySelector('.sparkline-loader');
@@ -65,32 +66,26 @@ export default Component.extend({
return;
}
- // Fill the timestamps for x axis.
- let data = series[0].data.map(d => {
- return { time: d[0] };
- });
- let keys = [];
- // Initialize zeros
- let summed = this.data.series[0].data.map(d => 0);
-
- for (var i = 0; i < series.length; i++) {
- let s = series[i];
- // Attach the value as a new field to the data grid.
- s.data.map((d, idx) => {
- data[idx][s.label] = d[1];
- summed[idx] += d[1];
- });
- keys.push(s.label);
- }
-
let st = stack()
.keys(keys)
.order(stackOrderReverse);
- let stackData = st(data);
+ let stackData = st(series);
+
+ // Sum all of the values for each point to get max range. Technically
+ // stackData contains this but I didn't find reliable documentation on
+ // whether we can rely on the highest stacked area to always be first/last
+ // in array etc. so this is simpler.
+ let summed = series.map(d => {
+ let sum = 0;
+ keys.forEach(l => {
+ sum = sum + d[l];
+ });
+ return sum;
+ });
let x = scaleTime()
- .domain(extent(data, d => d.time))
+ .domain(extent(series, d => d.time))
.range([0, w]);
let y = scaleLinear()
@@ -126,6 +121,7 @@ export default Component.extend({
let tooltip = select(this.element.querySelector('.tooltip'));
tooltip.selectAll('.sparkline-tt-legend').remove();
+ tooltip.selectAll('.sparkline-tt-sum').remove();
for (var k of keys) {
let legend = tooltip.append('div').attr('class', 'sparkline-tt-legend');
@@ -137,13 +133,24 @@ export default Component.extend({
legend
.append('span')
- .text(k + ': ')
+ .text(k)
.append('span')
.attr('class', 'sparkline-tt-legend-value');
}
let tipVals = tooltip.selectAll('.sparkline-tt-legend-value');
+ // Add a label for the summed value
+ if (keys.length > 1) {
+ tooltip
+ .append('div')
+ .attr('class', 'sparkline-tt-sum')
+ .append('span')
+ .text('Total')
+ .append('span')
+ .attr('class', 'sparkline-tt-sum-value');
+ }
+
let self = this;
svg
.on('mouseover', function(e) {
@@ -152,10 +159,30 @@ export default Component.extend({
// We update here since we might redraw the graph with user's cursor
// stationary over it. If that happens mouseover fires but not
// mousemove but the tooltip and cursor are wrong (based on old data).
- self.updateTooltip(e, data, stackData, keys, x, tooltip, tipVals, cursor);
+ self.updateTooltip(
+ e,
+ series,
+ stackData,
+ summed,
+ unitSuffix,
+ x,
+ tooltip,
+ tipVals,
+ cursor
+ );
})
- .on('mousemove', function(e, d, i) {
- self.updateTooltip(e, data, stackData, keys, x, tooltip, tipVals, cursor);
+ .on('mousemove', function(e) {
+ self.updateTooltip(
+ e,
+ series,
+ stackData,
+ summed,
+ unitSuffix,
+ x,
+ tooltip,
+ tipVals,
+ cursor
+ );
})
.on('mouseout', function(e) {
tooltip.style('visibility', 'hidden');
@@ -168,7 +195,17 @@ export default Component.extend({
this.svg.on('mouseover mousemove mouseout', null);
}
},
- updateTooltip: function(e, data, stackData, keys, x, tooltip, tipVals, cursor) {
+ updateTooltip: function(
+ e,
+ series,
+ stackData,
+ summed,
+ unitSuffix,
+ x,
+ tooltip,
+ tipVals,
+ cursor
+ ) {
let [mouseX] = pointer(e);
cursor.attr('x', mouseX);
@@ -176,7 +213,7 @@ export default Component.extend({
var bisectTime = bisector(function(d) {
return d.time;
}).left;
- let tipIdx = bisectTime(data, mouseTime);
+ let tipIdx = bisectTime(series, mouseTime);
tooltip
// 22 px is the correction to align the arrow on the tool tip with
@@ -185,23 +222,15 @@ export default Component.extend({
.select('.sparkline-time')
.text(niceTimeWithSeconds(mouseTime));
+ // Get the summed value - that's the one of the top most stack.
+ tooltip.select('.sparkline-tt-sum-value').text(`${shortNumStr(summed[tipIdx])}${unitSuffix}`);
+
tipVals.nodes().forEach((n, i) => {
let val = stackData[i][tipIdx][1] - stackData[i][tipIdx][0];
- select(n).text(this.formatTooltip(keys[i], val));
+ select(n).text(`${shortNumStr(val)}${unitSuffix}`);
});
cursor.attr('x', mouseX);
- },
-
- formatTooltip: function(label, val) {
- switch (label) {
- case 'Data rate received':
- // fallthrough
- case 'Data rate transmitted':
- return dataRateStr(val);
- default:
- return shortNumStr(val);
- }
- },
+ }
});
// Duplicated in vendor/metrics-providers/prometheus.js since we want that to
diff --git a/ui-v2/app/components/topology-metrics/series/layout.scss b/ui-v2/app/components/topology-metrics/series/layout.scss
index 0f01add1c0..036b002df4 100644
--- a/ui-v2/app/components/topology-metrics/series/layout.scss
+++ b/ui-v2/app/components/topology-metrics/series/layout.scss
@@ -17,7 +17,7 @@
position: absolute;
z-index: 100;
bottom: 78px;
- width: 250px;
+ width: 217px;
}
.sparkline-tt-legend-color {
@@ -37,3 +37,35 @@
}
}
+// Key modal
+.sparkline-key {
+ .sparkline-key-content {
+ width: 500px;
+ min-height: 100px;
+
+ dl {
+ padding: 10px 0 0 0;
+ }
+ dt {
+ width: 125px;
+ float: left;
+ }
+ dd {
+ margin: 0 0 12px 135px;
+ }
+ }
+}
+
+.sparkline-key-link {
+ visibility: hidden;
+ float: right;
+ // TODO: this is a massive hack but we want it to be actually outside of the
+ // bounding box of this component. We could move it into the parent component
+ // but it's pretty tied up to the state - should only show if we have metrics
+ // loaded etc. I expect there is a cleaner way to refactor this though.
+ margin-top: -35px;
+ margin-right: 12px;
+}
+#metrics-container:hover .sparkline-key-link {
+ visibility: visible;
+}
\ No newline at end of file
diff --git a/ui-v2/app/components/topology-metrics/series/skin.scss b/ui-v2/app/components/topology-metrics/series/skin.scss
index db4c286806..104539e72d 100644
--- a/ui-v2/app/components/topology-metrics/series/skin.scss
+++ b/ui-v2/app/components/topology-metrics/series/skin.scss
@@ -1,31 +1,41 @@
-#metrics-container div .sparkline-wrapper {
+#metrics-container .sparkline-wrapper {
svg path {
stroke-width: 0;
}
.tooltip {
- padding: 5px 10px 10px 10px;
+ padding: 0 0 10px;
font-size: 0.875em;
line-height: 1.5em;
font-weight: normal;
- border: 1px solid #BAC1CC;
+ border: 1px solid $gray-300;
background: #fff;
border-radius: 2px;
box-sizing: border-box;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.05), 0px 4px 4px rgba(0, 0, 0, 0.1);
.sparkline-time {
- padding: 0;
+ padding: 8px 10px;
font-weight: bold;
font-size: 14px;
color: #000;
- margin-bottom: 5px;
+ border-bottom: 1px solid $gray-200;
+ margin-bottom: 4px;
+ text-align: center;
}
- .sparkline-tt-legend {
+ .sparkline-tt-legend,
+ .sparkline-tt-sum {
border: 0;
+ padding: 3px 10px 0 10px;
}
-
+
+ .sparkline-tt-sum {
+ border-top: 1px solid $gray-200;
+ margin-top: 4px;
+ padding: 8px 10px 0 10px;
+ }
+
.sparkline-tt-legend-color {
width: 12px;
height: 12px;
@@ -33,6 +43,11 @@
margin: 0 5px 0 0;
padding: 0;
}
+
+ .sparkline-tt-legend-value,
+ .sparkline-tt-sum-value {
+ float: right;
+ }
}
div.tooltip:before{
@@ -43,7 +58,7 @@
height: 12px;
left: 15px;
bottom: -7px;
- border: 1px solid #BAC1CC;
+ border: 1px solid $gray-300;
border-top: 0;
border-left: 0;
background: #fff;
@@ -51,3 +66,37 @@
}
}
+// Key modal
+.sparkline-key {
+ h3::before {
+ @extend %with-info-circle-fill-mask, %as-pseudo;
+ margin: 2px 3px 0 0;
+ font-size: 14px;
+ }
+
+ h3 {
+ color: $gray-900;
+ font-size: 16px;
+ }
+
+ .sparkline-key-content {
+ dt {
+ font-weight: 600;
+ }
+ dd {
+ color: $gray-500;
+ }
+ }
+}
+
+.sparkline-key-link {
+ color: $gray-500;
+}
+.sparkline-key-link:hover {
+ color: $blue-500;
+}
+#metrics-container:hover .sparkline-key-link::before {
+ @extend %with-info-circle-fill-mask, %as-pseudo;
+ margin: 1px 3px 0 0;
+ font-size: 12px;
+}
\ No newline at end of file
diff --git a/ui-v2/app/services/repository/metrics.js b/ui-v2/app/services/repository/metrics.js
index a7139857f7..96fc9ef8f5 100644
--- a/ui-v2/app/services/repository/metrics.js
+++ b/ui-v2/app/services/repository/metrics.js
@@ -11,6 +11,7 @@ const meta = {
export default RepositoryService.extend({
cfg: service('ui-config'),
+ error: null,
init: function() {
this._super(...arguments);
@@ -21,10 +22,21 @@ export default RepositoryService.extend({
opts.metrics_proxy_enabled = uiCfg.metrics_proxy_enabled;
// Inject the base app URL
const provider = uiCfg.metrics_provider || 'prometheus';
- this.provider = window.consul.getMetricsProvider(provider, opts);
+
+ try {
+ this.provider = window.consul.getMetricsProvider(provider, opts);
+ } catch(e) {
+ this.error = new Error(`metrics provider not initialized: ${e}`);
+ // Show the user the error once for debugging their provider outside UI
+ // Dev.
+ console.error(this.error);
+ }
},
findServiceSummary: function(protocol, slug, dc, nspace, configuration = {}) {
+ if (this.error) {
+ return Promise.reject(this.error);
+ }
const promises = [
// TODO: support namespaces in providers
this.provider.serviceRecentSummarySeries(slug, protocol, {}),
@@ -33,13 +45,16 @@ export default RepositoryService.extend({
return Promise.all(promises).then(function(results) {
return {
meta: meta,
- series: results[0].series,
+ series: results[0],
stats: results[1].stats,
};
});
},
findUpstreamSummary: function(slug, dc, nspace, configuration = {}) {
+ if (this.error) {
+ return Promise.reject(this.error);
+ }
return this.provider.upstreamRecentSummaryStats(slug, {}).then(function(result) {
result.meta = meta;
return result;
@@ -47,9 +62,12 @@ export default RepositoryService.extend({
},
findDownstreamSummary: function(slug, dc, nspace, configuration = {}) {
+ if (this.error) {
+ return Promise.reject(this.error);
+ }
return this.provider.downstreamRecentSummaryStats(slug, {}).then(function(result) {
result.meta = meta;
return result;
});
- },
+ }
});
diff --git a/ui-v2/vendor/metrics-providers/prometheus.js b/ui-v2/vendor/metrics-providers/prometheus.js
index bb280d8b2f..994b72843e 100644
--- a/ui-v2/vendor/metrics-providers/prometheus.js
+++ b/ui-v2/vendor/metrics-providers/prometheus.js
@@ -1,54 +1,81 @@
/*eslint no-console: "off"*/
(function () {
+ var emptySeries = { unitSuffix: "", labels: {}, data: [] }
+
var prometheusProvider = {
options: {},
/**
- * init is called when the provide is first loaded.
+ * init is called when the provider is first loaded.
*
* options.providerOptions contains any operator configured parameters
* specified in the Consul agent config that is serving the UI.
*
- * options.proxy.baseURL contains the base URL if the agent has a metrics
- * proxy configured. If it doesn't options.proxy will be null. The provider
- * should throw an Exception (TODO: specific type?) if it requires a metrics
- * proxy and one is not configured.
+ * Consul will provider a boolean options.metrics_proxy_enabled to indicate
+ * whether the agent has a metrics proxy configured.
+ *
+ * The provider should throw an Exception if the options are not valid for
+ * example because it requires a metrics proxy and one is not configured.
*/
init: function(options) {
this.options = options;
+ if (!this.options.metrics_proxy_enabled) {
+ throw new Error("prometheus metrics provider currently requires the ui_config.metrics_proxy to be configured in the Consul agent.");
+ }
},
/**
* serviceRecentSummarySeries should return time series for a recent time
* period summarizing the usage of the named service.
*
- * If these metrics aren't available then empty series may be returned.
+ * If these metrics aren't available then an empty series array may be
+ * returned.
*
* The period may (later) be specified in options.startTime and
* options.endTime.
*
* The service's protocol must be given as one of Consul's supported
* protocols e.g. "tcp", "http", "http2", "grpc". If it is empty or the
- * provider doesn't recognize it it should treat it as "tcp" and provide
- * just basic connection stats.
+ * provider doesn't recognize the protocol, it should treat it as "tcp" and
+ * provide basic connection stats.
*
* The expected return value is a promise which resolves to an object that
* should look like the following:
*
* {
- * series: [
+ * // The unitSuffix is shown after the value in tooltips. Values will be
+ * // rounded and shortened. Larger values will already have a suffix
+ * // like "10k". The suffix provided here is concatenated directly
+ * // allowing for suffixes like "mbps/kbps" by using a suffix of "bps".
+ * // If the unit doesn't make sense in this format, include a
+ * // leading space for example " rps" would show as "1.2k rps".
+ * unitSuffix: " rps",
+ *
+ * // The set of labels to graph. The key should exactly correspond to a
+ * // property of every data point in the array below except for the
+ * // special case "Total" which is used to show the sum of all the
+ * // stacked graph values. The key is displayed in the tooltop so it
+ * // should be human-friendly but as concise as possible. The value is a
+ * // longer description that is displayed in the graph's key on request
+ * // to explain exactly what the metrics mean.
+ * labels: {
+ * "Total": "Total inbound requests per second.",
+ * "Successes": "Successful responses (with an HTTP response code not in the 5xx range) per second.",
+ * "Errors": "Error responses (with an HTTP response code in the 5xx range) per second.",
+ * },
+ *
+ * data: [
* {
- * label: "Requests per second",
- * data: [...]
+ * time: 1600944516286, // milliseconds since Unix epoch
+ * "Successes": 1234.5,
+ * "Errors": 2.3,
* },
* ...
* ]
* }
*
- * Each time series' data array is simple an array of tuples with the first
- * being a Date object and the second a floating point value:
- *
- * [[Date(1600944516286), 1234.9], [Date(1600944526286), 1234.9], ...]
+ * Every data point object should have a value for every series label
+ * (except for "Total") otherwise it will be assumed to be "0".
*/
serviceRecentSummarySeries: function(serviceName, protocol, options) {
// Fetch time-series
@@ -62,36 +89,11 @@
options.end = now;
if (this.hasL7Metrics(protocol)) {
- series.push(this.fetchRequestRateSeries(serviceName, options))
- labels.push("Requests per second")
- series.push(this.fetchErrorRateSeries(serviceName, options))
- labels.push("Errors per second")
- } else {
- // Fallback to just L4 metrics.
- series.push(this.fetchServiceRxSeries(serviceName, options))
- labels.push("Data rate received")
- series.push(this.fetchServiceTxSeries(serviceName, options))
- labels.push("Data rate transmitted")
+ return this.fetchRequestRateSeries(serviceName, options);
}
- var all = Promise.allSettled(series).
- then(function(results){
- var data = { series: [] }
- for (var i = 0; i < series.length; i++) {
- if (results[i].value) {
- data.series.push({
- label: labels[i],
- data: results[i].value
- });
- } else if (results[i].reason) {
- console.log("ERROR processing series", labels[i], results[i].reason)
- }
- }
- return data
- })
-
- // Fetch the metrics async, and return a promise to the result.
- return all
+ // Fallback to just L4 metrics.
+ return this.fetchDataRateSeries(serviceName, options);
},
/**
@@ -174,8 +176,8 @@
},
/**
- * downstreamRecentSummaryStats should return four summary statistics for each
- * downstream service over a recent time period.
+ * downstreamRecentSummaryStats should return four summary statistics for
+ * each downstream service over a recent time period.
*
* If these metrics aren't available then an empty array may be returned.
*
@@ -188,9 +190,10 @@
* stats: {
* // Each downstream will appear as an entry keyed by the downstream
* // service name. The value is an array of stats with the same
- * // format as serviceRecentSummaryStats response.stats. Note that
- * // different downstreams might show different stats depending on
- * // their protocol.
+ * // format as serviceRecentSummaryStats response.stats. Different
+ * // downstreams may display different stats if required although the
+ * // protocol should be the same for all as it is the target
+ * // service's protocol that matters here.
* "downstream_name": [
* {label: "SR", desc: "...", value: "99%"},
* ...
@@ -276,59 +279,102 @@
return all
},
- reformatSeries: function(response) {
- // Handle empty results from prometheus.
- if (!response || !response.data || !response.data.result
- || response.data.result.length < 1) {
- return [];
- }
- // Reformat the prometheus data to be the format we want which is
- // essentially the same but with Date objects instead of unix timestamps.
- return response.data.result[0].values.map(function(val){
- return [new Date(val[0]*1000), parseFloat(val[1])]
- })
+ reformatSeries: function(unitSuffix, labelMap) {
+ return function(response) {
+ // Handle empty result sets gracefully.
+ if (!response.data || !response.data.result || response.data.result.length == 0
+ || !response.data.result[0].values
+ || response.data.result[0].values.length == 0) {
+ return emptySeries;
+ }
+ // Reformat the prometheus data to be the format we want with stacked
+ // values as object properties.
+
+ // Populate time values first based on first result since Prometheus will
+ // always return all the same points for all series in the query.
+ let series = response.data.result[0].values.map(function(d, i) {
+ return {
+ time: Math.round(d[0] * 1000),
+ };
+ });
+
+ // Then for each series returned populate the labels and values in the
+ // points.
+ response.data.result.map(function(d) {
+ d.values.map(function(p, i) {
+ series[i][d.metric.label] = parseFloat(p[1]);
+ });
+ });
+
+ return {
+ unitSuffix: unitSuffix,
+ labels: labelMap,
+ data: series
+ };
+ };
},
fetchRequestRateSeries: function(serviceName, options){
- var q = `sum(irate(envoy_listener_http_downstream_rq_xx{local_cluster="${serviceName}",envoy_http_conn_manager_prefix="public_listener_http"}[10m]))`
- return this.fetchSeries(q, options).then(this.reformatSeries, function(xhr){
- // Failure. log to console and return an blank result for now.
- console.log("ERROR: failed to fetch requestRate", xhr.responseText)
- return []
+ // We need the sum of all non-500 error rates as one value and the 500
+ // error rate as a separate series so that they stack to show the full
+ // request rate. Some creative label replacement makes this possible in
+ // one query.
+ var q = `sum by (label) (`+
+ // The outer label_replace catches 5xx error and relabels them as
+ // err=yes
+ `label_replace(`+
+ // The inner label_replace relabels all !5xx rates as err=no so they
+ // will get summed together.
+ `label_replace(`+
+ // Get rate of requests to the service
+ `irate(envoy_listener_http_downstream_rq_xx{local_cluster="${serviceName}",envoy_http_conn_manager_prefix="public_listener_http"}[10m])`+
+ // ... inner replacement matches all code classes except "5" and
+ // applies err=no
+ `, "label", "Successes", "envoy_response_code_class", "[^5]")`+
+ // ... outer replacement matches code=5 and applies err=yes
+ `, "label", "Errors", "envoy_response_code_class", "5")`+
+ `)`
+ var labelMap = {
+ Total: 'Total inbound requests per second',
+ Successes: 'Successful responses (with an HTTP response code not in the 5xx range) per second.',
+ Errors: 'Error responses (with an HTTP response code in the 5xx range) per second.',
+ };
+ return this.fetchSeries(q, options)
+ .then(this.reformatSeries(" rps", labelMap), function(xhr){
+ // Failure. log to console and return a blank result for now.
+ console.log('ERROR: failed to fetch requestRate', xhr.responseText)
+ return emptySeries;
})
},
- fetchErrorRateSeries: function(serviceName, options){
- // 100 * to get a result in percent
- var q = `sum(`+
- `irate(envoy_listener_http_downstream_rq_xx{`+
- `local_cluster="${serviceName}",`+
- `envoy_http_conn_manager_prefix="public_listener_http",`+
- `envoy_response_code_class="5"}[10m]`+
- `)`+
- `)`;
- return this.fetchSeries(q, options).then(this.reformatSeries, function(xhr){
- // Failure. log to console and return an blank result for now.
- console.log("ERROR: failed to fetch errorRate", xhr.responseText)
- return []
- })
- },
-
- fetchServiceRxSeries: function(serviceName, options){
- var q = `8 * sum(irate(envoy_tcp_downstream_cx_rx_bytes_total{local_cluster="${serviceName}", envoy_tcp_prefix="public_listener_tcp"}[10m]))`
- return this.fetchSeries(q, options).then(this.reformatSeries, function(xhr){
- // Failure. log to console and return an blank result for now.
- console.log("ERROR: failed to fetch rx data rate", xhr.responseText)
- return []
- })
- },
-
- fetchServiceTxSeries: function(serviceName, options){
- var q = `8 * sum(irate(envoy_tcp_downstream_cx_tx_bytes_total{local_cluster="${serviceName}", envoy_tcp_prefix="public_listener_tcp"}[10m]))`
- return this.fetchSeries(q, options).then(this.reformatSeries, function(xhr){
- // Failure. log to console and return an blank result for now.
- console.log("ERROR: failed to fetch tx data rate", xhr.responseText)
- return []
+ fetchDataRateSeries: function(serviceName, options){
+ // 8 * converts from bytes/second to bits/second
+ var q = `8 * sum by (label) (`+
+ // Label replace generates a unique label per rx/tx metric to stop them
+ // being summed together.
+ `label_replace(`+
+ // Get the tx rate
+ `irate(envoy_tcp_downstream_cx_tx_bytes_total{local_cluster="${serviceName}",envoy_tcp_prefix="public_listener_tcp"}[10m])`+
+ // Match all and apply the tx label
+ `, "label", "Outbound", "__name__", ".*"`+
+ // Union those vectors with the RX ones
+ `) or label_replace(`+
+ // Get the rx rate
+ `irate(envoy_tcp_downstream_cx_rx_bytes_total{local_cluster="${serviceName}",envoy_tcp_prefix="public_listener_tcp"}[10m])`+
+ // Match all and apply the rx label
+ `, "label", "Inbound", "__name__", ".*"`+
+ `)`+
+ `)`
+ var labelMap = {
+ Total: 'Total bandwidth',
+ Inbound: 'Inbound data rate (data recieved) from the network in bits per second.',
+ Outbound: 'Outbound data rate (data transmitted) from the network in bits per second.',
+ };
+ return this.fetchSeries(q, options)
+ .then(this.reformatSeries("bps", labelMap), function(xhr){
+ // Failure. log to console and return a blank result for now.
+ console.log('ERROR: failed to fetch requestRate', xhr.responseText)
+ return emptySeries;
})
},