mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-21 13:47:39 +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 './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';
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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 = {
|
||||
|
Loading…
Reference in New Issue
Block a user