From 3f555710d25b84ad58b763fecdb7e1edb6dff7d9 Mon Sep 17 00:00:00 2001 From: ricoberger Date: Sat, 3 Jul 2021 23:49:48 +0200 Subject: [PATCH] Re-add Prometheus Plugin Re-add the Prometheus plugin as it already exists. We adjusted the existing Prometheus plugin to fit in the new plugin system for kobs. For that the code for the plugin lives now in the plugins folder. The Prometheus plugin supports the same features as the former implementation, but we are now using nivo to render the charts instead of the Patternfly charting library. We decided to change the library, because nivo can render canvas charts, which comes with a better performance for the dashboards. We also made some adjustments to the dashboards handling, so that we are now only calculating the dimensions for the dashboards once instead of for every dashboard. We also adjusted the tab handling, so that the currently selected dashboard is reflected in the url. We plan to extend this later, so that also the selected time range and variables are reflected in the url. --- app/package.json | 1 + app/src/index.tsx | 2 + deploy/docker/kobs/config.yaml | 15 +- go.mod | 1 + go.sum | 2 + .../middleware/roundtripper/roundtripper.go | 42 +++ pkg/api/plugins/plugins.go | 3 + .../src/components/page/Application.tsx | 4 +- .../src/components/panel/details/Details.tsx | 4 +- plugins/core/src/components/misc/Options.tsx | 31 +- .../core/src/components/plugin/PluginCard.tsx | 2 +- plugins/core/src/index.ts | 2 + plugins/core/src/utils/useDebounce.tsx | 17 ++ plugins/core/src/utils/useDimensions.tsx | 34 +++ .../src/components/dashboards/Dashboard.tsx | 43 ++- .../dashboards/DashboardToolbar.tsx | 1 + .../dashboards/DashboardToolbarVariable.tsx | 1 + .../dashboards/DashboardWrapper.tsx | 59 ---- .../src/components/dashboards/Dashboards.tsx | 51 +++- .../dashboards/DashboardsWrapper.tsx | 36 +++ plugins/dashboards/src/index.ts | 2 +- plugins/dashboards/src/utils/dashboard.ts | 29 +- plugins/dashboards/src/utils/interfaces.ts | 14 + plugins/prometheus/package.json | 28 ++ plugins/prometheus/pkg/instance/helpers.go | 54 ++++ plugins/prometheus/pkg/instance/instance.go | 286 ++++++++++++++++++ plugins/prometheus/pkg/instance/structs.go | 27 ++ plugins/prometheus/prometheus.go | 216 +++++++++++++ plugins/prometheus/src/assets/icon.png | Bin 0 -> 116521 bytes .../prometheus/src/components/page/Page.tsx | 60 ++++ .../src/components/page/PageChart.tsx | 117 +++++++ .../src/components/page/PageChartLegend.tsx | 68 +++++ .../src/components/page/PageChartWrapper.tsx | 90 ++++++ .../src/components/page/PageToolbar.tsx | 156 ++++++++++ .../page/PageToolbarAutocomplete.tsx | 155 ++++++++++ .../src/components/panel/Actions.tsx | 54 ++++ .../prometheus/src/components/panel/Chart.tsx | 73 +++++ .../src/components/panel/ChartLegend.tsx | 76 +++++ .../src/components/panel/ChartWrapper.tsx | 125 ++++++++ .../prometheus/src/components/panel/Panel.tsx | 49 +++ .../src/components/panel/Sparkline.tsx | 123 ++++++++ .../prometheus/src/components/panel/Table.tsx | 116 +++++++ plugins/prometheus/src/index.ts | 16 + plugins/prometheus/src/utils/colors.ts | 61 ++++ plugins/prometheus/src/utils/helpers.ts | 76 +++++ plugins/prometheus/src/utils/interfaces.ts | 81 +++++ plugins/prometheus/tsconfig.esm.json | 12 + plugins/prometheus/tsconfig.json | 20 ++ plugins/teams/src/components/page/Team.tsx | 4 +- yarn.lock | 277 ++++++++++++++++- 50 files changed, 2727 insertions(+), 89 deletions(-) create mode 100644 pkg/api/middleware/roundtripper/roundtripper.go create mode 100644 plugins/core/src/utils/useDebounce.tsx create mode 100644 plugins/core/src/utils/useDimensions.tsx delete mode 100644 plugins/dashboards/src/components/dashboards/DashboardWrapper.tsx create mode 100644 plugins/dashboards/src/components/dashboards/DashboardsWrapper.tsx create mode 100644 plugins/prometheus/package.json create mode 100644 plugins/prometheus/pkg/instance/helpers.go create mode 100644 plugins/prometheus/pkg/instance/instance.go create mode 100644 plugins/prometheus/pkg/instance/structs.go create mode 100644 plugins/prometheus/prometheus.go create mode 100644 plugins/prometheus/src/assets/icon.png create mode 100644 plugins/prometheus/src/components/page/Page.tsx create mode 100644 plugins/prometheus/src/components/page/PageChart.tsx create mode 100644 plugins/prometheus/src/components/page/PageChartLegend.tsx create mode 100644 plugins/prometheus/src/components/page/PageChartWrapper.tsx create mode 100644 plugins/prometheus/src/components/page/PageToolbar.tsx create mode 100644 plugins/prometheus/src/components/page/PageToolbarAutocomplete.tsx create mode 100644 plugins/prometheus/src/components/panel/Actions.tsx create mode 100644 plugins/prometheus/src/components/panel/Chart.tsx create mode 100644 plugins/prometheus/src/components/panel/ChartLegend.tsx create mode 100644 plugins/prometheus/src/components/panel/ChartWrapper.tsx create mode 100644 plugins/prometheus/src/components/panel/Panel.tsx create mode 100644 plugins/prometheus/src/components/panel/Sparkline.tsx create mode 100644 plugins/prometheus/src/components/panel/Table.tsx create mode 100644 plugins/prometheus/src/index.ts create mode 100644 plugins/prometheus/src/utils/colors.ts create mode 100644 plugins/prometheus/src/utils/helpers.ts create mode 100644 plugins/prometheus/src/utils/interfaces.ts create mode 100644 plugins/prometheus/tsconfig.esm.json create mode 100644 plugins/prometheus/tsconfig.json diff --git a/app/package.json b/app/package.json index bdc70047b..5b7c3ce57 100644 --- a/app/package.json +++ b/app/package.json @@ -7,6 +7,7 @@ "@kobsio/plugin-applications": "*", "@kobsio/plugin-core": "*", "@kobsio/plugin-dashboards": "*", + "@kobsio/plugin-prometheus": "*", "@kobsio/plugin-resources": "*", "@kobsio/plugin-teams": "*", "@types/node": "^15.14.0", diff --git a/app/src/index.tsx b/app/src/index.tsx index f0590a0dd..018b033a1 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -8,6 +8,7 @@ import resourcesPlugin from '@kobsio/plugin-resources'; import teamsPlugin from '@kobsio/plugin-teams'; import applicationsPlugin from '@kobsio/plugin-applications'; import dashboardsPlugin from '@kobsio/plugin-dashboards'; +import prometheusPlugin from '@kobsio/plugin-prometheus'; ReactDOM.render( @@ -16,6 +17,7 @@ ReactDOM.render( ...teamsPlugin, ...applicationsPlugin, ...dashboardsPlugin, + ...prometheusPlugin, }} /> , document.getElementById('root') diff --git a/deploy/docker/kobs/config.yaml b/deploy/docker/kobs/config.yaml index 543415319..749bc4c40 100644 --- a/deploy/docker/kobs/config.yaml +++ b/deploy/docker/kobs/config.yaml @@ -14,13 +14,14 @@ plugins: topologyCacheDuration: 1m teamsCacheDuration: 1m -prometheus: - - name: Prometheus - description: Prometheus can be used for the metrics of your application. - address: http://localhost:9090 - username: - password: - token: + prometheus: + - name: prometheus + displayName: Prometheus + description: Prometheus can be used for the metrics of your application. + address: http://localhost:9090 + username: + password: + token: elasticsearch: - name: Elasticsearch diff --git a/go.mod b/go.mod index ad0f7ee9a..a65cd3c72 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/go-chi/cors v1.2.0 github.com/go-chi/render v1.0.1 github.com/prometheus/client_golang v1.11.0 + github.com/prometheus/common v0.26.0 github.com/sirupsen/logrus v1.8.1 github.com/spf13/pflag v1.0.5 gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index 06d8d692f..fc686e4ed 100644 --- a/go.sum +++ b/go.sum @@ -218,6 +218,7 @@ github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -273,6 +274,7 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= diff --git a/pkg/api/middleware/roundtripper/roundtripper.go b/pkg/api/middleware/roundtripper/roundtripper.go new file mode 100644 index 000000000..602ff14c5 --- /dev/null +++ b/pkg/api/middleware/roundtripper/roundtripper.go @@ -0,0 +1,42 @@ +package roundtripper + +import ( + "net" + "net/http" + "time" +) + +// DefaultRoundTripper is our default RoundTripper. +var DefaultRoundTripper http.RoundTripper = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 10 * time.Second, +} + +// BasicAuthTransport is the struct to add basic auth to a RoundTripper. +type BasicAuthTransport struct { + Transport http.RoundTripper + Username string + Password string +} + +// RoundTrip implements the RoundTrip for our RoundTripper with basic auth support. +func (bat BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.SetBasicAuth(bat.Username, bat.Password) + return bat.Transport.RoundTrip(req) +} + +// TokenAuthTransporter is the struct to add token auth to a RoundTripper. +type TokenAuthTransporter struct { + Transport http.RoundTripper + Token string +} + +// RoundTrip implements the RoundTrip for our RoundTripper with token auth support. +func (tat TokenAuthTransporter) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Set("Authorization", "Bearer "+tat.Token) + return tat.Transport.RoundTrip(req) +} diff --git a/pkg/api/plugins/plugins.go b/pkg/api/plugins/plugins.go index 2a991e770..6b55cb131 100644 --- a/pkg/api/plugins/plugins.go +++ b/pkg/api/plugins/plugins.go @@ -13,6 +13,7 @@ import ( // the plugins folder. "github.com/kobsio/kobs/plugins/applications" "github.com/kobsio/kobs/plugins/dashboards" + "github.com/kobsio/kobs/plugins/prometheus" "github.com/kobsio/kobs/plugins/resources" "github.com/kobsio/kobs/plugins/teams" ) @@ -23,6 +24,7 @@ type Config struct { Resources resources.Config `yaml:"resources"` Teams teams.Config `yaml:"teams"` Dashboards dashboards.Config `yaml:"dashboards"` + Prometheus prometheus.Config `yaml:"prometheus"` } // Router implements the router for the plugins package. This only registeres one route which is used to return all the @@ -51,6 +53,7 @@ func Register(clusters *clusters.Clusters, config Config) chi.Router { router.Mount(resources.Route, resources.Register(clusters, router.plugins, config.Resources)) router.Mount(teams.Route, teams.Register(clusters, router.plugins, config.Teams)) router.Mount(dashboards.Route, dashboards.Register(clusters, router.plugins, config.Dashboards)) + router.Mount(prometheus.Route, prometheus.Register(clusters, router.plugins, config.Prometheus)) return router } diff --git a/plugins/applications/src/components/page/Application.tsx b/plugins/applications/src/components/page/Application.tsx index aeddffdf6..487595860 100644 --- a/plugins/applications/src/components/page/Application.tsx +++ b/plugins/applications/src/components/page/Application.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { UsersIcon } from '@patternfly/react-icons'; import { ExternalLink, Title } from '@kobsio/plugin-core'; -import { Dashboards } from '@kobsio/plugin-dashboards'; +import { DashboardsWrapper } from '@kobsio/plugin-dashboards'; import { IApplication } from '../../utils/interfaces'; interface IApplicationsParams { @@ -128,7 +128,7 @@ const Application: React.FunctionComponent = () => { {data.dashboards ? ( - + ) : ( )} diff --git a/plugins/applications/src/components/panel/details/Details.tsx b/plugins/applications/src/components/panel/details/Details.tsx index 52cdfcb92..989d46853 100644 --- a/plugins/applications/src/components/panel/details/Details.tsx +++ b/plugins/applications/src/components/panel/details/Details.tsx @@ -15,7 +15,7 @@ import React from 'react'; import { UsersIcon } from '@patternfly/react-icons'; import { ExternalLink, Title } from '@kobsio/plugin-core'; -import { Dashboards } from '@kobsio/plugin-dashboards'; +import { DashboardsWrapper } from '@kobsio/plugin-dashboards'; import DetailsLink from './DetailsLink'; import { IApplication } from '../../../utils/interfaces'; @@ -73,7 +73,7 @@ const Details: React.FunctionComponent = ({ application, close }: {application.dashboards ? ( - + ) : null}

 

