1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-06 01:15:28 +02:00
unleash.unleash/src/lib/features/metrics/instance/instance-service.ts

333 lines
12 KiB
TypeScript

import type EventEmitter from 'events';
import { APPLICATION_CREATED, CLIENT_REGISTER } from '../../../types/events';
import type { IApplication, IApplicationOverview } from './models';
import type { IUnleashStores } from '../../../types/stores';
import type { IUnleashConfig } from '../../../types/option';
import type { IEventStore } from '../../../types/stores/event-store';
import type {
IClientApplication,
IClientApplications,
IClientApplicationsSearchParams,
IClientApplicationsStore,
} from '../../../types/stores/client-applications-store';
import type { IFeatureToggleStore } from '../../feature-toggle/types/feature-toggle-store-type';
import type { IStrategyStore } from '../../../types/stores/strategy-store';
import type { IClientInstanceStore } from '../../../types/stores/client-instance-store';
import type { IClientApp, ISdkHeartbeat } from '../../../types/model';
import { clientRegisterSchema } from '../shared/schema';
import type { IClientMetricsStoreV2 } from '../client-metrics/client-metrics-store-v2-type';
import { clientMetricsSchema } from '../shared/schema';
import type { PartialSome } from '../../../types/partial';
import type { IPrivateProjectChecker } from '../../private-project/privateProjectCheckerType';
import { type IFlagResolver, SYSTEM_USER } from '../../../types';
import { ALL_PROJECTS, parseStrictSemVer } from '../../../util';
import type { Logger } from '../../../logger';
import { findOutdatedSDKs, isOutdatedSdk } from './findOutdatedSdks';
import type { OutdatedSdksSchema } from '../../../openapi/spec/outdated-sdks-schema';
import { CLIENT_REGISTERED } from '../../../metric-events';
export default class ClientInstanceService {
apps = {};
logger: Logger;
seenClients: Record<string, IClientApp> = {};
private clientMetricsStoreV2: IClientMetricsStoreV2;
private strategyStore: IStrategyStore;
private featureToggleStore: IFeatureToggleStore;
private clientApplicationsStore: IClientApplicationsStore;
private clientInstanceStore: IClientInstanceStore;
private eventStore: IEventStore;
private privateProjectChecker: IPrivateProjectChecker;
private flagResolver: IFlagResolver;
private eventBus: EventEmitter;
constructor(
{
clientMetricsStoreV2,
strategyStore,
featureToggleStore,
clientInstanceStore,
clientApplicationsStore,
eventStore,
}: Pick<
IUnleashStores,
| 'clientMetricsStoreV2'
| 'strategyStore'
| 'featureToggleStore'
| 'clientApplicationsStore'
| 'clientInstanceStore'
| 'eventStore'
>,
{
getLogger,
flagResolver,
eventBus,
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver' | 'eventBus'>,
privateProjectChecker: IPrivateProjectChecker,
) {
this.clientMetricsStoreV2 = clientMetricsStoreV2;
this.strategyStore = strategyStore;
this.featureToggleStore = featureToggleStore;
this.clientApplicationsStore = clientApplicationsStore;
this.clientInstanceStore = clientInstanceStore;
this.eventStore = eventStore;
this.eventBus = eventBus;
this.privateProjectChecker = privateProjectChecker;
this.flagResolver = flagResolver;
this.logger = getLogger(
'/services/client-metrics/client-instance-service.ts',
);
}
public async registerInstance(
data: PartialSome<IClientApp, 'instanceId'>,
clientIp: string,
): Promise<void> {
const value = await clientMetricsSchema.validateAsync(data);
await this.clientInstanceStore.setLastSeen({
appName: value.appName,
instanceId: value.instanceId,
environment: value.environment,
clientIp: clientIp,
});
}
public async registerClient(
data: PartialSome<IClientApp, 'instanceId'>,
clientIp: string,
): Promise<void> {
const value = await clientRegisterSchema.validateAsync(data);
value.clientIp = clientIp;
value.createdBy = SYSTEM_USER.username!;
this.seenClients[this.clientKey(value)] = value;
this.eventBus.emit(CLIENT_REGISTERED, value);
if (value.sdkVersion && value.sdkVersion.indexOf(':') > -1) {
const [sdkName, sdkVersion] = value.sdkVersion.split(':');
const heartbeatEvent: ISdkHeartbeat = {
sdkName,
sdkVersion,
metadata: {
platformName: data.platformName,
platformVersion: data.platformVersion,
yggdrasilVersion: data.yggdrasilVersion,
specVersion: data.specVersion,
},
};
this.eventStore.emit(CLIENT_REGISTER, heartbeatEvent);
}
}
async announceUnannounced(): Promise<void> {
if (this.clientApplicationsStore) {
const appsToAnnounce =
await this.clientApplicationsStore.setUnannouncedToAnnounced();
if (appsToAnnounce.length > 0) {
const events = appsToAnnounce.map((app) => ({
type: APPLICATION_CREATED,
createdBy: app.createdBy || SYSTEM_USER.username!,
data: app,
createdByUserId: app.createdByUserId || SYSTEM_USER.id,
ip: '', // TODO: fix this, how do we get the ip from the client? This comes from a row in the DB
}));
await this.eventStore.batchStore(events);
}
}
}
clientKey(client: IClientApp): string {
return `${client.appName}_${client.instanceId}`;
}
async bulkAdd(): Promise<void> {
if (
this &&
this.seenClients &&
this.clientApplicationsStore &&
this.clientInstanceStore
) {
const uniqueRegistrations = Object.values(this.seenClients);
const uniqueApps: Partial<IClientApplication>[] = Object.values(
uniqueRegistrations.reduce((soFar, reg) => {
// eslint-disable-next-line no-param-reassign
soFar[reg.appName] = reg;
return soFar;
}, {}),
);
this.seenClients = {};
try {
if (uniqueRegistrations.length > 0) {
await this.clientApplicationsStore.bulkUpsert(uniqueApps);
await this.clientInstanceStore.bulkUpsert(
uniqueRegistrations,
);
}
} catch (err) {
this.logger.warn('Failed to register clients', err);
}
}
}
async getApplications(
query: IClientApplicationsSearchParams,
userId: number,
): Promise<IClientApplications> {
const applications = await this.clientApplicationsStore.getApplications(
{ ...query, sortBy: query.sortBy || 'appName' },
);
const accessibleProjects =
await this.privateProjectChecker.getUserAccessibleProjects(userId);
if (accessibleProjects.mode === 'all') {
return applications;
} else {
return {
applications: applications.applications.map((application) => {
return {
...application,
usage: application.usage?.filter(
(usageItem) =>
usageItem.project === ALL_PROJECTS ||
accessibleProjects.projects.includes(
usageItem.project,
),
),
};
}),
total: applications.total,
};
}
}
async getApplication(appName: string): Promise<IApplication> {
const [seenToggles, application, instances, strategies, features] =
await Promise.all([
this.clientMetricsStoreV2.getSeenTogglesForApp(appName),
this.clientApplicationsStore.get(appName),
this.clientInstanceStore.getByAppName(appName),
this.strategyStore.getAll(),
this.featureToggleStore.getAll(),
]);
return {
appName: application.appName,
createdAt: application.createdAt,
description: application.description,
url: application.url,
color: application.color,
icon: application.icon,
strategies: application.strategies.map((name) => {
const found = strategies.find((f) => f.name === name);
return found || { name, notFound: true };
}),
instances,
seenToggles: seenToggles.map((name) => {
const found = features.find((f) => f.name === name);
return found || { name, notFound: true };
}),
links: {
self: `/api/applications/${application.appName}`,
},
};
}
async getApplicationOverview(
appName: string,
userId: number,
): Promise<IApplicationOverview> {
const result =
await this.clientApplicationsStore.getApplicationOverview(appName);
const accessibleProjects =
await this.privateProjectChecker.filterUserAccessibleProjects(
userId,
result.projects,
);
result.projects = accessibleProjects;
result.environments.forEach((environment) => {
environment.issues.outdatedSdks = findOutdatedSDKs(
environment.sdks,
);
});
return result;
}
async getRecentApplicationEnvironmentInstances(
appName: string,
environment: string,
) {
const instances =
await this.clientInstanceStore.getRecentByAppNameAndEnvironment(
appName,
environment,
);
return instances.map((instance) => ({
instanceId: instance.instanceId,
clientIp: instance.clientIp,
sdkVersion: instance.sdkVersion,
lastSeen: instance.lastSeen,
}));
}
async deleteApplication(appName: string): Promise<void> {
await this.clientInstanceStore.deleteForApplication(appName);
await this.clientApplicationsStore.delete(appName);
}
async createApplication(input: IApplication): Promise<void> {
await this.clientApplicationsStore.upsert(input);
}
async removeInstancesOlderThanTwoDays(): Promise<void> {
return this.clientInstanceStore.removeInstancesOlderThanTwoDays();
}
async getOutdatedSdks(): Promise<OutdatedSdksSchema['sdks']> {
const sdkApps = await this.clientInstanceStore.groupApplicationsBySdk();
return sdkApps.filter((sdkApp) => isOutdatedSdk(sdkApp.sdkVersion));
}
async getOutdatedSdksByProject(
projectId: string,
): Promise<OutdatedSdksSchema['sdks']> {
const sdkApps =
await this.clientInstanceStore.groupApplicationsBySdkAndProject(
projectId,
);
return sdkApps.filter((sdkApp) => isOutdatedSdk(sdkApp.sdkVersion));
}
async usesSdkOlderThan(
sdkName: string,
sdkVersion: string,
): Promise<boolean> {
const semver = parseStrictSemVer(sdkVersion);
const instancesOfSdk =
await this.clientInstanceStore.getBySdkName(sdkName);
return instancesOfSdk.some((instance) => {
if (instance.sdkVersion) {
const [_sdkName, sdkVersion] = instance.sdkVersion.split(':');
const instanceUsedSemver = parseStrictSemVer(sdkVersion);
return (
instanceUsedSemver !== null &&
semver !== null &&
instanceUsedSemver < semver
);
}
});
}
}