diff --git a/src/main/__test__/context-handler.test.ts b/src/main/__test__/context-handler.test.ts new file mode 100644 index 0000000000..ad3b42f5d2 --- /dev/null +++ b/src/main/__test__/context-handler.test.ts @@ -0,0 +1,210 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { UserStore } from "../../common/user-store"; +import { ContextHandler } from "../context-handler"; +import { PrometheusProvider, PrometheusProviderRegistry, PrometheusService } from "../prometheus"; + +enum ServiceResult { + Success, + Failure, + Undefined, +} + +class TestProvider extends PrometheusProvider { + name = "TestProvider1"; + rateAccuracy = "1h"; + isConfigurable = false; + + constructor(public id: string, public alwaysFail: ServiceResult) { + super(); + } + + getQuery(): string { + throw new Error("getQuery is not implemented."); + } + + async getPrometheusService(): Promise { + switch (this.alwaysFail) { + case ServiceResult.Success: + return { + id: this.id, + namespace: "default", + port: 7000, + service: "", + }; + case ServiceResult.Failure: + throw new Error("does fail"); + case ServiceResult.Undefined: + return undefined; + } + } +} + +function getHandler() { + return new ContextHandler(({ + getProxyKubeconfig: (): any => ({ + makeApiClient: (): any => undefined, + }), + apiUrl: "http://localhost:81", + }) as any); +} + +describe("ContextHandler", () => { + beforeEach(() => { + PrometheusProviderRegistry.createInstance(); + UserStore.createInstance(); + }); + + afterEach(() => { + PrometheusProviderRegistry.resetInstance(); + UserStore.resetInstance(); + }); + + describe("getPrometheusService", () => { + it.each([ + [0, 0], + [0, 1], + [0, 2], + [0, 3], + ])("should return undefined from %d success(es) after %d failure(s)", async (successes, failures) => { + const reg = PrometheusProviderRegistry.getInstance(); + let count = 0; + + for (let i = 0; i < failures; i += 1) { + const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined; + + reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult)); + } + + for (let i = 0; i < successes; i += 1) { + reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); + } + + const service = await getHandler().getPrometheusService(); + + expect(service).toBeUndefined(); + }); + + it.each([ + [1, 0], + [1, 1], + [1, 2], + [1, 3], + [2, 0], + [2, 1], + [2, 2], + [2, 3], + ])("should pick the first provider of %d success(es) after %d failure(s)", async (successes, failures) => { + const reg = PrometheusProviderRegistry.getInstance(); + let count = 0; + + for (let i = 0; i < failures; i += 1) { + const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined; + + reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult)); + } + + for (let i = 0; i < successes; i += 1) { + reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); + } + + const service = await getHandler().getPrometheusService(); + + expect(service.id === `id_${failures}`); + }); + + it.each([ + [1, 0], + [1, 1], + [1, 2], + [1, 3], + [2, 0], + [2, 1], + [2, 2], + [2, 3], + ])("should pick the first provider of %d success(es) before %d failure(s)", async (successes, failures) => { + const reg = PrometheusProviderRegistry.getInstance(); + let count = 0; + + for (let i = 0; i < successes; i += 1) { + reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); + } + + for (let i = 0; i < failures; i += 1) { + const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined; + + reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult)); + } + + const service = await getHandler().getPrometheusService(); + + expect(service.id === "id_0"); + }); + + it.each([ + [1, 0], + [1, 1], + [1, 2], + [1, 3], + [2, 0], + [2, 1], + [2, 2], + [2, 3], + ])("should pick the first provider of %d success(es) between %d failure(s)", async (successes, failures) => { + const reg = PrometheusProviderRegistry.getInstance(); + let count = 0; + const beforeSuccesses = Math.floor(successes / 2); + const afterSuccesses = successes - beforeSuccesses; + + for (let i = 0; i < beforeSuccesses; i += 1) { + reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); + } + + for (let i = 0; i < failures; i += 1) { + const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined; + + reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult)); + } + + for (let i = 0; i < afterSuccesses; i += 1) { + reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); + } + + const service = await getHandler().getPrometheusService(); + + expect(service.id === "id_0"); + }); + + it("shouldn't pick the second provider of 2 succcess(es) after 1 failure(s)", async () => { + const reg = PrometheusProviderRegistry.getInstance(); + let count = 0; + + reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Failure)); + reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); + reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); + + const service = await getHandler().getPrometheusService(); + + expect(service.id).not.toBe("id_2"); + }); + }); +}); diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts index 6fa7ca1ef5..04c2842a37 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -19,13 +19,13 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { PrometheusService } from "./prometheus/provider-registry"; +import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry"; +import { PrometheusProviderRegistry } from "./prometheus/provider-registry"; import type { ClusterPrometheusPreferences } from "../common/cluster-store"; import type { Cluster } from "./cluster"; import type httpProxy from "http-proxy"; import url, { UrlWithStringQuery } from "url"; import { CoreV1Api } from "@kubernetes/client-node"; -import { prometheusProviders } from "../common/prometheus-providers"; import logger from "./logger"; import { KubeAuthProxy } from "./kube-auth-proxy"; @@ -33,7 +33,7 @@ export class ContextHandler { public clusterUrl: UrlWithStringQuery; protected kubeAuthProxy?: KubeAuthProxy; protected apiTarget?: httpProxy.ServerOptions; - protected prometheusProvider: string; + protected prometheusProvider?: string; protected prometheusPath: string | null; constructor(protected cluster: Cluster) { @@ -72,18 +72,34 @@ export class ContextHandler { this.prometheusProvider = service.id; } - return prometheusProviders.find(p => p.id === this.prometheusProvider); + return PrometheusProviderRegistry.getInstance().getByKind(this.prometheusProvider); } - async getPrometheusService(): Promise { - const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders; - const prometheusPromises: Promise[] = providers.map(async provider => { - const apiClient = (await this.cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api); + protected listPotentialProviders(): PrometheusProvider[] { + const registry = PrometheusProviderRegistry.getInstance(); - return provider.getPrometheusService(apiClient); - }); + if (typeof this.prometheusProvider === "string") { + return [registry.getByKind(this.prometheusProvider)]; + } - return (await Promise.all(prometheusPromises)).find(Boolean); + return Array.from(registry.providers.values()); + } + + async getPrometheusService(): Promise { + const providers = this.listPotentialProviders(); + const proxyConfig = await this.cluster.getProxyKubeconfig(); + const apiClient = proxyConfig.makeApiClient(CoreV1Api); + const potentialServices = await Promise.allSettled( + providers.map(provider => provider.getPrometheusService(apiClient)) + ); + + for (const result of potentialServices) { + if (result.status === "fulfilled" && result.value) { + return result.value; + } + } + + return undefined; } async getPrometheusPath(): Promise { diff --git a/src/main/index.ts b/src/main/index.ts index 271010d922..bf8d4943e3 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -22,7 +22,6 @@ // Main process import "../common/system-ca"; -import "../common/prometheus-providers"; import * as Mobx from "mobx"; import * as LensExtensionsCoreApi from "../extensions/core-api"; import { app, autoUpdater, ipcMain, dialog, powerMonitor } from "electron"; @@ -56,6 +55,7 @@ import { HelmRepoManager } from "./helm/helm-repo-manager"; import { KubeconfigSyncManager } from "./catalog-sources"; import { handleWsUpgrade } from "./proxy/ws-upgrade"; import configurePackages from "../common/configure-packages"; +import { PrometheusProviderRegistry, registerDefaultPrometheusProviders } from "./prometheus"; const workingDir = path.join(app.getPath("appData"), appName); const cleanup = disposer(); @@ -124,6 +124,9 @@ app.on("ready", async () => { registerFileProtocol("static", __static); + PrometheusProviderRegistry.createInstance(); + registerDefaultPrometheusProviders(); + const userStore = UserStore.createInstance(); const clusterStore = ClusterStore.createInstance(); const hotbarStore = HotbarStore.createInstance(); diff --git a/src/main/prometheus/helm.ts b/src/main/prometheus/helm.ts index 0317b8499f..f583578ddb 100644 --- a/src/main/prometheus/helm.ts +++ b/src/main/prometheus/helm.ts @@ -22,32 +22,14 @@ import { PrometheusLens } from "./lens"; import type { CoreV1Api } from "@kubernetes/client-node"; import type { PrometheusService } from "./provider-registry"; -import logger from "../logger"; export class PrometheusHelm extends PrometheusLens { - id = "helm"; - name = "Helm"; - rateAccuracy = "5m"; + readonly id: string = "helm"; + readonly name: string = "Helm"; + readonly rateAccuracy: string = "5m"; + readonly isConfigurable: boolean = false; - public async getPrometheusService(client: CoreV1Api): Promise { - const labelSelector = "app=prometheus,component=server,heritage=Helm"; - - try { - const serviceList = await client.listServiceForAllNamespaces(false, "", null, labelSelector); - const service = serviceList.body.items[0]; - - if (!service) return; - - return { - id: this.id, - namespace: service.metadata.namespace, - service: service.metadata.name, - port: service.spec.ports[0].port - }; - } catch(error) { - logger.warn(`PrometheusHelm: failed to list services: ${error.toString()}`); - - return; - } + public async getPrometheusService(client: CoreV1Api): Promise { + return this.getFirstNamespacedServer(client, "app=prometheus,component=server,heritage=Helm"); } } diff --git a/src/common/prometheus-providers.ts b/src/main/prometheus/index.ts similarity index 64% rename from src/common/prometheus-providers.ts rename to src/main/prometheus/index.ts index e06c4f292f..c24024d8b8 100644 --- a/src/common/prometheus-providers.ts +++ b/src/main/prometheus/index.ts @@ -19,16 +19,19 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { PrometheusLens } from "../main/prometheus/lens"; -import { PrometheusHelm } from "../main/prometheus/helm"; -import { PrometheusOperator } from "../main/prometheus/operator"; -import { PrometheusStacklight } from "../main/prometheus/stacklight"; -import { PrometheusProviderRegistry } from "../main/prometheus/provider-registry"; +import { PrometheusHelm } from "./helm"; +import { PrometheusLens } from "./lens"; +import { PrometheusOperator } from "./operator"; +import { PrometheusProviderRegistry } from "./provider-registry"; +import { PrometheusStacklight } from "./stacklight"; -[PrometheusLens, PrometheusHelm, PrometheusOperator, PrometheusStacklight].forEach(providerClass => { - const provider = new providerClass(); +export * from "./provider-registry"; - PrometheusProviderRegistry.registerProvider(provider.id, provider); -}); - -export const prometheusProviders = PrometheusProviderRegistry.getProviders(); +export function registerDefaultPrometheusProviders() { + PrometheusProviderRegistry + .getInstance() + .registerProvider(new PrometheusLens()) + .registerProvider(new PrometheusHelm()) + .registerProvider(new PrometheusOperator()) + .registerProvider(new PrometheusStacklight()); +} diff --git a/src/main/prometheus/lens.ts b/src/main/prometheus/lens.ts index 4fb5c8d339..87e2367636 100644 --- a/src/main/prometheus/lens.ts +++ b/src/main/prometheus/lens.ts @@ -19,16 +19,18 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry"; +import { PrometheusProvider, PrometheusService } from "./provider-registry"; import type { CoreV1Api } from "@kubernetes/client-node"; import logger from "../logger"; +import { inspect } from "util"; -export class PrometheusLens implements PrometheusProvider { - id = "lens"; - name = "Lens"; - rateAccuracy = "1m"; +export class PrometheusLens extends PrometheusProvider { + readonly id: string = "lens"; + readonly name: string = "Lens"; + readonly rateAccuracy: string = "1m"; + readonly isConfigurable: boolean = true; - public async getPrometheusService(client: CoreV1Api): Promise { + public async getPrometheusService(client: CoreV1Api): Promise { try { const resp = await client.readNamespacedService("prometheus", "lens-metrics"); const service = resp.body; @@ -41,66 +43,101 @@ export class PrometheusLens implements PrometheusProvider { }; } catch(error) { logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`); + + return undefined; } } - public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | void { + public getQuery(opts: Record, queryName: string): string { switch(opts.category) { case "cluster": - return { - memoryUsage: ` - sum( - node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes) - ) by (kubernetes_name) - `.replace(/_bytes/g, `_bytes{kubernetes_node=~"${opts.nodes}"}`), - memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`, - memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`, - memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`, - cpuUsage: `sum(rate(node_cpu_seconds_total{kubernetes_node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`, - cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`, - cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`, - cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`, - podUsage: `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", instance=~"${opts.nodes}"})`, - podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`, - fsSize: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`, - fsUsage: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)` - }; + switch (queryName) { + case "memoryUsage": + return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_name)`.replace(/_bytes/g, `_bytes{kubernetes_node=~"${opts.nodes}"}`); + case "memoryRequests": + return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`; + case "memoryLimits": + return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`; + case "memoryCapacity": + return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`; + case "cpuUsage": + return `sum(rate(node_cpu_seconds_total{kubernetes_node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`; + case "cpuRequests": + return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`; + case "cpuLimits": + return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`; + case "cpuCapacity": + return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`; + case "podUsage": + return `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", instance=~"${opts.nodes}"})`; + case "podCapacity": + return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`; + case "fsSize": + return `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`; + case "fsUsage": + return `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`; + } + break; case "nodes": - return { - memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_node)`, - memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`, - cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(kubernetes_node)`, - cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`, - fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (kubernetes_node)`, - fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (kubernetes_node)` - }; + switch (queryName) { + case "memoryUsage": + return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_node)`; + case "memoryCapacity": + return `sum(kube_node_status_capacity{resource="memory"}) by (node)`; + case "cpuUsage": + return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(kubernetes_node)`; + case "cpuCapacity": + return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`; + case "fsSize": + return `sum(node_filesystem_size_bytes{mountpoint="/"}) by (kubernetes_node)`; + case "fsUsage": + return `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (kubernetes_node)`; + } + break; case "pods": - return { - cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, - cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, - cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, - memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, - memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, - memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, - fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, - networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, - networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})` - }; + switch (queryName) { + case "cpuUsage": + return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + case "cpuRequests": + return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "cpuLimits": + return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "memoryUsage": + return `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "memoryRequests": + return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "memoryLimits": + return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "fsUsage": + return `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "networkReceive": + return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + case "networkTransmit": + return `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + } + break; case "pvc": - return { - diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`, - diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)` - }; + switch (queryName) { + case "diskUsage": + return `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`; + case "diskCapacity": + return `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`; + } + break; case "ingress": - const bytesSent = (ingress: string, namespace: string, statuses: string) => - `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`; - - return { - bytesSentSuccess: bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"), - bytesSentFailure: bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"), - requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`, - responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)` - }; + switch (queryName) { + case "bytesSentSuccess": + return this.bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"); + case "bytesSentFailure": + return this.bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"); + case "requestDurationSeconds": + return `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`; + case "responseDurationSeconds": + return `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`; + } + break; } + + throw new Error(`Unknown query name ${inspect(queryName, false, undefined, false)} for category: ${inspect(opts.category, false, undefined, false)}`); } } diff --git a/src/main/prometheus/operator.ts b/src/main/prometheus/operator.ts index b546cac517..2ee3b50cf9 100644 --- a/src/main/prometheus/operator.ts +++ b/src/main/prometheus/operator.ts @@ -19,98 +19,110 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry"; -import type { CoreV1Api, V1Service } from "@kubernetes/client-node"; -import logger from "../logger"; +import { PrometheusProvider, PrometheusService } from "./provider-registry"; +import type { CoreV1Api } from "@kubernetes/client-node"; +import { inspect } from "util"; -export class PrometheusOperator implements PrometheusProvider { - rateAccuracy = "1m"; - id = "operator"; - name = "Prometheus Operator"; +export class PrometheusOperator extends PrometheusProvider { + readonly rateAccuracy: string = "1m"; + readonly id: string = "operator"; + readonly name: string = "Prometheus Operator"; + readonly isConfigurable: boolean = false; - public async getPrometheusService(client: CoreV1Api): Promise { - try { - let service: V1Service; - - for (const labelSelector of ["operated-prometheus=true", "self-monitor=true"]) { - if (!service) { - const serviceList = await client.listServiceForAllNamespaces(null, null, null, labelSelector); - - service = serviceList.body.items[0]; - } - } - if (!service) return; - - return { - id: this.id, - namespace: service.metadata.namespace, - service: service.metadata.name, - port: service.spec.ports[0].port - }; - } catch(error) { - logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`); - - return; - } + public async getPrometheusService(client: CoreV1Api): Promise { + return this.getFirstNamespacedServer(client, "operated-prometheus=true", "self-monitor=true"); } - public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | void { + public getQuery(opts: Record, queryName: string): string { switch(opts.category) { case "cluster": - return { - memoryUsage: ` - sum( - node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes) - ) - `.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`), - memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`, - memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"})`, - memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"})`, - cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`, - cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`, - cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"})`, - cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"})`, - podUsage: `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", node=~"${opts.nodes}"})`, - podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"})`, - fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`, - fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})` - }; + switch (queryName) { + case "memoryUsage": + return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`); + case "memoryRequests": + return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`; + case "memoryLimits": + return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"})`; + case "memoryCapacity": + return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"})`; + case "cpuUsage": + return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`; + case "cpuRequests": + return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`; + case "cpuLimits": + return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"})`; + case "cpuCapacity": + return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"})`; + case "podUsage": + return `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", node=~"${opts.nodes}"})`; + case "podCapacity": + return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"})`; + case "fsSize": + return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`; + case "fsUsage": + return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`; + } + break; case "nodes": - return { - memoryUsage: `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`, - memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`, - cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`, - cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`, - fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`, - fsUsage: `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod,namespace) group_left(node) kube_pod_info) by (node)` - }; + switch (queryName) { + case "memoryUsage": + return `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`; + case "memoryCapacity": + return `sum(kube_node_status_capacity{resource="memory"}) by (node)`; + case "cpuUsage": + return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`; + case "cpuCapacity": + return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`; + case "fsSize": + return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`; + case "fsUsage": + return `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`; + } + break; case "pods": - return { - cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, - cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, - cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, - memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, - memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, - memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, - fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, - networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, - networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})` - }; + switch (queryName) { + case "cpuUsage": + return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + case "cpuRequests": + return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "cpuLimits": + return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "memoryUsage": + return `sum(container_memory_working_set_bytes{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "memoryRequests": + return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "memoryLimits": + return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "fsUsage": + return `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "networkReceive": + return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + case "networkTransmit": + return `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + } + break; case "pvc": - return { - diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`, - diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)` - }; + switch (queryName) { + case "diskUsage": + return `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`; + case "diskCapacity": + return `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`; + } + break; case "ingress": - const bytesSent = (ingress: string, namespace: string, statuses: string) => - `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`; - - return { - bytesSentSuccess: bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"), - bytesSentFailure: bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"), - requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`, - responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)` - }; + switch (queryName) { + case "bytesSentSuccess": + return this.bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"); + case "bytesSentFailure": + return this.bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"); + case "requestDurationSeconds": + return `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`; + case "responseDurationSeconds": + return `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`; + } + break; } + + throw new Error(`Unknown query name ${inspect(queryName, false, undefined, false)} for category: ${inspect(opts.category, false, undefined, false)}`); } } diff --git a/src/main/prometheus/provider-registry.ts b/src/main/prometheus/provider-registry.ts index db40ec460b..be4dcef737 100644 --- a/src/main/prometheus/provider-registry.ts +++ b/src/main/prometheus/provider-registry.ts @@ -20,58 +20,8 @@ */ import type { CoreV1Api } from "@kubernetes/client-node"; - -export type PrometheusClusterQuery = { - memoryUsage: string; - memoryRequests: string; - memoryLimits: string; - memoryCapacity: string; - cpuUsage: string; - cpuRequests: string; - cpuLimits: string; - cpuCapacity: string; - podUsage: string; - podCapacity: string; -}; - -export type PrometheusNodeQuery = { - memoryUsage: string; - memoryCapacity: string; - cpuUsage: string; - cpuCapacity: string; - fsSize: string; - fsUsage: string; -}; - -export type PrometheusPodQuery = { - memoryUsage: string; - memoryRequests: string; - memoryLimits: string; - cpuUsage: string; - cpuRequests: string; - cpuLimits: string; - fsUsage: string; - networkReceive: string; - networkTransmit: string; -}; - -export type PrometheusPvcQuery = { - diskUsage: string; - diskCapacity: string; -}; - -export type PrometheusIngressQuery = { - bytesSentSuccess: string; - bytesSentFailure: string; - requestDurationSeconds: string; - responseDurationSeconds: string; -}; - -export type PrometheusQueryOpts = { - [key: string]: string | any; -}; - -export type PrometheusQuery = PrometheusNodeQuery | PrometheusClusterQuery | PrometheusPodQuery | PrometheusPvcQuery | PrometheusIngressQuery; +import { Singleton } from "../../common/utils"; +import logger from "../logger"; export type PrometheusService = { id: string; @@ -80,33 +30,61 @@ export type PrometheusService = { port: number; }; -export interface PrometheusProvider { - id: string; - name: string; - getQueries(opts: PrometheusQueryOpts): PrometheusQuery | void; - getPrometheusService(client: CoreV1Api): Promise; -} +export abstract class PrometheusProvider { + abstract readonly id: string; + abstract readonly name: string; + abstract readonly rateAccuracy: string; + abstract readonly isConfigurable: boolean; -export type PrometheusProviderList = { - [key: string]: PrometheusProvider; -}; + abstract getQuery(opts: Record, queryName: string): string; + abstract getPrometheusService(client: CoreV1Api): Promise; -export class PrometheusProviderRegistry { - private static prometheusProviders: PrometheusProviderList = {}; + protected bytesSent(ingress: string, namespace: string, statuses: string): string { + return `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`; + } - static getProvider(type: string): PrometheusProvider { - if (!this.prometheusProviders[type]) { - throw "Unknown Prometheus provider"; + protected async getFirstNamespacedServer(client: CoreV1Api, ...selectors: string[]): Promise { + try { + for (const selector of selectors) { + const { body: { items: [service] } } = await client.listServiceForAllNamespaces(null, null, null, selector); + + if (service) { + return { + id: this.id, + namespace: service.metadata.namespace, + service: service.metadata.name, + port: service.spec.ports[0].port + }; + } + } + } catch (error) { + logger.warn(`${this.name}: failed to list services: ${error.toString()}`); } - return this.prometheusProviders[type]; - } - - static registerProvider(key: string, provider: PrometheusProvider) { - this.prometheusProviders[key] = provider; - } - - static getProviders(): PrometheusProvider[] { - return Object.values(this.prometheusProviders); + return undefined; + } +} + +export class PrometheusProviderRegistry extends Singleton { + public providers = new Map(); + + getByKind(kind: string): PrometheusProvider { + const provider = this.providers.get(kind); + + if (!provider) { + throw new Error("Unknown Prometheus provider"); + } + + return provider; + } + + registerProvider(provider: PrometheusProvider): this { + if (this.providers.has(provider.id)) { + throw new Error("Provider already registered under that kind"); + } + + this.providers.set(provider.id, provider); + + return this; } } diff --git a/src/main/prometheus/stacklight.ts b/src/main/prometheus/stacklight.ts index 40c1963d3f..2e14508b22 100644 --- a/src/main/prometheus/stacklight.ts +++ b/src/main/prometheus/stacklight.ts @@ -19,16 +19,18 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry"; +import { PrometheusProvider, PrometheusService } from "./provider-registry"; import type { CoreV1Api } from "@kubernetes/client-node"; import logger from "../logger"; +import { inspect } from "util"; -export class PrometheusStacklight implements PrometheusProvider { - id = "stacklight"; - name = "Stacklight"; - rateAccuracy = "1m"; +export class PrometheusStacklight extends PrometheusProvider { + readonly id: string = "stacklight"; + readonly name: string = "Stacklight"; + readonly rateAccuracy: string = "1m"; + readonly isConfigurable: boolean = false; - public async getPrometheusService(client: CoreV1Api): Promise { + public async getPrometheusService(client: CoreV1Api): Promise { try { const resp = await client.readNamespacedService("prometheus-server", "stacklight"); const service = resp.body; @@ -41,66 +43,101 @@ export class PrometheusStacklight implements PrometheusProvider { }; } catch(error) { logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`); + + return undefined; } } - public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | void { + public getQuery(opts: Record, queryName: string): string { switch(opts.category) { case "cluster": - return { - memoryUsage: ` - sum( - node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes) - ) by (kubernetes_name) - `.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`), - memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`, - memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`, - memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`, - cpuUsage: `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`, - cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`, - cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`, - cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`, - podUsage: `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", instance=~"${opts.nodes}"})`, - podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`, - fsSize: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`, - fsUsage: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)` - }; + switch (queryName) { + case "memoryUsage": + return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_name)`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`); + case "memoryRequests": + return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`; + case "memoryLimits": + return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`; + case "memoryCapacity": + return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`; + case "cpuUsage": + return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`; + case "cpuRequests": + return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`; + case "cpuLimits": + return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`; + case "cpuCapacity": + return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`; + case "podUsage": + return `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", instance=~"${opts.nodes}"})`; + case "podCapacity": + return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`; + case "fsSize": + return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`; + case "fsUsage": + return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`; + } + break; case "nodes": - return { - memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`, - memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`, - cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`, - cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`, - fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`, - fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)` - }; + switch (queryName) { + case "memoryUsage": + return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`; + case "memoryCapacity": + return `sum(kube_node_status_capacity{resource="memory"}) by (node)`; + case "cpuUsage": + return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`; + case "cpuCapacity": + return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`; + case "fsSize": + return `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`; + case "fsUsage": + return `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`; + } + break; case "pods": - return { - cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, - cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, - cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, - memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, - memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, - memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, - fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, - networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, - networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})` - }; + switch (queryName) { + case "cpuUsage": + return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + case "cpuRequests": + return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "cpuLimits": + return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "memoryUsage": + return `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "memoryRequests": + return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "memoryLimits": + return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "fsUsage": + return `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`; + case "networkReceive": + return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + case "networkTransmit": + return `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`; + } + break; case "pvc": - return { - diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`, - diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)` - }; + switch (queryName) { + case "diskUsage": + return `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`; + case "diskCapacity": + return `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`; + } + break; case "ingress": - const bytesSent = (ingress: string, namespace: string, statuses: string) => - `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`; - - return { - bytesSentSuccess: bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"), - bytesSentFailure: bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"), - requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`, - responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)` - }; + switch (queryName) { + case "bytesSentSuccess": + return this.bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"); + case "bytesSentFailure": + return this.bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"); + case "requestDurationSeconds": + return `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`; + case "responseDurationSeconds": + return `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`; + } + break; } + + throw new Error(`Unknown query name ${inspect(queryName, false, undefined, false)} for category: ${inspect(opts.category, false, undefined, false)}`); } } diff --git a/src/main/router.ts b/src/main/router.ts index 33e8b39ec9..50175aa9de 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -177,6 +177,7 @@ export class Router { // Metrics API this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, MetricsRoute.routeMetrics); + this.router.add({ method: "get", path: `${apiPrefix}/metrics/providers` }, MetricsRoute.routeMetricsProviders); // Port-forward API this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, PortForwardRoute.routePortForward); diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts index 0599114219..c309b42ccf 100644 --- a/src/main/routes/metrics-route.ts +++ b/src/main/routes/metrics-route.ts @@ -26,6 +26,7 @@ import { Cluster, ClusterMetadataKey } from "../cluster"; import type { ClusterPrometheusMetadata } from "../../common/cluster-store"; import logger from "../logger"; import { getMetrics } from "../k8s-request"; +import { PrometheusProviderRegistry } from "../prometheus"; export type IMetricsQuery = string | string[] | { [metricName: string]: string; @@ -62,6 +63,12 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa return Promise.all(queries.map(loadMetric)); } +interface MetricProviderInfo { + name: string; + id: string; + isConfigurable: boolean; +} + export class MetricsRoute { static async routeMetrics({ response, cluster, payload, query }: LensApiRequest) { const queryParams: IMetricsQuery = Object.fromEntries(query.entries()); @@ -92,9 +99,10 @@ export class MetricsRoute { respondJson(response, data); } else { - const queries = Object.entries(payload).map(([queryName, queryOpts]) => ( - (prometheusProvider.getQueries(queryOpts) as Record)[queryName] - )); + const queries = Object.entries>(payload) + .map(([queryName, queryOpts]) => ( + prometheusProvider.getQuery(queryOpts, queryName) + )); const result = await loadMetrics(queries, cluster, prometheusPath, queryParams); const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]])); @@ -108,4 +116,14 @@ export class MetricsRoute { cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata; } } + + static async routeMetricsProviders({ response }: LensApiRequest) { + const providers: MetricProviderInfo[] = []; + + for (const { name, id, isConfigurable } of PrometheusProviderRegistry.getInstance().providers.values()) { + providers.push({ name, id, isConfigurable }); + } + + respondJson(response, providers); + } } diff --git a/src/renderer/api/endpoints/metrics.api.ts b/src/renderer/api/endpoints/metrics.api.ts index 00408678e0..3f5b6e66e7 100644 --- a/src/renderer/api/endpoints/metrics.api.ts +++ b/src/renderer/api/endpoints/metrics.api.ts @@ -46,6 +46,12 @@ export interface IMetricsResult { values: [number, string][]; } +export interface MetricProviderInfo { + name: string; + id: string; + isConfigurable: boolean; +} + export interface IMetricsReqParams { start?: number | string; // timestamp in seconds or valid date-string end?: number | string; @@ -75,6 +81,10 @@ export const metricsApi = { } }); }, + + async getMetricProviders(): Promise { + return apiBase.get("/metrics/providers"); + } }; export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics { diff --git a/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx b/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx index 3895d1b380..dfecec3573 100644 --- a/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx @@ -21,18 +21,14 @@ import React from "react"; import { observer, disposeOnUnmount } from "mobx-react"; -import { prometheusProviders } from "../../../../common/prometheus-providers"; import type { Cluster } from "../../../../main/cluster"; import { SubTitle } from "../../layout/sub-title"; import { Select, SelectOption } from "../../select"; import { Input } from "../../input"; import { observable, computed, autorun, makeObservable } from "mobx"; import { productName } from "../../../../common/vars"; - -const options: SelectOption[] = [ - { value: "", label: "Auto detect" }, - ...prometheusProviders.map(pp => ({value: pp.id, label: pp.name})) -]; +import { MetricProviderInfo, metricsApi } from "../../../api/endpoints/metrics.api"; +import { Spinner } from "../../spinner"; interface Props { cluster: Cluster; @@ -42,16 +38,27 @@ interface Props { export class ClusterPrometheusSetting extends React.Component { @observable path = ""; @observable provider = ""; + @observable loading = true; + @observable loadedOptions: MetricProviderInfo[] = []; + + @computed get options(): SelectOption[] { + return [ + { value: "", label: "Auto detect" }, + ...this.loadedOptions.map(({ name, id }) => ({ value: id, label: name })) + ]; + } constructor(props: Props) { super(props); makeObservable(this); } - @computed get canEditPrometheusPath() { - if (this.provider === "" || this.provider === "lens") return false; - - return true; + @computed get canEditPrometheusPath(): boolean { + return Boolean( + this.loadedOptions + .find(opt => opt.id === this.provider) + ?.isConfigurable + ); } componentDidMount() { @@ -74,6 +81,16 @@ export class ClusterPrometheusSetting extends React.Component { } }) ); + + metricsApi + .getMetricProviders() + .then(values => { + this.loading = false; + + if (values) { + this.loadedOptions = values; + } + }); } parsePrometheusPath = () => { @@ -110,15 +127,21 @@ export class ClusterPrometheusSetting extends React.Component { <>
- { + this.provider = value; + this.onSaveProvider(); + }} + options={this.options} + /> + What query format is used to fetch metrics from Prometheus + + }
{this.canEditPrometheusPath && (