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 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({
|
||||
id: "get-metrics",
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
import type { PrometheusProvider } from "./provider";
|
||||
import { createPrometheusProvider, bytesSent, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
|
||||
export const getHelmLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
|
||||
(opts, queryName) => {
|
||||
@ -105,6 +106,9 @@ export const getHelmLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
|
||||
case "ingress":
|
||||
switch (queryName) {
|
||||
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({
|
||||
rateAccuracy,
|
||||
ingress: opts.ingress,
|
||||
@ -112,6 +116,9 @@ export const getHelmLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
|
||||
statuses: "^2\\\\d*",
|
||||
});
|
||||
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({
|
||||
rateAccuracy,
|
||||
ingress: opts.ingress,
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
import { bytesSent, prometheusProviderInjectionToken, findNamespacedService, createPrometheusProvider } from "./provider";
|
||||
import type { PrometheusProvider } from "./provider";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
|
||||
export const getLensLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
|
||||
(opts, queryName) => {
|
||||
@ -105,6 +106,9 @@ export const getLensLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
|
||||
case "ingress":
|
||||
switch (queryName) {
|
||||
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({
|
||||
rateAccuracy,
|
||||
ingress: opts.ingress,
|
||||
@ -112,6 +116,9 @@ export const getLensLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
|
||||
statuses: "^2\\\\d*",
|
||||
});
|
||||
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({
|
||||
rateAccuracy,
|
||||
ingress: opts.ingress,
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
import type { PrometheusProvider } from "./provider";
|
||||
import { bytesSent, createPrometheusProvider, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
|
||||
export const getOperatorLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
|
||||
(opts, queryName) => {
|
||||
@ -105,6 +106,9 @@ export const getOperatorLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string
|
||||
case "ingress":
|
||||
switch (queryName) {
|
||||
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({
|
||||
rateAccuracy,
|
||||
ingress: opts.ingress,
|
||||
@ -112,6 +116,9 @@ export const getOperatorLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string
|
||||
statuses: "^2\\\\d*",
|
||||
});
|
||||
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({
|
||||
rateAccuracy,
|
||||
ingress: opts.ingress,
|
||||
|
||||
@ -22,7 +22,7 @@ export interface PrometheusProvider {
|
||||
readonly name: string;
|
||||
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>;
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ export interface CreatePrometheusProviderOpts {
|
||||
readonly name: string;
|
||||
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>;
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
import type { PrometheusProvider } from "./provider";
|
||||
import { bytesSent, createPrometheusProvider, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
|
||||
export const getStacklightLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
|
||||
(opts, queryName) => {
|
||||
@ -105,6 +106,9 @@ export const getStacklightLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: stri
|
||||
case "ingress":
|
||||
switch (queryName) {
|
||||
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({
|
||||
rateAccuracy,
|
||||
ingress: opts.ingress,
|
||||
@ -112,6 +116,9 @@ export const getStacklightLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: stri
|
||||
statuses: "^2\\\\d*",
|
||||
});
|
||||
case "bytesSentFailure":
|
||||
assert(opts.ingress, "Missing option 'ingress' for query='bytesSentFailure'");
|
||||
assert(opts.namespace, "Missing option 'namespace' for query='bytesSentFailure'");
|
||||
|
||||
return bytesSent({
|
||||
rateAccuracy,
|
||||
ingress: opts.ingress,
|
||||
|
||||
@ -8,50 +8,88 @@ import { getRouteInjectable } from "../../router/router.injectable";
|
||||
import type { ClusterPrometheusMetadata } from "../../../common/cluster-types";
|
||||
import { ClusterMetadataKey } from "../../../common/cluster-types";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { clusterRoute } from "../../router/route";
|
||||
import { isObject } from "lodash";
|
||||
import { isRequestError, object } from "../../../common/utils";
|
||||
import { payloadValidatedClusterRoute } from "../../router/route";
|
||||
import { getOrInsertWith, isRequestError, object } from "../../../common/utils";
|
||||
import type { GetMetrics } from "../../get-metrics.injectable";
|
||||
import getMetricsInjectable from "../../get-metrics.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.
|
||||
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 loaders = new Map<string, Promise<any>>();
|
||||
const loaders = new Map<string, Promise<AsyncResult<unknown, Error>>>();
|
||||
|
||||
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 (
|
||||
!isRequestError(error)
|
||||
|| lastAttempt
|
||||
|| (
|
||||
!lastAttempt && (
|
||||
typeof error.statusCode === "number" &&
|
||||
400 <= error.statusCode && error.statusCode < 500
|
||||
)
|
||||
async function loadMetric(query: string): Promise<AsyncResult<unknown, Error>> {
|
||||
async function loadMetricHelper(attempt: number): Promise<AsyncResult<unknown, Error>> {
|
||||
const lastAttempt = attempt === MAX_ATTEMPTS;
|
||||
|
||||
try {
|
||||
return {
|
||||
callWasSuccessful: true,
|
||||
response: await getMetrics(cluster, prometheusPath, { query, ...queryParams }),
|
||||
};
|
||||
} catch (error) {
|
||||
if (
|
||||
!isRequestError(error)
|
||||
|| lastAttempt
|
||||
|| (
|
||||
!lastAttempt && (
|
||||
typeof error.statusCode === "number" &&
|
||||
400 <= error.statusCode && error.statusCode < 500
|
||||
)
|
||||
) {
|
||||
throw new Error("Metrics not available", { cause: error });
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000)); // add delay before repeating request
|
||||
)
|
||||
) {
|
||||
return {
|
||||
callWasSuccessful: false,
|
||||
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({
|
||||
id: "add-metrics-route",
|
||||
|
||||
@ -60,9 +98,10 @@ const addMetricsRouteInjectable = getRouteInjectable({
|
||||
const loadMetrics = loadMetricsFor(getMetrics);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
return clusterRoute({
|
||||
return payloadValidatedClusterRoute({
|
||||
method: "post",
|
||||
path: `${apiPrefix}/metrics`,
|
||||
payloadValidator: clusterMetricsPayloadValidator,
|
||||
})(async ({ cluster, payload, query }) => {
|
||||
const queryParams: Partial<Record<string, string>> = Object.fromEntries(query.entries());
|
||||
const prometheusMetadata: ClusterPrometheusMetadata = {};
|
||||
@ -95,33 +134,46 @@ const addMetricsRouteInjectable = getRouteInjectable({
|
||||
|
||||
// return data in same structure as query
|
||||
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)) {
|
||||
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)) {
|
||||
const data = payload as Record<string, Record<string, string>>;
|
||||
const queries = object.entries(data)
|
||||
.map(([queryName, queryOpts]) => (
|
||||
provider.getQuery(queryOpts, queryName)
|
||||
));
|
||||
// Last option
|
||||
const queries = object.entries(payload)
|
||||
.map(([queryName, queryOpts]) => (
|
||||
provider.getQuery(queryOpts, queryName)
|
||||
));
|
||||
|
||||
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
|
||||
const response = object.fromEntries(object.keys(data).map((metricName, i) => [metricName, result[i]]));
|
||||
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
|
||||
|
||||
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