Skip to content

Commit f18d065

Browse files
authored
Merge pull request #2478 from RedisInsight/be/feature/RI-4811-add-telemtry-event
#RI-4811 - collect Client information when connecting to a database
2 parents 58081fe + 3754c58 commit f18d065

File tree

10 files changed

+178
-1
lines changed

10 files changed

+178
-1
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
export const mockRedisClientList = 'id=29 addr=172.17.0.1:60702 laddr=172.17.0.4:6379 fd=20 '
2+
+ 'name=redisinsight-common-f9d59780 age=319 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 '
3+
+ 'qbuf-free=40928 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default resp=2 redir=-1\n'
4+
+ 'id=31 addr=172.17.0.1:63984 laddr=172.17.0.4:6379 fd=22 name=redisinsight-cli-bc36ecf0 age=15 '
5+
+ 'idle=15 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 argv-mem=0 obl=0 oll=0 omem=0 '
6+
+ 'tot-mem=20512 events=r cmd=client user=default redir=-1\n';
7+
8+
export const mockRedisClientListResult = [
9+
{
10+
addr: '172.17.0.1:60702',
11+
age: '319',
12+
'argv-mem': '10',
13+
cmd: 'client',
14+
db: '0',
15+
events: 'r',
16+
fd: '20',
17+
flags: 'N',
18+
id: '29',
19+
idle: '0',
20+
laddr: '172.17.0.4:6379',
21+
multi: '-1',
22+
name: 'redisinsight-common-f9d59780',
23+
obl: '0',
24+
oll: '0',
25+
omem: '0',
26+
psub: '0',
27+
qbuf: '26',
28+
'qbuf-free': '40928',
29+
redir: '-1',
30+
sub: '0',
31+
'tot-mem': '61466',
32+
user: 'default',
33+
resp: '2',
34+
},
35+
{
36+
addr: '172.17.0.1:63984',
37+
age: '15',
38+
'argv-mem': '0',
39+
cmd: 'client',
40+
db: '0',
41+
events: 'r',
42+
fd: '22',
43+
flags: 'N',
44+
id: '31',
45+
idle: '15',
46+
laddr: '172.17.0.4:6379',
47+
multi: '-1',
48+
name: 'redisinsight-cli-bc36ecf0',
49+
obl: '0',
50+
oll: '0',
51+
omem: '0',
52+
psub: '0',
53+
qbuf: '0',
54+
'qbuf-free': '0',
55+
redir: '-1',
56+
sub: '0',
57+
'tot-mem': '20512',
58+
user: 'default',
59+
},
60+
];

redisinsight/api/src/__mocks__/databases.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from 'src/__mocks__/ssh';
1818
import { CloudDatabaseDetailsEntity } from 'src/modules/cloud/database/entities/cloud-database-details.entity';
1919
import { mockCloudDatabaseDetails, mockCloudDatabaseDetailsEntity } from 'src/__mocks__/cloud-database';
20+
import { mockRedisClientListResult } from 'src/__mocks__/database-info';
2021

2122
export const mockDatabaseId = 'a77b23c1-7816-4ea4-b61f-d37795a0f805-db-id';
2223

@@ -239,6 +240,7 @@ export const mockDatabaseInfoProvider = jest.fn(() => ({
239240
determineSentinelMasterGroups: jest.fn().mockReturnValue([mockSentinelMasterDto]),
240241
determineClusterNodes: jest.fn().mockResolvedValue(mockClusterNodes),
241242
getRedisGeneralInfo: jest.fn().mockResolvedValueOnce(mockRedisGeneralInfo),
243+
getClientListInfo: jest.fn().mockReturnValue(mockRedisClientListResult),
242244
}));
243245

