Skip to content

Commit

Permalink
feat: app env instances api (#6339)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Feb 26, 2024
1 parent 2c5d4ba commit 91c0859
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 2 deletions.
15 changes: 15 additions & 0 deletions src/lib/db/client-instance-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,21 @@ export default class ClientInstanceStore implements IClientInstanceStore {
return rows.map(mapRow);
}

async getByAppNameAndEnvironment(
appName: string,
environment: string,
): Promise<IClientInstance[]> {
const rows = await this.db
.select()
.from(TABLE)
.where('app_name', appName)
.where('environment', environment)
.orderBy('last_seen', 'desc')
.limit(1000);

return rows.map(mapRow);
}

async getBySdkName(sdkName: string): Promise<IClientInstance[]> {
const sdkPrefix = `${sdkName}%`;
const rows = await this.db
Expand Down
18 changes: 18 additions & 0 deletions src/lib/features/metrics/instance/instance-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,24 @@ export default class ClientInstanceService {
return this.clientApplicationsStore.getApplicationOverview(appName);
}

async getApplicationEnvironmentInstances(
appName: string,
environment: string,
) {
const instances =
await this.clientInstanceStore.getByAppNameAndEnvironment(
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);
Expand Down
2 changes: 2 additions & 0 deletions src/lib/openapi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ import { rolesSchema } from './spec/roles-schema';
import { applicationOverviewSchema } from './spec/application-overview-schema';
import { applicationOverviewEnvironmentSchema } from './spec/application-overview-environment-schema';
import { applicationOverviewIssuesSchema } from './spec/application-overview-issues-schema';
import { applicationEnvironmentInstancesSchema } from './spec/application-environment-instances-schema';

// Schemas must have an $id property on the form "#/components/schemas/mySchema".
export type SchemaId = (typeof schemas)[keyof typeof schemas]['$id'];
Expand Down Expand Up @@ -251,6 +252,7 @@ export const schemas: UnleashSchemas = {
applicationOverviewSchema,
applicationOverviewIssuesSchema,
applicationOverviewEnvironmentSchema,
applicationEnvironmentInstancesSchema,
applicationUsageSchema,
applicationsSchema,
batchFeaturesSchema,
Expand Down
53 changes: 53 additions & 0 deletions src/lib/openapi/spec/application-environment-instances-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { FromSchema } from 'json-schema-to-ts';

export const applicationEnvironmentInstancesSchema = {
$id: '#/components/schemas/applicationEnvironmentInstanceSchema',
type: 'object',
description:
'Data about an application environment instances that are connected to Unleash via an SDK.',
additionalProperties: false,
required: ['instances'],
properties: {
instances: {
type: 'array',
description: 'A list of instances',
items: {
type: 'object',
required: ['instanceId'],
additionalProperties: false,
properties: {
instanceId: {
description:
'A unique identifier identifying the instance of the application running the SDK. Often changes based on execution environment. For instance: two pods in Kubernetes will have two different instanceIds',
type: 'string',
example: 'b77f3d13-5f48-4a7b-a3f4-a449b97ce43a',
},
sdkVersion: {
type: 'string',
description:
'An SDK version identifier. Usually formatted as "unleash-client-<language>:<version>"',
example: 'unleash-client-java:7.0.0',
},
clientIp: {
type: 'string',
description:
'An IP address identifying the instance of the application running the SDK',
example: '192.168.0.1',
},
lastSeen: {
type: 'string',
format: 'date-time',
example: '2023-04-19T08:15:14.000Z',
description:
'The last time the application environment instance was seen',
},
},
},
},
},
components: {},
} as const;

export type ApplicationEnvironmentInstancesSchema = FromSchema<
typeof applicationEnvironmentInstancesSchema
>;
1 change: 1 addition & 0 deletions src/lib/openapi/spec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,4 @@ export * from './inactive-users-schema';
export * from './record-ui-error-schema';
export * from './project-application-schema';
export * from './project-applications-schema';
export * from './application-environment-instances-schema';
51 changes: 49 additions & 2 deletions src/lib/routes/admin-api/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import {
import { OpenApiService } from '../../services';
import { applicationsQueryParameters } from '../../openapi/spec/applications-query-parameters';
import { normalizeQueryParams } from '../../features/feature-search/search-utils';
import {
applicationEnvironmentInstancesSchema,
ApplicationEnvironmentInstancesSchema,
} from '../../openapi/spec/application-environment-instances-schema';

class MetricsController extends Controller {
private logger: Logger;
Expand Down Expand Up @@ -152,6 +156,27 @@ class MetricsController extends Controller {
}),
],
});
this.route({
method: 'get',
path: '/instances/:appName/:environment',
handler: this.getApplicationEnvironmentInstances,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['Unstable'],
operationId: 'getApplicationEnvironmentInstances',
summary: 'Get application environment instances',
description:
'Returns an overview of the instances for the given `appName` and `environment` that receive traffic.',
responses: {
200: createResponseSchema(
'applicationEnvironmentInstancesSchema',
),
...getStandardResponses(404),
},
}),
],
});
}