diff --git a/plugins/core/src/components/misc/Options.tsx b/plugins/core/src/components/misc/Options.tsx index 38ae1b873..649b4c27f 100644 --- a/plugins/core/src/components/misc/Options.tsx +++ b/plugins/core/src/components/misc/Options.tsx @@ -47,6 +47,26 @@ export type TTime = | 'last7Days' | 'last90Days'; +// TTimeOptions is an array with all available type for TTime. It is used to verify that the value of a string is an +//valid option for the TTime type. +export const TTimeOptions = [ + 'custom', + 'last12Hours', + 'last15Minutes', + 'last1Day', + 'last1Hour', + 'last1Year', + 'last2Days', + 'last30Days', + 'last30Minutes', + 'last3Hours', + 'last5Minutes', + 'last6Hours', + 'last6Months', + 'last7Days', + 'last90Days', +]; + // ITime is the interface for a time in the times map. It contains a label which should be displayed in the Options // component and the seconds between the start and the end time. interface ITime { @@ -88,6 +108,7 @@ interface IOptionsProps { timeEnd: number; timeStart: number; setOptions: ( + refresh: boolean, additionalFields: IOptionsAdditionalFields[] | undefined, time: TTime, timeEnd: number, @@ -142,6 +163,7 @@ export const Options: React.FunctionComponent = ({ setCustomTimeStartError(''); setCustomTimeEndError(''); setOptions( + false, additionalFields, 'custom', Math.floor(parsedTimeEnd.getTime() / 1000), @@ -154,7 +176,13 @@ export const Options: React.FunctionComponent = ({ // quick is the function for the quick select option. We always use the current time in seconds and substract the // seconds specified in the quick select option. const quick = (t: TTime): void => { - setOptions(additionalFields, t, Math.floor(Date.now() / 1000), Math.floor(Date.now() / 1000) - times[t].seconds); + setOptions( + false, + additionalFields, + t, + Math.floor(Date.now() / 1000), + Math.floor(Date.now() / 1000) - times[t].seconds, + ); setShow(false); }; @@ -172,6 +200,7 @@ export const Options: React.FunctionComponent = ({ const refreshTimes = (): void => { if (time !== 'custom') { setOptions( + true, additionalFields, time, Math.floor(Date.now() / 1000), diff --git a/plugins/core/src/components/plugin/PluginCard.tsx b/plugins/core/src/components/plugin/PluginCard.tsx index 4d882fbe5..6ef4df25a 100644 --- a/plugins/core/src/components/plugin/PluginCard.tsx +++ b/plugins/core/src/components/plugin/PluginCard.tsx @@ -5,7 +5,7 @@ interface IPluginCardProps { title: string; description?: string; transparent?: boolean; - children: React.ReactElement; + children: React.ReactElement | null; actions?: React.ReactElement; } diff --git a/plugins/core/src/index.ts b/plugins/core/src/index.ts index 93e6da75b..3fa0be63b 100644 --- a/plugins/core/src/index.ts +++ b/plugins/core/src/index.ts @@ -17,5 +17,7 @@ export * from './context/PluginsContext'; export * from './utils/manifests'; export * from './utils/resources'; export * from './utils/time'; +export * from './utils/useDebounce'; +export * from './utils/useDimensions'; export * from './utils/useWindowHeight'; export * from './utils/useWindowWidth'; diff --git a/plugins/core/src/utils/useDebounce.tsx b/plugins/core/src/utils/useDebounce.tsx new file mode 100644 index 000000000..883184abe --- /dev/null +++ b/plugins/core/src/utils/useDebounce.tsx @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react'; + +export const useDebounce = (value: T, delay: number): T => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return (): void => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +}; diff --git a/plugins/core/src/utils/useDimensions.tsx b/plugins/core/src/utils/useDimensions.tsx new file mode 100644 index 000000000..ccfb834b2 --- /dev/null +++ b/plugins/core/src/utils/useDimensions.tsx @@ -0,0 +1,34 @@ +import { useEffect, useLayoutEffect, useState } from 'react'; + +export interface IDimensions { + height: number; + width: number; +} + +export const useDimensions = (targetRef: React.RefObject): IDimensions => { + const getDimensions = (): IDimensions => { + return { + height: targetRef.current ? targetRef.current.offsetHeight : 0, + width: targetRef.current ? targetRef.current.offsetWidth : 0, + }; + }; + + const [dimensions, setDimensions] = useState(getDimensions); + + const handleResize = (): void => { + setDimensions(getDimensions()); + }; + + useEffect(() => { + window.addEventListener('resize', handleResize); + return (): void => window.removeEventListener('resize', handleResize); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useLayoutEffect(() => { + handleResize(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return dimensions; +}; diff --git a/plugins/dashboards/src/components/dashboards/Dashboard.tsx b/plugins/dashboards/src/components/dashboards/Dashboard.tsx index e94fd0e95..3835ca1e8 100644 --- a/plugins/dashboards/src/components/dashboards/Dashboard.tsx +++ b/plugins/dashboards/src/components/dashboards/Dashboard.tsx @@ -60,8 +60,12 @@ const Dashboard: React.FunctionComponent = ({ // that all child components should used the retunred data array instead of the formerly defined variables state, // because it contains the current values and selected value for a variable. The variables state is mainly there to // trigger this function everytime a new variable value is selected. + // Currently we support "core" variable, which can be used to select a cluster or plugin and we are supporting the + // Prometheus plugin. For the Prometheus plugin the user must specify the name of the Prometheus instance via the name + // parameter in the options. When the user changes the variables, we keep the old variable values, so that we not have + // to rerender all the panels in the dashboard. const { isError, error, data, refetch } = useQuery( - ['dashboard/variables', variables], + ['dashboard/variables', variables, times], async () => { if (!variables) { return []; @@ -86,6 +90,41 @@ const Dashboard: React.FunctionComponent = ({ } } } + } else { + const pluginDetails = pluginsContext.getPluginDetails(tmpVariables[i].plugin.name); + + if (pluginDetails?.type === 'prometheus') { + const response = await fetch(`/api/plugins/prometheus/variable/${tmpVariables[i].plugin.name}`, { + body: JSON.stringify({ + label: tmpVariables[i].plugin.options.label, + query: interpolate(tmpVariables[i].plugin.options.query, tmpVariables), + timeEnd: times.timeEnd, + timeStart: times.timeStart, + type: tmpVariables[i].plugin.options.type, + }), + method: 'post', + }); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + if (json && Array.isArray(json) && json.length > 0) { + if (tmpVariables[i].plugin.options.allowAll) { + json.unshift(json.join('|')); + } + + tmpVariables[i].values = json; + tmpVariables[i].value = json.includes(tmpVariables[i].value) ? tmpVariables[i].value : json[0]; + } else { + throw new Error(`No values for variable ${tmpVariables[i].label || tmpVariables[i].name}`); + } + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } } } @@ -94,6 +133,7 @@ const Dashboard: React.FunctionComponent = ({ throw err; } }, + { keepPreviousData: true }, ); // We do not use the dashboard.rows array directly to render the dashboard. Instead we are replacing all the variables @@ -126,6 +166,7 @@ const Dashboard: React.FunctionComponent = ({ ); } + return ( diff --git a/plugins/dashboards/src/components/dashboards/DashboardToolbar.tsx b/plugins/dashboards/src/components/dashboards/DashboardToolbar.tsx index 1ae27fbf3..75dd0f1da 100644 --- a/plugins/dashboards/src/components/dashboards/DashboardToolbar.tsx +++ b/plugins/dashboards/src/components/dashboards/DashboardToolbar.tsx @@ -52,6 +52,7 @@ const DashboardToolbar: React.FunctionComponent = ({ timeEnd={times.timeEnd} timeStart={times.timeStart} setOptions={( + refresh: boolean, additionalFields: IOptionsAdditionalFields[] | undefined, time: TTime, timeEnd: number, diff --git a/plugins/dashboards/src/components/dashboards/DashboardToolbarVariable.tsx b/plugins/dashboards/src/components/dashboards/DashboardToolbarVariable.tsx index be032ec3c..6c67caf91 100644 --- a/plugins/dashboards/src/components/dashboards/DashboardToolbarVariable.tsx +++ b/plugins/dashboards/src/components/dashboards/DashboardToolbarVariable.tsx @@ -46,6 +46,7 @@ const DashboardToolbarVariable: React.FunctionComponent {group} diff --git a/plugins/dashboards/src/components/dashboards/DashboardWrapper.tsx b/plugins/dashboards/src/components/dashboards/DashboardWrapper.tsx deleted file mode 100644 index a68836fcd..000000000 --- a/plugins/dashboards/src/components/dashboards/DashboardWrapper.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { memo, useEffect, useRef, useState } from 'react'; - -import { IPluginDefaults, useWindowWidth } from '@kobsio/plugin-core'; -import Dashboard from './Dashboard'; -import { IDashboard } from '../../utils/interfaces'; - -interface IDashboardWrapperProps { - defaults: IPluginDefaults; - dashboard: IDashboard; - showDetails?: (details: React.ReactNode) => void; -} - -// The DashboardWrapper component is a wrapper for the Dashboard component to determine the width of the dashboard. This -// is required so we can force the default span width for smaller screens. This boolean value is passed as property to -// the Dashboard component. -const DashboardWrapper: React.FunctionComponent = ({ - defaults, - dashboard, - showDetails, -}: IDashboardWrapperProps) => { - // width, refGrid and forceDefaultSpan are used to determine the current width of the dashboard (this isn't always the - // screen width, because the dashboard can also be used in a panel), so that we can adjust the size of the rows and - // columns in the grid. - const width = useWindowWidth(); - const refGrid = useRef(null); - const [forceDefaultSpan, setForceDefaultSpan] = useState(false); - - // useEffect is executed every time the window width changes, to determin the size of the grid and use a static span - // size of 12 if necessary. We have to use the with of the grid instead of the window width, because it is possible - // that the chart is rendered in a drawer (e.g. for applications in the applications page). - useEffect(() => { - if (refGrid && refGrid.current) { - if (refGrid.current.getBoundingClientRect().width >= 1200) { - setForceDefaultSpan(false); - } else { - setForceDefaultSpan(true); - } - } - }, [width]); - - return ( -
- -
- ); -}; - -export default memo(DashboardWrapper, (prevProps, nextProps) => { - if (JSON.stringify(prevProps) === JSON.stringify(nextProps)) { - return true; - } - - return false; -}); diff --git a/plugins/dashboards/src/components/dashboards/Dashboards.tsx b/plugins/dashboards/src/components/dashboards/Dashboards.tsx index c76487cf7..efe101992 100644 --- a/plugins/dashboards/src/components/dashboards/Dashboards.tsx +++ b/plugins/dashboards/src/components/dashboards/Dashboards.tsx @@ -14,29 +14,57 @@ import { Tabs, } from '@patternfly/react-core'; import { QueryObserverResult, useQuery } from 'react-query'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; -import { IDashboard, IReference } from '../../utils/interfaces'; -import DashboardWrapper from './DashboardWrapper'; +import { IDashboard, IDashboardsOptions, IReference } from '../../utils/interfaces'; +import Dashboard from './Dashboard'; import { IPluginDefaults } from '@kobsio/plugin-core'; +import { getOptionsFromSearch } from '../../utils/dashboard'; interface IDashboardsProps { defaults: IPluginDefaults; references: IReference[]; useDrawer: boolean; + forceDefaultSpan: boolean; } // The Dashboards component is used to fetch all the referenced dashboards in a team/application and show them as tabs. // The useDrawer property is used to decide if the dashboard should be used inside a drawer or not. For example if an // application is already displayed in a drawer we shouldn't use another drawer for the dashboards. -export const Dashboards: React.FunctionComponent = ({ +const Dashboards: React.FunctionComponent = ({ defaults, references, useDrawer, + forceDefaultSpan, }: IDashboardsProps) => { - const [activeDashboard, setActiveDashboard] = useState(references.length > 0 ? references[0].title : ''); + const location = useLocation(); + const history = useHistory(); + const [options, setOptions] = useState( + getOptionsFromSearch(location.search, references, useDrawer), + ); const [details, setDetails] = useState(undefined); + // changeOptions adjusts the search location (query paramters). We do not set the options directly (except when the + // Dashboards component is rendered inside a drawer), so that a user can share the url and a other users gets the same + // view. + const changeOptions = (opts: IDashboardsOptions): void => { + history.push({ + pathname: location.pathname, + search: `?dashboard=${opts.dashboard}`, + }); + }; + + // useEffect is used to set the options for the dashboards, everytime the query paramters for the current page are + // changed. This is only used, when the dashboards are not rendered in a drawer. When the dashboards are rendered in + // a drawer it can happen that we already show some dashboards in the main view and so we can not rely on the query + // parameters. + useEffect(() => { + if (useDrawer) { + setOptions(getOptionsFromSearch(location.search, references, useDrawer)); + } + }, [location.search, references, useDrawer]); + // Fetch all dashboards. The dashboards are available via the data variable. To fetch the dashboards we have to pass // the defaults and the references to the API. The defaults are required so that a user can omit the cluster and // namespace in the references. @@ -107,8 +135,12 @@ export const Dashboards: React.FunctionComponent = ({ // variable is true we render the tabs as content inside the drawer component. const tabs = ( setActiveDashboard(tabIndex.toString())} + activeKey={options.dashboard} + onSelect={(event, tabIndex): void => + useDrawer + ? changeOptions({ ...options, dashboard: tabIndex.toString() }) + : setOptions({ ...options, dashboard: tabIndex.toString() }) + } className="pf-u-mt-md kobsio-dashboards-tabs-without-margin-top" isFilled={true} mountOnEnter={true} @@ -116,9 +148,10 @@ export const Dashboards: React.FunctionComponent = ({ {data.map((dashboard) => ( {dashboard.title}}> - @@ -139,3 +172,5 @@ export const Dashboards: React.FunctionComponent = ({ ); }; + +export default Dashboards; diff --git a/plugins/dashboards/src/components/dashboards/DashboardsWrapper.tsx b/plugins/dashboards/src/components/dashboards/DashboardsWrapper.tsx new file mode 100644 index 000000000..f0c6bcbce --- /dev/null +++ b/plugins/dashboards/src/components/dashboards/DashboardsWrapper.tsx @@ -0,0 +1,36 @@ +import React, { useRef } from 'react'; + +import { IPluginDefaults, useDimensions } from '@kobsio/plugin-core'; +import Dashboards from './Dashboards'; +import { IReference } from '../../utils/interfaces'; + +interface IDashboardsWrapperProps { + defaults: IPluginDefaults; + references: IReference[]; + useDrawer: boolean; +} + +// The DashboardsWrapper component is a wrapper for the Dashboards component. It is used to determine the dimensions for +// the dashboards. This is needed, so that we can force the default value for the grid span, when the space were the +// dashboards are rendered is to small. We can not use the window width for this, because when the dashboards are +// rendered in a drawer (e.g. application details) the windows width would be incorrect. +// The DashboardsWrapper component should always used in other component. Never use the Dashboards component directly! +export const DashboardsWrapper: React.FunctionComponent = ({ + defaults, + references, + useDrawer, +}: IDashboardsWrapperProps) => { + const refTabs = useRef(null); + const tabsSize = useDimensions(refTabs); + + return ( +
+ +
+ ); +}; diff --git a/plugins/dashboards/src/index.ts b/plugins/dashboards/src/index.ts index 5f89114fb..e0afe4d5c 100644 --- a/plugins/dashboards/src/index.ts +++ b/plugins/dashboards/src/index.ts @@ -18,5 +18,5 @@ const dashboardsPlugin: IPluginComponents = { export default dashboardsPlugin; -export * from './components/dashboards/Dashboards'; +export * from './components/dashboards/DashboardsWrapper'; export type IDashboardReference = IReference; diff --git a/plugins/dashboards/src/utils/dashboard.ts b/plugins/dashboards/src/utils/dashboard.ts index 8ae5631d0..cacb7b9fe 100644 --- a/plugins/dashboards/src/utils/dashboard.ts +++ b/plugins/dashboards/src/utils/dashboard.ts @@ -1,6 +1,6 @@ import { gridSpans } from '@patternfly/react-core'; -import { IVariableValues } from './interfaces'; +import { IDashboardsOptions, IReference, IVariableValues } from './interfaces'; // toGridSpans is used to convert the provided col and row span value to the corresponding gridSpans value, so that it // can be used within the Patternfly Grid component. The function requires a default value which is 12 for columns and @@ -69,10 +69,35 @@ export const interpolate = ( s2[0] = s2[0] && vars.hasOwnProperty(s2[0].trim().substring(1)) ? vars[s2[0].trim().substring(1)] - : interpolator.join(''); + : interpolator.join(` ${s2[0]} `); } return s2.join(''); }) .join(''); }; + +// getOptionsFromSearch is used to parse the given search location and return is as options for Prometheus. This is +// needed, so that a user can explore his Prometheus data from a chart. When the user selects the explore action, we +// pass him to this page and pass the data via the URL parameters. +export const getOptionsFromSearch = ( + search: string, + references: IReference[], + useDrawer: boolean, +): IDashboardsOptions => { + const params = new URLSearchParams(search); + const dashboard = params.get('dashboard'); + + let isReferenced = false; + if (useDrawer) { + for (const reference of references) { + if (reference.title === dashboard) { + isReferenced = true; + } + } + } + + return { + dashboard: dashboard && isReferenced ? dashboard : references.length > 0 ? references[0].title : '', + }; +}; diff --git a/plugins/dashboards/src/utils/interfaces.ts b/plugins/dashboards/src/utils/interfaces.ts index 0ee1e60ab..99bba877c 100644 --- a/plugins/dashboards/src/utils/interfaces.ts +++ b/plugins/dashboards/src/utils/interfaces.ts @@ -1,3 +1,6 @@ +// IDashboard is the interface for the Dashboards CR, like it is implemented in the Go code. In contrast to the Go +// implementation we are sure that the cluster, namespace and name for the dashboard is set, because the values are set +// each time a dashboard is retrieved from the Kubernetes API. export interface IDashboard { cluster: string; namespace: string; @@ -42,6 +45,8 @@ export interface IPlugin { options?: any; } +// IReference is the interface for a dashboard reference in the Team or Application CR. If the cluster or namespace is +// not specified in the reference we assume the dashboard is in the same namespace as the team or application. export interface IReference { cluster?: string; namespace?: string; @@ -55,7 +60,16 @@ export interface IPlaceholders { [key: string]: string; } +// IVariableValues is an extension of the IVariable interface. It contains the additional fields for the selected +// variable value and all possible variable values. export interface IVariableValues extends IVariable { value: string; values: string[]; } + +// IDashboardsOptions are the options for the Dashboards component. Currently is only contains the active dashboard, but +// we can extend this later to also include the selected time and variables, so that we can pass this values with the +// current url as query parameters. +export interface IDashboardsOptions { + dashboard: string; +} diff --git a/plugins/prometheus/package.json b/plugins/prometheus/package.json new file mode 100644 index 000000000..38a064ca1 --- /dev/null +++ b/plugins/prometheus/package.json @@ -0,0 +1,28 @@ +{ + "name": "@kobsio/plugin-prometheus", + "version": "0.0.0", + "license": "MIT", + "private": false, + "main": "./lib/index.js", + "module": "./lib-esm/index.js", + "types": "./lib/index.d.ts", + "scripts": { + "plugin": "tsc && tsc --build tsconfig.esm.json && cp -r src/assets lib && cp -r src/assets lib-esm" + }, + "devDependencies": { + "@kobsio/plugin-core": "*", + "@nivo/core": "^0.72.0", + "@nivo/line": "^0.72.0", + "@patternfly/react-core": "^4.128.2", + "@patternfly/react-icons": "^4.11.0", + "@patternfly/react-table": "^4.29.0", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", + "@types/react-router-dom": "^5.1.7", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-query": "^3.17.2", + "react-router-dom": "^5.2.0", + "typescript": "^4.3.4" + } +} diff --git a/plugins/prometheus/pkg/instance/helpers.go b/plugins/prometheus/pkg/instance/helpers.go new file mode 100644 index 000000000..881587fd8 --- /dev/null +++ b/plugins/prometheus/pkg/instance/helpers.go @@ -0,0 +1,54 @@ +package instance + +import ( + "bytes" + "text/template" + "time" +) + +// appendIfMissing appends a value to a slice, when this values doesn't exist in the slice already. +func appendIfMissing(items []string, item string) []string { + for _, ele := range items { + if ele == item { + return items + } + } + + return append(items, item) +} + +// queryInterpolation is used to replace variables in a query. +func queryInterpolation(query string, variables map[string]string) (string, error) { + tpl, err := template.New("query").Delims("{%", "%}").Parse(query) + if err != nil { + return "", err + } + + var buf bytes.Buffer + err = tpl.Execute(&buf, variables) + if err != nil { + return "", err + } + + return buf.String(), nil +} + +// getSteps returns the duration for the Prometheus resolution for a given start and end time. +func getSteps(start, end int64) time.Duration { + switch seconds := end - start; { + case seconds <= 6*3600: + return time.Duration(30 * time.Second) + case seconds <= 12*3600: + return time.Duration(60 * time.Second) + case seconds <= 24*3600: + return time.Duration(120 * time.Second) + case seconds <= 2*24*3600: + return time.Duration(300 * time.Second) + case seconds <= 7*24*3600: + return time.Duration(1800 * time.Second) + case seconds <= 30*24*3600: + return time.Duration(3600 * time.Second) + default: + return time.Duration((end-start)/1000) * time.Second + } +} diff --git a/plugins/prometheus/pkg/instance/instance.go b/plugins/prometheus/pkg/instance/instance.go new file mode 100644 index 000000000..e633f7d4f --- /dev/null +++ b/plugins/prometheus/pkg/instance/instance.go @@ -0,0 +1,286 @@ +package instance + +import ( + "context" + "fmt" + "math" + "sort" + "strings" + "time" + + "github.com/kobsio/kobs/pkg/api/middleware/roundtripper" + + "github.com/prometheus/client_golang/api" + v1 "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/prometheus/common/model" + "github.com/sirupsen/logrus" +) + +var ( + log = logrus.WithFields(logrus.Fields{"package": "prometheus"}) +) + +// Config is the structure of the configuration for a single Prometheus instance. +type Config struct { + Name string `yaml:"name"` + DisplayName string `yaml:"displayName"` + Description string `yaml:"description"` + Address string `yaml:"address"` + Username string `yaml:"username"` + Password string `yaml:"password"` + Token string `yaml:"token"` +} + +// Instance represents a single Prometheus instance, which can be added via the configuration file. +type Instance struct { + Name string + labelValues model.LabelValues + labelValuesLastFetch time.Time + v1api v1.API +} + +// GetVariable returns all values for a label from the given query. For that we have to retrive the label sets from the +// Prometheus instance and so that we can add the values for the specified label to the values slice. +func (i *Instance) GetVariable(ctx context.Context, label, query, queryType string, timeStart, timeEnd int64) ([]string, error) { + log.WithFields(logrus.Fields{"query": query}).Tracef("Query variable values") + + labelSets, _, err := i.v1api.Series(ctx, []string{query}, time.Unix(timeStart, 0), time.Unix(timeEnd, 0)) + if err != nil { + return nil, err + } + + var values []string + for _, labelSet := range labelSets { + if value, ok := labelSet[model.LabelName(label)]; ok { + values = appendIfMissing(values, string(value)) + } + } + + sort.Strings(values) + + return values, nil +} + +// GetMetrics returns all metrics for all given queries. For each given query we have to make one call to the Prometheus +// API. Then we have to loop through the returned time series and transform them into a format, which can be processed +// by out React UI. +func (i *Instance) GetMetrics(ctx context.Context, queries []Query, resolution string, timeStart, timeEnd int64) ([]Metric, error) { + steps := getSteps(timeStart, timeEnd) + if resolution != "" { + parsedDuration, err := time.ParseDuration(resolution) + if err == nil { + steps = parsedDuration + } + } + + r := v1.Range{ + Start: time.Unix(timeStart, 0), + End: time.Unix(timeEnd, 0), + Step: steps, + } + + var metrics []Metric + + for queryIndex, query := range queries { + log.WithFields(logrus.Fields{"query": query.Query, "label": query.Label, "resolution": resolution, "start": r.Start, "end": r.End}).Tracef("Query time series") + + result, _, err := i.v1api.QueryRange(ctx, query.Query, r) + if err != nil { + return nil, err + } + + streams, ok := result.(model.Matrix) + if !ok { + return nil, err + } + + for streamIndex, stream := range streams { + var min float64 + var max float64 + var avg float64 + + var data []Datum + for index, value := range stream.Values { + val := float64(value.Value) + + if math.IsNaN(val) { + data = append(data, Datum{ + X: value.Timestamp.Unix() * 1000, + }) + } else { + avg = avg + val + + if index == 0 { + min = val + max = val + } else { + if val < min { + min = val + } else if val > max { + max = val + } + } + + data = append(data, Datum{ + X: value.Timestamp.Unix() * 1000, + Y: &val, + }) + } + } + + if avg != 0 { + avg = avg / float64(len(stream.Values)) + } + + var labels map[string]string + labels = make(map[string]string) + + for key, value := range stream.Metric { + labels[string(key)] = string(value) + } + + label, err := queryInterpolation(query.Label, labels) + if err != nil { + metrics = append(metrics, Metric{ + ID: fmt.Sprintf("%d-%d", queryIndex, streamIndex), + Label: query.Label, + Min: min, + Max: max, + Avg: avg, + Data: data, + }) + } else { + if label == "" { + label = stream.Metric.String() + } + + metrics = append(metrics, Metric{ + ID: fmt.Sprintf("%d-%d", queryIndex, streamIndex), + Label: label, + Min: min, + Max: max, + Avg: avg, + Data: data, + }) + } + } + } + + return metrics, nil +} + +// GetTableData returns the data, when the user selected the table view for the Prometheus plugin. To get the data we +// are running all prodived queries and join the results by the value for a label of a query. The value for a query is +// added as value-N column. We are also adding all other labels as fields to a single row. +func (i *Instance) GetTableData(ctx context.Context, queries []Query, timeEnd int64) (map[string]map[string]string, error) { + queryTime := time.Unix(timeEnd, 0) + + var rows map[string]map[string]string + rows = make(map[string]map[string]string) + + for queryIndex, query := range queries { + log.WithFields(logrus.Fields{"query": query, "time": queryTime}).Tracef("Query table data") + + result, _, err := i.v1api.Query(ctx, query.Query, queryTime) + if err != nil { + return nil, err + } + + streams, ok := result.(model.Vector) + if !ok { + return nil, err + } + + for _, stream := range streams { + var labels map[string]string + labels = make(map[string]string) + labels[fmt.Sprintf("value-%d", queryIndex+1)] = stream.Value.String() + + for key, value := range stream.Metric { + labels[string(key)] = string(value) + } + + label, err := queryInterpolation(query.Label, labels) + if err != nil { + return nil, err + } + + for key, value := range labels { + if _, ok := rows[label]; !ok { + rows[label] = make(map[string]string) + } + + if _, ok := rows[label][key]; !ok { + rows[label][key] = value + } + + rows[label][fmt.Sprintf("value-%d", queryIndex+1)] = stream.Value.String() + } + } + } + + return rows, nil +} + +// GetLabelValues returns all label values for a configured Prometheus instance. These labels are used to show the user +// a list of suggestions for his entered query. The returned label values from the Prometheus API are cached for one +// hour. +func (i *Instance) GetLabelValues(ctx context.Context, searchTerm string) ([]string, error) { + now := time.Now() + + // Fetch metric names if last fetch was more then a minute ago + if i.labelValuesLastFetch.Add(1 * time.Hour).Before(now) { + labelValues, _, err := i.v1api.LabelValues(ctx, model.MetricNameLabel, nil, now.Add(-1*time.Hour), now) + if err != nil { + return nil, err + } + + i.labelValues = labelValues + i.labelValuesLastFetch = now + log.WithFields(logrus.Fields{"instance": i.Name}).Tracef("Get metric names.") + } else { + log.WithFields(logrus.Fields{"instance": i.Name}).Tracef("Use cached metric names.") + } + + var names []string + for _, name := range i.labelValues { + if strings.Contains(string(name), searchTerm) { + names = append(names, string(name)) + } + } + + return names, nil +} + +// New returns a new Prometheus instance for the given configuration. +func New(config Config) (*Instance, error) { + roundTripper := roundtripper.DefaultRoundTripper + + if config.Username != "" && config.Password != "" { + roundTripper = roundtripper.BasicAuthTransport{ + Transport: roundTripper, + Username: config.Username, + Password: config.Password, + } + } + + if config.Token != "" { + roundTripper = roundtripper.TokenAuthTransporter{ + Transport: roundTripper, + Token: config.Token, + } + } + + client, err := api.NewClient(api.Config{ + Address: config.Address, + RoundTripper: roundTripper, + }) + if err != nil { + return nil, err + } + + return &Instance{ + Name: config.Name, + v1api: v1.NewAPI(client), + }, nil +} diff --git a/plugins/prometheus/pkg/instance/structs.go b/plugins/prometheus/pkg/instance/structs.go new file mode 100644 index 000000000..c0d545463 --- /dev/null +++ b/plugins/prometheus/pkg/instance/structs.go @@ -0,0 +1,27 @@ +package instance + +// Query is the structure of a query. Each query consists of the PromQL query and an optional label, which can be +// displayed in the legend of a chart. +type Query struct { + Query string `json:"query"` + Label string `json:"label"` +} + +// Metric is the response format for a single metric. Each metric must have an ID and label. We also add the min, max +// and average for the returned data. +type Metric struct { + ID string `json:"id"` + Label string `json:"label"` + Min float64 `json:"min"` + Max float64 `json:"max"` + Avg float64 `json:"avg"` + Data []Datum `json:"data"` +} + +// Datum is the structure of a single data point of a metric. The y value must be a pointer, because when the value is +// NaN we can not set the value (NaN in the JSON representation will throw an error). For that NaN values will always be +// null. +type Datum struct { + X int64 `json:"x"` + Y *float64 `json:"y"` +} diff --git a/plugins/prometheus/prometheus.go b/plugins/prometheus/prometheus.go new file mode 100644 index 000000000..87f8a61a0 --- /dev/null +++ b/plugins/prometheus/prometheus.go @@ -0,0 +1,216 @@ +package prometheus + +import ( + "encoding/json" + "net/http" + + "github.com/kobsio/kobs/pkg/api/clusters" + "github.com/kobsio/kobs/pkg/api/middleware/errresponse" + "github.com/kobsio/kobs/pkg/api/plugins/plugin" + "github.com/kobsio/kobs/plugins/prometheus/pkg/instance" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/sirupsen/logrus" +) + +// Route is the route under which the plugin should be registered in our router for the rest api. +const Route = "/prometheus" + +var ( + log = logrus.WithFields(logrus.Fields{"package": "prometheus"}) +) + +// Config is the structure of the configuration for the prometheus plugin. +type Config []instance.Config + +// Router implements the router for the resources plugin, which can be registered in the router for our rest api. +type Router struct { + *chi.Mux + clusters *clusters.Clusters + instances []*instance.Instance +} + +// getVariableRequest +type getVariableRequest struct { + Label string `json:"label"` + Query string `json:"query"` + TimeStart int64 `json:"timeStart"` + TimeEnd int64 `json:"timeEnd"` + Type string `json:"type"` +} + +// getMetricsRequest is the format of the request body for the getMetrics request. To get metrics from a Prometheus +// instance we need at least one query and the start and end time. Optionally the user can also set a resolution for the +// metrics to overwrite the default one. +type getMetricsRequest struct { + Queries []instance.Query `json:"queries"` + Resolution string `json:"Resolution"` + TimeStart int64 `json:"timeStart"` + TimeEnd int64 `json:"timeEnd"` +} + +func (router *Router) getInstance(name string) *instance.Instance { + for _, i := range router.instances { + if i.Name == name { + return i + } + } + + return nil +} + +// getVariable returns a list of variable values for a given label and query. The query and label are provided in the +// request body. The body also contains the type which should be used to get determine the label values and the start +// and end time. The Prometheus instance which should be used is defined via the name path parameter. All values are +// then passed to this instance via the GetVariable method. +func (router *Router) getVariable(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + + log.WithFields(logrus.Fields{"name": name}).Tracef("getVariables") + + i := router.getInstance(name) + if i == nil { + render.Render(w, r, errresponse.Render(nil, http.StatusBadRequest, "could not find instance name")) + return + } + + var data getVariableRequest + + err := json.NewDecoder(r.Body).Decode(&data) + if err != nil { + render.Render(w, r, errresponse.Render(err, http.StatusBadRequest, "could not decode request body")) + return + } + + values, err := i.GetVariable(r.Context(), data.Label, data.Query, data.Type, data.TimeStart, data.TimeEnd) + if err != nil { + render.Render(w, r, errresponse.Render(err, http.StatusBadRequest, "could not get variable")) + return + } + + log.WithFields(logrus.Fields{"values": len(values)}).Tracef("getVariables") + render.JSON(w, r, values) +} + +// getMetrics returns a list of metrics for the queries specified in the request body. To get the metrics we have to +// select the correct Prometheus instance, by the name path paramter. After that we can use the GetMetrics function of +// the instance to get a list of metrics. +func (router *Router) getMetrics(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + + log.WithFields(logrus.Fields{"name": name}).Tracef("getMetrics") + + i := router.getInstance(name) + if i == nil { + render.Render(w, r, errresponse.Render(nil, http.StatusBadRequest, "could not find instance name")) + return + } + + var data getMetricsRequest + + err := json.NewDecoder(r.Body).Decode(&data) + if err != nil { + render.Render(w, r, errresponse.Render(err, http.StatusBadRequest, "could not decode request body")) + return + } + + metrics, err := i.GetMetrics(r.Context(), data.Queries, data.Resolution, data.TimeStart, data.TimeEnd) + if err != nil { + render.Render(w, r, errresponse.Render(err, http.StatusBadRequest, "could not get metrics")) + return + } + + log.WithFields(logrus.Fields{"metrics": len(metrics)}).Tracef("getMetrics") + render.JSON(w, r, metrics) +} + +// getTable returns a table for a list of given Prometheus queries. The name of the Prometheus instance is set as path +// parameter. After we got the Prometheus instance, we are calling the GetTableData function of this instance to get a +// map of rows, which is then returned to the user. +func (router *Router) getTable(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + + log.WithFields(logrus.Fields{"name": name}).Tracef("getTable") + + i := router.getInstance(name) + if i == nil { + render.Render(w, r, errresponse.Render(nil, http.StatusBadRequest, "could not find instance name")) + return + } + + var data getMetricsRequest + + err := json.NewDecoder(r.Body).Decode(&data) + if err != nil { + render.Render(w, r, errresponse.Render(err, http.StatusBadRequest, "could not decode request body")) + return + } + + rows, err := i.GetTableData(r.Context(), data.Queries, data.TimeEnd) + if err != nil { + render.Render(w, r, errresponse.Render(err, http.StatusBadRequest, "could not get metrics")) + return + } + + log.WithFields(logrus.Fields{"rows": len(rows)}).Tracef("getTable") + render.JSON(w, r, rows) +} + +// getLabels returns a list of label values for the given searchTearm. The Prometheus instance is selected by the name +// path parameter. +func (router *Router) getLabels(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + searchTerm := r.URL.Query().Get("searchTerm") + + log.WithFields(logrus.Fields{"name": name, "searchTerm": searchTerm}).Tracef("getLabels") + + i := router.getInstance(name) + if i == nil { + render.Render(w, r, errresponse.Render(nil, http.StatusBadRequest, "could not find instance name")) + return + } + + labelValues, err := i.GetLabelValues(r.Context(), searchTerm) + if err != nil { + render.Render(w, r, errresponse.Render(err, http.StatusInternalServerError, "could not get label values")) + return + } + + log.WithFields(logrus.Fields{"labelValues": len(labelValues)}).Tracef("getLabels") + render.JSON(w, r, labelValues) +} + +// Register returns a new router which can be used in the router for the kobs rest api. +func Register(clusters *clusters.Clusters, plugins *plugin.Plugins, config Config) chi.Router { + var instances []*instance.Instance + + for _, cfg := range config { + instance, err := instance.New(cfg) + if err != nil { + log.WithError(err).WithFields(logrus.Fields{"name": cfg.Name}).Fatalf("Could not create Prometheus instance") + } + + instances = append(instances, instance) + + plugins.Append(plugin.Plugin{ + Name: cfg.Name, + DisplayName: cfg.DisplayName, + Description: cfg.Description, + Type: "prometheus", + }) + } + + router := Router{ + chi.NewRouter(), + clusters, + instances, + } + + router.Post("/variable/{name}", router.getVariable) + router.Post("/metrics/{name}", router.getMetrics) + router.Post("/table/{name}", router.getTable) + router.Get("/labels/{name}", router.getLabels) + + return router +} diff --git a/plugins/prometheus/src/assets/icon.png b/plugins/prometheus/src/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5e3b97c754cf1ca7ce7f7fdc9867921d024977ca GIT binary patch literal 116521 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 E0I56adH?_b literal 0 HcmV?d00001 diff --git a/plugins/prometheus/src/components/page/Page.tsx b/plugins/prometheus/src/components/page/Page.tsx new file mode 100644 index 000000000..530277ea3 --- /dev/null +++ b/plugins/prometheus/src/components/page/Page.tsx @@ -0,0 +1,60 @@ +import { PageSection, PageSectionVariants, Title } from '@patternfly/react-core'; +import React, { useEffect, useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; + +import { IOptions } from '../../utils/interfaces'; +import { IPluginPageProps } from '@kobsio/plugin-core'; +import PageChart from './PageChartWrapper'; +import PageToolbar from './PageToolbar'; +import { getOptionsFromSearch } from '../../utils/helpers'; + +const Page: React.FunctionComponent = ({ name, displayName, description }: IPluginPageProps) => { + const location = useLocation(); + const history = useHistory(); + const [options, setOptions] = useState(getOptionsFromSearch(location.search)); + + // changeOptions is used to change the options for an Prometheus query. Instead of directly modifying the options + // state we change the URL parameters. + const changeOptions = (opts: IOptions): void => { + const queries = opts.queries ? opts.queries.map((query) => `&query=${query}`) : []; + + history.push({ + pathname: location.pathname, + search: `?resolution=${opts.resolution}&time=${opts.times.time}&timeEnd=${opts.times.timeEnd}&timeStart=${ + opts.times.timeStart + }${queries.length > 0 ? queries.join('') : ''}`, + }); + }; + + // useEffect is used to set the options every time the search location for the current URL changes. The URL is changed + // via the changeOptions function. When the search location is changed we modify the options state. + useEffect(() => { + setOptions(getOptionsFromSearch(location.search)); + }, [location.search]); + + return ( + + + + {displayName} + +

{description}

+ +
+ + + {options.queries.length > 0 && options.queries[0] !== '' ? ( + + ) : null} + +
+ ); +}; + +export default Page; diff --git a/plugins/prometheus/src/components/page/PageChart.tsx b/plugins/prometheus/src/components/page/PageChart.tsx new file mode 100644 index 000000000..de2e6c420 --- /dev/null +++ b/plugins/prometheus/src/components/page/PageChart.tsx @@ -0,0 +1,117 @@ +import { Card, CardBody, Flex, FlexItem, ToggleGroup, ToggleGroupItem } from '@patternfly/react-core'; +import React, { useState } from 'react'; +import { ResponsiveLineCanvas, Serie } from '@nivo/line'; +import { SquareIcon } from '@patternfly/react-icons'; + +import { convertMetrics, formatAxisBottom } from '../../utils/helpers'; +import { COLOR_SCALE } from '../../utils/colors'; +import { IMetric } from '../../utils/interfaces'; +import { IPluginTimes } from '@kobsio/plugin-core'; +import PageChartLegend from './PageChartLegend'; + +interface IPageChartProps { + queries: string[]; + times: IPluginTimes; + metrics: IMetric[]; +} + +// The PageChart component is used to render the chart for the given metrics. Above the chart we display two toggle +// groups so that the user can adjust the basic style of the chart. The user can switch between a line and area chart +// and between a stacked and unstacked visualization. At the bottom of the page we are including the PageChartLegend +// component to render the legend for the metrics. +const PageChart: React.FunctionComponent = ({ queries, times, metrics }: IPageChartProps) => { + // series is an array for the converted metrics, which can be used by nivo. We convert the metrics to a series, so + // that we have to do this onyl once and not everytime the selected metrics are changed. + const seriesData = convertMetrics(metrics); + + const [type, setType] = useState('line'); + const [stacked, setStacked] = useState(false); + const [selectedSeries, setSelectedSeries] = useState(seriesData.series); + + // select is used to select a single metric, which should be shown in the rendered chart. If the currently selected + // metric is clicked again, the filter will be removed and all metrics will be shown in the chart. + const select = (color: string, metric: Serie): void => { + if (selectedSeries.length === 1 && selectedSeries[0].label === metric.label) { + setSelectedSeries(seriesData.series); + } else { + setSelectedSeries([{ ...metric, color: color }]); + } + }; + + return ( + + + + + + setType('line')} /> + setType('area')} /> + + + + + setStacked(false)} /> + setStacked(true)} /> + + + +

 