244246
export const mockDatabaseOverviewProvider = jest.fn(() => ({
@@ -259,4 +261,5 @@ export const mockDatabaseAnalytics = jest.fn(() => ({
259261
sendInstanceAddFailedEvent: jest.fn(),
260262
sendInstanceEditedEvent: jest.fn(),
261263
sendInstanceDeletedEvent: jest.fn(),
264+
sendDatabaseConnectedClientListEvent: jest.fn(),
262265
}));

redisinsight/api/src/__mocks__/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ export * from './cloud-user';
3232
export * from './cloud-common';
3333
export * from './session';
3434
export * from './cloud-session';
35+
export * from './database-info';
3536
export * from './cloud-job';

redisinsight/api/src/constants/telemetry-events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export enum TelemetryEvents {
55
AnalyticsPermission = 'ANALYTICS_PERMISSION',
66
SettingsScanThresholdChanged = 'SETTINGS_KEYS_TO_SCAN_CHANGED',
77
SettingsWorkbenchPipelineChanged = 'SETTINGS_WORKBENCH_PIPELINE_CHANGED',
8+
DatabaseConnectedClientList = 'DATABASE_CONNECTED_CLIENT_LIST',
89

910
// Events for redis instances
1011
RedisInstanceAdded = 'CONFIG_DATABASES_DATABASE_ADDED',

redisinsight/api/src/modules/database/database-connection.service.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { UnauthorizedException } from '@nestjs/common';
22
import { Test, TestingModule } from '@nestjs/testing';
3+
import { get } from 'lodash';
34
import {
45
mockCommonClientMetadata,
56
mockDatabase,
@@ -13,6 +14,7 @@ import {
1314
mockDatabaseRecommendationService,
1415
MockType,
1516
mockRedisGeneralInfo,
17+
mockRedisClientListResult,
1618
} from 'src/__mocks__';
1719
import { DatabaseAnalytics } from 'src/modules/database/database.analytics';
1820
import { DatabaseService } from 'src/modules/database/database.service';
@@ -122,6 +124,23 @@ describe('DatabaseConnectionService', () => {
122124
expect(databaseInfoProvider.determineDatabaseServer).toHaveBeenCalled();
123125
expect(databaseInfoProvider.determineDatabaseModules).toHaveBeenCalled();
124126
});
127+
128+
it('should call getClientListInfo', async () => {
129+
expect(await service.connect(mockCommonClientMetadata)).toEqual(undefined);
130+
131+
expect(databaseInfoProvider.getClientListInfo).toHaveBeenCalled();
132+
expect(analytics.sendDatabaseConnectedClientListEvent).toHaveBeenCalledWith(
133+
mockDatabase.id,
134+
{
135+
clients: mockRedisClientListResult.map((c) => ({
136+
version: mockRedisGeneralInfo.version,
137+
resp: get(c, 'resp', 'n/a'),
138+
libVer: get(c, 'lib-ver', 'n/a'),
139+
libName: get(c, 'lib-name', 'n/a'),
140+
})),
141+
},
142+
);
143+
});
125144
});
126145

127146
describe('getOrCreateClient', () => {

redisinsight/api/src/modules/database/database-connection.service.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ export class DatabaseConnectionService {
8989
{ client, databaseId: clientMetadata.databaseId },
9090
);
9191

92+
this.collectClientInfo(clientMetadata, client, generalInfo?.version);
93+
9294
this.logger.log(`Succeed to connect to database ${clientMetadata.databaseId}`);
9395
}
9496

@@ -159,4 +161,25 @@ export class DatabaseConnectionService {
159161
throw exception;
160162
}
161163
}
164+
165+
private async collectClientInfo(clientMetadata: ClientMetadata, client: any, version?: string) {
166+
try {
167+
const intVersion = parseInt(version, 10) || 0;
168+
const clients = await this.databaseInfoProvider.getClientListInfo(client) || [];
169+
170+
this.analytics.sendDatabaseConnectedClientListEvent(
171+
clientMetadata.databaseId,
172+
{
173+
clients: clients.map((c) => ({
174+
version: version || 'n/a',
175+
resp: intVersion < 7 ? undefined : c?.['resp'] || 'n/a',
176+
libName: intVersion < 7 ? undefined : c?.['lib-name'] || 'n/a',
177+
libVer: intVersion < 7 ? undefined : c?.['lib-ver'] || 'n/a',
178+
})),
179+
},
180+
);
181+
} catch (error) {
182+
// ignore errors
183+
}
184+
}
162185
}

redisinsight/api/src/modules/database/database.analytics.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,4 +344,22 @@ describe('DatabaseAnalytics', () => {
344344
);
345345
});
346346
});
347+
348+
describe('sendDatabaseConnectedClientListEvent', () => {
349+
it('should emit event', () => {
350+
service.sendDatabaseConnectedClientListEvent(mockDatabase.id, {
351+
version: mockDatabase.version,
352+
resp: '2',
353+
});
354+
355+
expect(sendEventSpy).toHaveBeenCalledWith(
356+
TelemetryEvents.DatabaseConnectedClientList,
357+
{
358+
instanceId: mockDatabase.id,
359+
version: mockDatabase.version,
360+
resp: '2',
361+
},
362+
);
363+
});
364+
});
347365
});

