1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Refactor PrometheusProviders (#2707)

This commit is contained in:
Sebastian Malton 2021-05-27 09:50:18 -04:00 committed by GitHub
parent 88edeeef43
commit 652319ac89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 670 additions and 340 deletions

View File

@ -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<PrometheusService> {
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");
});
});
});

View File

@ -19,13 +19,13 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 { ClusterPrometheusPreferences } from "../common/cluster-store";
import type { Cluster } from "./cluster"; import type { Cluster } from "./cluster";
import type httpProxy from "http-proxy"; import type httpProxy from "http-proxy";
import url, { UrlWithStringQuery } from "url"; import url, { UrlWithStringQuery } from "url";
import { CoreV1Api } from "@kubernetes/client-node"; import { CoreV1Api } from "@kubernetes/client-node";
import { prometheusProviders } from "../common/prometheus-providers";
import logger from "./logger"; import logger from "./logger";
import { KubeAuthProxy } from "./kube-auth-proxy"; import { KubeAuthProxy } from "./kube-auth-proxy";
@ -33,7 +33,7 @@ export class ContextHandler {
public clusterUrl: UrlWithStringQuery; public clusterUrl: UrlWithStringQuery;
protected kubeAuthProxy?: KubeAuthProxy; protected kubeAuthProxy?: KubeAuthProxy;
protected apiTarget?: httpProxy.ServerOptions; protected apiTarget?: httpProxy.ServerOptions;
protected prometheusProvider: string; protected prometheusProvider?: string;
protected prometheusPath: string | null; protected prometheusPath: string | null;
constructor(protected cluster: Cluster) { constructor(protected cluster: Cluster) {
@ -72,18 +72,34 @@ export class ContextHandler {
this.prometheusProvider = service.id; this.prometheusProvider = service.id;
} }
return prometheusProviders.find(p => p.id === this.prometheusProvider); return PrometheusProviderRegistry.getInstance().getByKind(this.prometheusProvider);
} }
async getPrometheusService(): Promise<PrometheusService | void> { protected listPotentialProviders(): PrometheusProvider[] {
const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders; const registry = PrometheusProviderRegistry.getInstance();
const prometheusPromises: Promise<PrometheusService | void>[] = providers.map(async provider => {
const apiClient = (await this.cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api);
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<PrometheusService | undefined> {
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<string> { async getPrometheusPath(): Promise<string> {

View File

@ -22,7 +22,6 @@
// Main process // Main process
import "../common/system-ca"; import "../common/system-ca";
import "../common/prometheus-providers";
import * as Mobx from "mobx"; import * as Mobx from "mobx";
import * as LensExtensionsCoreApi from "../extensions/core-api"; import * as LensExtensionsCoreApi from "../extensions/core-api";
import { app, autoUpdater, ipcMain, dialog, powerMonitor } from "electron"; 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 { KubeconfigSyncManager } from "./catalog-sources";
import { handleWsUpgrade } from "./proxy/ws-upgrade"; import { handleWsUpgrade } from "./proxy/ws-upgrade";
import configurePackages from "../common/configure-packages"; import configurePackages from "../common/configure-packages";
import { PrometheusProviderRegistry, registerDefaultPrometheusProviders } from "./prometheus";
const workingDir = path.join(app.getPath("appData"), appName); const workingDir = path.join(app.getPath("appData"), appName);
const cleanup = disposer(); const cleanup = disposer();
@ -124,6 +124,9 @@ app.on("ready", async () => {
registerFileProtocol("static", __static); registerFileProtocol("static", __static);
PrometheusProviderRegistry.createInstance();
registerDefaultPrometheusProviders();
const userStore = UserStore.createInstance(); const userStore = UserStore.createInstance();
const clusterStore = ClusterStore.createInstance(); const clusterStore = ClusterStore.createInstance();
const hotbarStore = HotbarStore.createInstance(); const hotbarStore = HotbarStore.createInstance();

View File

@ -22,32 +22,14 @@
import { PrometheusLens } from "./lens"; import { PrometheusLens } from "./lens";
import type { CoreV1Api } from "@kubernetes/client-node"; import type { CoreV1Api } from "@kubernetes/client-node";
import type { PrometheusService } from "./provider-registry"; import type { PrometheusService } from "./provider-registry";
import logger from "../logger";
export class PrometheusHelm extends PrometheusLens { export class PrometheusHelm extends PrometheusLens {
id = "helm"; readonly id: string = "helm";
name = "Helm"; readonly name: string = "Helm";
rateAccuracy = "5m"; readonly rateAccuracy: string = "5m";
readonly isConfigurable: boolean = false;
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | void> { public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
const labelSelector = "app=prometheus,component=server,heritage=Helm"; return this.getFirstNamespacedServer(client, "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;
}
} }
} }

View File

@ -19,16 +19,19 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { PrometheusLens } from "../main/prometheus/lens"; import { PrometheusHelm } from "./helm";
import { PrometheusHelm } from "../main/prometheus/helm"; import { PrometheusLens } from "./lens";
import { PrometheusOperator } from "../main/prometheus/operator"; import { PrometheusOperator } from "./operator";
import { PrometheusStacklight } from "../main/prometheus/stacklight"; import { PrometheusProviderRegistry } from "./provider-registry";
import { PrometheusProviderRegistry } from "../main/prometheus/provider-registry"; import { PrometheusStacklight } from "./stacklight";
[PrometheusLens, PrometheusHelm, PrometheusOperator, PrometheusStacklight].forEach(providerClass => { export * from "./provider-registry";
const provider = new providerClass();
PrometheusProviderRegistry.registerProvider(provider.id, provider); export function registerDefaultPrometheusProviders() {
}); PrometheusProviderRegistry
.getInstance()
export const prometheusProviders = PrometheusProviderRegistry.getProviders(); .registerProvider(new PrometheusLens())
.registerProvider(new PrometheusHelm())
.registerProvider(new PrometheusOperator())
.registerProvider(new PrometheusStacklight());
}

View File

@ -19,16 +19,18 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 type { CoreV1Api } from "@kubernetes/client-node";
import logger from "../logger"; import logger from "../logger";
import { inspect } from "util";
export class PrometheusLens implements PrometheusProvider { export class PrometheusLens extends PrometheusProvider {
id = "lens"; readonly id: string = "lens";
name = "Lens"; readonly name: string = "Lens";
rateAccuracy = "1m"; readonly rateAccuracy: string = "1m";
readonly isConfigurable: boolean = true;
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | void> { public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
try { try {
const resp = await client.readNamespacedService("prometheus", "lens-metrics"); const resp = await client.readNamespacedService("prometheus", "lens-metrics");
const service = resp.body; const service = resp.body;
@ -41,66 +43,101 @@ export class PrometheusLens implements PrometheusProvider {
}; };
} catch(error) { } catch(error) {
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`); logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`);
return undefined;
} }
} }
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | void { public getQuery(opts: Record<string, string>, queryName: string): string {
switch(opts.category) { switch(opts.category) {
case "cluster": case "cluster":
return { switch (queryName) {
memoryUsage: ` case "memoryUsage":
sum( 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}"}`);
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes) case "memoryRequests":
) by (kubernetes_name) return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`;
`.replace(/_bytes/g, `_bytes{kubernetes_node=~"${opts.nodes}"}`), case "memoryLimits":
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`, return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`;
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`, case "memoryCapacity":
memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`, return `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}]))`, case "cpuUsage":
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`, return `sum(rate(node_cpu_seconds_total{kubernetes_node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`;
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`, case "cpuRequests":
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`, return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
podUsage: `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", instance=~"${opts.nodes}"})`, case "cpuLimits":
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`, return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
fsSize: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`, case "cpuCapacity":
fsUsage: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)` 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": case "nodes":
return { switch (queryName) {
memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_node)`, case "memoryUsage":
memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`, return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_node)`;
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(kubernetes_node)`, case "memoryCapacity":
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`, return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (kubernetes_node)`, case "cpuUsage":
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (kubernetes_node)` 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": case "pods":
return { switch (queryName) {
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, case "cpuUsage":
cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, case "cpuRequests":
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, case "cpuLimits":
memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, case "memoryUsage":
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, return `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`;
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) 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": case "pvc":
return { switch (queryName) {
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`, case "diskUsage":
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)` 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": case "ingress":
const bytesSent = (ingress: string, namespace: string, statuses: string) => switch (queryName) {
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`; case "bytesSentSuccess":
return this.bytesSent(opts.ingress, opts.namespace, "^2\\\\d*");
return { case "bytesSentFailure":
bytesSentSuccess: bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"), return this.bytesSent(opts.ingress, opts.namespace, "^5\\\\d*");
bytesSentFailure: bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"), case "requestDurationSeconds":
requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`, return `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)` 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)}`);
} }
} }

View File

@ -19,98 +19,110 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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, V1Service } from "@kubernetes/client-node"; import type { CoreV1Api } from "@kubernetes/client-node";
import logger from "../logger"; import { inspect } from "util";
export class PrometheusOperator implements PrometheusProvider { export class PrometheusOperator extends PrometheusProvider {
rateAccuracy = "1m"; readonly rateAccuracy: string = "1m";
id = "operator"; readonly id: string = "operator";
name = "Prometheus Operator"; readonly name: string = "Prometheus Operator";
readonly isConfigurable: boolean = false;
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | void> { public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
try { return this.getFirstNamespacedServer(client, "operated-prometheus=true", "self-monitor=true");
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 getQueries(opts: PrometheusQueryOpts): PrometheusQuery | void { public getQuery(opts: Record<string, string>, queryName: string): string {
switch(opts.category) { switch(opts.category) {
case "cluster": case "cluster":
return { switch (queryName) {
memoryUsage: ` case "memoryUsage":
sum( 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}"}`);
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes) case "memoryRequests":
) return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`;
`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`), case "memoryLimits":
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`, return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"})`;
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"})`, case "memoryCapacity":
memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"})`, return `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}"})`, case "cpuUsage":
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`, return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"})`, case "cpuRequests":
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"})`, return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`;
podUsage: `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", node=~"${opts.nodes}"})`, case "cpuLimits":
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"})`, return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"})`;
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`, case "cpuCapacity":
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}"})` 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": case "nodes":
return { switch (queryName) {
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)`, case "memoryUsage":
memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`, 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)`;
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`, case "memoryCapacity":
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`, return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`, case "cpuUsage":
fsUsage: `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod,namespace) group_left(node) kube_pod_info) by (node)` 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": case "pods":
return { switch (queryName) {
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, case "cpuUsage":
cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, case "cpuRequests":
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, case "cpuLimits":
memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, case "memoryUsage":
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, return `sum(container_memory_working_set_bytes{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`;
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) 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": case "pvc":
return { switch (queryName) {
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`, case "diskUsage":
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)` 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": case "ingress":
const bytesSent = (ingress: string, namespace: string, statuses: string) => switch (queryName) {
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`; case "bytesSentSuccess":
return this.bytesSent(opts.ingress, opts.namespace, "^2\\\\d*");
return { case "bytesSentFailure":
bytesSentSuccess: bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"), return this.bytesSent(opts.ingress, opts.namespace, "^5\\\\d*");
bytesSentFailure: bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"), case "requestDurationSeconds":
requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`, return `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)` 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)}`);
} }
} }

View File

@ -20,58 +20,8 @@
*/ */
import type { CoreV1Api } from "@kubernetes/client-node"; import type { CoreV1Api } from "@kubernetes/client-node";
import { Singleton } from "../../common/utils";
export type PrometheusClusterQuery = { import logger from "../logger";
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;
export type PrometheusService = { export type PrometheusService = {
id: string; id: string;
@ -80,33 +30,61 @@ export type PrometheusService = {
port: number; port: number;
}; };
export interface PrometheusProvider { export abstract class PrometheusProvider {
id: string; abstract readonly id: string;
name: string; abstract readonly name: string;
getQueries(opts: PrometheusQueryOpts): PrometheusQuery | void; abstract readonly rateAccuracy: string;
getPrometheusService(client: CoreV1Api): Promise<PrometheusService | void>; abstract readonly isConfigurable: boolean;
}
export type PrometheusProviderList = { abstract getQuery(opts: Record<string, string>, queryName: string): string;
[key: string]: PrometheusProvider; abstract getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined>;
};
export class PrometheusProviderRegistry { protected bytesSent(ingress: string, namespace: string, statuses: string): string {
private static prometheusProviders: PrometheusProviderList = {}; 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 { protected async getFirstNamespacedServer(client: CoreV1Api, ...selectors: string[]): Promise<PrometheusService | undefined> {
if (!this.prometheusProviders[type]) { try {
throw "Unknown Prometheus provider"; 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]; return undefined;
} }
}
static registerProvider(key: string, provider: PrometheusProvider) {
this.prometheusProviders[key] = provider; export class PrometheusProviderRegistry extends Singleton {
} public providers = new Map<string, PrometheusProvider>();
static getProviders(): PrometheusProvider[] { getByKind(kind: string): PrometheusProvider {
return Object.values(this.prometheusProviders); 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;
} }
} }

View File

@ -19,16 +19,18 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 type { CoreV1Api } from "@kubernetes/client-node";
import logger from "../logger"; import logger from "../logger";
import { inspect } from "util";
export class PrometheusStacklight implements PrometheusProvider { export class PrometheusStacklight extends PrometheusProvider {
id = "stacklight"; readonly id: string = "stacklight";
name = "Stacklight"; readonly name: string = "Stacklight";
rateAccuracy = "1m"; readonly rateAccuracy: string = "1m";
readonly isConfigurable: boolean = false;
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | void> { public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
try { try {
const resp = await client.readNamespacedService("prometheus-server", "stacklight"); const resp = await client.readNamespacedService("prometheus-server", "stacklight");
const service = resp.body; const service = resp.body;
@ -41,66 +43,101 @@ export class PrometheusStacklight implements PrometheusProvider {
}; };
} catch(error) { } catch(error) {
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`); logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`);
return undefined;
} }
} }
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | void { public getQuery(opts: Record<string, string>, queryName: string): string {
switch(opts.category) { switch(opts.category) {
case "cluster": case "cluster":
return { switch (queryName) {
memoryUsage: ` case "memoryUsage":
sum( 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}"}`);
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes) case "memoryRequests":
) by (kubernetes_name) return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`;
`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`), case "memoryLimits":
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`, return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`;
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`, case "memoryCapacity":
memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`, return `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}]))`, case "cpuUsage":
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`, return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`;
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`, case "cpuRequests":
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`, return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
podUsage: `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", instance=~"${opts.nodes}"})`, case "cpuLimits":
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`, return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
fsSize: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`, case "cpuCapacity":
fsUsage: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)` 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": case "nodes":
return { switch (queryName) {
memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`, case "memoryUsage":
memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`, return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`;
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`, case "memoryCapacity":
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`, return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`, case "cpuUsage":
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)` 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": case "pods":
return { switch (queryName) {
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, case "cpuUsage":
cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, case "cpuRequests":
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, case "cpuLimits":
memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, case "memoryUsage":
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, return `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`;
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) 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": case "pvc":
return { switch (queryName) {
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`, case "diskUsage":
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)` 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": case "ingress":
const bytesSent = (ingress: string, namespace: string, statuses: string) => switch (queryName) {
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`; case "bytesSentSuccess":
return this.bytesSent(opts.ingress, opts.namespace, "^2\\\\d*");
return { case "bytesSentFailure":
bytesSentSuccess: bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"), return this.bytesSent(opts.ingress, opts.namespace, "^5\\\\d*");
bytesSentFailure: bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"), case "requestDurationSeconds":
requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`, return `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)` 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)}`);
} }
} }

View File

@ -177,6 +177,7 @@ export class Router {
// Metrics API // Metrics API
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, MetricsRoute.routeMetrics); this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, MetricsRoute.routeMetrics);
this.router.add({ method: "get", path: `${apiPrefix}/metrics/providers` }, MetricsRoute.routeMetricsProviders);
// Port-forward API // Port-forward API
this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, PortForwardRoute.routePortForward); this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, PortForwardRoute.routePortForward);

View File

@ -26,6 +26,7 @@ import { Cluster, ClusterMetadataKey } from "../cluster";
import type { ClusterPrometheusMetadata } from "../../common/cluster-store"; import type { ClusterPrometheusMetadata } from "../../common/cluster-store";
import logger from "../logger"; import logger from "../logger";
import { getMetrics } from "../k8s-request"; import { getMetrics } from "../k8s-request";
import { PrometheusProviderRegistry } from "../prometheus";
export type IMetricsQuery = string | string[] | { export type IMetricsQuery = string | string[] | {
[metricName: string]: string; [metricName: string]: string;
@ -62,6 +63,12 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa
return Promise.all(queries.map(loadMetric)); return Promise.all(queries.map(loadMetric));
} }
interface MetricProviderInfo {
name: string;
id: string;
isConfigurable: boolean;
}
export class MetricsRoute { export class MetricsRoute {
static async routeMetrics({ response, cluster, payload, query }: LensApiRequest) { static async routeMetrics({ response, cluster, payload, query }: LensApiRequest) {
const queryParams: IMetricsQuery = Object.fromEntries(query.entries()); const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
@ -92,9 +99,10 @@ export class MetricsRoute {
respondJson(response, data); respondJson(response, data);
} else { } else {
const queries = Object.entries(payload).map(([queryName, queryOpts]) => ( const queries = Object.entries<Record<string, string>>(payload)
(prometheusProvider.getQueries(queryOpts) as Record<string, string>)[queryName] .map(([queryName, queryOpts]) => (
)); prometheusProvider.getQuery(queryOpts, queryName)
));
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams); const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]])); 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; 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);
}
} }

View File

@ -46,6 +46,12 @@ export interface IMetricsResult {
values: [number, string][]; values: [number, string][];
} }
export interface MetricProviderInfo {
name: string;
id: string;
isConfigurable: boolean;
}
export interface IMetricsReqParams { export interface IMetricsReqParams {
start?: number | string; // timestamp in seconds or valid date-string start?: number | string; // timestamp in seconds or valid date-string
end?: number | string; end?: number | string;
@ -75,6 +81,10 @@ export const metricsApi = {
} }
}); });
}, },
async getMetricProviders(): Promise<MetricProviderInfo[]> {
return apiBase.get("/metrics/providers");
}
}; };
export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics { export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {

View File

@ -21,18 +21,14 @@
import React from "react"; import React from "react";
import { observer, disposeOnUnmount } from "mobx-react"; import { observer, disposeOnUnmount } from "mobx-react";
import { prometheusProviders } from "../../../../common/prometheus-providers";
import type { Cluster } from "../../../../main/cluster"; import type { Cluster } from "../../../../main/cluster";
import { SubTitle } from "../../layout/sub-title"; import { SubTitle } from "../../layout/sub-title";
import { Select, SelectOption } from "../../select"; import { Select, SelectOption } from "../../select";
import { Input } from "../../input"; import { Input } from "../../input";
import { observable, computed, autorun, makeObservable } from "mobx"; import { observable, computed, autorun, makeObservable } from "mobx";
import { productName } from "../../../../common/vars"; import { productName } from "../../../../common/vars";
import { MetricProviderInfo, metricsApi } from "../../../api/endpoints/metrics.api";
const options: SelectOption<string>[] = [ import { Spinner } from "../../spinner";
{ value: "", label: "Auto detect" },
...prometheusProviders.map(pp => ({value: pp.id, label: pp.name}))
];
interface Props { interface Props {
cluster: Cluster; cluster: Cluster;
@ -42,16 +38,27 @@ interface Props {
export class ClusterPrometheusSetting extends React.Component<Props> { export class ClusterPrometheusSetting extends React.Component<Props> {
@observable path = ""; @observable path = "";
@observable provider = ""; @observable provider = "";
@observable loading = true;
@observable loadedOptions: MetricProviderInfo[] = [];
@computed get options(): SelectOption<string>[] {
return [
{ value: "", label: "Auto detect" },
...this.loadedOptions.map(({ name, id }) => ({ value: id, label: name }))
];
}
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
makeObservable(this); makeObservable(this);
} }
@computed get canEditPrometheusPath() { @computed get canEditPrometheusPath(): boolean {
if (this.provider === "" || this.provider === "lens") return false; return Boolean(
this.loadedOptions
return true; .find(opt => opt.id === this.provider)
?.isConfigurable
);
} }
componentDidMount() { componentDidMount() {
@ -74,6 +81,16 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
} }
}) })
); );
metricsApi
.getMetricProviders()
.then(values => {
this.loading = false;
if (values) {
this.loadedOptions = values;
}
});
} }
parsePrometheusPath = () => { parsePrometheusPath = () => {
@ -110,15 +127,21 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
<> <>
<section> <section>
<SubTitle title="Prometheus"/> <SubTitle title="Prometheus"/>
<Select {
value={this.provider} this.loading
onChange={({value}) => { ? <Spinner />
this.provider = value; : <>
this.onSaveProvider(); <Select
}} value={this.provider}
options={options} onChange={({ value }) => {
/> this.provider = value;
<small className="hint">What query format is used to fetch metrics from Prometheus</small> this.onSaveProvider();
}}
options={this.options}
/>
<small className="hint">What query format is used to fetch metrics from Prometheus</small>
</>
}
</section> </section>
{this.canEditPrometheusPath && ( {this.canEditPrometheusPath && (
<section> <section>