+
+ -.2f', + legend: 'Value', + legendOffset: -40, + legendPosition: 'middle', + }} + colors={COLOR_SCALE} + curve="monotoneX" + data={selectedSeries} + enableArea={type === 'area'} + enableGridX={false} + enableGridY={true} + enablePoints={false} + xFormat="time:%Y-%m-%d %H:%M:%S" + lineWidth={1} + margin={{ bottom: 25, left: 50, right: 0, top: 0 }} + theme={{ + background: '#ffffff', + fontFamily: 'RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif', + fontSize: 10, + textColor: '#000000', + }} + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + tooltip={(tooltip) => ( +
+
+ {tooltip.point.data.xFormatted} +
+
+ {seriesData.labels[tooltip.point.id.split('.')[0]]}:{' '} + {tooltip.point.data.yFormatted} +
+
+ )} + xScale={{ type: 'time' }} + yScale={{ stacked: stacked, type: 'linear' }} + yFormat=" >-.2f" + /> +
+

 

+ +
+
+ ); +}; + +export default PageChart; diff --git a/plugins/prometheus/src/components/page/PageChartLegend.tsx b/plugins/prometheus/src/components/page/PageChartLegend.tsx new file mode 100644 index 000000000..657a07b5c --- /dev/null +++ b/plugins/prometheus/src/components/page/PageChartLegend.tsx @@ -0,0 +1,68 @@ +import { Button, ButtonVariant } from '@patternfly/react-core'; +import { EyeSlashIcon, SquareIcon } from '@patternfly/react-icons'; +import { TableComposable, TableVariant, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import React from 'react'; +import { Serie } from '@nivo/line'; + +import { getColor } from '../../utils/colors'; + +interface IPageChartLegendProps { + queries: string[]; + series: Serie[]; + selected: Serie[]; + select: (color: string, serie: Serie) => void; +} + +// The PageChartLegend component renders the legend for the returned metrics. The legend is displayed in a table format +// and also includes the min, max and avg value for a metric. When the user clicks on a row he can add / remove a metric +// from the selected metrics. We then only display the selected metrics in the chart. +const PageChartLegend: React.FunctionComponent = ({ + queries, + series, + selected, + select, +}: IPageChartLegendProps) => { + return ( + + + + Name + Min + Max + Avg + Current + + + + {series.map((metric, index) => ( + + + + + {metric.min} + {metric.max} + {metric.avg} + {metric.data[metric.data.length - 1].y} + + ))} + + + ); +}; + +export default PageChartLegend; diff --git a/plugins/prometheus/src/components/page/PageChartWrapper.tsx b/plugins/prometheus/src/components/page/PageChartWrapper.tsx new file mode 100644 index 000000000..3f238478b --- /dev/null +++ b/plugins/prometheus/src/components/page/PageChartWrapper.tsx @@ -0,0 +1,90 @@ +import { Alert, AlertActionLink, AlertVariant, Spinner } from '@patternfly/react-core'; +import { QueryObserverResult, useQuery } from 'react-query'; +import React from 'react'; +import { useHistory } from 'react-router-dom'; + +import { IMetric, IOptions } from '../../utils/interfaces'; +import PageChart from './PageChart'; + +interface IPageChartWrapperProps extends IOptions { + name: string; +} + +// The PageChartWrapper component is used as a wrapper for the PageChart component. The component is responsible for +// loading the metrics for the users entered queries and time range. For that we have to refetch the metrics everytime +// the queries, time range or resolution is changed. +const PageChartWrapper: React.FunctionComponent = ({ + name, + queries, + resolution, + times, +}: IPageChartWrapperProps) => { + const history = useHistory(); + + const { isError, isLoading, error, data, refetch } = useQuery( + ['prometheus/metrics', queries, resolution, times], + async () => { + try { + const response = await fetch(`/api/plugins/prometheus/metrics/${name}`, { + body: JSON.stringify({ + queries: queries.map((query) => { + return { label: '', query: query }; + }), + resolution: resolution, + timeEnd: times.timeEnd, + timeStart: times.timeStart, + }), + method: 'post', + }); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } catch (err) { + throw err; + } + }, + ); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( + + history.push('/')}>Home + > => refetch()}> + Retry + +
+ } + > +

