mirror of https://github.com/linkerd/linkerd2.git
Add dashboard support for externally hosted Grafana (#7596)
Fixes #7429 Currently Linkerd assumes that the Grafana instance is hosted on-cluster. Some users would like to use externally hosted Grafana instances, such as Grafana Cloud or AWS Managed Grafana. In general users will have multiple Linkerd clusters with dashboards in the same Grafana workspace, so we need to introduce a prefix for the Grafana dashboard UID's so that they remain unique. This PR adds two new viz config values, `grafana.uidPrefix` and `grafana.externalUrl`. When grafana.uidPrefix is set, it will insert the user-supplied prefix in the URL's for the Grafana dashboards. When grafana.externalUrl is set, its value will be used for the links to Grafana dashboards instead of using the grafana reverse proxy. Signed-off-by: Jack Gill <jack.gill@elationhealth.com>
This commit is contained in:
parent
85a4138442
commit
6c3da0279d
|
|
@ -96,7 +96,9 @@ Kubernetes: `>=1.20.0-0`
|
|||
| defaultUID | int | `2103` | UID for all the viz components |
|
||||
| enablePSP | bool | `false` | Create Roles and RoleBindings to associate this extension's ServiceAccounts to the control plane PSP resource. This requires that `enabledPSP` is set to true on the control plane install. Note PSP has been deprecated since k8s v1.21 |
|
||||
| enablePodAntiAffinity | bool | `false` | Enables Pod Anti Affinity logic to balance the placement of replicas across hosts and zones for High Availability. Enable this only when you have multiple replicas of components. |
|
||||
| grafana.url | string | `nil` | url of a Grafana instance with reverse proxy configured, used by the Linkerd viz web dashboard to provide direct links to specific Grafana dashboards. See the [Linkerd documentation](https://linkerd.io/2/tasks/grafana) for more information |
|
||||
| grafana.externalUrl | string | `nil` | url of a Grafana instance hosted off-cluster. Cannot be set if grafana.url is set. The reverse proxy will not be used for this URL. |
|
||||
| grafana.uidPrefix | string | `nil` | prefix for Grafana dashboard UID's, used when grafana.externalUrl is set. |
|
||||
| grafana.url | string | `nil` | url of an in-cluster Grafana instance with reverse proxy configured, used by the Linkerd viz web dashboard to provide direct links to specific Grafana dashboards. Cannot be set if grafana.externalUrl is set. See the [Linkerd documentation](https://linkerd.io/2/tasks/grafana) for more information |
|
||||
| identityTrustDomain | string | clusterDomain | Trust domain used for identity |
|
||||
| imagePullSecrets | list | `[]` | For Private docker registries, authentication is needed. Registry secrets are applied to the respective service accounts |
|
||||
| jaegerUrl | string | `""` | url of external jaeger instance Set this to `jaeger.linkerd-jaeger.svc.<clusterDomain>` if you plan to use jaeger extension |
|
||||
|
|
|
|||
|
|
@ -73,9 +73,18 @@ spec:
|
|||
- args:
|
||||
- -linkerd-metrics-api-addr=metrics-api.{{.Release.Namespace}}.svc.{{.Values.clusterDomain}}:8085
|
||||
- -cluster-domain={{.Values.clusterDomain}}
|
||||
{{- if and .Values.grafana.url .Values.grafana.externalUrl }}
|
||||
{{ fail "Cannot set both grafana.url (on-cluster Grafana) and grafana.externalUrl (off-cluster Grafana)"}}
|
||||
{{- end}}
|
||||
{{- if .Values.grafana.url }}
|
||||
- -grafana-addr={{.Values.grafana.url}}
|
||||
{{- end}}
|
||||
{{- if .Values.grafana.externalUrl }}
|
||||
- -grafana-external-addr={{.Values.grafana.externalUrl}}
|
||||
{{- end}}
|
||||
{{- if .Values.grafana.uidPrefix }}
|
||||
- -grafana-prefix={{.Values.grafana.uidPrefix}}
|
||||
{{- end}}
|
||||
{{- if .Values.jaegerUrl }}
|
||||
- -jaeger-addr={{.Values.jaegerUrl}}
|
||||
{{- end}}
|
||||
|
|
|
|||
|
|
@ -344,11 +344,17 @@ dashboard:
|
|||
# resources:
|
||||
|
||||
grafana:
|
||||
# -- url of a Grafana instance with reverse proxy configured, used by the
|
||||
# -- url of an in-cluster Grafana instance with reverse proxy configured, used by the
|
||||
# Linkerd viz web dashboard to provide direct links to specific Grafana
|
||||
# dashboards. See the [Linkerd
|
||||
# dashboards. Cannot be set if grafana.externalUrl is set. See the [Linkerd
|
||||
# documentation](https://linkerd.io/2/tasks/grafana) for more information
|
||||
url:
|
||||
# -- url of a Grafana instance hosted off-cluster. Cannot be set if
|
||||
# grafana.url is set. The reverse proxy will not be used for this URL.
|
||||
externalUrl:
|
||||
# -- prefix for Grafana dashboard UID's, used when grafana.externalUrl is
|
||||
# set.
|
||||
uidPrefix:
|
||||
|
||||
prometheus:
|
||||
# -- toggle field to enable or disable prometheus
|
||||
|
|
|
|||
|
|
@ -3,19 +3,49 @@ import React from 'react';
|
|||
import _isEmpty from 'lodash/isEmpty';
|
||||
import { grafanaIcon } from './util/SvgWrappers.jsx';
|
||||
|
||||
const GrafanaLink = function({ PrefixedLink, name, namespace, resource }) {
|
||||
let link = `/grafana/d/linkerd-${resource}?var-${resource}=${name}`;
|
||||
const GrafanaLink = function({ PrefixedLink, name, namespace, resource, grafanaExternalUrl, grafanaPrefix }) {
|
||||
let link = '/grafana/d/';
|
||||
|
||||
if (grafanaExternalUrl !== '') {
|
||||
let baseUrl = grafanaExternalUrl;
|
||||
// strip trailing slash if present, to avoid double slash in final URL
|
||||
if (grafanaExternalUrl.charAt(grafanaExternalUrl.length - 1) === '/') {
|
||||
baseUrl = grafanaExternalUrl.slice(0, grafanaExternalUrl.length - 1);
|
||||
}
|
||||
|
||||
// grafanaPrefix is used for externally hosted Grafana instances, which do not use the /grafana proxy.
|
||||
// When dashboards for multiple Linkerd instances are deployed to the same Grafana host,
|
||||
// the dashboard UID needs a user-specified prefix to be unique.
|
||||
link = `${baseUrl}/d/${grafanaPrefix}`;
|
||||
}
|
||||
|
||||
link += `linkerd-${resource}?var-${resource}=${name}`;
|
||||
|
||||
if (!_isEmpty(namespace)) {
|
||||
link += `&var-namespace=${namespace}`;
|
||||
}
|
||||
return (
|
||||
<PrefixedLink
|
||||
to={link}
|
||||
targetBlank>
|
||||
|
||||
{grafanaIcon}
|
||||
</PrefixedLink>
|
||||
);
|
||||
|
||||
if (grafanaExternalUrl !== '') {
|
||||
return (
|
||||
// <a> instead of <PrefixedLink> because <Link> doesn't work with external URL's
|
||||
<a
|
||||
href={link}
|
||||
rel="noreferrer"
|
||||
target="_blank">
|
||||
|
||||
{grafanaIcon}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<PrefixedLink
|
||||
to={link}
|
||||
targetBlank>
|
||||
|
||||
{grafanaIcon}
|
||||
</PrefixedLink>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
GrafanaLink.propTypes = {
|
||||
|
|
@ -23,10 +53,14 @@ GrafanaLink.propTypes = {
|
|||
namespace: PropTypes.string,
|
||||
PrefixedLink: PropTypes.func.isRequired,
|
||||
resource: PropTypes.string.isRequired,
|
||||
grafanaExternalUrl: PropTypes.string,
|
||||
grafanaPrefix: PropTypes.string,
|
||||
};
|
||||
|
||||
GrafanaLink.defaultProps = {
|
||||
namespace: '',
|
||||
grafanaExternalUrl: '',
|
||||
grafanaPrefix: '',
|
||||
};
|
||||
|
||||
export default GrafanaLink;
|
||||
|
|
|
|||
|
|
@ -47,4 +47,25 @@ describe('GrafanaLink', () => {
|
|||
expect(href).not.toContain("namespace");
|
||||
expect(href).toContain(expectedVarNameStr);
|
||||
});
|
||||
|
||||
it('Use grafana.externalUrl', () => {
|
||||
let grafanaPrefix = "test-prefix";
|
||||
let grafanaExternalUrl = "https://example.com";
|
||||
let api = ApiHelpers('');
|
||||
let linkProps = {
|
||||
resource: "replicationcontroller",
|
||||
name: "aldksf-3409823049823",
|
||||
namespace: "myns",
|
||||
grafanaPrefix: grafanaPrefix,
|
||||
grafanaExternalUrl: grafanaExternalUrl,
|
||||
};
|
||||
let component = mount(routerWrap(GrafanaLink, linkProps));
|
||||
|
||||
expect(component.find("GrafanaLink")).toHaveLength(1);
|
||||
|
||||
const href = component.find('a').props().href;
|
||||
|
||||
expect(href).toContain(grafanaPrefix);
|
||||
expect(href).toContain(grafanaExternalUrl);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ const gatewayColumns = [
|
|||
},
|
||||
];
|
||||
|
||||
const columnDefinitions = (resource, showNamespaceColumn, showNameColumn, PrefixedLink, isTcpTable, hasTsStats, grafana, jaeger) => {
|
||||
const columnDefinitions = (resource, showNamespaceColumn, showNameColumn, PrefixedLink, isTcpTable, hasTsStats, grafana, grafanaExternalUrl, grafanaPrefix, jaeger) => {
|
||||
const isAuthorityTable = resource === 'authority';
|
||||
const isTrafficSplitTable = resource === 'trafficsplit';
|
||||
const isServicesTable = resource === 'service';
|
||||
|
|
@ -228,6 +228,8 @@ const columnDefinitions = (resource, showNamespaceColumn, showNameColumn, Prefix
|
|||
name={row.name}
|
||||
namespace={row.namespace}
|
||||
resource={row.type}
|
||||
grafanaExternalUrl={grafanaExternalUrl}
|
||||
grafanaPrefix={grafanaPrefix}
|
||||
PrefixedLink={PrefixedLink} />
|
||||
);
|
||||
},
|
||||
|
|
@ -304,7 +306,7 @@ const columnDefinitions = (resource, showNamespaceColumn, showNameColumn, Prefix
|
|||
}
|
||||
|
||||
if (!isTrafficSplitTable) {
|
||||
if (grafana !== '') {
|
||||
if (grafana !== '' || grafanaExternalUrl !== '') {
|
||||
columns = columns.concat(grafanaColumn);
|
||||
}
|
||||
if (jaeger !== '') {
|
||||
|
|
@ -331,13 +333,13 @@ const preprocessMetrics = metrics => {
|
|||
return tableData;
|
||||
};
|
||||
|
||||
const MetricsTable = function({ metrics, resource, showNamespaceColumn, showName, title, api, isTcpTable, selectedNamespace, grafana, jaeger }) {
|
||||
const MetricsTable = function({ metrics, resource, showNamespaceColumn, showName, title, api, isTcpTable, selectedNamespace, grafana, grafanaExternalUrl, grafanaPrefix, jaeger }) {
|
||||
const showNsColumn = resource === 'namespace' || selectedNamespace !== '_all' ? false : showNamespaceColumn;
|
||||
const showNameColumn = resource !== 'trafficsplit' ? true : showName;
|
||||
let orderBy = 'name';
|
||||
if (resource === 'trafficsplit' && !showNameColumn) { orderBy = 'leaf'; }
|
||||
const hasTsStats = _some(metrics, m => m.tsStats);
|
||||
const columns = columnDefinitions(resource, showNsColumn, showNameColumn, api.PrefixedLink, isTcpTable, hasTsStats, grafana, jaeger);
|
||||
const columns = columnDefinitions(resource, showNsColumn, showNameColumn, api.PrefixedLink, isTcpTable, hasTsStats, grafana, grafanaExternalUrl, grafanaPrefix, jaeger);
|
||||
const rows = preprocessMetrics(metrics);
|
||||
return (
|
||||
<BaseTable
|
||||
|
|
@ -363,6 +365,8 @@ MetricsTable.propTypes = {
|
|||
showNamespaceColumn: PropTypes.bool,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
grafana: PropTypes.string,
|
||||
grafanaExternalUrl: PropTypes.string,
|
||||
grafanaPrefix: PropTypes.string,
|
||||
jaeger: PropTypes.string,
|
||||
};
|
||||
|
||||
|
|
@ -371,6 +375,8 @@ MetricsTable.defaultProps = {
|
|||
showName: true,
|
||||
title: '',
|
||||
grafana: '',
|
||||
grafanaExternalUrl: '',
|
||||
grafanaPrefix: '',
|
||||
jaeger: '',
|
||||
isTcpTable: false,
|
||||
metrics: [],
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ func main() {
|
|||
metricsAddr := cmd.String("metrics-addr", ":9994", "address to serve scrapable metrics on")
|
||||
vizAPIAddr := cmd.String("linkerd-metrics-api-addr", "127.0.0.1:8085", "address of the linkerd-metrics-api service")
|
||||
grafanaAddr := cmd.String("grafana-addr", "", "address of the linkerd-grafana service")
|
||||
grafanaExternalAddr := cmd.String("grafana-external-addr", "", "address of the external grafana service")
|
||||
grafanaPrefix := cmd.String("grafana-prefix", "", "prefix for Grafana dashboard UID's")
|
||||
jaegerAddr := cmd.String("jaeger-addr", "", "address of the jaeger service")
|
||||
templateDir := cmd.String("template-dir", "templates", "directory to search for template files")
|
||||
staticDir := cmd.String("static-dir", "app/dist", "directory to search for static files")
|
||||
|
|
@ -94,7 +96,7 @@ func main() {
|
|||
log.Fatalf("invalid --enforced-host parameter: %s", err)
|
||||
}
|
||||
|
||||
server := srv.NewServer(*addr, *grafanaAddr, *jaegerAddr, *templateDir, *staticDir, uuid, version,
|
||||
server := srv.NewServer(*addr, *grafanaAddr, *grafanaExternalAddr, *grafanaPrefix, *jaegerAddr, *templateDir, *staticDir, uuid, version,
|
||||
*controllerNamespace, *clusterDomain, *reload, reHost, client, k8sAPI, hc)
|
||||
|
||||
go func() {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ type (
|
|||
controllerNamespace string
|
||||
clusterDomain string
|
||||
grafana string
|
||||
grafanaExternalURL string
|
||||
grafanaPrefix string
|
||||
jaeger string
|
||||
grafanaProxy *reverseProxy
|
||||
jaegerProxy *reverseProxy
|
||||
|
|
@ -49,6 +51,8 @@ func (h *handler) handleIndex(w http.ResponseWriter, req *http.Request, p httpro
|
|||
ControllerNamespace: h.controllerNamespace,
|
||||
PathPrefix: pathPfx,
|
||||
Grafana: h.grafana,
|
||||
GrafanaExternalURL: h.grafanaExternalURL,
|
||||
GrafanaPrefix: h.grafanaPrefix,
|
||||
Jaeger: h.jaeger,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ type (
|
|||
PathPrefix string
|
||||
Jaeger string
|
||||
Grafana string
|
||||
GrafanaExternalURL string
|
||||
GrafanaPrefix string
|
||||
}
|
||||
|
||||
healthChecker interface {
|
||||
|
|
@ -83,6 +85,8 @@ Please see https://linkerd.io/dns-rebinding for an explanation of what is happen
|
|||
func NewServer(
|
||||
addr string,
|
||||
grafanaAddr string,
|
||||
grafanaExternalAddr string,
|
||||
grafanaPrefix string,
|
||||
jaegerAddr string,
|
||||
templateDir string,
|
||||
staticDir string,
|
||||
|
|
@ -117,14 +121,20 @@ func NewServer(
|
|||
version: version,
|
||||
controllerNamespace: controllerNamespace,
|
||||
clusterDomain: clusterDomain,
|
||||
grafanaProxy: newReverseProxy(grafanaAddr, "/grafana"),
|
||||
jaegerProxy: newReverseProxy(jaegerAddr, ""),
|
||||
grafana: grafanaAddr,
|
||||
grafanaExternalURL: grafanaExternalAddr,
|
||||
grafanaPrefix: grafanaPrefix,
|
||||
jaeger: jaegerAddr,
|
||||
hc: hc,
|
||||
statCache: cache.New(statExpiration, statCleanupInterval),
|
||||
}
|
||||
|
||||
// Only create the grafana reverse proxy if we aren't using external grafana
|
||||
if grafanaExternalAddr == "" {
|
||||
handler.grafanaProxy = newReverseProxy(grafanaAddr, "/grafana")
|
||||
}
|
||||
|
||||
httpServer := &http.Server{
|
||||
Addr: addr,
|
||||
ReadTimeout: timeout,
|
||||
|
|
@ -201,8 +211,10 @@ func NewServer(
|
|||
server.router.GET("/api/gateways", handler.handleAPIGateways)
|
||||
server.router.GET("/api/extensions", handler.handleGetExtensions)
|
||||
|
||||
// grafana proxy
|
||||
server.handleAllOperationsForPath("/grafana/*grafanapath", handler.handleGrafana)
|
||||
// grafana proxy, only used if external grafana is not in use
|
||||
if grafanaExternalAddr == "" {
|
||||
server.handleAllOperationsForPath("/grafana/*grafanapath", handler.handleGrafana)
|
||||
}
|
||||
|
||||
// jaeger proxy
|
||||
server.handleAllOperationsForPath("/jaeger/*jaegerpath", handler.handleJaeger)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
data-controller-namespace="{{.ControllerNamespace}}"
|
||||
data-uuid="{{.UUID}}"
|
||||
data-grafana="{{.Grafana}}"
|
||||
data-grafana-external-url="{{.GrafanaExternalURL}}"
|
||||
data-grafana-prefix="{{.GrafanaPrefix}}"
|
||||
data-jaeger="{{.Jaeger}}">
|
||||
{{ if .Error }}
|
||||
<p>Failed to call public API: {{ .ErrorMessage }}</p>
|
||||
|
|
|
|||
Loading…
Reference in New Issue