mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix change to get metrics route signature
- The change was responding with an error instead of an empty response Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
c0e17f9482
commit
a8856861f1
@ -7,7 +7,7 @@ import type { Cluster } from "../common/cluster/cluster";
|
|||||||
import type { RequestMetricsParams } from "../common/k8s-api/endpoints/metrics.api/request-metrics.injectable";
|
import type { RequestMetricsParams } from "../common/k8s-api/endpoints/metrics.api/request-metrics.injectable";
|
||||||
import k8sRequestInjectable from "./k8s-request.injectable";
|
import k8sRequestInjectable from "./k8s-request.injectable";
|
||||||
|
|
||||||
export type GetMetrics = (cluster: Cluster, prometheusPath: string, queryParams: RequestMetricsParams & { query: string }) => Promise<any>;
|
export type GetMetrics = (cluster: Cluster, prometheusPath: string, queryParams: RequestMetricsParams & { query: string }) => Promise<unknown>;
|
||||||
|
|
||||||
const getMetricsInjectable = getInjectable({
|
const getMetricsInjectable = getInjectable({
|
||||||
id: "get-metrics",
|
id: "get-metrics",
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
import type { PrometheusProvider } from "./provider";
|
import type { PrometheusProvider } from "./provider";
|
||||||
import { createPrometheusProvider, bytesSent, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider";
|
import { createPrometheusProvider, bytesSent, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider";
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import assert from "assert";
|
||||||
|
|
||||||
export const getHelmLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
|
export const getHelmLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
|
||||||
(opts, queryName) => {
|
(opts, queryName) => {
|
||||||
@ -105,6 +106,9 @@ export const getHelmLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
|
|||||||
case "ingress":
|
case "ingress":
|
||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "bytesSentSuccess":
|
case "bytesSentSuccess":
|
||||||
|
assert(opts.ingress, "Missing option 'ingress' for query='bytesSentSuccess' for category='ingress'");
|
||||||
|
assert(opts.namespace, "Missing option 'namespace' for query='bytesSentSuccess' for category='ingress'");
|
||||||
|
|
||||||
return bytesSent({
|
return bytesSent({
|
||||||
rateAccuracy,
|
rateAccuracy,
|
||||||
ingress: opts.ingress,
|
ingress: opts.ingress,
|
||||||
@ -112,6 +116,9 @@ export const getHelmLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
|
|||||||
statuses: "^2\\\\d*",
|
statuses: "^2\\\\d*",
|
||||||
});
|
});
|
||||||
case "bytesSentFailure":
|
case "bytesSentFailure":
|
||||||
|
assert(opts.ingress, "Missing option 'ingress' for query='bytesSentFailure' for category='ingress'");
|
||||||
|
assert(opts.namespace, "Missing option 'namespace' for query='bytesSentFailure' for category='ingress'");
|
||||||
|
|
||||||
return bytesSent({
|
return bytesSent({
|
||||||
rateAccuracy,
|
rateAccuracy,
|
||||||
ingress: opts.ingress,
|
ingress: opts.ingress,
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
import { bytesSent, prometheusProviderInjectionToken, findNamespacedService, createPrometheusProvider } from "./provider";
|
import { bytesSent, prometheusProviderInjectionToken, findNamespacedService, createPrometheusProvider } from "./provider";
|
||||||
import type { PrometheusProvider } from "./provider";
|
import type { PrometheusProvider } from "./provider";
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import assert from "assert";
|
||||||
|
|
||||||
export const getLensLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
|
export const getLensLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
|
||||||
(opts, queryName) => {
|
(opts, queryName) => {
|
||||||
@ -105,6 +106,9 @@ export const getLensLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
|
|||||||
case "ingress":
|
case "ingress":
|
||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "bytesSentSuccess":
|
case "bytesSentSuccess":
|
||||||
|
assert(opts.ingress, "Missing option 'ingress' for query='bytesSentSuccess' for category='ingress'");
|
||||||
|
assert(opts.namespace, "Missing option 'namespace' for query='bytesSentSuccess' for category='ingress'");
|
||||||
|
|
||||||
return bytesSent({
|
return bytesSent({
|
||||||
rateAccuracy,
|
rateAccuracy,
|
||||||
ingress: opts.ingress,
|
ingress: opts.ingress,
|
||||||
@ -112,6 +116,9 @@ export const getLensLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
|
|||||||
statuses: "^2\\\\d*",
|
statuses: "^2\\\\d*",
|
||||||
});
|
});
|
||||||
case "bytesSentFailure":
|
case "bytesSentFailure":
|
||||||
|
assert(opts.ingress, "Missing option 'ingress' for query='bytesSentFailure' for category='ingress'");
|
||||||
|
assert(opts.namespace, "Missing option 'namespace' for query='bytesSentFailure' for category='ingress'");
|
||||||
|
|
||||||
return bytesSent({
|
return bytesSent({
|
||||||
rateAccuracy,
|
rateAccuracy,
|
||||||
ingress: opts.ingress,
|
ingress: opts.ingress,
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
import type { PrometheusProvider } from "./provider";
|
import type { PrometheusProvider } from "./provider";
|
||||||
import { bytesSent, createPrometheusProvider, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider";
|
import { bytesSent, createPrometheusProvider, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider";
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import assert from "assert";
|
||||||
|
|
||||||
export const getOperatorLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
|
export const getOperatorLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
|
||||||
(opts, queryName) => {
|
(opts, queryName) => {
|
||||||
@ -105,6 +106,9 @@ export const getOperatorLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string
|
|||||||
case "ingress":
|
case "ingress":
|
||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "bytesSentSuccess":
|
case "bytesSentSuccess":
|
||||||
|
assert(opts.ingress, "Missing option 'ingress' for query='bytesSentSuccess' for category='ingress'");
|
||||||
|
assert(opts.namespace, "Missing option 'namespace' for query='bytesSentSuccess' for category='ingress'");
|
||||||
|
|
||||||
return bytesSent({
|
return bytesSent({
|
||||||
rateAccuracy,
|
rateAccuracy,
|
||||||
ingress: opts.ingress,
|
ingress: opts.ingress,
|
||||||
@ -112,6 +116,9 @@ export const getOperatorLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string
|
|||||||
statuses: "^2\\\\d*",
|
statuses: "^2\\\\d*",
|
||||||
});
|
});
|
||||||
case "bytesSentFailure":
|
case "bytesSentFailure":
|
||||||
|
assert(opts.ingress, "Missing option 'ingress' for query='bytesSentFailure' for category='ingress'");
|
||||||
|
assert(opts.namespace, "Missing option 'namespace' for query='bytesSentFailure' for category='ingress'");
|
||||||
|
|
||||||
return bytesSent({
|
return bytesSent({
|
||||||
rateAccuracy,
|
rateAccuracy,
|
||||||
ingress: opts.ingress,
|
ingress: opts.ingress,
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export interface PrometheusProvider {
|
|||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly isConfigurable: boolean;
|
readonly isConfigurable: boolean;
|
||||||
|
|
||||||
getQuery(opts: Record<string, string>, queryName: string): string;
|
getQuery(opts: Partial<Record<string, string>>, queryName: string): string;
|
||||||
getPrometheusService(client: CoreV1Api): Promise<PrometheusService>;
|
getPrometheusService(client: CoreV1Api): Promise<PrometheusService>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ export interface CreatePrometheusProviderOpts {
|
|||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly isConfigurable: boolean;
|
readonly isConfigurable: boolean;
|
||||||
|
|
||||||
getQuery(opts: Record<string, string>, queryName: string): string;
|
getQuery(opts: Partial<Record<string, string>>, queryName: string): string;
|
||||||
getService(client: CoreV1Api): Promise<PrometheusServiceInfo>;
|
getService(client: CoreV1Api): Promise<PrometheusServiceInfo>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
import type { PrometheusProvider } from "./provider";
|
import type { PrometheusProvider } from "./provider";
|
||||||
import { bytesSent, createPrometheusProvider, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider";
|
import { bytesSent, createPrometheusProvider, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider";
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import assert from "assert";
|
||||||
|
|
||||||
export const getStacklightLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
|
export const getStacklightLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
|
||||||
(opts, queryName) => {
|
(opts, queryName) => {
|
||||||
@ -105,6 +106,9 @@ export const getStacklightLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: stri
|
|||||||
case "ingress":
|
case "ingress":
|
||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "bytesSentSuccess":
|
case "bytesSentSuccess":
|
||||||
|
assert(opts.ingress, "Missing option 'ingress' for query='bytesSentSuccess' for category='ingress'");
|
||||||
|
assert(opts.namespace, "Missing option 'namespace' for query='bytesSentSuccess' for category='ingress'");
|
||||||
|
|
||||||
return bytesSent({
|
return bytesSent({
|
||||||
rateAccuracy,
|
rateAccuracy,
|
||||||
ingress: opts.ingress,
|
ingress: opts.ingress,
|
||||||
@ -112,6 +116,9 @@ export const getStacklightLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: stri
|
|||||||
statuses: "^2\\\\d*",
|
statuses: "^2\\\\d*",
|
||||||
});
|
});
|
||||||
case "bytesSentFailure":
|
case "bytesSentFailure":
|
||||||
|
assert(opts.ingress, "Missing option 'ingress' for query='bytesSentFailure'");
|
||||||
|
assert(opts.namespace, "Missing option 'namespace' for query='bytesSentFailure'");
|
||||||
|
|
||||||
return bytesSent({
|
return bytesSent({
|
||||||
rateAccuracy,
|
rateAccuracy,
|
||||||
ingress: opts.ingress,
|
ingress: opts.ingress,
|
||||||
|
|||||||
@ -8,50 +8,88 @@ import { getRouteInjectable } from "../../router/router.injectable";
|
|||||||
import type { ClusterPrometheusMetadata } from "../../../common/cluster-types";
|
import type { ClusterPrometheusMetadata } from "../../../common/cluster-types";
|
||||||
import { ClusterMetadataKey } from "../../../common/cluster-types";
|
import { ClusterMetadataKey } from "../../../common/cluster-types";
|
||||||
import type { Cluster } from "../../../common/cluster/cluster";
|
import type { Cluster } from "../../../common/cluster/cluster";
|
||||||
import { clusterRoute } from "../../router/route";
|
import { payloadValidatedClusterRoute } from "../../router/route";
|
||||||
import { isObject } from "lodash";
|
import { getOrInsertWith, isRequestError, object } from "../../../common/utils";
|
||||||
import { isRequestError, object } from "../../../common/utils";
|
|
||||||
import type { GetMetrics } from "../../get-metrics.injectable";
|
import type { GetMetrics } from "../../get-metrics.injectable";
|
||||||
import getMetricsInjectable from "../../get-metrics.injectable";
|
import getMetricsInjectable from "../../get-metrics.injectable";
|
||||||
import loggerInjectable from "../../../common/logger.injectable";
|
import loggerInjectable from "../../../common/logger.injectable";
|
||||||
|
import type { AsyncResult } from "../../../common/utils/async-result";
|
||||||
|
import Joi from "joi";
|
||||||
|
|
||||||
// This is used for backoff retry tracking.
|
// This is used for backoff retry tracking.
|
||||||
const ATTEMPTS = [false, false, false, false, true];
|
const MAX_ATTEMPTS = 5;
|
||||||
|
|
||||||
const loadMetricsFor = (getMetrics: GetMetrics) => async (promQueries: string[], cluster: Cluster, prometheusPath: string, queryParams: Partial<Record<string, string>>): Promise<any[]> => {
|
const loadMetricsFor = (getMetrics: GetMetrics) => async (promQueries: string[], cluster: Cluster, prometheusPath: string, queryParams: Partial<Record<string, string>>): Promise<AsyncResult<unknown[], Error>> => {
|
||||||
const queries = promQueries.map(p => p.trim());
|
const queries = promQueries.map(p => p.trim());
|
||||||
const loaders = new Map<string, Promise<any>>();
|
const loaders = new Map<string, Promise<AsyncResult<unknown, Error>>>();
|
||||||
|
|
||||||
async function loadMetric(query: string): Promise<any> {
|
async function loadMetric(query: string): Promise<AsyncResult<unknown, Error>> {
|
||||||
async function loadMetricHelper(): Promise<any> {
|
async function loadMetricHelper(attempt: number): Promise<AsyncResult<unknown, Error>> {
|
||||||
for (const [attempt, lastAttempt] of ATTEMPTS.entries()) { // retry
|
const lastAttempt = attempt === MAX_ATTEMPTS;
|
||||||
try {
|
|
||||||
return await getMetrics(cluster, prometheusPath, { query, ...queryParams });
|
try {
|
||||||
} catch (error) {
|
return {
|
||||||
if (
|
callWasSuccessful: true,
|
||||||
!isRequestError(error)
|
response: await getMetrics(cluster, prometheusPath, { query, ...queryParams }),
|
||||||
|| lastAttempt
|
};
|
||||||
|| (
|
} catch (error) {
|
||||||
!lastAttempt && (
|
if (
|
||||||
typeof error.statusCode === "number" &&
|
!isRequestError(error)
|
||||||
400 <= error.statusCode && error.statusCode < 500
|
|| lastAttempt
|
||||||
)
|
|| (
|
||||||
|
!lastAttempt && (
|
||||||
|
typeof error.statusCode === "number" &&
|
||||||
|
400 <= error.statusCode && error.statusCode < 500
|
||||||
)
|
)
|
||||||
) {
|
)
|
||||||
throw new Error("Metrics not available", { cause: error });
|
) {
|
||||||
}
|
return {
|
||||||
|
callWasSuccessful: false,
|
||||||
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000)); // add delay before repeating request
|
error: new Error("Metrics not available", { cause: error }),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000)); // add delay before repeating request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return loadMetricHelper(attempt + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return loaders.get(query) ?? loaders.set(query, loadMetricHelper()).get(query);
|
return getOrInsertWith(loaders, query, () => loadMetricHelper(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(queries.map(loadMetric));
|
const responses: unknown[] = [];
|
||||||
|
|
||||||
|
for await (const result of queries.map(loadMetric)) {
|
||||||
|
if (result.callWasSuccessful) {
|
||||||
|
responses.push(result.response);
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
callWasSuccessful: true,
|
||||||
|
response: responses,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ClusterMetricsPayload = string | string[] | Partial<Record<string, Partial<Record<string, string>>>>;
|
||||||
|
|
||||||
|
const clusterMetricsPayloadValidator = Joi.alternatives<ClusterMetricsPayload>(
|
||||||
|
Joi.string(),
|
||||||
|
Joi.array().items(Joi.string()),
|
||||||
|
Joi.object()
|
||||||
|
.pattern(
|
||||||
|
Joi.string(),
|
||||||
|
Joi.object()
|
||||||
|
.pattern(
|
||||||
|
Joi.string(),
|
||||||
|
Joi.string(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const addMetricsRouteInjectable = getRouteInjectable({
|
const addMetricsRouteInjectable = getRouteInjectable({
|
||||||
id: "add-metrics-route",
|
id: "add-metrics-route",
|
||||||
|
|
||||||
@ -60,9 +98,10 @@ const addMetricsRouteInjectable = getRouteInjectable({
|
|||||||
const loadMetrics = loadMetricsFor(getMetrics);
|
const loadMetrics = loadMetricsFor(getMetrics);
|
||||||
const logger = di.inject(loggerInjectable);
|
const logger = di.inject(loggerInjectable);
|
||||||
|
|
||||||
return clusterRoute({
|
return payloadValidatedClusterRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: `${apiPrefix}/metrics`,
|
path: `${apiPrefix}/metrics`,
|
||||||
|
payloadValidator: clusterMetricsPayloadValidator,
|
||||||
})(async ({ cluster, payload, query }) => {
|
})(async ({ cluster, payload, query }) => {
|
||||||
const queryParams: Partial<Record<string, string>> = Object.fromEntries(query.entries());
|
const queryParams: Partial<Record<string, string>> = Object.fromEntries(query.entries());
|
||||||
const prometheusMetadata: ClusterPrometheusMetadata = {};
|
const prometheusMetadata: ClusterPrometheusMetadata = {};
|
||||||
@ -95,33 +134,46 @@ const addMetricsRouteInjectable = getRouteInjectable({
|
|||||||
|
|
||||||
// return data in same structure as query
|
// return data in same structure as query
|
||||||
if (typeof payload === "string") {
|
if (typeof payload === "string") {
|
||||||
const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams);
|
const result = await loadMetrics([payload], cluster, prometheusPath, queryParams);
|
||||||
|
|
||||||
return { response: data };
|
if (result.callWasSuccessful) {
|
||||||
|
return { response: result.response[0] };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(`[METRICS-ROUTE]: failed to get metrics for clusterId=${cluster.id}:`, result.error);
|
||||||
|
|
||||||
|
return { response: {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(payload)) {
|
if (Array.isArray(payload)) {
|
||||||
const data = await loadMetrics(payload, cluster, prometheusPath, queryParams);
|
const result = await loadMetrics(payload, cluster, prometheusPath, queryParams);
|
||||||
|
|
||||||
return { response: data };
|
if (result.callWasSuccessful) {
|
||||||
|
return { response: result.response };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(`[METRICS-ROUTE]: failed to get metrics for clusterId=${cluster.id}:`, result.error);
|
||||||
|
|
||||||
|
return { response: {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isObject(payload)) {
|
// Last option
|
||||||
const data = payload as Record<string, Record<string, string>>;
|
const queries = object.entries(payload)
|
||||||
const queries = object.entries(data)
|
.map(([queryName, queryOpts]) => (
|
||||||
.map(([queryName, queryOpts]) => (
|
provider.getQuery(queryOpts, queryName)
|
||||||
provider.getQuery(queryOpts, queryName)
|
));
|
||||||
));
|
|
||||||
|
|
||||||
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
|
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
|
||||||
const response = object.fromEntries(object.keys(data).map((metricName, i) => [metricName, result[i]]));
|
|
||||||
|
|
||||||
prometheusMetadata.success = true;
|
if (!result.callWasSuccessful) {
|
||||||
|
logger.warn(`[METRICS-ROUTE]: failed to get metrics for clusterId=${cluster.id}:`, result.error);
|
||||||
|
|
||||||
return { response };
|
return { response: {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { response: {}};
|
const response = object.fromEntries(object.keys(payload).map((metricName, i) => [metricName, result.response[i]]));
|
||||||
|
|
||||||
|
return { response };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user