redisinsight/api/src/modules/database/database.analytics.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,18 @@ export class DatabaseAnalytics extends TelemetryBaseService {
132132
},
133133
);
134134
}
135+
136+
sendDatabaseConnectedClientListEvent(instanceId: string, additionalData: object = {}): void {
137+
try {
138+
this.sendEvent(
139+
TelemetryEvents.DatabaseConnectedClientList,
140+
{
141+
instanceId,
142+
...additionalData,
143+
},
144+
);
145+
} catch (e) {
146+
// continue regardless of error
147+
}
148+
}
135149
}

redisinsight/api/src/modules/database/providers/database-info.provider.spec.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
mockIOClusterNode2,
88
mockIORedisClient,
99
mockIORedisCluster,
10+
mockRedisClientList,
11+
mockRedisClientListResult,
1012
mockRedisClientsInfoResponse,
1113
mockRedisClusterFailInfoResponse,
1214
mockRedisClusterNodesResponse,
@@ -23,7 +25,7 @@ import {
2325
import { REDIS_MODULES_COMMANDS, AdditionalRedisModuleName } from 'src/constants';
2426
import { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider';
2527
import { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';
26-
import { BadRequestException, ForbiddenException } from '@nestjs/common';
28+
import { BadRequestException, ForbiddenException, InternalServerErrorException } from '@nestjs/common';
2729
import { SentinelMasterStatus } from 'src/modules/redis-sentinel/models/sentinel-master';
2830
import ERROR_MESSAGES from 'src/constants/error-messages';
2931
import { FeatureService } from 'src/modules/feature/feature.service';
@@ -219,6 +221,29 @@ describe('DatabaseInfoProvider', () => {
219221
});
220222
});
221223

224+
describe('getClientListInfo', () => {
225+
it('get client list info', async () => {
226+
when(mockIORedisClient.call)
227+
.calledWith('client', ['list'])
228+
.mockResolvedValue(mockRedisClientList);
229+
230+
const result = await service.getClientListInfo(mockIORedisClient);
231+
232+
expect(result).toEqual(mockRedisClientListResult);
233+
});
234+
it('failed to get client list', async () => {
235+
when(mockIORedisClient.call)
236+
.calledWith('client', ['list'])
237+
.mockRejectedValue(new Error("unknown command 'client'"));
238+
239+
try {
240+
await service.getClientListInfo(mockIORedisClient)
241+
} catch (err) {
242+
expect(err).toBeInstanceOf(InternalServerErrorException);
243+
}
244+
});
245+
});
246+
222247
describe('getDatabaseCountFromKeyspace', () => {
223248
it('should return 1 since db0 keys presented only', async () => {
224249
const result = await service['getDatabaseCountFromKeyspace']({

redisinsight/api/src/modules/database/providers/database-info.provider.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,19 @@ export class DatabaseInfoProvider {
302302
}
303303
}
304304

305+
public async getClientListInfo(client: any): Promise<any[]> {
306+
try {
307+
const clientListResponse = await client.call('client', ['list']);
308+
309+
return clientListResponse
310+
.split(/\r?\n/)
311+
.filter(Boolean)
312+
.map((r) => convertBulkStringsToObject(r, ' ', '='));
313+
} catch (error) {
314+
throw catchAclError(error);
315+
}
316+
}
317+
305318
/**
306319
* Try to determine number of logical database from the `info keyspace`
307320
*

0 commit comments

Comments
 (0)