ui: Replace NaN and undefined metrics values with `-` (#9200)

* ui: Add functionality to metrics mocks:

1. More randomness during blocking queries
2. NaN and undefined values that come from prometheus
3. General trivial amends to bring things closer to the style of the
project

* Provider should always provide data as a string or undefined

* Use a placeholder `-` if the metrics endpoint responds with undefined data
This commit is contained in:
John Cowen 2020-11-16 15:22:24 +00:00 committed by GitHub
parent 292058c569
commit 4515d6141b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 171 additions and 164 deletions

View File

@ -4,23 +4,50 @@
"resultType": "vector", "resultType": "vector",
"result": [ "result": [
${ ${
[1].map(function(_){ [1].map(item => {
var type = "service"; const dc = 'dc1';
var proto = "tcp"; const generateTargets = function(num) {
// Seed faker by the number of results we want to make it deterministic
// here and in other correlated endpoints.
fake.seed(num);
return range(num).map(i => {
const nspace = i === 0 ? `default` : `${fake.hacker.noun()}-ns-${i}`;
return {
Name: `service-${fake.random.number({min:0, max:99})}`,
Datacenter: `${dc}`,
Namespace: `${nspace}`
}
})
};
var q = location.search.query; // little helper to get a deterministic number from the target service
// name string. NOTE: this should be the same as in metrics-proxy/.../query
// endpoint so metrics match what is requested.
const hashStr = function(s) {
for(var i = 0, h = 0xdeadbeef; i < s.length; i++)
h = Math.imul(h ^ s.charCodeAt(i), 2654435761);
return (h ^ h >>> 16) >>> 0;
};
const randExp = function(max, lambda) {
return (-Math.log(1-(1-Math.exp(-lambda))*Math.random())/lambda) * max;
};
const q = location.search.query;
let type = 'service';
let proto = 'tcp';
// Match the relabel arguments since "downstream" appears in both // Match the relabel arguments since "downstream" appears in both
// "service" and "upstream" type queries' metric names while // "service" and "upstream" type queries' metric names while
// "upstream" appears in downstream query metric names (confusingly). // "upstream" appears in downstream query metric names (confusingly).
if (q.match('"upstream"')) { if (q.match('"upstream"')) {
type = "upstream"; type = 'upstream';
} else if (q.match('"downstream"')) { } else if (q.match('"downstream"')) {
type = "downstream"; type = 'downstream';
} }
if (q.match('envoy_http_')) { if (q.match('envoy_http_')) {
proto = "http"; proto = 'http';
} }
// NOTE!!! The logic below to pick the upstream/downstream service // NOTE!!! The logic below to pick the upstream/downstream service
@ -30,151 +57,136 @@
// Pick a number of down/upstreams to return based on the cookie variable. // Pick a number of down/upstreams to return based on the cookie variable.
// If you change anything about this variable or it's default, you'll need // If you change anything about this variable or it's default, you'll need
// to change the topology endpoint to match. // to change the topology endpoint to match.
var numResults = 1; let numResults = 1;
if (type === "upstream") { if (type === 'upstream') {
numResults = env("CONSUL_UPSTREAM_COUNT", 3); numResults = parseInt(env('CONSUL_UPSTREAM_COUNT', 3));
} }
if (type === "downstream") { if (type === 'downstream') {
numResults = env("CONSUL_DOWNSTREAM_COUNT", 5); numResults = parseInt(env('CONSUL_DOWNSTREAM_COUNT', 5));
} }
var genFakeServiceNames = function(num) {
// Seed faker by the number of results we want to make it deterministic
// here and in other correlated endpoints.
fake.seed(num);
var serviceNames = [];
for (var i = 0; i < num; i++) {
serviceNames.push(`service-${fake.random.number({min:0, max:99})}`)
}
return serviceNames
};
// Figure out the actual name for the target service // Figure out the actual name for the target service
var targetService = "invalid-local-cluster"; let targetService = 'invalid-local-cluster';
var m = q.match(/local_cluster="([^"]*)"/); let m = q.match(/local_cluster="([^"]*)"/);
if (m && m.length >= 2 && m[1] != "") { if (m && m.length >= 2 && m[1] !== '') {
targetService = m[1]; targetService = m[1];
} }
m = q.match(/consul_service="([^"]*)"/); m = q.match(/consul_service="([^"]*)"/);
if (type == "downstream" && m && m.length >= 2 && m[1] != "") { if (type === 'downstream' && m && m.length >= 2 && m[1] !== '') {
// downstreams don't have the same selector for the main service // downstreams don't have the same selector for the main service
// name. // name.
targetService = m[1]; targetService = m[1];
} }
var serviceNames = []; let targets = [];
switch(type) { switch(type) {
case "downstream": // fallthrough case 'downstream': // fallthrough
case "upstream": case 'upstream':
serviceNames = genFakeServiceNames(numResults); targets = generateTargets(numResults);
break; break;
default: default:
// fallthrough // fallthrough
case "service": case 'service':
serviceNames = [targetService]; targets = [targetService];
break; break;
} }
// little helper to get a deterministic number from the target service let serviceProto = 'tcp';
// name string. NOTE: this should be the same as in service-topology
// endpoint so metrics match what is requested.
var hashStr = function(s) {
for(var i = 0, h = 0xdeadbeef; i < s.length; i++)
h = Math.imul(h ^ s.charCodeAt(i), 2654435761);
return (h ^ h >>> 16) >>> 0;
};
var serviceProto = "tcp"
// Randomly pick the serviceProtocol which will affect which types of // Randomly pick the serviceProtocol which will affect which types of
// stats we return for downstream clusters. But we need it to be // stats we return for downstream clusters. But we need it to be
// deterministic for a given service name so that all the downstream // deterministic for a given service name so that all the downstream
// stats are consistently typed. // stats are consistently typed.
fake.seed(hashStr(targetService)) fake.seed(hashStr(targetService))
if (fake.random.number(1) > 0.5) { if (fake.random.number(1) > 0.5) {
serviceProto = "http"; serviceProto = 'http';
} }
// For up/downstreams only return HTTP metrics half of the time. // For up/downstreams only return HTTP metrics half of the time.
// For upstreams it's based on the upstream's protocol which might be // For upstreams it's based on the upstream's protocol which might be
// mixed so alternate protocols for upstreams. // mixed so alternate protocols for upstreams.
if (type == "upstream") { if (type === "upstream") {
// Pretend all odd service indexes are tcp and even are http // Pretend all odd service indexes are tcp and even are http
var wantMod = 0; const wantMod = proto === 'tcp' ? 1 : 0;
if (proto == "tcp") { targets = targets.filter((item, i) => i % 2 === wantMod);
wantMod = 1;
}
serviceNames = serviceNames.filter(function(x, i){ return i%2 == wantMod })
} }
// For downstreams it's based on the target's protocol which we // For downstreams it's based on the target's protocol which we
// don't really know but all downstreams should be the same type // don't really know but all downstreams should be the same type
// so only return metrics for that protocol. // so only return metrics for that protocol.
if (type == "downstream" && proto == "http" && serviceProto != "http") { if (type === 'downstream' && proto === 'http' && serviceProto !== 'http') {
serviceNames = []; targets = [];
} }
// Work out which metric is being queried to make them more realistic. // Work out which metric is being queried to make them more realistic.
var range = 100; let max = 100;
switch(proto) { switch(proto) {
case "http": case 'http':
if (q.match('envoy_response_code_class="5"')) { if (q.match('envoy_response_code_class="5"')) {
// It's error rate make it a percentage // It's error rate make it a percentage
range = 30; max = 30;
} else if (q.match("rq_completed")) { } else if (q.match("rq_completed")) {
// Requests per second // Requests per second
range = 1000; max = 1000;
} else if (q.match("quantile\\(0.99")) { } else if (q.match("quantile\\(0.99")) {
// 99 percentile time in ms make it longer than 50 percentile // 99 percentile time in ms make it longer than 50 percentile
range = 5000; max = 5000;
} else if (q.match("quantile\\(0.5")) { } else if (q.match("quantile\\(0.5")) {
// 50th percentile // 50th percentile
range = 500; max = 500;
} }
break; break;
case "tcp": case 'tcp':
if (q.match('cx_total')) { if (q.match('cx_total')) {
// New conns per second // New conns per second
range = 100; max = 100;
} else if (q.match('cx_rx_bytes')) { } else if (q.match('cx_rx_bytes')) {
// inbound data rate tends to be lower than outbound // inbound data rate tends to be lower than outbound
range = 0.5 * 1e9; max = 0.5 * 1e9;
} else if (q.match('cx_tx_bytes')) { } else if (q.match('cx_tx_bytes')) {
// inbound data rate // inbound data rate
range = 1e9; max = 1e9;
} }
// no route/connect faile are OK with default 0-100 // no route/connect failed are OK with default 0-100
break; break;
} }
var randExp = function(max, lambda) {
return (-Math.log(1-(1-Math.exp(-lambda))*Math.random())/lambda) * max;
}
// Now generate the data points // Now generate the data points
return serviceNames.map(function(name, i){ return targets.map((item, i) => {
var metric = `{}`; let metric = `{}`;
switch(type) { switch(type) {
default: case 'upstream':
break;
case "upstream":
// TODO: this should really return tcp proxy label for tcp // TODO: this should really return tcp proxy label for tcp
// metrics but we don't look at that for now. // metrics but we don't look at that for now.
metric = `{"upstream": "${name}", "envoy_http_conn_manager_prefix": "${name}"}`; metric = `{"upstream": "${item.Name}", "envoy_http_conn_manager_prefix": "${item.Name}"}`;
break; break;
case "downstream": case 'downstream':
metric = `{"downstream": "${name}", "local_cluster": "${name}"}`; metric = `{"downstream": "${item.Name}", "local_cluster": "${item.Name}"}`;
break;
}
const timestamp = Date.now() / 1000;
let value = randExp(max, 20);
// prometheus can sometimes generate NaN and undefined strings, so
// replicate that randomly
const num = fake.random.number({min: 0, max: 10});
switch(true) {
case num > 8:
value = 'NaN';
break;
case num > 5:
value = 'undefined';
break; break;
} }
return `{ return `{
"metric": ${metric}, "metric": ${metric},
"value": [ "value": [
${Date.now()/1000}, ${timestamp},
"${randExp(range, 20)}" "${value}"
] ]
}`; }`;
}).join(",") })
})
})[0]
} }
] ]
} }

