1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-21 13:47:39 +02:00

demo: prometheus

This commit is contained in:
Tymoteusz Czech 2025-06-17 20:30:25 +02:00
parent 998ab41900
commit 1d06e30a28
No known key found for this signature in database
GPG Key ID: 133555230D88D75F
8 changed files with 268 additions and 0 deletions

View File

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

View 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,
);
}
}

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

View 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>;

View File

@ -113,6 +113,7 @@ export * from './health-overview-schema.js';
export * from './health-report-schema.js';
export * from './id-schema.js';
export * from './ids-schema.js';
export * from './impact-metrics-schema.js';
export * from './import-toggles-schema.js';
export * from './import-toggles-validate-item-schema.js';
export * from './import-toggles-validate-schema.js';

View File

@ -35,6 +35,7 @@ import { InactiveUsersController } from '../../users/inactive/inactive-users-con
import { UiObservabilityController } from '../../features/ui-observability-controller/ui-observability-controller.js';
import { SearchApi } from './search/index.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 type { IUnleashServices } from '../../services/index.js';
import CustomMetricsController from '../../features/metrics/custom/custom-metrics-controller.js';
@ -137,6 +138,10 @@ export class AdminApi extends Controller {
'/personal-dashboard',
new PersonalDashboardController(config, services).router,
);
this.app.use(
'/impact-metrics',
new ImpactMetricsController(config, services).router,
);
this.app.use(
'/environments',
new EnvironmentsController(config, services).router,

View File

@ -171,6 +171,11 @@ import type {
import type { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType.js';
import { UnknownFlagsService } from '../features/metrics/unknown-flags/unknown-flags-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 = (
stores: IUnleashStores,
@ -441,6 +446,10 @@ export const createServices = (
? withTransactional(createUserSubscriptionsService(config), db)
: withFakeTransactional(createFakeUserSubscriptionsService(config));
const impactMetricsService = db
? createImpactMetricsService(config)
: createFakeImpactMetricsService(config);
return {
transactionalAccessService,
accessService,
@ -507,6 +516,7 @@ export const createServices = (
integrationEventsService,
onboardingService,
personalDashboardService,
impactMetricsService,
projectStatusService,
transactionalUserSubscriptionsService,
uniqueConnectionService,
@ -638,6 +648,7 @@ export interface IUnleashServices {
integrationEventsService: IntegrationEventsService;
onboardingService: OnboardingService;
personalDashboardService: PersonalDashboardService;
impactMetricsService: ImpactMetricsService;
projectStatusService: ProjectStatusService;
transactionalUserSubscriptionsService: WithTransactional<UserSubscriptionsService>;
uniqueConnectionService: UniqueConnectionService;

View File

@ -292,6 +292,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_IMPROVED_JSON_DIFF,
false,
),
impactMetrics: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_IMPACT_METRICS,
false,
),
};
export const defaultExperimentalOptions: IExperimentalOptions = {