mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Cleanup Prometheus detection (#4316)
This commit is contained in:
parent
8081d35f9c
commit
5d172b89db
@ -110,7 +110,7 @@ describe("ContextHandler", () => {
|
|||||||
[0, 1],
|
[0, 1],
|
||||||
[0, 2],
|
[0, 2],
|
||||||
[0, 3],
|
[0, 3],
|
||||||
])("should return undefined from %d success(es) after %d failure(s)", async (successes, failures) => {
|
])("should throw from %d success(es) after %d failure(s)", async (successes, failures) => {
|
||||||
const reg = PrometheusProviderRegistry.getInstance();
|
const reg = PrometheusProviderRegistry.getInstance();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
@ -124,9 +124,7 @@ describe("ContextHandler", () => {
|
|||||||
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
|
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = await getHandler().getPrometheusService();
|
expect(() => (getHandler() as any).getPrometheusService()).rejects.toBeDefined();
|
||||||
|
|
||||||
expect(service).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
@ -152,7 +150,7 @@ describe("ContextHandler", () => {
|
|||||||
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
|
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = await getHandler().getPrometheusService();
|
const service = await (getHandler() as any).getPrometheusService();
|
||||||
|
|
||||||
expect(service.id === `id_${failures}`);
|
expect(service.id === `id_${failures}`);
|
||||||
});
|
});
|
||||||
@ -180,7 +178,7 @@ describe("ContextHandler", () => {
|
|||||||
reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
|
reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = await getHandler().getPrometheusService();
|
const service = await (getHandler() as any).getPrometheusService();
|
||||||
|
|
||||||
expect(service.id === "id_0");
|
expect(service.id === "id_0");
|
||||||
});
|
});
|
||||||
@ -214,7 +212,7 @@ describe("ContextHandler", () => {
|
|||||||
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
|
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = await getHandler().getPrometheusService();
|
const service = await (getHandler() as any).getPrometheusService();
|
||||||
|
|
||||||
expect(service.id === "id_0");
|
expect(service.id === "id_0");
|
||||||
});
|
});
|
||||||
@ -227,7 +225,7 @@ describe("ContextHandler", () => {
|
|||||||
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
|
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
|
||||||
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
|
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
|
||||||
|
|
||||||
const service = await getHandler().getPrometheusService();
|
const service = await (getHandler() as any).getPrometheusService();
|
||||||
|
|
||||||
expect(service.id).not.toBe("id_2");
|
expect(service.id).not.toBe("id_2");
|
||||||
});
|
});
|
||||||
|
|||||||
@ -29,6 +29,11 @@ import { CoreV1Api } from "@kubernetes/client-node";
|
|||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { KubeAuthProxy } from "./kube-auth-proxy";
|
import { KubeAuthProxy } from "./kube-auth-proxy";
|
||||||
|
|
||||||
|
export interface PrometheusDetails {
|
||||||
|
prometheusPath: string;
|
||||||
|
provider: PrometheusProvider;
|
||||||
|
}
|
||||||
|
|
||||||
export class ContextHandler {
|
export class ContextHandler {
|
||||||
public clusterUrl: UrlWithStringQuery;
|
public clusterUrl: UrlWithStringQuery;
|
||||||
protected kubeAuthProxy?: KubeAuthProxy;
|
protected kubeAuthProxy?: KubeAuthProxy;
|
||||||
@ -52,23 +57,21 @@ export class ContextHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async resolvePrometheusPath(): Promise<string> {
|
public async getPrometheusDetails(): Promise<PrometheusDetails> {
|
||||||
const prometheusService = await this.getPrometheusService();
|
const service = await this.getPrometheusService();
|
||||||
|
const prometheusPath = this.ensurePrometheusPath(service);
|
||||||
|
const provider = this.ensurePrometheusProvider(service);
|
||||||
|
|
||||||
if (!prometheusService) return null;
|
return { prometheusPath, provider };
|
||||||
const { service, namespace, port } = prometheusService;
|
|
||||||
|
|
||||||
return `${namespace}/services/${service}:${port}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPrometheusProvider() {
|
protected ensurePrometheusPath({ service, namespace, port }: PrometheusService): string {
|
||||||
if (!this.prometheusProvider) {
|
return this.prometheusPath ||= `${namespace}/services/${service}:${port}`;
|
||||||
const service = await this.getPrometheusService();
|
}
|
||||||
|
|
||||||
if (!service) {
|
protected ensurePrometheusProvider(service: PrometheusService): PrometheusProvider {
|
||||||
return null;
|
if (!this.prometheusProvider) {
|
||||||
}
|
logger.info(`[CONTEXT-HANDLER]: using ${service.id} as prometheus provider for clusterId=${this.cluster.id}`);
|
||||||
logger.info(`using ${service.id} as prometheus provider`);
|
|
||||||
this.prometheusProvider = service.id;
|
this.prometheusProvider = service.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,37 +80,40 @@ export class ContextHandler {
|
|||||||
|
|
||||||
protected listPotentialProviders(): PrometheusProvider[] {
|
protected listPotentialProviders(): PrometheusProvider[] {
|
||||||
const registry = PrometheusProviderRegistry.getInstance();
|
const registry = PrometheusProviderRegistry.getInstance();
|
||||||
|
const provider = this.prometheusProvider && registry.getByKind(this.prometheusProvider);
|
||||||
|
|
||||||
if (typeof this.prometheusProvider === "string") {
|
if (provider) {
|
||||||
return [registry.getByKind(this.prometheusProvider)];
|
return [provider];
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(registry.providers.values());
|
return Array.from(registry.providers.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPrometheusService(): Promise<PrometheusService | undefined> {
|
protected async getPrometheusService(): Promise<PrometheusService> {
|
||||||
const providers = this.listPotentialProviders();
|
const providers = this.listPotentialProviders();
|
||||||
const proxyConfig = await this.cluster.getProxyKubeconfig();
|
const proxyConfig = await this.cluster.getProxyKubeconfig();
|
||||||
const apiClient = proxyConfig.makeApiClient(CoreV1Api);
|
const apiClient = proxyConfig.makeApiClient(CoreV1Api);
|
||||||
const potentialServices = await Promise.allSettled(
|
const potentialServices = await Promise.allSettled(
|
||||||
providers.map(provider => provider.getPrometheusService(apiClient)),
|
providers.map(provider => provider.getPrometheusService(apiClient)),
|
||||||
);
|
);
|
||||||
|
const errors: any[] = [];
|
||||||
|
|
||||||
for (const result of potentialServices) {
|
for (const res of potentialServices) {
|
||||||
if (result.status === "fulfilled" && result.value) {
|
switch (res.status) {
|
||||||
return result.value;
|
case "rejected":
|
||||||
|
if (res.reason) {
|
||||||
|
errors.push(String(res.reason));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "fulfilled":
|
||||||
|
if (res.value) {
|
||||||
|
return res.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
throw Object.assign(new Error("No Prometheus service found"), { cause: errors });
|
||||||
}
|
|
||||||
|
|
||||||
async getPrometheusPath(): Promise<string> {
|
|
||||||
if (!this.prometheusPath) {
|
|
||||||
this.prometheusPath = await this.resolvePrometheusPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.prometheusPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveAuthProxyUrl() {
|
async resolveAuthProxyUrl() {
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export class PrometheusHelm extends PrometheusLens {
|
|||||||
readonly rateAccuracy: string = "5m";
|
readonly rateAccuracy: string = "5m";
|
||||||
readonly isConfigurable: boolean = true;
|
readonly isConfigurable: boolean = true;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
|
||||||
return this.getFirstNamespacedServer(client, "app=prometheus,component=server,heritage=Helm");
|
return this.getFirstNamespacedService(client, "app=prometheus,component=server,heritage=Helm");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,6 @@
|
|||||||
|
|
||||||
import { PrometheusProvider, PrometheusService } from "./provider-registry";
|
import { PrometheusProvider, PrometheusService } from "./provider-registry";
|
||||||
import type { CoreV1Api } from "@kubernetes/client-node";
|
import type { CoreV1Api } from "@kubernetes/client-node";
|
||||||
import logger from "../logger";
|
|
||||||
import { inspect } from "util";
|
import { inspect } from "util";
|
||||||
|
|
||||||
export class PrometheusLens extends PrometheusProvider {
|
export class PrometheusLens extends PrometheusProvider {
|
||||||
@ -30,22 +29,8 @@ export class PrometheusLens extends PrometheusProvider {
|
|||||||
readonly rateAccuracy: string = "1m";
|
readonly rateAccuracy: string = "1m";
|
||||||
readonly isConfigurable: boolean = false;
|
readonly isConfigurable: boolean = false;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
|
||||||
try {
|
return this.getNamespacedService(client, "prometheus", "lens-metrics");
|
||||||
const resp = await client.readNamespacedService("prometheus", "lens-metrics");
|
|
||||||
const service = resp.body;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
namespace: service.metadata.namespace,
|
|
||||||
service: service.metadata.name,
|
|
||||||
port: service.spec.ports[0].port,
|
|
||||||
};
|
|
||||||
} catch(error) {
|
|
||||||
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`);
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getQuery(opts: Record<string, string>, queryName: string): string {
|
public getQuery(opts: Record<string, string>, queryName: string): string {
|
||||||
|
|||||||
@ -29,8 +29,8 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
readonly name: string = "Prometheus Operator";
|
readonly name: string = "Prometheus Operator";
|
||||||
readonly isConfigurable: boolean = true;
|
readonly isConfigurable: boolean = true;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
|
||||||
return this.getFirstNamespacedServer(client, "operated-prometheus=true", "self-monitor=true");
|
return this.getFirstNamespacedService(client, "operated-prometheus=true", "self-monitor=true");
|
||||||
}
|
}
|
||||||
|
|
||||||
public getQuery(opts: Record<string, string>, queryName: string): string {
|
public getQuery(opts: Record<string, string>, queryName: string): string {
|
||||||
|
|||||||
@ -20,8 +20,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CoreV1Api } from "@kubernetes/client-node";
|
import type { CoreV1Api } from "@kubernetes/client-node";
|
||||||
|
import { inspect } from "util";
|
||||||
import { Singleton } from "../../common/utils";
|
import { Singleton } from "../../common/utils";
|
||||||
import logger from "../logger";
|
|
||||||
|
|
||||||
export type PrometheusService = {
|
export type PrometheusService = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -43,7 +43,7 @@ export abstract class PrometheusProvider {
|
|||||||
return `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`;
|
return `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getFirstNamespacedServer(client: CoreV1Api, ...selectors: string[]): Promise<PrometheusService | undefined> {
|
protected async getFirstNamespacedService(client: CoreV1Api, ...selectors: string[]): Promise<PrometheusService> {
|
||||||
try {
|
try {
|
||||||
for (const selector of selectors) {
|
for (const selector of selectors) {
|
||||||
const { body: { items: [service] }} = await client.listServiceForAllNamespaces(null, null, null, selector);
|
const { body: { items: [service] }} = await client.listServiceForAllNamespaces(null, null, null, selector);
|
||||||
@ -58,10 +58,26 @@ export abstract class PrometheusProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`${this.name}: failed to list services: ${error.toString()}`);
|
throw new Error(`Failed to list services for Prometheus${this.name} in all namespaces: ${error.response.body.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
throw new Error(`No service found for Prometheus${this.name} from any namespace`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getNamespacedService(client: CoreV1Api, name: string, namespace: string): Promise<PrometheusService> {
|
||||||
|
try {
|
||||||
|
const resp = await client.readNamespacedService(name, namespace);
|
||||||
|
const service = resp.body;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
namespace: service.metadata.namespace,
|
||||||
|
service: service.metadata.name,
|
||||||
|
port: service.spec.ports[0].port,
|
||||||
|
};
|
||||||
|
} catch(error) {
|
||||||
|
throw new Error(`Failed to list services for Prometheus${this.name} in namespace=${inspect(namespace, false, undefined, false)}: ${error.response.body.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,6 @@
|
|||||||
|
|
||||||
import { PrometheusProvider, PrometheusService } from "./provider-registry";
|
import { PrometheusProvider, PrometheusService } from "./provider-registry";
|
||||||
import type { CoreV1Api } from "@kubernetes/client-node";
|
import type { CoreV1Api } from "@kubernetes/client-node";
|
||||||
import logger from "../logger";
|
|
||||||
import { inspect } from "util";
|
import { inspect } from "util";
|
||||||
|
|
||||||
export class PrometheusStacklight extends PrometheusProvider {
|
export class PrometheusStacklight extends PrometheusProvider {
|
||||||
@ -30,22 +29,8 @@ export class PrometheusStacklight extends PrometheusProvider {
|
|||||||
readonly rateAccuracy: string = "1m";
|
readonly rateAccuracy: string = "1m";
|
||||||
readonly isConfigurable: boolean = true;
|
readonly isConfigurable: boolean = true;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
|
||||||
try {
|
return this.getNamespacedService(client, "prometheus-server", "stacklight");
|
||||||
const resp = await client.readNamespacedService("prometheus-server", "stacklight");
|
|
||||||
const service = resp.body;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
namespace: service.metadata.namespace,
|
|
||||||
service: service.metadata.name,
|
|
||||||
port: service.spec.ports[0].port,
|
|
||||||
};
|
|
||||||
} catch(error) {
|
|
||||||
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`);
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getQuery(opts: Record<string, string>, queryName: string): string {
|
public getQuery(opts: Record<string, string>, queryName: string): string {
|
||||||
|
|||||||
@ -73,12 +73,9 @@ export class MetricsRoute {
|
|||||||
const prometheusMetadata: ClusterPrometheusMetadata = {};
|
const prometheusMetadata: ClusterPrometheusMetadata = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [prometheusPath, prometheusProvider] = await Promise.all([
|
const { prometheusPath, provider } = await cluster.contextHandler.getPrometheusDetails();
|
||||||
cluster.contextHandler.getPrometheusPath(),
|
|
||||||
cluster.contextHandler.getPrometheusProvider(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
prometheusMetadata.provider = prometheusProvider?.id;
|
prometheusMetadata.provider = provider?.id;
|
||||||
prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type;
|
prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type;
|
||||||
|
|
||||||
if (!prometheusPath) {
|
if (!prometheusPath) {
|
||||||
@ -99,7 +96,7 @@ export class MetricsRoute {
|
|||||||
} else {
|
} else {
|
||||||
const queries = Object.entries<Record<string, string>>(payload)
|
const queries = Object.entries<Record<string, string>>(payload)
|
||||||
.map(([queryName, queryOpts]) => (
|
.map(([queryName, queryOpts]) => (
|
||||||
prometheusProvider.getQuery(queryOpts, queryName)
|
provider.getQuery(queryOpts, queryName)
|
||||||
));
|
));
|
||||||
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
|
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
|
||||||
const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]]));
|
const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]]));
|
||||||
@ -107,9 +104,10 @@ export class MetricsRoute {
|
|||||||
respondJson(response, data);
|
respondJson(response, data);
|
||||||
}
|
}
|
||||||
prometheusMetadata.success = true;
|
prometheusMetadata.success = true;
|
||||||
} catch {
|
} catch (error) {
|
||||||
prometheusMetadata.success = false;
|
prometheusMetadata.success = false;
|
||||||
respondJson(response, {});
|
respondJson(response, {});
|
||||||
|
logger.warn(`[METRICS-ROUTE]: failed to get metrics for clusterId=${cluster.id}:`, error);
|
||||||
} finally {
|
} finally {
|
||||||
cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata;
|
cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user