diff --git a/src/common/k8s-api/endpoints/metrics.api.ts b/src/common/k8s-api/endpoints/metrics.api.ts index 29a7ac4ee7..88efc166d1 100644 --- a/src/common/k8s-api/endpoints/metrics.api.ts +++ b/src/common/k8s-api/endpoints/metrics.api.ts @@ -7,7 +7,7 @@ import moment from "moment"; import { apiBase } from "../index"; -import type { IMetricsQuery } from "../../../main/routes/metrics-route"; +import type { IMetricsQuery } from "../../../main/routes/metrics/metrics-query"; export interface IMetrics { status: string; diff --git a/src/main/router.ts b/src/main/router.ts index 0832ace304..983762a874 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -11,7 +11,7 @@ import path from "path"; import { readFile } from "fs-extra"; import type { Cluster } from "../common/cluster/cluster"; import { apiPrefix, appName, publicPath } from "../common/vars"; -import { KubeconfigRoute, MetricsRoute, VersionRoute } from "./routes"; +import { KubeconfigRoute, VersionRoute } from "./routes"; import logger from "./logger"; export interface RouterRequestOpts { @@ -171,10 +171,6 @@ export class Router { this.router.add({ method: "get", path: "/version" }, VersionRoute.getVersion); this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, KubeconfigRoute.routeServiceAccountRoute); - - // Metrics API - this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, MetricsRoute.routeMetrics); - this.router.add({ method: "get", path: `${apiPrefix}/metrics/providers` }, MetricsRoute.routeMetricsProviders); } } diff --git a/src/main/routes/index.ts b/src/main/routes/index.ts index c08e2719ac..402b358ba1 100644 --- a/src/main/routes/index.ts +++ b/src/main/routes/index.ts @@ -4,5 +4,4 @@ */ export * from "./kubeconfig-route"; -export * from "./metrics-route"; export * from "./version-route"; diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts deleted file mode 100644 index 083801680f..0000000000 --- a/src/main/routes/metrics-route.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { LensApiRequest } from "../router"; -import { respondJson } from "../utils/http-responses"; -import type { Cluster } from "../../common/cluster/cluster"; -import { ClusterMetadataKey, ClusterPrometheusMetadata } from "../../common/cluster-types"; -import logger from "../logger"; -import { getMetrics } from "../k8s-request"; -import { PrometheusProviderRegistry } from "../prometheus"; - -export type IMetricsQuery = string | string[] | { - [metricName: string]: string; -}; - -// This is used for backoff retry tracking. -const ATTEMPTS = [false, false, false, false, true]; - -// prometheus metrics loader -async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPath: string, queryParams: Record): Promise { - const queries = promQueries.map(p => p.trim()); - const loaders = new Map>(); - - async function loadMetric(query: string): Promise { - async function loadMetricHelper(): Promise { - for (const [attempt, lastAttempt] of ATTEMPTS.entries()) { // retry - try { - return await getMetrics(cluster, prometheusPath, { query, ...queryParams }); - } catch (error) { - if (lastAttempt || (error?.statusCode >= 400 && error?.statusCode < 500)) { - logger.error("[Metrics]: metrics not available", error?.response ? error.response?.body : error); - throw new Error("Metrics not available"); - } - - await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000)); // add delay before repeating request - } - } - } - - return loaders.get(query) ?? loaders.set(query, loadMetricHelper()).get(query); - } - - 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()); - const prometheusMetadata: ClusterPrometheusMetadata = {}; - - try { - const { prometheusPath, provider } = await cluster.contextHandler.getPrometheusDetails(); - - prometheusMetadata.provider = provider?.id; - prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type; - - if (!prometheusPath) { - prometheusMetadata.success = false; - - return respondJson(response, {}); - } - - // return data in same structure as query - if (typeof payload === "string") { - const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams); - - respondJson(response, data); - } else if (Array.isArray(payload)) { - const data = await loadMetrics(payload, cluster, prometheusPath, queryParams); - - respondJson(response, data); - } else { - const queries = Object.entries>(payload) - .map(([queryName, queryOpts]) => ( - provider.getQuery(queryOpts, queryName) - )); - const result = await loadMetrics(queries, cluster, prometheusPath, queryParams); - const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]])); - - respondJson(response, data); - } - prometheusMetadata.success = true; - } catch (error) { - prometheusMetadata.success = false; - respondJson(response, {}); - logger.warn(`[METRICS-ROUTE]: failed to get metrics for clusterId=${cluster.id}:`, error); - } finally { - 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/main/routes/metrics/add-metrics-route.injectable.ts b/src/main/routes/metrics/add-metrics-route.injectable.ts new file mode 100644 index 0000000000..b65f5b793b --- /dev/null +++ b/src/main/routes/metrics/add-metrics-route.injectable.ts @@ -0,0 +1,103 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectable } from "@ogre-tools/injectable"; +import { apiPrefix } from "../../../common/vars"; +import type { LensApiRequest } from "../../router"; +import { respondJson } from "../../utils/http-responses"; +import { routeInjectionToken } from "../../router/router.injectable"; +import { ClusterMetadataKey, ClusterPrometheusMetadata } from "../../../common/cluster-types"; +import logger from "../../logger"; +import type { Cluster } from "../../../common/cluster/cluster"; +import { getMetrics } from "../../k8s-request"; +import type { IMetricsQuery } from "./metrics-query"; + +// This is used for backoff retry tracking. +const ATTEMPTS = [false, false, false, false, true]; + +async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPath: string, queryParams: Record): Promise { + const queries = promQueries.map(p => p.trim()); + const loaders = new Map>(); + + async function loadMetric(query: string): Promise { + async function loadMetricHelper(): Promise { + for (const [attempt, lastAttempt] of ATTEMPTS.entries()) { // retry + try { + return await getMetrics(cluster, prometheusPath, { query, ...queryParams }); + } catch (error) { + if (lastAttempt || (error?.statusCode >= 400 && error?.statusCode < 500)) { + logger.error("[Metrics]: metrics not available", error?.response ? error.response?.body : error); + throw new Error("Metrics not available"); + } + + await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000)); // add delay before repeating request + } + } + } + + return loaders.get(query) ?? loaders.set(query, loadMetricHelper()).get(query); + } + + return Promise.all(queries.map(loadMetric)); +} + +const addMetricsRoute = async ({ response, cluster, payload, query }: LensApiRequest) => { + const queryParams: IMetricsQuery = Object.fromEntries(query.entries()); + const prometheusMetadata: ClusterPrometheusMetadata = {}; + + try { + const { prometheusPath, provider } = await cluster.contextHandler.getPrometheusDetails(); + + prometheusMetadata.provider = provider?.id; + prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type; + + if (!prometheusPath) { + prometheusMetadata.success = false; + + return respondJson(response, {}); + } + + // return data in same structure as query + if (typeof payload === "string") { + const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams); + + respondJson(response, data); + } else if (Array.isArray(payload)) { + const data = await loadMetrics(payload, cluster, prometheusPath, queryParams); + + respondJson(response, data); + } else { + const queries = Object.entries>(payload) + .map(([queryName, queryOpts]) => ( + provider.getQuery(queryOpts, queryName) + )); + const result = await loadMetrics(queries, cluster, prometheusPath, queryParams); + const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]])); + + respondJson(response, data); + } + prometheusMetadata.success = true; + } catch (error) { + prometheusMetadata.success = false; + respondJson(response, {}); + logger.warn(`[METRICS-ROUTE]: failed to get metrics for clusterId=${cluster.id}:`, error); + } finally { + cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata; + } +}; + +const addMetricsRouteInjectable = getInjectable({ + id: "add-metrics-route", + + instantiate: () => ({ + method: "post", + path: `${apiPrefix}/metrics`, + handler: addMetricsRoute, + }), + + injectionToken: routeInjectionToken, +}); + +export default addMetricsRouteInjectable; diff --git a/src/main/routes/metrics/get-metric-providers-route.injectable.ts b/src/main/routes/metrics/get-metric-providers-route.injectable.ts new file mode 100644 index 0000000000..73a56913e3 --- /dev/null +++ b/src/main/routes/metrics/get-metric-providers-route.injectable.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectable } from "@ogre-tools/injectable"; +import { apiPrefix } from "../../../common/vars"; +import type { LensApiRequest } from "../../router"; +import { respondJson } from "../../utils/http-responses"; +import { routeInjectionToken } from "../../router/router.injectable"; +import { PrometheusProviderRegistry } from "../../prometheus"; +import type { MetricProviderInfo } from "../../../common/k8s-api/endpoints/metrics.api"; + +const getMetricProvidersRouteInjectable = getInjectable({ + id: "get-metric-providers-route", + + instantiate: () => ({ + method: "get", + path: `${apiPrefix}/metrics/providers`, + + handler: async (request: LensApiRequest) => { + const providers: MetricProviderInfo[] = []; + + for (const { name, id, isConfigurable } of PrometheusProviderRegistry.getInstance().providers.values()) { + providers.push({ name, id, isConfigurable }); + } + + respondJson(request.response, providers); + }, + }), + + injectionToken: routeInjectionToken, +}); + +export default getMetricProvidersRouteInjectable; diff --git a/src/main/routes/metrics/metrics-query.ts b/src/main/routes/metrics/metrics-query.ts new file mode 100644 index 0000000000..d6d9532b3b --- /dev/null +++ b/src/main/routes/metrics/metrics-query.ts @@ -0,0 +1,7 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +export type IMetricsQuery = string | string[] | { + [metricName: string]: string; +};