Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Count daily and monthly peaks of concurrent connections #30573

Merged
merged 18 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/slow-coats-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/core-services": patch
"@rocket.chat/core-typings": patch
"@rocket.chat/model-typings": patch
"@rocket.chat/rest-typings": patch
"@rocket.chat/presence": patch
---

Count daily and monthly peaks of concurrent connections
- Added `dailyPeakConnections` statistic for monitoring the daily peak of concurrent connections in a workspace;
- Added `presence.getMonthlyPeakConnections` endpoint to enable workspaces to retrieve their monthly peak of concurrent connections.
12 changes: 12 additions & 0 deletions apps/meteor/app/api/server/v1/presence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
'presence.getMonthlyPeakConnections',
sampaiodiego marked this conversation as resolved.
Show resolved Hide resolved
{ authRequired: true, permissionsRequired: ['manage-user-status'] },
{
async get() {
const result = await Presence.getMonthlyPeakConnections();

return API.v1.success(result);
},
},
);

API.v1.addRoute(
'presence.enableBroadcast',
{ authRequired: true, permissionsRequired: ['manage-user-status'], twoFactorRequired: true },
Expand Down
5 changes: 4 additions & 1 deletion apps/meteor/app/statistics/server/lib/statistics.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { log } from 'console';
import os from 'os';

import { Analytics, Team, VideoConf } from '@rocket.chat/core-services';
import { Analytics, Team, VideoConf, Presence } from '@rocket.chat/core-services';
import type { IRoom, IStats } from '@rocket.chat/core-typings';
import { UserStatus } from '@rocket.chat/core-typings';
import {
Expand Down Expand Up @@ -544,6 +544,8 @@ export const statistics = {
const defaultLoggedInCustomScript = (await Settings.findOneById('Custom_Script_Logged_In'))?.packageValue;
statistics.loggedInCustomScriptChanged = settings.get('Custom_Script_Logged_In') !== defaultLoggedInCustomScript;

statistics.dailyPeakConnections = await Presence.getDailyPeakConnections();

statistics.matrixFederation = await getMatrixFederationStatistics();

// Omnichannel call stats
Expand All @@ -559,6 +561,7 @@ export const statistics = {
const rcStatistics = await statistics.get();
rcStatistics.createdAt = new Date();
await Statistics.insertOne(rcStatistics);
await Presence.resetPeakConnections();
return rcStatistics;
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ export default {
totalWebRTCCalls: 0,
uncaughtExceptionsCount: 0,
push: 0,
dailyPeakConnections: 0,
matrixFederation: {
enabled: false,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ export default {
totalWebRTCCalls: 0,
uncaughtExceptionsCount: 0,
push: 0,
dailyPeakConnections: 0,
matrixFederation: {
enabled: false,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ export default {
totalWebRTCCalls: 0,
uncaughtExceptionsCount: 0,
push: 0,
dailyPeakConnections: 0,
matrixFederation: {
enabled: false,
},
Expand Down
22 changes: 22 additions & 0 deletions apps/meteor/server/models/raw/Statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,26 @@ export class StatisticsRaw extends BaseRaw<IStats> implements IStatisticsModel {
).toArray();
return records?.[0];
}

async findMaxMonthlyPeakConnections(): Promise<Pick<IStats, 'dailyPeakConnections' | 'createdAt'> | undefined> {
const oneMonthAgo = new Date();
oneMonthAgo.setDate(oneMonthAgo.getDate() - 30.5);
oneMonthAgo.setHours(0, 0, 0, 0);

const record = await this.findOne(
{
createdAt: { $gte: oneMonthAgo },
},
{
sort: {
dailyPeakConnections: -1,
},
projection: {
dailyPeakConnections: 1,
createdAt: 1,
},
},
);
return record ?? undefined;
}
}
55 changes: 55 additions & 0 deletions apps/meteor/tests/end-to-end/api/27-presence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,61 @@ describe('[Presence]', function () {
});
});

describe('[/presence.getMonthlyPeakConnections]', () => {
it('should throw an error if not authenticated', async () => {
await request
.get(api('presence.getMonthlyPeakConnections'))
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res: Response) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
});

it('should throw an error if user is unauthorized', async () => {
await request
.get(api('presence.getMonthlyPeakConnections'))
.set(unauthorizedUserCredentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
});
});

it("should throw an error if doesn't have required permission", async () => {
await updatePermission('manage-user-status', []);

await request
.get(api('presence.getMonthlyPeakConnections'))
.set(unauthorizedUserCredentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
});

await updatePermission('manage-user-status', ['admin']);
});

it('should return current and max connections of 200', async () => {
await request
.get(api('presence.getMonthlyPeakConnections'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('peak').to.be.a('number');
expect(res.body).to.have.property('date');
expect(res.body).to.have.property('max').to.be.a('number').and.to.be.equal(200);
});
});
});

describe('[/presence.enableBroadcast]', () => {
it('should throw an error if not authenticated', async () => {
await request
Expand Down
41 changes: 39 additions & 2 deletions ee/packages/presence/src/Presence.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { IPresence, IBrokerNode } from '@rocket.chat/core-services';
import { License, ServiceClass } from '@rocket.chat/core-services';
import type { IUser } from '@rocket.chat/core-typings';
import type { IUser, IStats } from '@rocket.chat/core-typings';
import { UserStatus } from '@rocket.chat/core-typings';
import { Settings, Users, UsersSessions } from '@rocket.chat/models';
import { Settings, Users, UsersSessions, Statistics } from '@rocket.chat/models';

import { processPresenceAndStatus } from './lib/processConnectionStatus';

Expand All @@ -19,6 +19,10 @@ export class Presence extends ServiceClass implements IPresence {

private connsPerInstance = new Map<string, number>();

private monthlyPeakConnections: Pick<IStats, 'dailyPeakConnections' | 'createdAt'>;

private dailyPeakConnections = this.getTotalConnections();

constructor() {
super();

Expand All @@ -35,6 +39,10 @@ export class Presence extends ServiceClass implements IPresence {
if (diff?.hasOwnProperty('extraInformation.conns')) {
this.connsPerInstance.set(id, diff['extraInformation.conns']);

this.dailyPeakConnections = Math.max(this.dailyPeakConnections, this.getTotalConnections());
if (this.dailyPeakConnections > this.monthlyPeakConnections.dailyPeakConnections) {
this.setDailyPeakAsMonthly();
}
this.validateAvailability();
}
});
Expand All @@ -57,6 +65,7 @@ export class Presence extends ServiceClass implements IPresence {
}

async started(): Promise<void> {
await this.resetPeakConnections();
this.lostConTimeout = setTimeout(async () => {
const affectedUsers = await this.removeLostConnections();
return affectedUsers.forEach((uid) => this.updateUserPresence(uid));
Expand Down Expand Up @@ -97,6 +106,14 @@ export class Presence extends ServiceClass implements IPresence {
};
}

getMonthlyPeakConnections(): { peak: number; date: Date; max: number } {
return {
peak: this.monthlyPeakConnections.dailyPeakConnections,
date: new Date(this.monthlyPeakConnections.createdAt),
max: MAX_CONNECTIONS,
};
}

async newConnection(
uid: string | undefined,
session: string | undefined,
Expand Down Expand Up @@ -251,4 +268,24 @@ export class Presence extends ServiceClass implements IPresence {
private getTotalConnections(): number {
return Array.from(this.connsPerInstance.values()).reduce((acc, conns) => acc + conns, 0);
}

private setDailyPeakAsMonthly(): void {
this.monthlyPeakConnections = {
dailyPeakConnections: this.dailyPeakConnections,
createdAt: new Date(),
};
}

getDailyPeakConnections(): number {
return this.dailyPeakConnections;
}

async resetPeakConnections(): Promise<void> {
this.dailyPeakConnections = this.getTotalConnections();
const monthlyPeakConnections = await Statistics.findMaxMonthlyPeakConnections();
if (!monthlyPeakConnections || this.dailyPeakConnections > monthlyPeakConnections.dailyPeakConnections) {
return this.setDailyPeakAsMonthly();
}
this.monthlyPeakConnections = monthlyPeakConnections;
}
}
3 changes: 3 additions & 0 deletions packages/core-services/src/types/IPresence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ export interface IPresence extends IServiceClass {
updateUserPresence(uid: string): Promise<void>;
toggleBroadcast(enabled: boolean): void;
getConnectionCount(): { current: number; max: number };
getMonthlyPeakConnections(): { peak: number; date: Date; max: number };
getDailyPeakConnections(): number;
resetPeakConnections(): Promise<void>;
}
1 change: 1 addition & 0 deletions packages/core-typings/src/IStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ export interface IStats {
totalWebRTCCalls: number;
uncaughtExceptionsCount: number;
push: number;
dailyPeakConnections: number;
matrixFederation: {
enabled: boolean;
};
Expand Down
1 change: 1 addition & 0 deletions packages/model-typings/src/models/IStatisticsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import type { IBaseModel } from './IBaseModel';

export interface IStatisticsModel extends IBaseModel<IStats> {
findLast(): Promise<IStats>;
findMaxMonthlyPeakConnections(): Promise<Pick<IStats, 'dailyPeakConnections' | 'createdAt'> | undefined>;
}
7 changes: 7 additions & 0 deletions packages/rest-typings/src/v1/presence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ export type PresenceEndpoints = {
max: number;
};
};
'/v1/presence.getMonthlyPeakConnections': {
GET: () => {
peak: number;
date: Date;
max: number;
};
};
'/v1/presence.enableBroadcast': {
POST: () => void;
};
Expand Down
Loading