async deprecated(req: Request, res: Response): Promise<void> {
Expand Down Expand Up @@ -223,7 +248,7 @@ class MetricsController extends Controller {
}

async getApplication(
req: Request,
req: Request<{ appName: string }>,
res: Response<ApplicationSchema>,
): Promise<void> {
const { appName } = req.params;
Expand All @@ -234,7 +259,7 @@ class MetricsController extends Controller {
}

async getApplicationOverview(
req: Request,
req: Request<{ appName: string }>,
res: Response<ApplicationOverviewSchema>,
): Promise<void> {
if (!this.flagResolver.isEnabled('sdkReporting')) {
Expand All @@ -251,6 +276,28 @@ class MetricsController extends Controller {
serializeDates(overview),
);
}

async getApplicationEnvironmentInstances(
req: Request<{ appName: string; environment: string }>,
res: Response<ApplicationEnvironmentInstancesSchema>,
): Promise<void> {
if (!this.flagResolver.isEnabled('sdkReporting')) {
throw new NotFoundError();
}
const { appName, environment } = req.params;
const instances =
await this.clientInstanceService.getApplicationEnvironmentInstances(
appName,
environment,
);

this.openApiService.respondWithValidation(
200,
res,
applicationEnvironmentInstancesSchema.$id,
serializeDates({ instances }),
);
}
}

export default MetricsController;
4 changes: 4 additions & 0 deletions src/lib/types/stores/client-instance-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export interface IClientInstanceStore
setLastSeen(INewClientInstance): Promise<void>;
insert(details: INewClientInstance): Promise<void>;
getByAppName(appName: string): Promise<IClientInstance[]>;
getByAppNameAndEnvironment(
appName: string,
environment: string,
): Promise<IClientInstance[]>;
getBySdkName(sdkName: string): Promise<IClientInstance[]>;
getDistinctApplications(): Promise<string[]>;
getDistinctApplicationsCount(daysBefore?: number): Promise<number>;
Expand Down
14 changes: 14 additions & 0 deletions src/test/e2e/api/admin/applications.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,20 @@ test('should show correct number of total', async () => {
};

expect(body).toMatchObject(expected);

const { body: instancesBody } = await app.request
.get(`/api/admin/metrics/instances/${metrics.appName}/default`)
.expect(200);

expect(instancesBody).toMatchObject({
instances: [
{ instanceId: 'instanceId', sdkVersion: 'unleash-client-test' },
{
instanceId: 'another-instance',
sdkVersion: 'unleash-client-test2',
},
],
});
});

test('should show missing features and strategies', async () => {
Expand Down
9 changes: 9 additions & 0 deletions src/test/fixtures/fake-client-instance-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ export default class FakeClientInstanceStore implements IClientInstanceStore {
return this.instances.filter((i) => i.appName === appName);
}

async getByAppNameAndEnvironment(
appName: string,
environment: string,
): Promise<IClientInstance[]> {
return this.instances
.filter((i) => i.appName === appName)
.filter((i) => i.environment === environment);
}

async getDistinctApplications(): Promise<string[]> {
const apps = new Set<string>();
this.instances.forEach((i) => apps.add(i.appName));
Expand Down

0 comments on commit 91c0859

Please sign in to comment.