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
This commit is contained in:
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{}{
"ContentPath": cfg.UIConfig.ContentPath,
"ACLsEnabled": cfg.ACLsEnabled,
"UIConfig": uiCfg,
"ContentPath": cfg.UIConfig.ContentPath,
"ACLsEnabled": cfg.ACLsEnabled,
"UIConfig": uiCfg,
"LocalDatacenter": cfg.Datacenter,
}
// 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
// is there as a placeholder so the end result is an actual JSON bool not a
// 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 {
// Trim the prefix and __ suffix
varName := strings.TrimSuffix(strings.TrimPrefix(str, "%22__RUNTIME_BOOL_"), "__%22")
if v, ok := tplData[varName].(bool); ok && v {
return "true"
// Trim the prefix and suffix
pair := strings.TrimSuffix(strings.TrimPrefix(str, "%22__RUNTIME_"), "__%22")
parts := strings.SplitN(pair, "_", 2)
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{

View File

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

View File

@ -47,9 +47,19 @@
</div>
{{#if @hasMetricsProvider }}
{{#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}}
<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}}
</a>

View File

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

View File

@ -5,6 +5,7 @@ import { inject as service } from '@ember/service';
export default class TopologyMetrics extends Component {
@service('ui-config') cfg;
@service('env') env;
// =attributes
@tracked centerDimensions;
@ -13,10 +14,21 @@ export default class TopologyMetrics extends Component {
@tracked upView;
@tracked upLines = [];
@tracked hasMetricsProvider = false;
@tracked noMetricsReason = null;
constructor(owner, args) {
super(owner, args);
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

View File

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

View File

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

View File

@ -25,16 +25,11 @@
display: inline-block;
}
div.sparkline-loader {
font-weight: normal;
// extra padding for the status sub-component that's not needed for the stats
// status
.topology-metrics-error,
.topology-metrics-loader {
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
@src={{uri nspace dc 'metrics' @endpoint @service @protocol}}
@onchange={{action 'statsUpdate'}}
/>
{{#unless @noMetricsReason}}
<DataSource
@src={{uri nspace dc 'metrics' @endpoint @service @protocol}}
@onchange={{action 'statsUpdate'}}
@onerror={{action (mut error) value="error"}}
/>
{{/unless}}
<div class="stats">
{{#if hasLoaded }}
@ -19,6 +22,9 @@
<span>No Metrics Available</span>
{{/each}}
{{else}}
<span class="loader">Loading Metrics</span>
<TopologyMetrics::Status
@noMetricsReason={{@noMetricsReason}}
@error={{error}}
/>
{{/if}}
</div>

View File

@ -18,11 +18,4 @@
dd {
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);
}
const promises = [
// TODO: support namespaces in providers
this.provider.serviceRecentSummarySeries(slug, protocol, {}),
this.provider.serviceRecentSummaryStats(slug, protocol, {}),
this.provider.serviceRecentSummarySeries(dc, nspace, slug, protocol, {}),
this.provider.serviceRecentSummaryStats(dc, nspace, slug, protocol, {}),
];
return Promise.all(promises).then(function(results) {
return {
@ -55,7 +54,7 @@ export default RepositoryService.extend({
if (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;
return result;
});
@ -65,7 +64,7 @@ export default RepositoryService.extend({
if (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;
return result;
});

View File

@ -64,3 +64,4 @@
@import 'consul-ui/components/topology-metrics';
@import 'consul-ui/components/topology-metrics/series';
@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_NSPACES_ENABLED: false,
CONSUL_SSO_ENABLED: false,
CONSUL_DATACENTER_LOCAL: env('CONSUL_DATACENTER_LOCAL', 'dc1'),
// Static variables used in multiple places throughout the UI
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"
// depending on whether the named variable is true or false in the data
// 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_SSO_ENABLED: '__RUNTIME_BOOL_SSOEnabled__',
CONSUL_NSPACES_ENABLED: '__RUNTIME_BOOL_NamespacesEnabled__',
CONSUL_DATACENTER_LOCAL: '__RUNTIME_STRING_LocalDatacenter__',
});
break;
}

View File

@ -56,7 +56,7 @@
"@ember/render-modifiers": "^1.0.2",
"@glimmer/component": "^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",
"@xstate/fsm": "^1.4.0",
"babel-eslint": "^10.0.3",

View File

@ -26,7 +26,8 @@
/**
* 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
* returned.
@ -60,8 +61,8 @@
* // 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.",
* "Successes": "Successful responses (with an HTTP response code ...",
* "Errors": "Error responses (with an HTTP response code in the ...",
* },
*
* data: [
@ -77,7 +78,7 @@
* 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) {
serviceRecentSummarySeries: function(serviceDC, namespace, serviceName, protocol, options) {
// Fetch time-series
var series = []
var labels = []
@ -98,7 +99,8 @@
/**
* 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.
*
@ -118,8 +120,10 @@
* {
* // label should be 3 chars or fewer as an abbreviation
* label: "SR",
*
* // desc describes the stat in a tooltip
* 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
* // units as appropriate. It should be as compact as possible.
* value: "98%",
@ -127,7 +131,7 @@
* ]
* }
*/
serviceRecentSummaryStats: function(serviceName, protocol, options) {
serviceRecentSummaryStats: function(serviceDC, namespace, serviceName, protocol, options) {
// Fetch stats
var stats = [];
if (this.hasL7Metrics(protocol)) {
@ -147,7 +151,14 @@
/**
* 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.
*
@ -171,13 +182,25 @@
* }
* }
*/
upstreamRecentSummaryStats: function(serviceName, upstreamName, options) {
upstreamRecentSummaryStats: function(serviceDC, namespace, serviceName, upstreamName, options) {
return this.fetchRecentSummaryStats(serviceName, "upstream", options)
},
/**
* 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.
*
@ -202,7 +225,7 @@
* }
* }
*/
downstreamRecentSummaryStats: function(serviceName, options) {
downstreamRecentSummaryStats: function(serviceDC, namespace, serviceName, options) {
return this.fetchRecentSummaryStats(serviceName, "downstream", options)
},

View File

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