View File

@ -6,73 +6,74 @@
${ ${
// We need 15 minutes worth of data at 10 second resoution. Currently we // We need 15 minutes worth of data at 10 second resoution. Currently we
// always query for two series together so loop twice over this. // always query for two series together so loop twice over this.
[0, 1].map(function(i){ [0, 1].map((i) => {
var timePeriodMins = 15; const timePeriodMins = 15;
var resolutionSecs = 10; const resolutionSecs = 10;
var numPoints = (timePeriodMins*60)/resolutionSecs; const numPoints = (timePeriodMins * 60) / resolutionSecs;
var time = (Date.now()/1000) - (timePeriodMins*60); let time = (Date.now() / 1000) - (timePeriodMins * 60);
var q = location.search.query; const q = location.search.query;
var proto = "tcp"; let proto = 'tcp';
var range = 1000; let max = 1000;
var riseBias = 10; let riseBias = 10;
var fallBias = 10; let fallBias = 10;
var volatility = 0.2; let volatility = 0.2;
var label = ""; let label = '';
if (q.match('envoy_listener_http_downstream_rq_xx')) { if (q.match('envoy_listener_http_downstream_rq_xx')) {
proto = "http" proto = 'http'
// Switch random value ranges for total vs error rates // Switch random value ranges for total vs error rates
switch(i) { switch(i) {
case 0: case 0:
range = 1000; // up to 1000 rps for success max = 1000; // up to 1000 rps for success
label = "Successes"; label = 'Successes';
break; break;
case 1: case 1:
range = 500; // up to 500 errors per second max = 500; // up to 500 errors per second
fallBias = 1; // fall quicker than we rise fallBias = 1; // fall quicker than we rise
riseBias = 30; // start low generally riseBias = 30; // start low generally
volatility = 1; volatility = 1;
label = "Errors"; label = 'Errors';
break; break;
} }
} else { } else {
// Type tcp // Type tcp
switch(i) { switch(i) {
case 0: case 0:
range = 0.5 * 1e9; // up to 500 mbps recieved max = 0.5 * 1e9; // up to 500 mbps recieved
label = "Inbound"; label = 'Inbound';
break; break;
case 1: case 1:
range = 1e9; // up to 1 gbps max = 1e9; // up to 1 gbps
label = "Outbound" label = 'Outbound';
break; break;
} }
} }
var randExp = function(max, lambda) { const randExp = function(max, lambda) {
return (-Math.log(1-(1-Math.exp(-lambda))*Math.random())/lambda) * max; return (-Math.log(1-(1-Math.exp(-lambda))*Math.random())/lambda) * max;
} }
// Starting value // Starting value
var value = randExp(range, riseBias); let value = randExp(max, riseBias);
if (value > range) { if (value > max) {
value = range; value = max;
} }
var points = []; const points = [];
let rising;
for (var i = 0; i < numPoints; i++) { for (var i = 0; i < numPoints; i++) {
points.push(`[${time}, "${value}"]`); points.push(`[${time}, "${value}"]`);
time = time + resolutionSecs; time = time + resolutionSecs;
var rising = (Math.random() > 0.5); rising = (Math.random() > 0.5);
delta = volatility * randExp(range, rising ? riseBias : fallBias); delta = volatility * randExp(max, rising ? riseBias : fallBias);
if (!rising) { if (!rising) {
// Make it a negative change // Make it a negative change
delta = 0-delta; delta = 0 - delta;
} }
value = value + delta value = value + delta
if (value > range) { if (value > max) {
value = range; value = max;
} }
if (value < 0) { if (value < 0) {
value = 0; value = 0;
@ -87,4 +88,4 @@
} }
] ]
} }
} }

View File

@ -1,6 +1,29 @@
${ ${
[1].map(() => { [1].map(() => {
const dc = location.search.dc; const dc = location.search.dc;
const generateTargets = function(num) {
// Seed faker by the number of results we want to make it deterministic
// here and in other correlated endpoints.
fake.seed(num);
return range(num).map(i => {
const nspace = i === 0 ? `default` : `${fake.hacker.noun()}-ns-${i}`;
return {
Name: `service-${fake.random.number({min:0, max:99})}`,
Datacenter: `${dc}`,
Namespace: `${nspace}`
}
})
};
// little helper to get a deterministic number from the target service
// name string. NOTE: this should be the same as in metrics-proxy/.../query
// endpoint so metrics match what is requested.
const hashStr = function(s) {
for(var i = 0, h = 0xdeadbeef; i < s.length; i++)
h = Math.imul(h ^ s.charCodeAt(i), 2654435761);
return (h ^ h >>> 16) >>> 0;
};
// NOTE!!! The logic below to pick the upstream/downstream service // NOTE!!! The logic below to pick the upstream/downstream service
// names must exactly match the logic in internal/ui/metrics-proxy/.../query // names must exactly match the logic in internal/ui/metrics-proxy/.../query
@ -9,64 +32,41 @@ ${
// Pick a number of down/upstreams to return based on the cookie variable. // Pick a number of down/upstreams to return based on the cookie variable.
// If you change anything about this variable or it's default, you'll need // If you change anything about this variable or it's default, you'll need
// to change the topology endpoint to match. // to change the topology endpoint to match.
var numUp = env("CONSUL_UPSTREAM_COUNT", 3); const numUp = parseInt(env('CONSUL_UPSTREAM_COUNT', 3));
var numDown = env("CONSUL_DOWNSTREAM_COUNT", 5); const numDown = parseInt(env('CONSUL_DOWNSTREAM_COUNT', 5));
var genFakeServiceNames = function(num) { const index = parseInt(location.search.index || 0);
// Seed faker by the number of results we want to make it deterministic const targetService = location.pathname.get(4)
// here and in other correlated endpoints.
fake.seed(num);
var serviceNames = [];
for (var i = 0; i < num; i++) {
serviceNames.push(`service-${fake.random.number({min:0, max:99})}`)
}
return serviceNames
};
var upstreams = genFakeServiceNames(numUp); const upstreams = generateTargets(numUp);
var downstreams = genFakeServiceNames(numDown); const downstreams = generateTargets(numDown);
const targetService = location.pathname.toString().replace('/v1/internal/ui/service-topology/', '')
// little helper to get a deterministic number from the target service
// name string. NOTE: this should be the same as in metrics-proxy/.../query
// endpoint so metrics match what is requested.
var hashStr = function(s) {
for(var i = 0, h = 0xdeadbeef; i < s.length; i++)
h = Math.imul(h ^ s.charCodeAt(i), 2654435761);
return (h ^ h >>> 16) >>> 0;
};
var serviceProto = "tcp"
// Randomly pick the serviceProtocol which will affect which types of // Randomly pick the serviceProtocol which will affect which types of
// stats we return for downstream clusters. But we need it to be // stats we return for downstream clusters. But we need it to be
// deterministic for a given service name so that all the downstream // deterministic for a given service name so that all the downstream
// stats are consistently typed. // stats are consistently typed.
let serviceProto = 'tcp';
fake.seed(hashStr(targetService)) fake.seed(hashStr(targetService))
if (fake.random.number(1) > 0.5) { if (fake.random.number(1) > 0.5) {
serviceProto = "http"; serviceProto = 'http';
} }
fake.seed(index);
return ` return `
{ {
"Protocol": "${serviceProto}", "Protocol": "${serviceProto}",
"FilteredByACLs": ${fake.random.boolean()}, "FilteredByACLs": ${fake.random.boolean()},
"Upstreams": "Upstreams": [
[
${ ${
upstreams.map((item, i) => { upstreams.map((item, i) => {
let hasPerms = fake.random.boolean(); const hasPerms = fake.random.boolean();
// if hasPerms is true allowed is always false as some restrictions apply // if hasPerms is true allowed is always false as some restrictions apply
let allowed = hasPerms ? false : fake.random.boolean(); const allowed = hasPerms ? false : fake.random.boolean();
return ` return `
{ {
"Name": "${item}", "Name": "${item.Name}",
"Datacenter": "${dc}", "Datacenter": "${item.Datacenter}",
${i === 1 ? ` "Namespace": "${item.Namespace}",
"Namespace": "default",
` : `
"Namespace": "${fake.hacker.noun()}-ns-${i}",
`}
"ChecksPassing":${fake.random.number({min: 1, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})}, "ChecksPassing":${fake.random.number({min: 1, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})},
"ChecksWarning":${fake.random.number({min: 0, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})}, "ChecksWarning":${fake.random.number({min: 0, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})},
"ChecksCritical":${fake.random.number({min: 0, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})}, "ChecksCritical":${fake.random.number({min: 0, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})},
@ -79,22 +79,17 @@ ${
} }
`})} `})}
], ],
"Downstreams": "Downstreams": [
[
${ ${
downstreams.map((item, i) => { downstreams.map((item, i) => {
let hasPerms = fake.random.boolean(); const hasPerms = fake.random.boolean();
// if hasPerms is true allowed is always false as some restrictions apply // if hasPerms is true allowed is always false as some restrictions apply
let allowed = hasPerms ? false : fake.random.boolean(); const allowed = hasPerms ? false : fake.random.boolean();
return ` return `
{ {
"Name": "${item}", "Name": "${item.Name}",
"Datacenter": "${dc}", "Datacenter": "${item.Datacenter}",
${i === 1 ? ` "Namespace": "${item.Namespace}",
"Namespace": "default",
` : `
"Namespace": "${fake.hacker.noun()}-ns-${i}",
`}
"ChecksPassing":${fake.random.number({min: 1, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})}, "ChecksPassing":${fake.random.number({min: 1, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})},
"ChecksWarning":${fake.random.number({min: 0, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})}, "ChecksWarning":${fake.random.number({min: 0, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})},
"ChecksCritical":${fake.random.number({min: 0, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})}, "ChecksCritical":${fake.random.number({min: 0, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})},
@ -110,4 +105,4 @@ ${
}` }`
}) })
} }

View File

@ -659,7 +659,6 @@
// no result as a zero not a missing stat. // no result as a zero not a missing stat.
promql += ' OR on() vector(0)'; promql += ' OR on() vector(0)';
} }
//console.log(promql)
var params = { var params = {
query: promql, query: promql,
time: new Date().getTime() / 1000, time: new Date().getTime() / 1000,
@ -671,7 +670,7 @@
return { return {
label: label, label: label,
desc: desc, desc: desc,
value: formatter(v), value: isNaN(v) ? '-' : formatter(v),
}; };
} }
@ -683,7 +682,7 @@
data[groupName] = { data[groupName] = {
label: label, label: label,
desc: desc.replace('{{GROUP}}', groupName), desc: desc.replace('{{GROUP}}', groupName),
value: formatter(v), value: isNaN(v) ? '-' : formatter(v),
}; };
} }
return data; return data;