Add webrtc metric to OTel (#974)
* stats: Add summery report --------- Co-authored-by: David Baker <dave@matrix.org>
This commit is contained in:
parent
28196a2e9d
commit
390442a4c3
7 changed files with 478 additions and 26 deletions
|
|
@ -32,9 +32,17 @@ import {
|
|||
CallsByUserAndDevice,
|
||||
GroupCallError,
|
||||
GroupCallEvent,
|
||||
GroupCallStatsReport,
|
||||
} from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import {
|
||||
ConnectionStatsReport,
|
||||
ByteSentStatsReport,
|
||||
SummaryStatsReport,
|
||||
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
|
||||
import { setSpan } from "@opentelemetry/api/build/esm/trace/context-utils";
|
||||
|
||||
import { ElementCallOpenTelemetry } from "./otel";
|
||||
import { ObjectFlattener } from "./ObjectFlattener";
|
||||
|
||||
/**
|
||||
* Flattens out an object into a single layer with components
|
||||
|
|
@ -91,16 +99,26 @@ interface CallTrackingInfo {
|
|||
export class OTelGroupCallMembership {
|
||||
private callMembershipSpan?: Span;
|
||||
private groupCallContext?: Context;
|
||||
private myUserId: string;
|
||||
private myUserId = "unknown";
|
||||
private myDeviceId: string;
|
||||
private myMember: RoomMember;
|
||||
private myMember?: RoomMember;
|
||||
private callsByCallId = new Map<string, CallTrackingInfo>();
|
||||
private statsReportSpan: {
|
||||
span: Span | undefined;
|
||||
stats: OTelStatsReportEvent[];
|
||||
};
|
||||
|
||||
constructor(private groupCall: GroupCall, client: MatrixClient) {
|
||||
this.myUserId = client.getUserId();
|
||||
this.myDeviceId = client.getDeviceId();
|
||||
this.myMember = groupCall.room.getMember(client.getUserId());
|
||||
|
||||
const clientId = client.getUserId();
|
||||
if (clientId) {
|
||||
this.myUserId = clientId;
|
||||
const myMember = groupCall.room.getMember(clientId);
|
||||
if (myMember) {
|
||||
this.myMember = myMember;
|
||||
}
|
||||
}
|
||||
this.myDeviceId = client.getDeviceId() || "unknown";
|
||||
this.statsReportSpan = { span: undefined, stats: [] };
|
||||
this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged);
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +143,7 @@ export class OTelGroupCallMembership {
|
|||
this.callMembershipSpan.setAttribute("matrix.deviceId", this.myDeviceId);
|
||||
this.callMembershipSpan.setAttribute(
|
||||
"matrix.displayName",
|
||||
this.myMember.name
|
||||
this.myMember ? this.myMember.name : "unknown-name"
|
||||
);
|
||||
|
||||
this.groupCallContext = opentelemetry.trace.setSpan(
|
||||
|
|
@ -308,4 +326,80 @@ export class OTelGroupCallMembership {
|
|||
"sender.userId": event.getSender(),
|
||||
});
|
||||
}
|
||||
|
||||
public onConnectionStatsReport(
|
||||
statsReport: GroupCallStatsReport<ConnectionStatsReport>
|
||||
) {
|
||||
const type = OTelStatsReportType.ConnectionReport;
|
||||
const data =
|
||||
ObjectFlattener.flattenConnectionStatsReportObject(statsReport);
|
||||
this.buildStatsEventSpan({ type, data });
|
||||
}
|
||||
|
||||
public onByteSentStatsReport(
|
||||
statsReport: GroupCallStatsReport<ByteSentStatsReport>
|
||||
) {
|
||||
const type = OTelStatsReportType.ByteSentReport;
|
||||
const data = ObjectFlattener.flattenByteSentStatsReportObject(statsReport);
|
||||
this.buildStatsEventSpan({ type, data });
|
||||
}
|
||||
|
||||
public onSummaryStatsReport(
|
||||
statsReport: GroupCallStatsReport<SummaryStatsReport>
|
||||
) {
|
||||
const type = OTelStatsReportType.SummaryReport;
|
||||
const data = ObjectFlattener.flattenSummaryStatsReportObject(statsReport);
|
||||
this.buildStatsEventSpan({ type, data });
|
||||
}
|
||||
|
||||
private buildStatsEventSpan(event: OTelStatsReportEvent): void {
|
||||
// @ TODO: fix this - Because on multiple calls we receive multiple stats report spans.
|
||||
// This could be break if stats arrived in same time from different call objects.
|
||||
if (this.statsReportSpan.span === undefined && this.callMembershipSpan) {
|
||||
const ctx = setSpan(
|
||||
opentelemetry.context.active(),
|
||||
this.callMembershipSpan
|
||||
);
|
||||
this.statsReportSpan.span =
|
||||
ElementCallOpenTelemetry.instance.tracer.startSpan(
|
||||
"matrix.groupCallMembership.statsReport",
|
||||
undefined,
|
||||
ctx
|
||||
);
|
||||
this.statsReportSpan.span.setAttribute(
|
||||
"matrix.confId",
|
||||
this.groupCall.groupCallId
|
||||
);
|
||||
this.statsReportSpan.span.setAttribute("matrix.userId", this.myUserId);
|
||||
this.statsReportSpan.span.setAttribute(
|
||||
"matrix.displayName",
|
||||
this.myMember ? this.myMember.name : "unknown-name"
|
||||
);
|
||||
|
||||
this.statsReportSpan.span.addEvent(event.type, event.data);
|
||||
this.statsReportSpan.stats.push(event);
|
||||
} else if (
|
||||
this.statsReportSpan.span !== undefined &&
|
||||
this.callMembershipSpan
|
||||
) {
|
||||
this.statsReportSpan.span.addEvent(event.type, event.data);
|
||||
this.statsReportSpan.stats.push(event);
|
||||
// if received all three types of stats close this
|
||||
if (this.statsReportSpan.stats.length === 3) {
|
||||
this.statsReportSpan.span.end();
|
||||
this.statsReportSpan = { span: undefined, stats: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface OTelStatsReportEvent {
|
||||
type: OTelStatsReportType;
|
||||
data: Attributes;
|
||||
}
|
||||
|
||||
enum OTelStatsReportType {
|
||||
ConnectionReport = "matrix.stats.connection",
|
||||
ByteSentReport = "matrix.stats.byteSent",
|
||||
SummaryReport = "matrix.stats.summary",
|
||||
}
|
||||
|
|
|
|||
97
src/otel/ObjectFlattener.ts
Normal file
97
src/otel/ObjectFlattener.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { Attributes } from "@opentelemetry/api";
|
||||
import { GroupCallStatsReport } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import {
|
||||
ByteSentStatsReport,
|
||||
ConnectionStatsReport,
|
||||
SummaryStatsReport,
|
||||
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
|
||||
|
||||
export class ObjectFlattener {
|
||||
public static flattenConnectionStatsReportObject(
|
||||
statsReport: GroupCallStatsReport<ConnectionStatsReport>
|
||||
): Attributes {
|
||||
const flatObject = {};
|
||||
ObjectFlattener.flattenObjectRecursive(
|
||||
statsReport.report,
|
||||
flatObject,
|
||||
"matrix.stats.conn.",
|
||||
0
|
||||
);
|
||||
return flatObject;
|
||||
}
|
||||
|
||||
public static flattenByteSentStatsReportObject(
|
||||
statsReport: GroupCallStatsReport<ByteSentStatsReport>
|
||||
): Attributes {
|
||||
const flatObject = {};
|
||||
ObjectFlattener.flattenObjectRecursive(
|
||||
statsReport.report,
|
||||
flatObject,
|
||||
"matrix.stats.bytesSent.",
|
||||
0
|
||||
);
|
||||
return flatObject;
|
||||
}
|
||||
|
||||
static flattenSummaryStatsReportObject(
|
||||
statsReport: GroupCallStatsReport<SummaryStatsReport>
|
||||
) {
|
||||
const flatObject = {};
|
||||
ObjectFlattener.flattenObjectRecursive(
|
||||
statsReport.report,
|
||||
flatObject,
|
||||
"matrix.stats.summary.",
|
||||
0
|
||||
);
|
||||
return flatObject;
|
||||
}
|
||||
|
||||
public static flattenObjectRecursive(
|
||||
obj: Object,
|
||||
flatObject: Attributes,
|
||||
prefix: string,
|
||||
depth: number
|
||||
): void {
|
||||
if (depth > 10)
|
||||
throw new Error(
|
||||
"Depth limit exceeded: aborting VoipEvent recursion. Prefix is " +
|
||||
prefix
|
||||
);
|
||||
let entries;
|
||||
if (obj instanceof Map) {
|
||||
entries = obj.entries();
|
||||
} else {
|
||||
entries = Object.entries(obj);
|
||||
}
|
||||
for (const [k, v] of entries) {
|
||||
if (["string", "number", "boolean"].includes(typeof v) || v === null) {
|
||||
let value;
|
||||
value = v === null ? "null" : v;
|
||||
value = typeof v === "number" && Number.isNaN(v) ? "NaN" : value;
|
||||
flatObject[prefix + k] = value;
|
||||
} else if (typeof v === "object") {
|
||||
ObjectFlattener.flattenObjectRecursive(
|
||||
v,
|
||||
flatObject,
|
||||
prefix + k + ".",
|
||||
depth + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -48,7 +48,7 @@ export class ElementCallOpenTelemetry {
|
|||
return sharedInstance;
|
||||
}
|
||||
|
||||
constructor(collectorUrl: string) {
|
||||
constructor(collectorUrl: string | undefined) {
|
||||
const otlpExporter = new OTLPTraceExporter({
|
||||
url: collectorUrl,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,12 +22,19 @@ import {
|
|||
GroupCallErrorCode,
|
||||
GroupCallUnknownDeviceError,
|
||||
GroupCallError,
|
||||
GroupCallStatsReportEvent,
|
||||
GroupCallStatsReport,
|
||||
} from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IWidgetApiRequest } from "matrix-widget-api";
|
||||
import { MatrixClient } from "matrix-js-sdk";
|
||||
import {
|
||||
ByteSentStatsReport,
|
||||
ConnectionStatsReport,
|
||||
SummaryStatsReport,
|
||||
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
|
||||
|
||||
import { usePageUnload } from "./usePageUnload";
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
|
|
@ -337,6 +344,24 @@ export function useGroupCall(
|
|||
}
|
||||
}
|
||||
|
||||
function onConnectionStatsReport(
|
||||
report: GroupCallStatsReport<ConnectionStatsReport>
|
||||
): void {
|
||||
groupCallOTelMembership?.onConnectionStatsReport(report);
|
||||
}
|
||||
|
||||
function onByteSentStatsReport(
|
||||
report: GroupCallStatsReport<ByteSentStatsReport>
|
||||
): void {
|
||||
groupCallOTelMembership?.onByteSentStatsReport(report);
|
||||
}
|
||||
|
||||
function onSummaryStatsReport(
|
||||
report: GroupCallStatsReport<SummaryStatsReport>
|
||||
): void {
|
||||
groupCallOTelMembership?.onSummaryStatsReport(report);
|
||||
}
|
||||
|
||||
groupCall.on(GroupCallEvent.GroupCallStateChanged, onGroupCallStateChanged);
|
||||
groupCall.on(GroupCallEvent.UserMediaFeedsChanged, onUserMediaFeedsChanged);
|
||||
groupCall.on(
|
||||
|
|
@ -353,6 +378,18 @@ export function useGroupCall(
|
|||
groupCall.on(GroupCallEvent.ParticipantsChanged, onParticipantsChanged);
|
||||
groupCall.on(GroupCallEvent.Error, onError);
|
||||
|
||||
groupCall.on(
|
||||
GroupCallStatsReportEvent.ConnectionStats,
|
||||
onConnectionStatsReport
|
||||
);
|
||||
|
||||
groupCall.on(
|
||||
GroupCallStatsReportEvent.ByteSentStats,
|
||||
onByteSentStatsReport
|
||||
);
|
||||
|
||||
groupCall.on(GroupCallStatsReportEvent.SummaryStats, onSummaryStatsReport);
|
||||
|
||||
updateState({
|
||||
error: null,
|
||||
state: groupCall.state,
|
||||
|
|
@ -399,6 +436,18 @@ export function useGroupCall(
|
|||
onParticipantsChanged
|
||||
);
|
||||
groupCall.removeListener(GroupCallEvent.Error, onError);
|
||||
groupCall.removeListener(
|
||||
GroupCallStatsReportEvent.ConnectionStats,
|
||||
onConnectionStatsReport
|
||||
);
|
||||
groupCall.removeListener(
|
||||
GroupCallStatsReportEvent.ByteSentStats,
|
||||
onByteSentStatsReport
|
||||
);
|
||||
groupCall.removeListener(
|
||||
GroupCallStatsReportEvent.SummaryStats,
|
||||
onSummaryStatsReport
|
||||
);
|
||||
leaveCall();
|
||||
};
|
||||
}, [groupCall, updateState, leaveCall]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue