1
0
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:
Sebastian Malton 2021-11-23 14:34:45 -05:00 committed by GitHub
parent 8081d35f9c
commit 5d172b89db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 73 additions and 85 deletions

View File

@ -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");
}); });

View File

@ -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();
if (!prometheusService) return null;
const { service, namespace, port } = prometheusService;
return `${namespace}/services/${service}:${port}`;
}
async getPrometheusProvider() {
if (!this.prometheusProvider) {
const service = await this.getPrometheusService(); const service = await this.getPrometheusService();
const prometheusPath = this.ensurePrometheusPath(service);
const provider = this.ensurePrometheusProvider(service);
if (!service) { return { prometheusPath, provider };
return null;
} }
logger.info(`using ${service.id} as prometheus provider`);
protected ensurePrometheusPath({ service, namespace, port }: PrometheusService): string {
return this.prometheusPath ||= `${namespace}/services/${service}:${port}`;
}
protected ensurePrometheusProvider(service: PrometheusService): PrometheusProvider {
if (!this.prometheusProvider) {
logger.info(`[CONTEXT-HANDLER]: using ${service.id} as prometheus provider for clusterId=${this.cluster.id}`);
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() {

View File

@ -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");
} }
} }

View File

@ -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 {

View File

@ -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 {

View File

@ -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}`);
}
} }
} }

View File

@ -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 {

View File

@ -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;
} }