mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Switch to using injectable to setup route for metrics
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
4bac753f76
commit
ef92a98755
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { apiBase } from "../index";
|
import { apiBase } from "../index";
|
||||||
import type { IMetricsQuery } from "../../../main/routes/metrics-route";
|
import type { IMetricsQuery } from "../../../main/routes/metrics/metrics-query";
|
||||||
|
|
||||||
export interface IMetrics {
|
export interface IMetrics {
|
||||||
status: string;
|
status: string;
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import path from "path";
|
|||||||
import { readFile } from "fs-extra";
|
import { readFile } from "fs-extra";
|
||||||
import type { Cluster } from "../common/cluster/cluster";
|
import type { Cluster } from "../common/cluster/cluster";
|
||||||
import { apiPrefix, appName, publicPath } from "../common/vars";
|
import { apiPrefix, appName, publicPath } from "../common/vars";
|
||||||
import { KubeconfigRoute, MetricsRoute, VersionRoute } from "./routes";
|
import { KubeconfigRoute, VersionRoute } from "./routes";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
|
||||||
export interface RouterRequestOpts {
|
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: "/version" }, VersionRoute.getVersion);
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, KubeconfigRoute.routeServiceAccountRoute);
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,5 +4,4 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./kubeconfig-route";
|
export * from "./kubeconfig-route";
|
||||||
export * from "./metrics-route";
|
|
||||||
export * from "./version-route";
|
export * from "./version-route";
|
||||||
|
|||||||
@ -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<string, string>): Promise<any[]> {
|
|
||||||
const queries = promQueries.map(p => p.trim());
|
|
||||||
const loaders = new Map<string, Promise<any>>();
|
|
||||||
|
|
||||||
async function loadMetric(query: string): Promise<any> {
|
|
||||||
async function loadMetricHelper(): Promise<any> {
|
|
||||||
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<Record<string, string>>(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
103
src/main/routes/metrics/add-metrics-route.injectable.ts
Normal file
103
src/main/routes/metrics/add-metrics-route.injectable.ts
Normal file
@ -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<string, string>): Promise<any[]> {
|
||||||
|
const queries = promQueries.map(p => p.trim());
|
||||||
|
const loaders = new Map<string, Promise<any>>();
|
||||||
|
|
||||||
|
async function loadMetric(query: string): Promise<any> {
|
||||||
|
async function loadMetricHelper(): Promise<any> {
|
||||||
|
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<Record<string, string>>(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;
|
||||||
@ -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;
|
||||||
7
src/main/routes/metrics/metrics-query.ts
Normal file
7
src/main/routes/metrics/metrics-query.ts
Normal file
@ -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;
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user