diff --git a/src/main/cluster.ts b/src/main/cluster.ts index b88fe6c7ef..600b377729 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -253,16 +253,12 @@ export class Cluster implements ClusterModel, ClusterState { } protected async k8sRequest(path: string, options: RequestPromiseOptions = {}): Promise { - const apiUrl = this.kubeProxyUrl + path; - return request(apiUrl, { - json: true, - timeout: 30000, - ...options, - headers: { - Host: `${this.id}.${new URL(this.kubeProxyUrl).host}`, // required in ClusterManager.getClusterForRequest() - ...(options.headers || {}), - }, - }) + options.headers ??= {} + options.json ??= true + options.timeout ??= 30000 + options.headers.Host = `${this.id}.${new URL(this.kubeProxyUrl).host}` // required in ClusterManager.getClusterForRequest() + + return request(this.kubeProxyUrl + path, options) } getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) { diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts index 2665fca5f2..dc77f7fb9f 100644 --- a/src/main/routes/metrics-route.ts +++ b/src/main/routes/metrics-route.ts @@ -1,72 +1,73 @@ import { LensApiRequest } from "../router" import { LensApi } from "../lens-api" -import { PrometheusClusterQuery, PrometheusIngressQuery, PrometheusNodeQuery, PrometheusPodQuery, PrometheusProvider, PrometheusPvcQuery, PrometheusQueryOpts } from "../prometheus/provider-registry" +import { Cluster } from "../cluster" +import _ from "lodash" export type IMetricsQuery = string | string[] | { [metricName: string]: string; } +// This is used for backoff retry tracking. +const MAX_ATTEMPTS = 5 +const ATTEMPTS = [...(_.fill(Array(MAX_ATTEMPTS - 1), 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 cluster.getMetrics(prometheusPath, { query, ...queryParams }) + } catch (error) { + if (lastAttempt || error?.statusCode === 404) { + return { + status: error.toString(), + data: { result: [] }, + } + } + + 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)) +} + class MetricsRoute extends LensApi { - async routeMetrics(request: LensApiRequest) { - const { response, cluster, payload } = request - const queryParams: IMetricsQuery = {} - request.query.forEach((value: string, key: string) => { - queryParams[key] = value - }) - let prometheusPath: string - let prometheusProvider: PrometheusProvider + async routeMetrics({ response, cluster, payload, query }: LensApiRequest) { + const queryParams: IMetricsQuery = Object.fromEntries(query.entries()) + try { - [prometheusPath, prometheusProvider] = await Promise.all([ + const [prometheusPath, prometheusProvider] = await Promise.all([ cluster.contextHandler.getPrometheusPath(), cluster.contextHandler.getPrometheusProvider() ]) + + // return data in same structure as query + if (typeof payload === "string") { + const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams) + this.respondJson(response, data) + } else if (Array.isArray(payload)) { + const data = await loadMetrics(payload, cluster, prometheusPath, queryParams) + this.respondJson(response, data) + } else { + const queries = Object.entries(payload).map(([queryName, queryOpts]) => ( + (prometheusProvider.getQueries(queryOpts) as Record)[queryName] + )) + const result = await loadMetrics(queries, cluster, prometheusPath, queryParams) + const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]])) + this.respondJson(response, data) + } } catch { this.respondJson(response, {}) - return } - // prometheus metrics loader - const attempts: { [query: string]: number } = {}; - const maxAttempts = 5; - const loadMetrics = (promQuery: string): Promise => { - const query = promQuery.trim() - const attempt = attempts[query] = (attempts[query] || 0) + 1; - return cluster.getMetrics(prometheusPath, { query, ...queryParams }).catch(async error => { - if (attempt < maxAttempts && (error.statusCode && error.statusCode != 404)) { - await new Promise(resolve => setTimeout(resolve, attempt * 1000)); // add delay before repeating request - return loadMetrics(query); - } - return { - status: error.toString(), - data: { - result: [] - } - } - }) - }; - - // return data in same structure as query - let data: any; - if (typeof payload === "string") { - data = await loadMetrics(payload) - } else if (Array.isArray(payload)) { - data = await Promise.all(payload.map(loadMetrics)); - } else { - data = {}; - const result = await Promise.all( - Object.entries(payload).map((queryEntry: any) => { - const queryName: string = queryEntry[0] - const queryOpts: PrometheusQueryOpts = queryEntry[1] - const queries = prometheusProvider.getQueries(queryOpts) - const q = queries[queryName as keyof (PrometheusNodeQuery | PrometheusClusterQuery | PrometheusPodQuery | PrometheusPvcQuery | PrometheusIngressQuery)] - return loadMetrics(q) - }) - ); - Object.keys(payload).forEach((metricName, index) => { - data[metricName] = result[index]; - }); - } - - this.respondJson(response, data) } }