{error?.message}

+ + ); + } + + if (!data) { + return null; + } + + return ; +}; + +export default PageChartWrapper; diff --git a/plugins/prometheus/src/components/page/PageToolbar.tsx b/plugins/prometheus/src/components/page/PageToolbar.tsx new file mode 100644 index 000000000..c5845de25 --- /dev/null +++ b/plugins/prometheus/src/components/page/PageToolbar.tsx @@ -0,0 +1,156 @@ +import { + Button, + ButtonVariant, + Flex, + FlexItem, + InputGroup, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, + ToolbarToggleGroup, +} from '@patternfly/react-core'; +import { FilterIcon, MinusIcon, PlusIcon, SearchIcon } from '@patternfly/react-icons'; +import React, { useState } from 'react'; + +import { IOptionsAdditionalFields, Options, TTime } from '@kobsio/plugin-core'; +import { IOptions } from '../../utils/interfaces'; +import { PageToolbarAutocomplete } from './PageToolbarAutocomplete'; + +// IPageToolbarProps is the interface for all properties, which can be passed to the PageToolbar component. This are all +// available Prometheus options and a function to write changes to these properties back to the parent component. +interface IPageToolbarProps extends IOptions { + name: string; + setOptions: (data: IOptions) => void; +} + +// PageToolbar is the toolbar for the Prometheus plugin page. It allows a user to specify query and to select a start +// time, end time and resolution for the query. +const PageToolbar: React.FunctionComponent = ({ + name, + queries, + resolution, + times, + setOptions, +}: IPageToolbarProps) => { + const [data, setData] = useState({ + queries: queries, + resolution: resolution, + times: times, + }); + + const addQuery = (): void => { + const tmpQueries = [...data.queries]; + tmpQueries.push(''); + setData({ ...data, queries: tmpQueries }); + }; + + const removeQuery = (index: number): void => { + const tmpQueries = [...data.queries]; + tmpQueries.splice(index, 1); + setData({ ...data, queries: tmpQueries }); + }; + + // changeQuery changes the value of a single query. + const changeQuery = (index: number, value: string): void => { + const tmpQueries = [...data.queries]; + tmpQueries[index] = value; + setData({ ...data, queries: tmpQueries }); + }; + + // changeOptions changes the Prometheus options. This function is passed to the shared Options component. + const changeOptions = ( + refresh: boolean, + additionalFields: IOptionsAdditionalFields[] | undefined, + time: TTime, + timeEnd: number, + timeStart: number, + ): void => { + if (additionalFields && additionalFields.length === 1) { + const tmpData = { ...data }; + + if (refresh) { + setOptions({ + ...tmpData, + resolution: additionalFields[0].value, + times: { time: time, timeEnd: timeEnd, timeStart: timeStart }, + }); + } + + setData({ + ...tmpData, + resolution: additionalFields[0].value, + times: { time: time, timeEnd: timeEnd, timeStart: timeStart }, + }); + } + }; + + // onEnter is used to detect if the user pressed the "ENTER" key. If this is the case we will not add a newline. + // Instead of this we are calling the setOptions function to trigger the search. To enter a newline the user has to + // use "SHIFT" + "ENTER". + const onEnter = (e: React.KeyboardEvent | undefined): void => { + if (e?.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + setOptions(data); + } + }; + + return ( + + + } breakpoint="lg"> + + + + {data.queries.map((query, index) => ( + + + changeQuery(index, value)} + onEnter={onEnter} + /> + {index === 0 ? ( + + ) : ( + + )} + + + ))} + + + + + + + + + + + + + ); +}; + +export default PageToolbar; diff --git a/plugins/prometheus/src/components/page/PageToolbarAutocomplete.tsx b/plugins/prometheus/src/components/page/PageToolbarAutocomplete.tsx new file mode 100644 index 000000000..d4816bd09 --- /dev/null +++ b/plugins/prometheus/src/components/page/PageToolbarAutocomplete.tsx @@ -0,0 +1,155 @@ +import { Button, ButtonVariant, TextArea } from '@patternfly/react-core'; +import React, { useRef, useState } from 'react'; +import { TimesIcon } from '@patternfly/react-icons'; +import { useQuery } from 'react-query'; + +import { useDebounce } from '@kobsio/plugin-core'; + +interface IPageToolbarAutocomplete { + name: string; + query: string; + setQuery: (q: string) => void; + onEnter: (e: React.KeyboardEvent | undefined) => void; +} + +// PageToolbarAutocomplete is used as input for the PromQL query. The component also fetches a list of suggestions for +// the provided input. +export const PageToolbarAutocomplete: React.FunctionComponent = ({ + name, + query, + setQuery, + onEnter, +}: IPageToolbarAutocomplete) => { + const debouncedQuery = useDebounce(query, 500); + const inputRef = useRef(null); + const [inputFocused, setInputFocused] = useState(false); + const [selectedIndex, setSelectedIndex] = useState(-1); + const [hoveringMetricNamesList, setHovering] = useState(false); + + const { data } = useQuery(['prometheus/labelvalues', debouncedQuery], async () => { + try { + if (debouncedQuery === '') { + return undefined; + } + + const response = await fetch(`/api/plugins/prometheus/labels/${name}?searchTerm=${debouncedQuery}`, { + method: 'get', + }); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } catch (err) { + throw err; + } + }); + + // onFocus is used to set the inputFocused variable to true, when the TextArea is focused. + const onFocus = (): void => { + setInputFocused(true); + }; + + // onBlur is used to set the inputFocused variable to false, when the TextArea looses the focus. + const onBlur = (): void => { + setInputFocused(false); + }; + + // onKeyDown is used to navigate to the suggestion list via the arrow up and arrow down key. When a item is selected + // and the user presses the enter key, the selected item will be used for the query. When no item is selected and the + // user presses the enter key, the search is executed. + const onKeyDown = (e: React.KeyboardEvent | undefined): void => { + if (e?.key === 'ArrowUp') { + if (data && selectedIndex === 0) { + setSelectedIndex(data.length - 1); + } else { + setSelectedIndex(selectedIndex - 1); + } + } else if (data && e?.key === 'ArrowDown') { + if (selectedIndex + 1 === data.length) { + setSelectedIndex(0); + } else { + setSelectedIndex(selectedIndex + 1); + } + } else if (data && e?.key === 'Enter' && selectedIndex > -1) { + e.preventDefault(); + setQuery(data[selectedIndex]); + setSelectedIndex(-1); + } else { + onEnter(e); + } + }; + + // onMouseDown is used, when a user selects an item from the suggestion via the mouse. When the item is selected, we + // switch the focus back to the TextArea component. + const onMouseDown = (result: string): void => { + setQuery(result); + setHovering(false); + + setTimeout(() => { + inputRef.current?.focus(); + }, 50); + }; + + return ( + +
+
+ +