mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
demo: prometheus
This commit is contained in:
parent
998ab41900
commit
1d06e30a28
@ -0,0 +1,10 @@
|
|||||||
|
import type { IUnleashConfig } from '../../types/index.js';
|
||||||
|
import ImpactMetricsService from './impact-metrics-service.js';
|
||||||
|
|
||||||
|
export const createImpactMetricsService = (config: IUnleashConfig) => {
|
||||||
|
return new ImpactMetricsService(config);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createFakeImpactMetricsService = (config: IUnleashConfig) => {
|
||||||
|
return new ImpactMetricsService(config);
|
||||||
|
};
|
67
src/lib/features/impact-metrics/impact-metrics-controller.ts
Normal file
67
src/lib/features/impact-metrics/impact-metrics-controller.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import type { Response } from 'express';
|
||||||
|
import Controller from '../../routes/controller.js';
|
||||||
|
import type { IAuthRequest } from '../../routes/unleash-types.js';
|
||||||
|
import type { IUnleashConfig } from '../../types/option.js';
|
||||||
|
import { NONE } from '../../types/permissions.js';
|
||||||
|
import type { OpenApiService } from '../../services/openapi-service.js';
|
||||||
|
import { getStandardResponses } from '../../openapi/util/standard-responses.js';
|
||||||
|
import { createResponseSchema } from '../../openapi/util/create-response-schema.js';
|
||||||
|
import type ImpactMetricsService from './impact-metrics-service.js';
|
||||||
|
import {
|
||||||
|
impactMetricsSchema,
|
||||||
|
type ImpactMetricsSchema,
|
||||||
|
} from '../../openapi/spec/impact-metrics-schema.js';
|
||||||
|
|
||||||
|
interface ImpactMetricsServices {
|
||||||
|
impactMetricsService: ImpactMetricsService;
|
||||||
|
openApiService: OpenApiService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ImpactMetricsController extends Controller {
|
||||||
|
private impactMetricsService: ImpactMetricsService;
|
||||||
|
private openApiService: OpenApiService;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
config: IUnleashConfig,
|
||||||
|
{ impactMetricsService, openApiService }: ImpactMetricsServices,
|
||||||
|
) {
|
||||||
|
super(config);
|
||||||
|
this.impactMetricsService = impactMetricsService;
|
||||||
|
this.openApiService = openApiService;
|
||||||
|
|
||||||
|
this.route({
|
||||||
|
method: 'get',
|
||||||
|
path: '',
|
||||||
|
handler: this.getImpactMetrics,
|
||||||
|
permission: NONE,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['Unstable'],
|
||||||
|
operationId: 'getImpactMetrics',
|
||||||
|
summary: 'Get impact metrics',
|
||||||
|
description:
|
||||||
|
'Retrieves impact metrics data from Prometheus and forwards it to the frontend.',
|
||||||
|
responses: {
|
||||||
|
200: createResponseSchema('impactMetricsSchema'),
|
||||||
|
...getStandardResponses(401, 403, 404),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getImpactMetrics(
|
||||||
|
req: IAuthRequest,
|
||||||
|
res: Response<ImpactMetricsSchema>,
|
||||||
|
): Promise<void> {
|
||||||
|
const metrics =
|
||||||
|
await this.impactMetricsService.getMetricsFromPrometheus();
|
||||||
|
|
||||||
|
this.openApiService.respondWithValidation(
|
||||||
|
200,
|
||||||
|
res,
|
||||||
|
impactMetricsSchema.$id,
|
||||||
|
metrics,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
155
src/lib/features/impact-metrics/impact-metrics-service.ts
Normal file
155
src/lib/features/impact-metrics/impact-metrics-service.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import type { Logger } from '../../logger.js';
|
||||||
|
import type { IUnleashConfig } from '../../types/index.js';
|
||||||
|
|
||||||
|
interface PrometheusResponse {
|
||||||
|
status: string;
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetricSeries {
|
||||||
|
name: string;
|
||||||
|
available: boolean;
|
||||||
|
labels: Record<string, string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IImpactMetricsService {
|
||||||
|
getMetricsFromPrometheus(): Promise<{
|
||||||
|
availableMetrics: MetricSeries[];
|
||||||
|
totalCount: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: URL
|
||||||
|
const prometheusUrl = 'http://localhost:9090';
|
||||||
|
|
||||||
|
export default class ImpactMetricsService implements IImpactMetricsService {
|
||||||
|
private logger: Logger;
|
||||||
|
private config: IUnleashConfig;
|
||||||
|
|
||||||
|
constructor(config: IUnleashConfig) {
|
||||||
|
this.logger = config.getLogger(
|
||||||
|
'impact-metrics/impact-metrics-service.ts',
|
||||||
|
);
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetricsFromPrometheus(): Promise<{
|
||||||
|
availableMetrics: MetricSeries[];
|
||||||
|
totalCount: number;
|
||||||
|
}> {
|
||||||
|
const testingMetrics = [
|
||||||
|
'node_cpu_seconds_total',
|
||||||
|
'node_load5',
|
||||||
|
'node_memory_MemAvailable_bytes',
|
||||||
|
'node_disk_read_bytes_total',
|
||||||
|
'node_disk_written_bytes_total',
|
||||||
|
'node_network_receive_bytes_total',
|
||||||
|
'node_network_transmit_bytes_total',
|
||||||
|
'node_filesystem_avail_bytes',
|
||||||
|
'node_filesystem_size_bytes',
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.logger.info('Fetching all metrics from Prometheus');
|
||||||
|
|
||||||
|
const metricsResponse = await fetch(
|
||||||
|
`${prometheusUrl}/api/v1/label/__name__/values`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!metricsResponse.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch metrics: ${metricsResponse.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metricsData =
|
||||||
|
(await metricsResponse.json()) as PrometheusResponse;
|
||||||
|
if (metricsData.status !== 'success') {
|
||||||
|
throw new Error('Prometheus API returned error status');
|
||||||
|
}
|
||||||
|
|
||||||
|
const allMetrics: string[] = metricsData.data;
|
||||||
|
this.logger.info(
|
||||||
|
`Found ${allMetrics.length} total metrics in Prometheus`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const availableMetrics: MetricSeries[] = [];
|
||||||
|
|
||||||
|
for (const metricName of testingMetrics) {
|
||||||
|
const isAvailable = allMetrics.includes(metricName);
|
||||||
|
|
||||||
|
let labels: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
if (isAvailable) {
|
||||||
|
try {
|
||||||
|
const seriesUrl = `${prometheusUrl}/api/v1/series?match[]=${encodeURIComponent(metricName)}`;
|
||||||
|
this.logger.debug(
|
||||||
|
`Fetching series for metric: ${metricName}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const seriesResponse = await fetch(seriesUrl);
|
||||||
|
const seriesData =
|
||||||
|
(await seriesResponse.json()) as PrometheusResponse;
|
||||||
|
if (seriesData.status === 'success') {
|
||||||
|
const labelMap: Record<string, Set<string>> = {};
|
||||||
|
|
||||||
|
seriesData.data.forEach(
|
||||||
|
(series: Record<string, string>) => {
|
||||||
|
Object.entries(series).forEach(
|
||||||
|
([key, value]) => {
|
||||||
|
if (key !== '__name__') {
|
||||||
|
if (!labelMap[key]) {
|
||||||
|
labelMap[key] = new Set();
|
||||||
|
}
|
||||||
|
labelMap[key].add(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
labels = Object.fromEntries(
|
||||||
|
Object.entries(labelMap).map(
|
||||||
|
([key, valueSet]) => [
|
||||||
|
key,
|
||||||
|
Array.from(valueSet).sort(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to fetch labels for metric ${metricName}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
availableMetrics.push({
|
||||||
|
name: metricName,
|
||||||
|
available: isAvailable,
|
||||||
|
labels,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Metric ${metricName}: available=${isAvailable}, labelCount=${Object.keys(labels).length}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableCount = availableMetrics.filter(
|
||||||
|
(m) => m.available,
|
||||||
|
).length;
|
||||||
|
this.logger.info(
|
||||||
|
`Found ${availableCount}/${testingMetrics.length} test metrics available in Prometheus`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
availableMetrics,
|
||||||
|
totalCount: allMetrics.length,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching metrics from Prometheus:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
src/lib/openapi/spec/impact-metrics-schema.ts
Normal file
15
src/lib/openapi/spec/impact-metrics-schema.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import type { FromSchema } from 'json-schema-to-ts';
|
||||||
|
|
||||||
|
export const impactMetricsSchema = {
|
||||||
|
$id: '#/components/schemas/impactMetricsSchema',
|
||||||
|
type: 'object',
|
||||||
|
description: 'Impact metrics data from Prometheus',
|
||||||
|
additionalProperties: true,
|
||||||
|
required: [],
|
||||||
|
properties: {},
|
||||||
|
components: {
|
||||||
|
schemas: {},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ImpactMetricsSchema = FromSchema<typeof impactMetricsSchema>;
|
@ -113,6 +113,7 @@ export * from './health-overview-schema.js';
|
|||||||
export * from './health-report-schema.js';
|
export * from './health-report-schema.js';
|
||||||
export * from './id-schema.js';
|
export * from './id-schema.js';
|
||||||
export * from './ids-schema.js';
|
export * from './ids-schema.js';
|
||||||
|
export * from './impact-metrics-schema.js';
|
||||||
export * from './import-toggles-schema.js';
|
export * from './import-toggles-schema.js';
|
||||||
export * from './import-toggles-validate-item-schema.js';
|
export * from './import-toggles-validate-item-schema.js';
|
||||||
export * from './import-toggles-validate-schema.js';
|
export * from './import-toggles-validate-schema.js';
|
||||||
|
@ -35,6 +35,7 @@ import { InactiveUsersController } from '../../users/inactive/inactive-users-con
|
|||||||
import { UiObservabilityController } from '../../features/ui-observability-controller/ui-observability-controller.js';
|
import { UiObservabilityController } from '../../features/ui-observability-controller/ui-observability-controller.js';
|
||||||
import { SearchApi } from './search/index.js';
|
import { SearchApi } from './search/index.js';
|
||||||
import PersonalDashboardController from '../../features/personal-dashboard/personal-dashboard-controller.js';
|
import PersonalDashboardController from '../../features/personal-dashboard/personal-dashboard-controller.js';
|
||||||
|
import ImpactMetricsController from '../../features/impact-metrics/impact-metrics-controller.js';
|
||||||
import FeatureLifecycleCountController from '../../features/feature-lifecycle/feature-lifecycle-count-controller.js';
|
import FeatureLifecycleCountController from '../../features/feature-lifecycle/feature-lifecycle-count-controller.js';
|
||||||
import type { IUnleashServices } from '../../services/index.js';
|
import type { IUnleashServices } from '../../services/index.js';
|
||||||
import CustomMetricsController from '../../features/metrics/custom/custom-metrics-controller.js';
|
import CustomMetricsController from '../../features/metrics/custom/custom-metrics-controller.js';
|
||||||
@ -137,6 +138,10 @@ export class AdminApi extends Controller {
|
|||||||
'/personal-dashboard',
|
'/personal-dashboard',
|
||||||
new PersonalDashboardController(config, services).router,
|
new PersonalDashboardController(config, services).router,
|
||||||
);
|
);
|
||||||
|
this.app.use(
|
||||||
|
'/impact-metrics',
|
||||||
|
new ImpactMetricsController(config, services).router,
|
||||||
|
);
|
||||||
this.app.use(
|
this.app.use(
|
||||||
'/environments',
|
'/environments',
|
||||||
new EnvironmentsController(config, services).router,
|
new EnvironmentsController(config, services).router,
|
||||||
|
@ -171,6 +171,11 @@ import type {
|
|||||||
import type { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType.js';
|
import type { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType.js';
|
||||||
import { UnknownFlagsService } from '../features/metrics/unknown-flags/unknown-flags-service.js';
|
import { UnknownFlagsService } from '../features/metrics/unknown-flags/unknown-flags-service.js';
|
||||||
import type FeatureLinkService from '../features/feature-links/feature-link-service.js';
|
import type FeatureLinkService from '../features/feature-links/feature-link-service.js';
|
||||||
|
import {
|
||||||
|
createFakeImpactMetricsService,
|
||||||
|
createImpactMetricsService,
|
||||||
|
} from '../features/impact-metrics/createImpactMetricsService.js';
|
||||||
|
import type ImpactMetricsService from '../features/impact-metrics/impact-metrics-service.js';
|
||||||
|
|
||||||
export const createServices = (
|
export const createServices = (
|
||||||
stores: IUnleashStores,
|
stores: IUnleashStores,
|
||||||
@ -441,6 +446,10 @@ export const createServices = (
|
|||||||
? withTransactional(createUserSubscriptionsService(config), db)
|
? withTransactional(createUserSubscriptionsService(config), db)
|
||||||
: withFakeTransactional(createFakeUserSubscriptionsService(config));
|
: withFakeTransactional(createFakeUserSubscriptionsService(config));
|
||||||
|
|
||||||
|
const impactMetricsService = db
|
||||||
|
? createImpactMetricsService(config)
|
||||||
|
: createFakeImpactMetricsService(config);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transactionalAccessService,
|
transactionalAccessService,
|
||||||
accessService,
|
accessService,
|
||||||
@ -507,6 +516,7 @@ export const createServices = (
|
|||||||
integrationEventsService,
|
integrationEventsService,
|
||||||
onboardingService,
|
onboardingService,
|
||||||
personalDashboardService,
|
personalDashboardService,
|
||||||
|
impactMetricsService,
|
||||||
projectStatusService,
|
projectStatusService,
|
||||||
transactionalUserSubscriptionsService,
|
transactionalUserSubscriptionsService,
|
||||||
uniqueConnectionService,
|
uniqueConnectionService,
|
||||||
@ -638,6 +648,7 @@ export interface IUnleashServices {
|
|||||||
integrationEventsService: IntegrationEventsService;
|
integrationEventsService: IntegrationEventsService;
|
||||||
onboardingService: OnboardingService;
|
onboardingService: OnboardingService;
|
||||||
personalDashboardService: PersonalDashboardService;
|
personalDashboardService: PersonalDashboardService;
|
||||||
|
impactMetricsService: ImpactMetricsService;
|
||||||
projectStatusService: ProjectStatusService;
|
projectStatusService: ProjectStatusService;
|
||||||
transactionalUserSubscriptionsService: WithTransactional<UserSubscriptionsService>;
|
transactionalUserSubscriptionsService: WithTransactional<UserSubscriptionsService>;
|
||||||
uniqueConnectionService: UniqueConnectionService;
|
uniqueConnectionService: UniqueConnectionService;
|
||||||
|
@ -292,6 +292,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_IMPROVED_JSON_DIFF,
|
process.env.UNLEASH_EXPERIMENTAL_IMPROVED_JSON_DIFF,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
impactMetrics: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_IMPACT_METRICS,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
Loading…
Reference in New Issue
Block a user