ui: Dev/Test environment configurable metrics (#9345)

In order to test certain setups for our metrics visualizations we need to be able to setup several different `ui_config` settings during development/testing. Generally in the UI, we use the Web Inspector to set various cookie values to configure the UI how we need to see it whilst developing, so this PR:

1. Routes `ui_config` through a dev time only `CONSUL_UI_CONFIG` env variable so we can change it via cookies vars.
2. Adds `CONSUL_METRICS_PROXY_ENABLE`, `CONSUL_METRICS_PROVIDER` and `CONSUL_SERVICE_DASHBOARD_URL` so it's easy to set/unset these only values during development.
3. Adds an acceptance testing step so we can setup `ui_config` to whatever we want during testing.
4. Adds an async 'repository-like' method to the `UiConfig` Service so it feels like a repository - incase we ever need to get this via an HTTP API+blocking query.
5. Vaguely unrelated: we allow cookie values to be set via the location.hash whilst in development only e.g. `/ui/services#CONSUL_METRICS_PROXY_ENABLE=1` so we can link to different setups if we ever need to.

All values added here are empty/falsey by default, so in order to see how it was previously you'll need to set the appropriate cookies values, but you can now also easily preview/test the the metrics viz in different/disabled states (with differing `ui_config`)
This commit is contained in:
John Cowen 2020-12-15 15:34:54 +00:00 committed by GitHub
parent eae9df241d
commit c24c70af46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 122 additions and 51 deletions

View File

@ -1,20 +1,13 @@
import { inject as service } from '@ember/service';
import RepositoryService from 'consul-ui/services/repository';
import { env } from 'consul-ui/env';
// meta is used by DataSource to configure polling. The interval controls how
// long between each poll to the metrics provider. TODO - make this configurable
// in the UI settings.
const meta = {
interval: env('CONSUL_METRICS_POLL_INTERVAL') || 10000,
};
// CONSUL_METRICS_POLL_INTERVAL controls how long between each poll to the
// metrics provider
export default class MetricsService extends RepositoryService {
@service('ui-config')
cfg;
@service('client/http')
client;
@service('ui-config') cfg;
@service('env') env;
@service('client/http') client;
error = null;
@ -49,9 +42,11 @@ export default class MetricsService extends RepositoryService {
this.provider.serviceRecentSummarySeries(slug, dc, nspace, protocol, {}),
this.provider.serviceRecentSummaryStats(slug, dc, nspace, protocol, {}),
];
return Promise.all(promises).then(function (results) {
return Promise.all(promises).then(results => {
return {
meta: meta,
meta: {
interval: this.env.var('CONSUL_METRICS_POLL_INTERVAL') || 10000,
},
series: results[0],
stats: results[1].stats,
};
@ -62,8 +57,10 @@ export default class MetricsService extends RepositoryService {
if (this.error) {
return Promise.reject(this.error);
}
return this.provider.upstreamRecentSummaryStats(slug, dc, nspace, {}).then(function (result) {
result.meta = meta;
return this.provider.upstreamRecentSummaryStats(slug, dc, nspace, {}).then(result => {
result.meta = {
interval: this.env.var('CONSUL_METRICS_POLL_INTERVAL') || 10000,
};
return result;
});
}
@ -72,8 +69,10 @@ export default class MetricsService extends RepositoryService {
if (this.error) {
return Promise.reject(this.error);
}
return this.provider.downstreamRecentSummaryStats(slug, dc, nspace, {}).then(function (result) {
result.meta = meta;
return this.provider.downstreamRecentSummaryStats(slug, dc, nspace, {}).then(result => {
result.meta = {
interval: this.env.var('CONSUL_METRICS_POLL_INTERVAL') || 10000,
};
return result;
});
}

View File

@ -1,15 +1,14 @@
import Service from '@ember/service';
import Service, { inject as service } from '@ember/service';
import { get } from '@ember/object';
export default class UiConfigService extends Service {
config = undefined;
@service('env') env;
async findByPath(path, configuration = {}) {
return get(this.get(), path);
}
get() {
if (this.config === undefined) {
// Load config from our special meta tag for now. Later it might come from
// an API instead/as well.
var meta = unescape(document.getElementsByName('consul-ui/ui_config')[0].content);
this.config = JSON.parse(meta);
}
return this.config;
return this.env.var('CONSUL_UI_CONFIG');
}
}

View File

@ -1,6 +1,26 @@
import { runInDebug } from '@ember/debug';
// 'environment' getter
// there are currently 3 levels of environment variables:
// 1. Those that can be set by the user by setting localStorage values
// 2. Those that can be set by the operator either via ui_config, or inferring
// from other server type properties (protocol)
// 3. Those that can be set only during development by adding cookie values
// via the browsers Web Inspector, or via the browsers hash (#COOKIE_NAME=1),
// which is useful for showing the UI with various settings enabled/disabled
export default function(config = {}, win = window, doc = document) {
const dev = function() {
return doc.cookie
// look at the hash in the URL and transfer anything after the hash into
// cookies to enable linking of the UI with various settings enabled
runInDebug(() => {
if (
typeof win.location !== 'undefined' &&
typeof win.location.hash === 'string' &&
win.location.hash.length > 0
) {
doc.cookie = win.location.hash.substr(1);
}
});
const dev = function(str = doc.cookie) {
return str
.split(';')
.filter(item => item !== '')
.map(item => item.trim().split('='));
@ -20,6 +40,7 @@ export default function(config = {}, win = window, doc = document) {
return {};
}
};
const ui_config = JSON.parse(unescape(doc.getElementsByName('consul-ui/ui_config')[0].content));
const scripts = doc.getElementsByTagName('script');
// we use the currently executing script as a reference
// to figure out where we are for other things such as
@ -33,6 +54,21 @@ export default function(config = {}, win = window, doc = document) {
const operator = function(str, env) {
let protocol;
switch (str) {
case 'CONSUL_UI_CONFIG':
const dashboards = {};
const provider = env('CONSUL_METRICS_PROVIDER');
const proxy = env('CONSUL_METRICS_PROXY_ENABLED');
dashboards.service = env('CONSUL_SERVICE_DASHBOARD_URL');
if (provider) {
ui_config.metrics_provider = provider;
}
if (proxy) {
ui_config.metrics_proxy_enabled = proxy;
}
if (dashboards.service) {
ui_config.dashboard_url_templates = dashboards;
}
return ui_config;
case 'CONSUL_BASE_UI_URL':
return currentSrc
.split('/')
@ -85,6 +121,12 @@ export default function(config = {}, win = window, doc = document) {
case 'CONSUL_SSO_ENABLE':
prev['CONSUL_SSO_ENABLED'] = !!JSON.parse(String(value).toLowerCase());
break;
case 'CONSUL_METRICS_PROXY_ENABLE':
prev['CONSUL_METRICS_PROXY_ENABLED'] = !!JSON.parse(String(value).toLowerCase());
break;
case 'CONSUL_UI_CONFIG':
prev['CONSUL_UI_CONFIG'] = JSON.parse(value);
break;
default:
prev[key] = value;
}
@ -109,7 +151,10 @@ export default function(config = {}, win = window, doc = document) {
case 'CONSUL_UI_REALTIME_RUNNER':
// these are strings
return user(str) || ui(str);
case 'CONSUL_UI_CONFIG':
case 'CONSUL_METRICS_PROVIDER':
case 'CONSUL_METRICS_PROXY_ENABLE':
case 'CONSUL_SERVICE_DASHBOARD_URL':
case 'CONSUL_BASE_UI_URL':
case 'CONSUL_HTTP_PROTOCOL':
case 'CONSUL_HTTP_MAX_CONNECTIONS':

View File

@ -3,15 +3,7 @@ module.exports = ({ appName, environment, rootURL, config }) => `
<meta name="consul-ui/ui_config" content="${
environment === 'production'
? `{{ jsonEncodeAndEscape .UIConfig }}`
: escape(
JSON.stringify({
metrics_provider: 'prometheus',
metrics_proxy_enabled: true,
dashboard_url_templates: {
service: 'https://example.com?{{Service.Name}}&{{Datacenter}}',
},
})
)
: escape(JSON.stringify({}))
}" />
<link rel="icon" type="image/png" href="${rootURL}assets/favicon-32x32.png" sizes="32x32">

View File

@ -107,10 +107,15 @@ Feature: dc / services / show: Show Service
---
Scenario: Given a dashboard template has been set
Given 1 datacenter model with the value "dc1"
And ui_config from yaml
---
dashboard_url_templates:
service: https://something.com?{{Service.Name}}&{{Datacenter}}
---
When I visit the service page for yaml
---
dc: dc1
service: service-0
---
# The Metrics dashboard should use the Service.Name not the ID
And I see href on the metricsAnchor like "https://example.com?service-0&dc1"
And I see href on the metricsAnchor like "https://something.com?service-0&dc1"

View File

@ -9,7 +9,23 @@ import dictionary from '../dictionary';
const getDictionary = dictionary(utils);
const staticClassList = [...document.documentElement.classList];
const getCookies = () => {
return Object.fromEntries(document.cookie.split(';').map(item => item.split('=')));
};
const getResetCookies = function() {
const start = getCookies();
return () => {
const startKeys = Object.keys(start);
const endKeys = Object.keys(getCookies());
const diff = endKeys.filter(key => !startKeys.includes(key));
diff.forEach(item => {
document.cookie = `${item}= ; expires=${new Date(0)}`;
});
};
};
let resetCookies;
const reset = function() {
resetCookies();
window.localStorage.clear();
api.server.reset();
const list = document.documentElement.classList;
@ -21,6 +37,7 @@ const reset = function() {
});
};
const startup = function() {
resetCookies = getResetCookies();
api.server.setCookie('CONSUL_LATENCY', 0);
};

View File

@ -1,4 +1,4 @@
export default function(scenario, create) {
export default function(scenario, create, win = window, doc = document) {
scenario
.given(['an external edit results in $number $model model[s]?'], function(number, model) {
return create(number, model);
@ -17,7 +17,10 @@ export default function(scenario, create) {
)
.given(['settings from yaml\n$yaml'], function(data) {
return Object.keys(data).forEach(function(key) {
window.localStorage[key] = JSON.stringify(data[key]);
win.localStorage[key] = JSON.stringify(data[key]);
});
})
.given(['ui_config from yaml\n$yaml'], function(data) {
doc.cookie = `CONSUL_UI_CONFIG=${JSON.stringify(data)}`;
});
}

View File

@ -9,11 +9,12 @@ const getEntriesByType = function(type) {
},
];
};
const makeGetElementsByTagName = function(src) {
const makeGetElementsBy = function(str) {
return function(name) {
return [
{
src: src,
src: str,
content: str,
},
];
};
@ -22,13 +23,17 @@ const win = {
performance: {
getEntriesByType: getEntriesByType,
},
location: {
hash: '',
},
localStorage: {
getItem: function(key) {},
},
};
const doc = {
cookie: '',
getElementsByTagName: makeGetElementsByTagName(''),
getElementsByTagName: makeGetElementsBy(''),
getElementsByName: makeGetElementsBy('{}'),
};
module('Unit | Utility | getEnvironment', function() {
test('it returns a function', function(assert) {
@ -55,14 +60,16 @@ module('Unit | Utility | getEnvironment', function() {
let expected = 'http://localhost/ui';
let doc = {
cookie: '',
getElementsByTagName: makeGetElementsByTagName(`${expected}/assets/consul-ui.js`),
getElementsByTagName: makeGetElementsBy(`${expected}/assets/consul-ui.js`),
getElementsByName: makeGetElementsBy('{}'),
};
let env = getEnvironment(config, win, doc);
assert.equal(env('CONSUL_BASE_UI_URL'), expected);
expected = 'http://localhost/somewhere/else';
doc = {
cookie: '',
getElementsByTagName: makeGetElementsByTagName(`${expected}/assets/consul-ui.js`),
getElementsByTagName: makeGetElementsBy(`${expected}/assets/consul-ui.js`),
getElementsByName: makeGetElementsBy('{}'),
};
env = getEnvironment(config, win, doc);
assert.equal(env('CONSUL_BASE_UI_URL'), expected);
@ -135,7 +142,8 @@ module('Unit | Utility | getEnvironment', function() {
};
let doc = {
cookie: 'CONSUL_NSPACES_ENABLE=1',
getElementsByTagName: makeGetElementsByTagName(''),
getElementsByTagName: makeGetElementsBy(''),
getElementsByName: makeGetElementsBy('{}'),
};
let env = getEnvironment(config, win, doc);
assert.ok(env('CONSUL_NSPACES_ENABLED'));
@ -145,7 +153,8 @@ module('Unit | Utility | getEnvironment', function() {
};
doc = {
cookie: 'CONSUL_NSPACES_ENABLE=0',
getElementsByTagName: makeGetElementsByTagName(''),
getElementsByTagName: makeGetElementsBy(''),
getElementsByName: makeGetElementsBy('{}'),
};
env = getEnvironment(config, win, doc);
assert.notOk(env('CONSUL_NSPACES_ENABLED'));
@ -169,7 +178,8 @@ module('Unit | Utility | getEnvironment', function() {
};
let doc = {
cookie: 'CONSUL_NSPACES_ENABLE=1',
getElementsByTagName: makeGetElementsByTagName(''),
getElementsByTagName: makeGetElementsBy(''),
getElementsByName: makeGetElementsBy('{}'),
};
let env = getEnvironment(config, win, doc);
assert.notOk(env('CONSUL_NSPACES_ENABLED'));
@ -179,7 +189,8 @@ module('Unit | Utility | getEnvironment', function() {
};
doc = {
cookie: 'CONSUL_NSPACES_ENABLE=0',
getElementsByTagName: makeGetElementsByTagName(''),
getElementsByTagName: makeGetElementsBy(''),
getElementsByName: makeGetElementsBy('{}'),
};
env = getEnvironment(config, win, doc);
assert.ok(env('CONSUL_NSPACES_ENABLED'));