Skip to content

Commit 90235fc

Browse files
committed
feat: metrics on reachability subnet
1 parent f027038 commit 90235fc

File tree

4 files changed

+122
-5
lines changed

4 files changed

+122
-5
lines changed

packages/@webex/plugin-meetings/src/meeting/index.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,7 @@ export default class Meeting extends StatelessWebexPlugin {
722722
private rtcMetrics?: RtcMetrics;
723723
private uploadLogsTimer?: ReturnType<typeof setTimeout>;
724724
private logUploadIntervalIndex: number;
725+
private mediaServerIp: string;
725726

726727
/**
727728
* @param {Object} attrs
@@ -1598,6 +1599,15 @@ export default class Meeting extends StatelessWebexPlugin {
15981599
* @memberof Meeting
15991600
*/
16001601
this.#isoLocalClientMeetingJoinTime = undefined;
1602+
1603+
/**
1604+
* IP Address of remote media server
1605+
* @instance
1606+
* @type {number}
1607+
* @private
1608+
* @memberof Meeting
1609+
*/
1610+
this.mediaServerIp = null;
16011611
}
16021612

16031613
/**
@@ -6329,6 +6339,11 @@ export default class Meeting extends StatelessWebexPlugin {
63296339
? MeetingsUtil.getMediaServer(roapMessage.sdp)
63306340
: undefined;
63316341

6342+
const mediaServerIp =
6343+
roapMessage.messageType === 'ANSWER'
6344+
? MeetingsUtil.getMediaServerIp(roapMessage.sdp)
6345+
: undefined;
6346+
63326347
if (this.isMultistream && mediaServer && mediaServer !== 'homer') {
63336348
throw new MultistreamNotSupportedError(
63346349
`Client asked for multistream backend (Homer), but got ${mediaServer} instead`
@@ -6339,6 +6354,10 @@ export default class Meeting extends StatelessWebexPlugin {
63396354
if (mediaServer) {
63406355
this.mediaProperties.webrtcMediaConnection.mediaServer = mediaServer;
63416356
}
6357+
6358+
if (this.isMultistream && mediaServerIp) {
6359+
this.mediaServerIp = mediaServerIp;
6360+
}
63426361
};
63436362

63446363
/**
@@ -7741,6 +7760,11 @@ export default class Meeting extends StatelessWebexPlugin {
77417760
const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics();
77427761
const iceCandidateErrors = Object.fromEntries(this.iceCandidateErrors);
77437762

7763+
const isSubnetReachable = this.getWebexObject().meetings.reachability.isSubnetReachable(
7764+
this.mediaConnections[0]?.mediaAgentCluster,
7765+
this.mediaServerIp
7766+
);
7767+
77447768
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
77457769
correlation_id: this.correlationId,
77467770
locus_id: this.locusUrl.split('/').pop(),
@@ -7750,6 +7774,7 @@ export default class Meeting extends StatelessWebexPlugin {
77507774
isMultistream: this.isMultistream,
77517775
retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
77527776
isJoinWithMediaRetry: this.joinWithMediaRetryInfo.isRetry,
7777+
isSubnetReachable,
77537778
...reachabilityStats,
77547779
...iceCandidateErrors,
77557780
iceCandidatesCount: this.iceCandidatesCount,
@@ -7779,6 +7804,11 @@ export default class Meeting extends StatelessWebexPlugin {
77797804

77807805
const iceCandidateErrors = Object.fromEntries(this.iceCandidateErrors);
77817806

7807+
const isSubnetReachable = this.getWebexObject().meetings.reachability.isSubnetReachable(
7808+
this.mediaConnections[0]?.mediaAgentCluster,
7809+
this.mediaServerIp
7810+
);
7811+
77827812
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
77837813
correlation_id: this.correlationId,
77847814
locus_id: this.locusUrl.split('/').pop(),
@@ -7808,6 +7838,7 @@ export default class Meeting extends StatelessWebexPlugin {
78087838
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
78097839
'unknown',
78107840
...reachabilityMetrics,
7841+
isSubnetReachable,
78117842
...iceCandidateErrors,
78127843
iceCandidatesCount: this.iceCandidatesCount,
78137844
});

packages/@webex/plugin-meetings/src/meetings/util.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,28 @@ MeetingsUtil.getMediaServer = (sdp) => {
9999
return mediaServer;
100100
};
101101

102+
MeetingsUtil.getMediaServerIp = (sdp) => {
103+
let mediaServerIp;
104+
105+
// Attempt to collect the media server from the roap message.
106+
try {
107+
mediaServerIp = sdp
108+
.split('\r\n')
109+
.find(
110+
(line) =>
111+
// Make sure we non-FQDN
112+
line.startsWith('a=candidate') && line.includes('UDP') && !line.includes('webex.com')
113+
)
114+
.match(/a=candidate:\d+ \d+ \w+ \d+ ([\d.]+) \d+ typ \w+/)?.[1]
115+
.toLowerCase()
116+
.trim();
117+
} catch {
118+
mediaServerIp = undefined;
119+
}
120+
121+
return mediaServerIp;
122+
};
123+
102124
MeetingsUtil.checkForCorrelationId = (deviceUrl, locus) => {
103125
let devices = [];
104126

packages/@webex/plugin-meetings/src/reachability/clusterReachability.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export class ClusterReachability extends EventsScope {
4949
private srflxIceCandidates: RTCIceCandidate[] = [];
5050
public readonly isVideoMesh: boolean;
5151
public readonly name;
52+
private reachedSubnets: Set<string> = new Set();
5253

5354
/**
5455
* Constructor for ClusterReachability
@@ -264,9 +265,15 @@ export class ClusterReachability extends EventsScope {
264265
* @param {string} protocol
265266
* @param {number} latency
266267
* @param {string|null} [publicIp]
268+
* @param {string|null} [serverIp]
267269
* @returns {void}
268270
*/
269-
private saveResult(protocol: 'udp' | 'tcp' | 'xtls', latency: number, publicIp?: string | null) {
271+
private saveResult(
272+
protocol: 'udp' | 'tcp' | 'xtls',
273+
latency: number,
274+
publicIp?: string | null,
275+
serverIp?: string | null
276+
) {
270277
const result = this.result[protocol];
271278

272279
if (result.latencyInMilliseconds === undefined) {
@@ -294,6 +301,10 @@ export class ClusterReachability extends EventsScope {
294301
} else {
295302
this.addPublicIP(protocol, publicIp);
296303
}
304+
305+
if (serverIp) {
306+
this.reachedSubnets.add(serverIp);
307+
}
297308
}
298309

299310
/**
@@ -351,16 +362,25 @@ export class ClusterReachability extends EventsScope {
351362

352363
if (e.candidate) {
353364
if (e.candidate.type === CANDIDATE_TYPES.SERVER_REFLEXIVE) {
354-
this.saveResult('udp', latencyInMilliseconds, e.candidate.address);
365+
let serverIp = null;
366+
if ('url' in e.candidate) {
367+
const regex = /stun:([\d.]+):\d+/;
368+
369+
const match = (e.candidate as any).url.match(regex);
370+
if (match) {
371+
// eslint-disable-next-line prefer-destructuring
372+
serverIp = match[1];
373+
}
374+
}
375+
376+
this.saveResult('udp', latencyInMilliseconds, e.candidate.address, serverIp);
355377

356378
this.determineNatType(e.candidate);
357379
}
358380

359381
if (e.candidate.type === CANDIDATE_TYPES.RELAY) {
360382
const protocol = e.candidate.port === TURN_TLS_PORT ? 'xtls' : 'tcp';
361-
this.saveResult(protocol, latencyInMilliseconds);
362-
// we don't add public IP for TCP, because in the case of relay candidates
363-
// e.candidate.address is the TURN server address, not the client's public IP
383+
this.saveResult(protocol, latencyInMilliseconds, null, e.candidate.address);
364384
}
365385

366386
if (this.haveWeGotAllResults()) {
@@ -429,4 +449,28 @@ export class ClusterReachability extends EventsScope {
429449

430450
return this.defer.promise;
431451
}
452+
453+
/**
454+
* Checks if the media server is reachable
455+
* @param {boolean} mediaServerIp - media server ip
456+
* @returns {boolean} true if reachable, false otherwise
457+
*/
458+
public isMediaServerReachable(mediaServerIp: string): boolean {
459+
const subnetFirstOctet = mediaServerIp.split('.')[0];
460+
461+
const foundSubnet = Array.from(this.reachedSubnets).find((reachedSubnet) =>
462+
reachedSubnet.startsWith(subnetFirstOctet)
463+
);
464+
465+
if (!foundSubnet) {
466+
let errorMessage = `Reachability:ClusterReachability#isSubnetReachable --> Subnet ${subnetFirstOctet} in ${this.name} is not reachable, reached subnets: \n`;
467+
this.reachedSubnets.forEach((s) => {
468+
errorMessage += `\t${s}\n`;
469+
});
470+
471+
LoggerProxy.logger.error(errorMessage);
472+
}
473+
474+
return !!foundSubnet;
475+
}
432476
}

packages/@webex/plugin-meetings/src/reachability/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,26 @@ export default class Reachability extends EventsScope {
138138
}
139139
}
140140

141+
/**
142+
* Checks if the given subnet is reachable
143+
* @param {string} selectedCluster - cluster to check
144+
* @param {string} mediaServerIp - media server ip
145+
* @returns {boolean} true if reachable, false otherwise
146+
* @public
147+
* @memberof Reachability
148+
*/
149+
public isSubnetReachable(selectedCluster: string, mediaServerIp: string): boolean {
150+
const cluster = Object.values(this.clusterReachability).find((c) =>
151+
c.name.startsWith(selectedCluster)
152+
);
153+
154+
if (!cluster) {
155+
return false;
156+
}
157+
158+
return cluster.isMediaServerReachable(mediaServerIp);
159+
}
160+
141161
/**
142162
* Gets a list of media clusters from the backend and performs reachability checks on all the clusters
143163
* @param {string} trigger - explains the reason for starting reachability

0 commit comments

Comments
 (0)