UI metrics provider dc (#9001)

* Plumb Datacenter and Namespace to metrics provider in preparation for them being usable.

* Move metrics loader/status to a new component and show reason for being disabled.

* Remove stray console.log

* Rebuild AssetFS to resolve conflicts

* Yarn upgrade

* mend
pull/9041/head
Paul Banks 2020-10-26 19:48:23 +00:00 committed by GitHub
parent b25a6a8d85
commit 52d7283cd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 260 additions and 134 deletions

File diff suppressed because one or more lines are too long

View File

@ -28,9 +28,10 @@ func uiTemplateDataFromConfig(cfg *config.RuntimeConfig) (map[string]interface{}
} }
d := map[string]interface{}{ d := map[string]interface{}{
"ContentPath": cfg.UIConfig.ContentPath, "ContentPath": cfg.UIConfig.ContentPath,
"ACLsEnabled": cfg.ACLsEnabled, "ACLsEnabled": cfg.ACLsEnabled,
"UIConfig": uiCfg, "UIConfig": uiCfg,
"LocalDatacenter": cfg.Datacenter,
} }
// Also inject additional provider scripts if needed, otherwise strip the // Also inject additional provider scripts if needed, otherwise strip the

View File

@ -251,15 +251,38 @@ func (h *Handler) renderIndex(cfg *config.RuntimeConfig, fs http.FileSystem) ([]
// have to match the encoded double quotes around the JSON string value that // have to match the encoded double quotes around the JSON string value that
// is there as a placeholder so the end result is an actual JSON bool not a // is there as a placeholder so the end result is an actual JSON bool not a
// string containing "false" etc. // string containing "false" etc.
re := regexp.MustCompile(`%22__RUNTIME_BOOL_[A-Za-z0-9-_]+__%22`) re := regexp.MustCompile(`%22__RUNTIME_(BOOL|STRING)_([A-Za-z0-9-_]+)__%22`)
content = []byte(re.ReplaceAllStringFunc(string(content), func(str string) string { content = []byte(re.ReplaceAllStringFunc(string(content), func(str string) string {
// Trim the prefix and __ suffix // Trim the prefix and suffix
varName := strings.TrimSuffix(strings.TrimPrefix(str, "%22__RUNTIME_BOOL_"), "__%22") pair := strings.TrimSuffix(strings.TrimPrefix(str, "%22__RUNTIME_"), "__%22")
if v, ok := tplData[varName].(bool); ok && v { parts := strings.SplitN(pair, "_", 2)
return "true" switch parts[0] {
case "BOOL":
if v, ok := tplData[parts[1]].(bool); ok && v {
return "true"
}
return "false"
case "STRING":
if v, ok := tplData[parts[1]].(string); ok {
if bs, err := json.Marshal(v); err == nil {
return url.PathEscape(string(bs))
}
// Error!
h.logger.Error("Encoding JSON value for UI template failed",
"placeholder", str,
"value", v,
)
// Fall through to return the empty string to make JSON parse
}
return `""` // Empty JSON string
} }
return "false" // Unknown type is likely an error
h.logger.Error("Unknown placeholder type in UI template",
"placeholder", str,
)
// Return a literal empty string so the JSON still parses
return `""`
})) }))
tpl, err := template.New("index").Funcs(template.FuncMap{ tpl, err := template.New("index").Funcs(template.FuncMap{

View File

@ -34,14 +34,19 @@ func TestUIServerIndex(t *testing.T) {
path: "/", // Note /index.html redirects to / path: "/", // Note /index.html redirects to /
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantContains: []string{"<!-- CONSUL_VERSION:"}, wantContains: []string{"<!-- CONSUL_VERSION:"},
wantNotContains: []string{
"__RUNTIME_BOOL_",
"__RUNTIME_STRING_",
},
wantEnv: map[string]interface{}{ wantEnv: map[string]interface{}{
"CONSUL_ACLS_ENABLED": false, "CONSUL_ACLS_ENABLED": false,
"CONSUL_DATACENTER_LOCAL": "dc1",
}, },
}, },
{ {
// TODO: is this really what we want? It's what we've always done but // We do this redirect just for UI dir since the app is a single page app
// seems a bit odd to not do an actual 301 but instead serve the // and any URL under the path should just load the index and let Ember do
// index.html from every path... It also breaks the UI probably. // it's thing unless it's a specific asset URL in the filesystem.
name: "unknown paths to serve index", name: "unknown paths to serve index",
cfg: basicUIEnabledConfig(), cfg: basicUIEnabledConfig(),
path: "/foo-bar-bazz-qux", path: "/foo-bar-bazz-qux",
@ -202,6 +207,7 @@ func basicUIEnabledConfig(opts ...cfgFunc) *config.RuntimeConfig {
Enabled: true, Enabled: true,
ContentPath: "/ui/", ContentPath: "/ui/",
}, },
Datacenter: "dc1",
} }
for _, f := range opts { for _, f := range opts {
f(cfg) f(cfg)

View File

@ -47,9 +47,19 @@
</div> </div>
{{#if @hasMetricsProvider }} {{#if @hasMetricsProvider }}
{{#if (eq @type 'upstream')}} {{#if (eq @type 'upstream')}}
<TopologyMetrics::Stats @endpoint='upstream-summary-for-service' @service={{@service}} @item={{item.Name}} /> <TopologyMetrics::Stats
@endpoint='upstream-summary-for-service'
@service={{@service}}
@item={{item.Name}}
@noMetricsReason={{@noMetricsReason}}
/>
{{else}} {{else}}
<TopologyMetrics::Stats @endpoint='downstream-summary-for-service' @service={{@service}} @item={{item.Name}} /> <TopologyMetrics::Stats
@endpoint='downstream-summary-for-service'
@service={{@service}}
@item={{item.Name}}
@noMetricsReason={{@noMetricsReason}}
/>
{{/if}} {{/if}}
{{/if}} {{/if}}
</a> </a>

View File

@ -16,6 +16,7 @@
@service={{@service.Service.Service}} @service={{@service.Service.Service}}
@dc={{@dc}} @dc={{@dc}}
@hasMetricsProvider={{this.hasMetricsProvider}} @hasMetricsProvider={{this.hasMetricsProvider}}
@noMetricsReason={{noMetricsReason}}
/> />
</div> </div>
{{/if}} {{/if}}
@ -24,8 +25,19 @@
{{@service.Service.Service}} {{@service.Service.Service}}
</div> </div>
{{#if this.hasMetricsProvider }} {{#if this.hasMetricsProvider }}
<TopologyMetrics::Series @service={{@service.Service.Service}} @protocol={{@protocol}} /> <TopologyMetrics::Series
<TopologyMetrics::Stats @endpoint='summary-for-service' @service={{@service.Service.Service}} @protocol={{@protocol}} /> @service={{@service.Service.Service}}
@dc={{@dc}}
@protocol={{@protocol}}
@noMetricsReason={{noMetricsReason}}
/>
<TopologyMetrics::Stats
@endpoint='summary-for-service'
@service={{@service.Service.Service}}
@dc={{@dc}}
@protocol={{@protocol}}
@noMetricsReason={{noMetricsReason}}
/>
{{/if}} {{/if}}
<div class="link"> <div class="link">
{{#if @metricsHref}} {{#if @metricsHref}}
@ -55,6 +67,7 @@
@dc={{@dc}} @dc={{@dc}}
@type='upstream' @type='upstream'
@hasMetricsProvider={{this.hasMetricsProvider}} @hasMetricsProvider={{this.hasMetricsProvider}}
@noMetricsReason={{noMetricsReason}}
/> />
</div> </div>
{{/each-in}} {{/each-in}}

View File

@ -5,6 +5,7 @@ import { inject as service } from '@ember/service';
export default class TopologyMetrics extends Component { export default class TopologyMetrics extends Component {
@service('ui-config') cfg; @service('ui-config') cfg;
@service('env') env;
// =attributes // =attributes
@tracked centerDimensions; @tracked centerDimensions;
@ -13,10 +14,21 @@ export default class TopologyMetrics extends Component {
@tracked upView; @tracked upView;
@tracked upLines = []; @tracked upLines = [];
@tracked hasMetricsProvider = false; @tracked hasMetricsProvider = false;
@tracked noMetricsReason = null;
constructor(owner, args) { constructor(owner, args) {
super(owner, args); super(owner, args);
this.hasMetricsProvider = !!this.cfg.get().metrics_provider; this.hasMetricsProvider = !!this.cfg.get().metrics_provider;
// Disable metrics fetching if we are not in the local DC since we don't
// currently support that for all providers.
//
// TODO we can make the configurable even before we have a full solution for
// multi-DC forwarding for Prometheus so providers that are global for all
// DCs like an external managed APM can still load in all DCs.
if (this.env.var('CONSUL_DATACENTER_LOCAL') != this.args.dc) {
this.noMetricsReason = 'Unable to fetch metrics for a remote datacenter';
}
} }
// =methods // =methods

View File

@ -1,6 +1,10 @@
<DataSource {{#unless @noMetricsReason}}
@src={{uri nspace dc 'metrics' 'summary-for-service' @service @protocol}} <DataSource
@onchange={{action 'change'}} /> @src={{uri nspace dc 'metrics' 'summary-for-service' @service @protocol}}
@onchange={{action 'change'}}
@onerror={{action (mut error) value="error"}}
/>
{{/unless}}
{{on-window 'resize' (action 'redraw')}} {{on-window 'resize' (action 'redraw')}}
@ -12,7 +16,12 @@
<div class="tooltip"> <div class="tooltip">
<div class="sparkline-time">Timestamp</div> <div class="sparkline-time">Timestamp</div>
</div> </div>
<div class="sparkline-loader"><span>Loading Metrics</span></div> {{#unless data}}
<TopologyMetrics::Status
@noMetricsReason={{@noMetricsReason}}
@error={{error}}
/>
{{/unless}}
<svg class="sparkline"></svg> <svg class="sparkline"></svg>
</div> </div>
@ -30,7 +39,7 @@
<dl> <dl>
{{#each-in data.labels as |label desc| }} {{#each-in data.labels as |label desc| }}
<dt>{{label}}</dt> <dt>{{label}}</dt>
<dd>{{{desc}}}</dd> <dd>{{desc}}</dd>
{{/each-in}} {{/each-in}}
</dl> </dl>
{{#unless data.labels}} {{#unless data.labels}}

View File

@ -28,7 +28,6 @@ export default Component.extend({
}, },
change: function(evt) { change: function(evt) {
this.set('data', evt.data.series); this.set('data', evt.data.series);
this.element.querySelector('.sparkline-loader').style.display = 'none';
this.drawGraphs(); this.drawGraphs();
this.rerender(); this.rerender();
}, },

View File

@ -25,16 +25,11 @@
display: inline-block; display: inline-block;
} }
div.sparkline-loader { // extra padding for the status sub-component that's not needed for the stats
font-weight: normal; // status
.topology-metrics-error,
.topology-metrics-loader {
padding-top: 15px; padding-top: 15px;
font-size: 0.875rem;
color: $gray-500;
text-align: center;
span::after {
@extend %with-loading-icon, %as-pseudo;
}
} }
} }

View File

@ -1,7 +1,10 @@
<DataSource {{#unless @noMetricsReason}}
@src={{uri nspace dc 'metrics' @endpoint @service @protocol}} <DataSource
@onchange={{action 'statsUpdate'}} @src={{uri nspace dc 'metrics' @endpoint @service @protocol}}
/> @onchange={{action 'statsUpdate'}}
@onerror={{action (mut error) value="error"}}
/>
{{/unless}}
<div class="stats"> <div class="stats">
{{#if hasLoaded }} {{#if hasLoaded }}
@ -19,6 +22,9 @@
<span>No Metrics Available</span> <span>No Metrics Available</span>
{{/each}} {{/each}}
{{else}} {{else}}
<span class="loader">Loading Metrics</span> <TopologyMetrics::Status
@noMetricsReason={{@noMetricsReason}}
@error={{error}}
/>
{{/if}} {{/if}}
</div> </div>

View File

@ -18,11 +18,4 @@
dd { dd {
color: $gray-400 !important; color: $gray-400 !important;
} }
span {
margin: 0 auto !important;
color: $gray-500;
}
span.loader::after {
@extend %with-loading-icon, %as-pseudo;
}
} }

View File

@ -0,0 +1,12 @@
{{#if @noMetricsReason}}
<span class="topology-metrics-error">
Unable to load metrics
<span>
<Tooltip>{{@noMetricsReason}}</Tooltip>
</span>
</span>
{{else if @error}}
<span class="topology-metrics-error">Unable to load metrics</span>
{{else}}
<span class="topology-metrics-loader">Loading Metrics</span>
{{/if}}

View File

@ -0,0 +1,18 @@
.topology-metrics-error,
.topology-metrics-loader {
font-weight: normal;
font-size: 0.875rem;
color: $gray-500;
text-align: center;
margin: 0 auto !important;
display: block;
span::before {
@extend %with-info-circle-outline-mask, %as-pseudo;
background-color: $gray-500;
}
}
span.topology-metrics-loader::after {
@extend %with-loading-icon, %as-pseudo;
}

View File

@ -38,9 +38,8 @@ export default RepositoryService.extend({
return Promise.reject(this.error); return Promise.reject(this.error);
} }
const promises = [ const promises = [
// TODO: support namespaces in providers this.provider.serviceRecentSummarySeries(dc, nspace, slug, protocol, {}),
this.provider.serviceRecentSummarySeries(slug, protocol, {}), this.provider.serviceRecentSummaryStats(dc, nspace, slug, protocol, {}),
this.provider.serviceRecentSummaryStats(slug, protocol, {}),
]; ];
return Promise.all(promises).then(function(results) { return Promise.all(promises).then(function(results) {
return { return {
@ -55,7 +54,7 @@ export default RepositoryService.extend({
if (this.error) { if (this.error) {
return Promise.reject(this.error); return Promise.reject(this.error);
} }
return this.provider.upstreamRecentSummaryStats(slug, {}).then(function(result) { return this.provider.upstreamRecentSummaryStats(dc, nspace, slug, {}).then(function(result) {
result.meta = meta; result.meta = meta;
return result; return result;
}); });
@ -65,7 +64,7 @@ export default RepositoryService.extend({
if (this.error) { if (this.error) {
return Promise.reject(this.error); return Promise.reject(this.error);
} }
return this.provider.downstreamRecentSummaryStats(slug, {}).then(function(result) { return this.provider.downstreamRecentSummaryStats(dc, nspace, slug, {}).then(function(result) {
result.meta = meta; result.meta = meta;
return result; return result;
}); });

View File

@ -64,3 +64,4 @@
@import 'consul-ui/components/topology-metrics'; @import 'consul-ui/components/topology-metrics';
@import 'consul-ui/components/topology-metrics/series'; @import 'consul-ui/components/topology-metrics/series';
@import 'consul-ui/components/topology-metrics/stats'; @import 'consul-ui/components/topology-metrics/stats';
@import 'consul-ui/components/topology-metrics/status';

View File

@ -96,6 +96,7 @@ module.exports = function(environment, $ = process.env) {
CONSUL_ACLS_ENABLED: false, CONSUL_ACLS_ENABLED: false,
CONSUL_NSPACES_ENABLED: false, CONSUL_NSPACES_ENABLED: false,
CONSUL_SSO_ENABLED: false, CONSUL_SSO_ENABLED: false,
CONSUL_DATACENTER_LOCAL: env('CONSUL_DATACENTER_LOCAL', 'dc1'),
// Static variables used in multiple places throughout the UI // Static variables used in multiple places throughout the UI
CONSUL_HOME_URL: 'https://www.consul.io', CONSUL_HOME_URL: 'https://www.consul.io',
@ -164,9 +165,14 @@ module.exports = function(environment, $ = process.env) {
// __RUNTIME_BOOL_Xxxx__ will be replaced with either "true" or "false" // __RUNTIME_BOOL_Xxxx__ will be replaced with either "true" or "false"
// depending on whether the named variable is true or false in the data // depending on whether the named variable is true or false in the data
// returned from `uiTemplateDataFromConfig`. // returned from `uiTemplateDataFromConfig`.
//
// __RUNTIME_STRING_Xxxx__ will be replaced with the literal string in
// the named variable in the data returned from
// `uiTemplateDataFromConfig`. It may be empty.
CONSUL_ACLS_ENABLED: '__RUNTIME_BOOL_ACLsEnabled__', CONSUL_ACLS_ENABLED: '__RUNTIME_BOOL_ACLsEnabled__',
CONSUL_SSO_ENABLED: '__RUNTIME_BOOL_SSOEnabled__', CONSUL_SSO_ENABLED: '__RUNTIME_BOOL_SSOEnabled__',
CONSUL_NSPACES_ENABLED: '__RUNTIME_BOOL_NamespacesEnabled__', CONSUL_NSPACES_ENABLED: '__RUNTIME_BOOL_NamespacesEnabled__',
CONSUL_DATACENTER_LOCAL: '__RUNTIME_STRING_LocalDatacenter__',
}); });
break; break;
} }

View File

@ -56,7 +56,7 @@
"@ember/render-modifiers": "^1.0.2", "@ember/render-modifiers": "^1.0.2",
"@glimmer/component": "^1.0.0", "@glimmer/component": "^1.0.0",
"@glimmer/tracking": "^1.0.0", "@glimmer/tracking": "^1.0.0",
"@hashicorp/consul-api-double": "^5.3.7", "@hashicorp/consul-api-double": "^6.1.0",
"@hashicorp/ember-cli-api-double": "^3.1.0", "@hashicorp/ember-cli-api-double": "^3.1.0",
"@xstate/fsm": "^1.4.0", "@xstate/fsm": "^1.4.0",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",

View File

@ -26,7 +26,8 @@
/** /**
* serviceRecentSummarySeries should return time series for a recent time * serviceRecentSummarySeries should return time series for a recent time
* period summarizing the usage of the named service. * period summarizing the usage of the named service in the indicated
* datacenter. In Consul Enterprise a non-empty namespace is also provided.
* *
* If these metrics aren't available then an empty series array may be * If these metrics aren't available then an empty series array may be
* returned. * returned.
@ -60,8 +61,8 @@
* // to explain exactly what the metrics mean. * // to explain exactly what the metrics mean.
* labels: { * labels: {
* "Total": "Total inbound requests per second.", * "Total": "Total inbound requests per second.",
* "Successes": "Successful responses (with an HTTP response code not in the 5xx range) per second.", * "Successes": "Successful responses (with an HTTP response code ...",
* "Errors": "Error responses (with an HTTP response code in the 5xx range) per second.", * "Errors": "Error responses (with an HTTP response code in the ...",
* }, * },
* *
* data: [ * data: [
@ -77,7 +78,7 @@
* Every data point object should have a value for every series label * Every data point object should have a value for every series label
* (except for "Total") otherwise it will be assumed to be "0". * (except for "Total") otherwise it will be assumed to be "0".
*/ */
serviceRecentSummarySeries: function(serviceName, protocol, options) { serviceRecentSummarySeries: function(serviceDC, namespace, serviceName, protocol, options) {
// Fetch time-series // Fetch time-series
var series = [] var series = []
var labels = [] var labels = []
@ -98,7 +99,8 @@
/** /**
* serviceRecentSummaryStats should return four summary statistics for a * serviceRecentSummaryStats should return four summary statistics for a
* recent time period for the named service. * recent time period for the named service in the indicated datacenter. In
* Consul Enterprise a non-empty namespace is also provided.
* *
* If these metrics aren't available then an empty array may be returned. * If these metrics aren't available then an empty array may be returned.
* *
@ -118,8 +120,10 @@
* { * {
* // label should be 3 chars or fewer as an abbreviation * // label should be 3 chars or fewer as an abbreviation
* label: "SR", * label: "SR",
*
* // desc describes the stat in a tooltip * // desc describes the stat in a tooltip
* desc: "Success Rate - the percentage of all requests that were not 5xx status", * desc: "Success Rate - the percentage of all requests that were not 5xx status",
*
* // value is a string allowing the provider to format it and add * // value is a string allowing the provider to format it and add
* // units as appropriate. It should be as compact as possible. * // units as appropriate. It should be as compact as possible.
* value: "98%", * value: "98%",
@ -127,7 +131,7 @@
* ] * ]
* } * }
*/ */
serviceRecentSummaryStats: function(serviceName, protocol, options) { serviceRecentSummaryStats: function(serviceDC, namespace, serviceName, protocol, options) {
// Fetch stats // Fetch stats
var stats = []; var stats = [];
if (this.hasL7Metrics(protocol)) { if (this.hasL7Metrics(protocol)) {
@ -147,7 +151,14 @@
/** /**
* upstreamRecentSummaryStats should return four summary statistics for each * upstreamRecentSummaryStats should return four summary statistics for each
* upstream service over a recent time period. * upstream service over a recent time period, relative to the named service
* in the indicated datacenter. In Consul Enterprise a non-empty namespace
* is also provided.
*
* Note that the upstreams themselves might be in different datacenters but
* we only pass the target service DC since typically these metrics should
* be from the outbound listener of the target service in this DC even if
* they eventually end up in another DC.
* *
* If these metrics aren't available then an empty array may be returned. * If these metrics aren't available then an empty array may be returned.
* *
@ -171,13 +182,25 @@
* } * }
* } * }
*/ */
upstreamRecentSummaryStats: function(serviceName, upstreamName, options) { upstreamRecentSummaryStats: function(serviceDC, namespace, serviceName, upstreamName, options) {
return this.fetchRecentSummaryStats(serviceName, "upstream", options) return this.fetchRecentSummaryStats(serviceName, "upstream", options)
}, },
/** /**
* downstreamRecentSummaryStats should return four summary statistics for * downstreamRecentSummaryStats should return four summary statistics for
* each downstream service over a recent time period. * each downstream service over a recent time period, relative to the named
* service in the indicated datacenter. In Consul Enterprise a non-empty
* namespace is also provided.
*
* Note that the service may have downstreams in different datacenters. For
* some metrics systems which are per-datacenter this makes it hard to query
* for all downstream metrics from one source. For now the UI will only show
* downstreams in the same datacenter as the target service. In the future
* this method may be called multiple times, once for each DC that contains
* downstream services to gather metrics from each. In that case a separate
* option for target datacenter will be used since the target service's DC
* is still needed to correctly identify the outbound clusters that will
* route to it from the remote DC.
* *
* If these metrics aren't available then an empty array may be returned. * If these metrics aren't available then an empty array may be returned.
* *
@ -202,7 +225,7 @@
* } * }
* } * }
*/ */
downstreamRecentSummaryStats: function(serviceName, options) { downstreamRecentSummaryStats: function(serviceDC, namespace, serviceName, options) {
return this.fetchRecentSummaryStats(serviceName, "downstream", options) return this.fetchRecentSummaryStats(serviceName, "downstream", options)
}, },

View File

@ -1243,10 +1243,10 @@
faker "^4.1.0" faker "^4.1.0"
js-yaml "^3.13.1" js-yaml "^3.13.1"
"@hashicorp/consul-api-double@^5.3.7": "@hashicorp/consul-api-double@^6.1.0":
version "5.4.0" version "6.1.2"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-5.4.0.tgz#fc75e064c3e50385f4fb8c5dd9068875806d8901" resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-6.1.2.tgz#06f1d5e81014b34d6cf5d4935cec1c2eafec1000"
integrity sha512-vAi580MyPoFhjDl8WhSviMzFJ1/PZesLqYCuGy8vuxqFaKCQET4AR8gRuungWSdRf5432aJXUNtXLhMHdJeNPg== integrity sha512-UtM0TuViKS79QD9MuS2LwOassjrNlO0+yy858gXCo1CsxYDRdDNaeFSfKmp2mMmhjxXlxUeXwl4eSZPRczKdAQ==
"@hashicorp/ember-cli-api-double@^3.1.0": "@hashicorp/ember-cli-api-double@^3.1.0":
version "3.1.2" version "3.1.2"