From 4c59638d00f28751466f43f822b472be88d7509a Mon Sep 17 00:00:00 2001 From: Timo K Date: Fri, 10 Mar 2023 10:33:54 +0100 Subject: [PATCH 01/34] otel for call start, end and mute This is send over zipkin. And it uses a posthog exporter to export events to posthog using a _otel prefix --- package.json | 8 + src/analytics/OtelPosthogExporter.ts | 57 ++++++ src/analytics/PosthogAnalytics.ts | 16 ++ src/room/GroupCallView.tsx | 5 +- src/room/useGroupCall.ts | 3 + src/telemetry/otel.ts | 128 ++++++++++++ yarn.lock | 285 +++++++++++++++++++++++++-- 7 files changed, 490 insertions(+), 12 deletions(-) create mode 100644 src/analytics/OtelPosthogExporter.ts create mode 100644 src/telemetry/otel.ts diff --git a/package.json b/package.json index 4242b19..0adbe94 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,14 @@ "dependencies": { "@juggle/resize-observer": "^3.3.1", "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz", + "@opentelemetry/api": "^1.4.0", + "@opentelemetry/context-zone": "^1.9.1", + "@opentelemetry/exporter-jaeger": "^1.9.1", + "@opentelemetry/exporter-trace-otlp-http": "^0.35.1", + "@opentelemetry/exporter-zipkin": "^1.9.1", + "@opentelemetry/instrumentation-document-load": "^0.31.1", + "@opentelemetry/instrumentation-user-interaction": "^0.32.1", + "@opentelemetry/sdk-trace-web": "^1.9.1", "@react-aria/button": "^3.3.4", "@react-aria/dialog": "^3.1.4", "@react-aria/focus": "^3.5.0", diff --git a/src/analytics/OtelPosthogExporter.ts b/src/analytics/OtelPosthogExporter.ts new file mode 100644 index 0000000..ee72155 --- /dev/null +++ b/src/analytics/OtelPosthogExporter.ts @@ -0,0 +1,57 @@ +import { SpanExporter } from "@opentelemetry/sdk-trace-base"; +import { ReadableSpan } from "@opentelemetry/sdk-trace-base"; +import { ExportResult, ExportResultCode } from "@opentelemetry/core"; + +import { PosthogAnalytics } from "./PosthogAnalytics"; +/** + * This is implementation of {@link SpanExporter} that prints spans to the + * console. This class can be used for diagnostic purposes. + */ +export class PosthogSpanExporter implements SpanExporter { + /** + * Export spans. + * @param spans + * @param resultCallback + */ + async export( + spans: ReadableSpan[], + resultCallback: (result: ExportResult) => void + ): Promise { + console.log("POSTHOGEXPORTER", spans); + for (let i = 0; i < spans.length; i++) { + const span = spans[i]; + const sendInstantly = + span.name == "otel_callEnded" || + span.name == "otel_otherSentInstantlyEventName"; + + await PosthogAnalytics.instance.trackFromSpan( + { eventName: span.name, ...span.attributes }, + { + send_instantly: sendInstantly, + } + ); + resultCallback({ code: ExportResultCode.SUCCESS }); + } + } + /** + * Shutdown the exporter. + */ + shutdown(): Promise { + console.log("POSTHOGEXPORTER shutdown of otelPosthogExporter"); + return new Promise((resolve, _reject) => { + resolve(); + }); + } + /** + * converts span info into more readable format + * @param span + */ + // private _exportInfo; + /** + * Showing spans in console + * @param spans + * @param done + */ + // private _sendSpans; +} +//# sourceMappingURL=ConsoleSpanExporter.d.ts.map diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index e2e8fda..718a49c 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -385,6 +385,22 @@ export class PosthogAnalytics { this.capture(eventName, properties, options); } + public async trackFromSpan( + { eventName, ...properties }, + options?: CaptureOptions + ): Promise { + if (this.identificationPromise) { + // only make calls to posthog after the identificaion is done + await this.identificationPromise; + } + if ( + this.anonymity == Anonymity.Disabled || + this.anonymity == Anonymity.Anonymous + ) + return; + this.capture(eventName, properties, options); + } + public startListeningToSettingsChanges(): void { // Listen to account data changes from sync so we can observe changes to relevant flags and update. // This is called - diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 7af31d6..242e0e5 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -35,6 +35,7 @@ import { useLocationNavigation } from "../useLocationNavigation"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { useMediaHandler } from "../settings/useMediaHandler"; import { findDeviceByName, getDevices } from "../media-utils"; +import { callTracer } from "../telemetry/otel"; declare global { interface Window { @@ -143,7 +144,7 @@ export function GroupCallView({ ]); await groupCall.enter(); - + callTracer.startCall(groupCall.groupCallId); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); @@ -164,6 +165,7 @@ export function GroupCallView({ if (isEmbedded && !preload) { // In embedded mode, bypass the lobby and just enter the call straight away groupCall.enter(); + callTracer.startCall(groupCall.groupCallId); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); @@ -187,6 +189,7 @@ export function GroupCallView({ // In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent, // therefore we want the event to be sent instantly without getting queued/batched. + callTracer.endCall(); const sendInstantly = !!widget; PosthogAnalytics.instance.eventCallEnded.track( groupCall.groupCallId, diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 37484b4..a325073 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -32,6 +32,7 @@ import { usePageUnload } from "./usePageUnload"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { TranslatedError, translatedError } from "../TranslatedError"; import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget"; +import { callTracer } from "../telemetry/otel"; export enum ConnectionState { EstablishingCall = "establishing call", // call hasn't been established yet @@ -375,6 +376,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { ) { return; } + callTracer.startCall(groupCall.groupCallId); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); @@ -399,6 +401,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { const setMicrophoneMuted = useCallback( (setMuted) => { groupCall.setMicrophoneMuted(setMuted); + callTracer.muteMic(setMuted); PosthogAnalytics.instance.eventMuteMicrophone.track( setMuted, groupCall.groupCallId diff --git a/src/telemetry/otel.ts b/src/telemetry/otel.ts new file mode 100644 index 0000000..c883669 --- /dev/null +++ b/src/telemetry/otel.ts @@ -0,0 +1,128 @@ +/* document-load.ts|js file - the code is the same for both the languages */ +import { + ConsoleSpanExporter, + SimpleSpanProcessor, +} from "@opentelemetry/sdk-trace-base"; +import { ZipkinExporter } from "@opentelemetry/exporter-zipkin"; +// import { JaegerExporter } from "@opentelemetry/exporter-jaeger"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; +import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; +import { ZoneContextManager } from "@opentelemetry/context-zone"; +import { registerInstrumentations } from "@opentelemetry/instrumentation"; +import opentelemetry from "@opentelemetry/api"; +import { Resource } from "@opentelemetry/resources"; +import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; + +import { PosthogSpanExporter } from "../analytics/OtelPosthogExporter"; + +const SERVICE_NAME = "element-call"; +// It is really important to set the correct content type here. Otherwise the Jaeger will crash and not accept the zipkin event +// Additionally jaeger needs to be started with zipkin on port 9411 +const optionsZipkin = { + // url: `http://localhost:9411/api/v2/spans`, + // serviceName: SERVICE_NAME, + headers: { + "Content-Type": "application/json", + }, +}; +// We DO NOT use the OTLPTraceExporter. This somehow does not hit the right endpoint and also causes issues with CORS +const collectorOptions = { + // url: `http://localhost:14268/api/v2/spans`, // url is optional and can be omitted - default is http://localhost:4318/v1/traces + headers: { "Access-Control-Allow-Origin": "*" }, // an optional object containing custom headers to be sent with each request + concurrencyLimit: 10, // an optional limit on pending requests +}; +const otlpExporter = new OTLPTraceExporter(collectorOptions); +const consoleExporter = new ConsoleSpanExporter(); +// The zipkin exporter is the actual exporter we need for web based otel applications +const zipkin = new ZipkinExporter(optionsZipkin); +const posthogExporter = new PosthogSpanExporter(); + +// This is how we can make Jaeger show a reaonsable service in the dropdown on the left. +const providerConfig = { + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, + }), +}; +const provider = new WebTracerProvider(providerConfig); + +provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); +// We can add as many processors and exporters as we want to. The zipkin one is the important one for Jaeger +provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); +provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); +provider.addSpanProcessor(new SimpleSpanProcessor(zipkin)); + +// This is unecassary i think... +provider.register({ + // Changing default contextManager to use ZoneContextManager - supports asynchronous operations - optional + contextManager: new ZoneContextManager(), +}); + +// Registering instrumentations (These are automated span collectors for the Http request during page loading, switching) +registerInstrumentations({ + instrumentations: [ + // new DocumentLoadInstrumentation(), + // new UserInteractionInstrumentation(), + ], +}); + +// This is not the serviceName shown in jaeger +export const tracer = opentelemetry.trace.getTracer( + "my-element-call-otl-tracer" +); + +class CallTracer { + // We create one tracer class for each main context. + // Even if differnt tracer classes overlap in time space, we might want to visulaize them seperately. + // The Call Tracer should only contain spans/events that are relevant to understand the procedure of the individual candidates. + // Another Tracer Class (for example a ConnectionTracer) can contain a very granular list of all steps to connect to a call. + + private callSpan; + private callContext; + private muteSpan?; + public startCall(callId: string) { + // The main context will be set when initiating the main/parent span. + + // Create an initial context with the callId param + const callIdContext = opentelemetry.context + .active() + .setValue(Symbol("callId"), callId); + + // Create the main span that tracks the whole call + this.callSpan = tracer.startSpan("otel_callSpan", undefined, callIdContext); + + // Create a new call based on the callIdContext. This context also has a span assigned to it. + // Other spans can use this context to extract the parent span. + // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) + this.callContext = opentelemetry.trace.setSpan( + opentelemetry.context.active(), + this.callSpan + ); + + // Here we start a very short span. This is a hack to trigger the posthog exporter. + // Only ended spans are processed by the exporter. + // We want the exporter to know that a call has started + const startCallSpan = tracer.startSpan( + "otel_startCallSpan", + undefined, + this.callContext + ); + startCallSpan.end(); + } + public muteMic(muteState: boolean) { + if (muteState) { + this.muteSpan = tracer.startSpan( + "otel_muteSpan", + undefined, + this.callContext + ); + } else if (this.muteSpan) { + this.muteSpan.end(); + this.muteSpan = null; + } + } + public endCall() { + this.callSpan?.end(); + } +} + +export const callTracer = new CallTracer(); diff --git a/yarn.lock b/yarn.lock index 7980fa2..8e51837 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1910,6 +1910,148 @@ mkdirp "^1.0.4" rimraf "^3.0.2" +"@opentelemetry/api@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.4.0.tgz#2c91791a9ba6ca0a0f4aaac5e45d58df13639ac8" + integrity sha512-IgMK9i3sFGNUqPMbjABm0G26g0QCKCUBfglhQ7rQq6WcxbKfEHRcmwsoER4hZcuYqJgkYn2OeuoJIv7Jsftp7g== + +"@opentelemetry/context-zone-peer-dep@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-zone-peer-dep/-/context-zone-peer-dep-1.9.1.tgz#634b1a25eebc68484d3568865ee5a2321b6b020d" + integrity sha512-4qaNi2noNMlT3DhOzXN4qKDiyZFjowj2vnfdtcAHZUwpIP0MQlpE3JYCr+2w7zKGJDfEOp2hg2A9Dkn8TqvzSw== + +"@opentelemetry/context-zone@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-zone/-/context-zone-1.9.1.tgz#1f1c48fb491283ab32320b3d95e542a3a3e86035" + integrity sha512-Kx2n9ftRokgHUAI6CIxsNepCsEP/fggDBH3GT27GdZkqgPYZqBn+nlTS23dB6etjWcSRd0piJnT3OIEnaxyIGA== + dependencies: + "@opentelemetry/context-zone-peer-dep" "1.9.1" + zone.js "^0.11.0" + +"@opentelemetry/core@1.9.1", "@opentelemetry/core@^1.8.0": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.9.1.tgz#e343337e1a7bf30e9a6aef3ef659b9b76379762a" + integrity sha512-6/qon6tw2I8ZaJnHAQUUn4BqhTbTNRS0WP8/bA0ynaX+Uzp/DDbd0NS0Cq6TMlh8+mrlsyqDE7mO50nmv2Yvlg== + dependencies: + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/exporter-jaeger@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-jaeger/-/exporter-jaeger-1.9.1.tgz#941d39c2d425021c734354bbc280a4ae19f95aad" + integrity sha512-6F10NMOtBT3HdxpT0IwYf1BX8RzZB7SpqHTvZsB2vzUvxVAyoLX8+cuo6Ke9sHS9YMqoTA3rER5x9kC6NOxEMQ== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/sdk-trace-base" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + jaeger-client "^3.15.0" + +"@opentelemetry/exporter-trace-otlp-http@^0.35.1": + version "0.35.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.35.1.tgz#9bf988f91fb145b29a051bce8ff5ef85029ca575" + integrity sha512-EJgAsrvscKsqb/GzF1zS74vM+Z/aQRhrFE7hs/1GK1M9bLixaVyMGwg2pxz1wdYdjxS1mqpHMhXU+VvMvFCw1w== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/otlp-exporter-base" "0.35.1" + "@opentelemetry/otlp-transformer" "0.35.1" + "@opentelemetry/resources" "1.9.1" + "@opentelemetry/sdk-trace-base" "1.9.1" + +"@opentelemetry/exporter-zipkin@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.9.1.tgz#0bcddf2f3bcb1b26b94a090c953996a28087d21f" + integrity sha512-KBgf3w84luP5vWLlrqVFKmbwFK4lXM//t6K7H4nsg576htbz1RpBbQfybADjPdXTjGHqDTtLiC5MC90hxS7Z2w== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/resources" "1.9.1" + "@opentelemetry/sdk-trace-base" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/instrumentation-document-load@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-document-load/-/instrumentation-document-load-0.31.1.tgz#a535a5d1d71706701d3ff560a700b9dd03e4fb59" + integrity sha512-Ej4EB3m7GXzj4o8zF73amcnqXroN6/QdURjDAOgxN27zvvurR84larzGD7PjqgzzdtV+T7e/0BK07M0I2eA8PQ== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.35.1" + "@opentelemetry/sdk-trace-base" "^1.0.0" + "@opentelemetry/sdk-trace-web" "^1.8.0" + "@opentelemetry/semantic-conventions" "^1.0.0" + +"@opentelemetry/instrumentation-user-interaction@^0.32.1": + version "0.32.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-user-interaction/-/instrumentation-user-interaction-0.32.1.tgz#654c0352c2f7d5bb6cc21f07f9ec56f18f2cc854" + integrity sha512-27we7cENzEtO2oCRiEkYG4cFe1v94ybeLvM+5jqNDkZF7UY0GlctCW+jvqf569Z3Gs7yHrakO2sZf4EMEfTFWg== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.35.1" + "@opentelemetry/sdk-trace-web" "^1.8.0" + +"@opentelemetry/instrumentation@^0.35.1": + version "0.35.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.35.1.tgz#065bdbc4771137347e648eb4c6c6de6e9e97e4d1" + integrity sha512-EZsvXqxenbRTSNsft6LDcrT4pjAiyZOx3rkDNeqKpwZZe6GmZtsXaZZKuDkJtz9fTjOGjDHjZj9/h80Ya9iIJw== + dependencies: + require-in-the-middle "^5.0.3" + semver "^7.3.2" + shimmer "^1.2.1" + +"@opentelemetry/otlp-exporter-base@0.35.1": + version "0.35.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.35.1.tgz#535166608d5d36e6c959b2857d01245ee3a916b1" + integrity sha512-Sc0buJIs8CfUeQCL/12vDDjBREgsqHdjboBa/kPQDgMf008OBJSM02Ijj6T1TH+QVHl/VHBBEVJF+FTf0EH9Vg== + dependencies: + "@opentelemetry/core" "1.9.1" + +"@opentelemetry/otlp-transformer@0.35.1": + version "0.35.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.35.1.tgz#d4333b71324b83dbb1b0b3a4cfd769b3e214c6f9" + integrity sha512-c0HXcJ49MKoWSaA49J8PXlVx48CeEFpL0odP6KBkVT+Bw6kAe8JlI3mIezyN05VCDJGtS2I5E6WEsE+DJL1t9A== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/resources" "1.9.1" + "@opentelemetry/sdk-metrics" "1.9.1" + "@opentelemetry/sdk-trace-base" "1.9.1" + +"@opentelemetry/resources@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.9.1.tgz#5ad3d80ba968a3a0e56498ce4bc82a6a01f2682f" + integrity sha512-VqBGbnAfubI+l+yrtYxeLyOoL358JK57btPMJDd3TCOV3mV5TNBmzvOfmesM4NeTyXuGJByd3XvOHvFezLn3rQ== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/sdk-metrics@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.9.1.tgz#babc162a81df9884c16b1e67c2dd26ab478f3080" + integrity sha512-AyhKDcA8NuV7o1+9KvzRMxNbATJ8AcrutKilJ6hWSo9R5utnzxgffV4y+Hp4mJn84iXxkv+CBb99GOJ2A5OMzA== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/resources" "1.9.1" + lodash.merge "4.6.2" + +"@opentelemetry/sdk-trace-base@1.9.1", "@opentelemetry/sdk-trace-base@^1.0.0": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.9.1.tgz#c349491b432a7e0ae7316f0b48b2d454d79d2b84" + integrity sha512-Y9gC5M1efhDLYHeeo2MWcDDMmR40z6QpqcWnPCm4Dmh+RHAMf4dnEBBntIe1dDpor686kyU6JV1D29ih1lZpsQ== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/resources" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/sdk-trace-web@^1.8.0", "@opentelemetry/sdk-trace-web@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-web/-/sdk-trace-web-1.9.1.tgz#9734c62dfb554336779c0eb4f78bb622d8bde988" + integrity sha512-VCnr8IYW1GYonGF8M3nDqUGFjf2jcL3nlhnNyF3PKGw6OI7xNCBR+65IgW5Va7QhDP0D01jRVJ9oNuTshrVewA== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/sdk-trace-base" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/semantic-conventions@1.9.1", "@opentelemetry/semantic-conventions@^1.0.0": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.9.1.tgz#ad3367684a57879392513479e0a436cb2ac46dad" + integrity sha512-oPQdbFDmZvjXk5ZDoBGXG8B4tSB/qW5vQunJWQMFUBp7Xe8O1ByPANueJ+Jzg58esEBegyyxZ7LRmfJr7kFcFg== + "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": version "0.5.7" resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz#58f8217ba70069cc6a73f5d7e05e85b458c150e2" @@ -4164,6 +4306,11 @@ ansi-align@^3.0.0: dependencies: string-width "^4.1.0" +ansi-color@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-color/-/ansi-color-0.2.1.tgz#3e75c037475217544ed763a8db5709fa9ae5bf9a" + integrity sha512-bF6xLaZBLpOQzgYUtYEhJx090nPSZk1BQ/q2oyBK9aMMcJHzx9uXGCjI2Y+LebsN4Jwoykr0V9whbPiogdyHoQ== + ansi-colors@^3.0.0: version "3.2.4" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" @@ -4985,6 +5132,16 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +bufrw@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bufrw/-/bufrw-1.3.0.tgz#28d6cfdaf34300376836310f5c31d57eeb40c8fa" + integrity sha512-jzQnSbdJqhIltU9O5KUiTtljP9ccw2u5ix59McQy4pV2xGhVLhRZIndY8GIrgh5HjXa6+QJ9AQhOd2QWQizJFQ== + dependencies: + ansi-color "^0.2.1" + error "^7.0.0" + hexer "^1.5.0" + xtend "^4.0.0" + builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -5132,15 +5289,10 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001359: - version "1.0.30001363" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001363.tgz#26bec2d606924ba318235944e1193304ea7c4f15" - integrity sha512-HpQhpzTGGPVMnCjIomjt+jvyUu8vNFo3TaDiZ/RcoTrlOq/5+tC8zHdsbgFB6MxmaY+jCpsH09aD80Bb4Ow3Sg== - -caniuse-lite@^1.0.30001400: - version "1.0.30001425" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001425.tgz#52917791a453eb3265143d2cd08d80629e82c735" - integrity sha512-/pzFv0OmNG6W0ym80P3NtapU0QEiDS3VuYAZMGoLLqiC7f6FJFe1MjpQDREGApeenD9wloeytmVDj+JLXPC6qw== +caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001359, caniuse-lite@^1.0.30001400: + version "1.0.30001460" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz" + integrity sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ== case-sensitive-paths-webpack-plugin@^2.3.0: version "2.4.0" @@ -6901,6 +7053,21 @@ error-stack-parser@^2.0.6: dependencies: stackframe "^1.3.4" +error@7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02" + integrity sha512-UtVv4l5MhijsYUxPJo4390gzfZvAnTHreNnDjnTZaKIiZ/SemXxAhBkYSKtWa5RtBXbLP8tMgn/n0RUa/H7jXw== + dependencies: + string-template "~0.2.1" + xtend "~4.0.0" + +error@^7.0.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/error/-/error-7.2.1.tgz#eab21a4689b5f684fc83da84a0e390de82d94894" + integrity sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA== + dependencies: + string-template "~0.2.1" + es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.1: version "1.20.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" @@ -8581,6 +8748,16 @@ heimdalljs@^0.2.6: dependencies: rsvp "~3.2.1" +hexer@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/hexer/-/hexer-1.5.0.tgz#b86ce808598e8a9d1892c571f3cedd86fc9f0653" + integrity sha512-dyrPC8KzBzUJ19QTIo1gXNqIISRXQ0NwteW6OeQHRN4ZuZeHkdODfj0zHBdOlHbRY8GqbqK57C9oWSvQZizFsg== + dependencies: + ansi-color "^0.2.1" + minimist "^1.1.0" + process "^0.10.0" + xtend "^4.0.0" + highlight.js@^10.4.1, highlight.js@~10.7.0: version "10.7.3" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" @@ -9468,6 +9645,17 @@ iterate-value@^1.0.2: es-get-iterator "^1.0.2" iterate-iterator "^1.0.1" +jaeger-client@^3.15.0: + version "3.19.0" + resolved "https://registry.yarnpkg.com/jaeger-client/-/jaeger-client-3.19.0.tgz#9b5bd818ebd24e818616ee0f5cffe1722a53ae6e" + integrity sha512-M0c7cKHmdyEUtjemnJyx/y9uX16XHocL46yQvyqDlPdvAcwPDbHrIbKjQdBqtiE4apQ/9dmr+ZLJYYPGnurgpw== + dependencies: + node-int64 "^0.4.0" + opentracing "^0.14.4" + thriftrw "^3.5.0" + uuid "^8.3.2" + xorshift "^1.1.1" + jest-changed-files@^29.2.0: version "29.2.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.2.0.tgz#b6598daa9803ea6a4dce7968e20ab380ddbee289" @@ -10211,7 +10399,7 @@ lodash.flow@^3.3.0: resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" integrity sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw== -lodash.merge@^4.6.2: +lodash.merge@4.6.2, lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== @@ -10239,6 +10427,11 @@ loglevel@^1.7.1: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114" integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA== +long@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/long/-/long-2.4.0.tgz#9fa180bb1d9500cdc29c4156766a1995e1f4524f" + integrity sha512-ijUtjmO/n2A5PaosNG9ZGDsQ3vxJg7ZW8vsY8Kp0f2yIZWhSJvjmegV7t+9RPQKxKrvj8yKGehhS+po14hPLGQ== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -10619,6 +10812,11 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimist@^1.1.0: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -10701,6 +10899,11 @@ mktemp@~0.4.0: resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b" integrity sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A== +module-details-from-path@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" + integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A== + moment-mini@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment-mini/-/moment-mini-2.24.0.tgz#fa68d98f7fe93ae65bf1262f6abb5fb6983d8d18" @@ -11083,6 +11286,11 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +opentracing@^0.14.4: + version "0.14.7" + resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.14.7.tgz#25d472bd0296dc0b64d7b94cbc995219031428f5" + integrity sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q== + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -11940,6 +12148,11 @@ process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/process/-/process-0.10.1.tgz#842457cc51cfed72dc775afeeafb8c6034372725" + integrity sha512-dyIett8dgGIZ/TXKUzeYExt7WA6ldDzys9vTDU/cCA9L17Ypme+KzS+NjQCjpn9xsvi/shbMC+yP/BcFMBz0NA== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -12670,6 +12883,15 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-in-the-middle@^5.0.3: + version "5.2.0" + resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz#4b71e3cc7f59977100af9beb76bf2d056a5a6de2" + integrity sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg== + dependencies: + debug "^4.1.1" + module-details-from-path "^1.0.3" + resolve "^1.22.1" + requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -12714,7 +12936,7 @@ resolve.exports@^1.1.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.3.2: +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1, resolve@^1.3.2: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -13067,6 +13289,11 @@ shelljs@0.8.4: interpret "^1.0.0" rechoir "^0.6.2" +shimmer@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" + integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== + side-channel@^1.0.3, side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -13356,6 +13583,11 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" +string-template@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" + integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== + "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -13677,6 +13909,15 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +thriftrw@^3.5.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/thriftrw/-/thriftrw-3.12.0.tgz#30857847755e7f036b2e0a79d11c9f55075539d9" + integrity sha512-4YZvR4DPEI41n4Opwr4jmrLGG4hndxr7387kzRFIIzxHQjarPusH4lGXrugvgb7TtPrfZVTpZCVe44/xUxowEw== + dependencies: + bufrw "^1.3.0" + error "7.0.2" + long "^2.4.0" + through2-filter@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" @@ -13866,6 +14107,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@^2.3.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -14315,6 +14561,11 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -14833,6 +15084,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xorshift@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/xorshift/-/xorshift-1.2.0.tgz#30a4cdd8e9f8d09d959ed2a88c42a09c660e8148" + integrity sha512-iYgNnGyeeJ4t6U11NpA/QiKy+PXn5Aa3Azg5qkwIFz1tBLllQrjjsk9yzD7IAK0naNU4JxdeDgqW9ov4u/hc4g== + xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -14904,6 +15160,13 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zone.js@^0.11.0: + version "0.11.8" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.11.8.tgz#40dea9adc1ad007b5effb2bfed17f350f1f46a21" + integrity sha512-82bctBg2hKcEJ21humWIkXRlLBBmrc3nN7DFh5LGGhcyycO2S7FN8NmdvlcKaGFDNVL4/9kFLmwmInTavdJERA== + dependencies: + tslib "^2.3.0" + zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" From 0cca5ae1741f672a674a5a258bcc4afcdb33a177 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 15 Mar 2023 14:35:10 +0000 Subject: [PATCH 02/34] Slightly evolved but not-yet-working OpenTelemetry More usefully, including docker config for starting a CORS enabled OTLP collector so we don't have to use zipkin. --- config/otel_dev/README.md | 7 + config/otel_dev/collector-gateway.yaml | 38 +++++ config/otel_dev/docker-compose.yaml | 23 +++ src/otel/OTelGroupCallMembership.ts | 206 +++++++++++++++++++++++++ src/otel/otel.ts | 96 ++++++++++++ 5 files changed, 370 insertions(+) create mode 100644 config/otel_dev/README.md create mode 100644 config/otel_dev/collector-gateway.yaml create mode 100644 config/otel_dev/docker-compose.yaml create mode 100644 src/otel/OTelGroupCallMembership.ts create mode 100644 src/otel/otel.ts diff --git a/config/otel_dev/README.md b/config/otel_dev/README.md new file mode 100644 index 0000000..19bf00d --- /dev/null +++ b/config/otel_dev/README.md @@ -0,0 +1,7 @@ +# OpenTelemetry Collector for development + +This directory contains a docker compose file that starts a jaeger all-in-one instance +with an in-memory database, along with a standalong OpenTelemetry collector that forwards +traces into the jaeger. Jaeger has a built-in OpenTelemetry collector, but it can't be +configured to send CORS headers so can't be used from a browser. This sets the config on +the collector to send CORS headers. diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml new file mode 100644 index 0000000..2c70fb4 --- /dev/null +++ b/config/otel_dev/collector-gateway.yaml @@ -0,0 +1,38 @@ +receivers: + otlp: + protocols: + http: + endpoint: 0.0.0.0:4318 + cors: + allowed_origins: + - "http://*" + allowed_headers: + - "*" +processors: + batch: + timeout: 1s + resource: + attributes: + - key: test.key + value: "test-value" + action: insert +exporters: + logging: + loglevel: info + jaeger: + endpoint: jaeger-all-in-one:14250 + tls: + insecure: true +extensions: + health_check: + pprof: + endpoint: :1888 + zpages: + endpoint: :55679 +service: + extensions: [pprof, zpages, health_check] + pipelines: + traces: + receivers: [otlp] + processors: [batch, resource] + exporters: [logging, jaeger] diff --git a/config/otel_dev/docker-compose.yaml b/config/otel_dev/docker-compose.yaml new file mode 100644 index 0000000..478f80a --- /dev/null +++ b/config/otel_dev/docker-compose.yaml @@ -0,0 +1,23 @@ +version: "2" +services: + # Jaeger + jaeger-all-in-one: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "14268" + - "14250" + # Collector + collector-gateway: + image: otel/opentelemetry-collector:latest + volumes: + - ./collector-gateway.yaml:/etc/collector-gateway.yaml + command: [ "--config=/etc/collector-gateway.yaml" ] + ports: + - "1888:1888" # pprof extension + - "13133:13133" # health_check extension + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP HTTP receiver + - "55670:55679" # zpages extension + depends_on: + - jaeger-all-in-one diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts new file mode 100644 index 0000000..56a8cf8 --- /dev/null +++ b/src/otel/OTelGroupCallMembership.ts @@ -0,0 +1,206 @@ +/* +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 opentelemetry, { Context, Span } from "@opentelemetry/api"; +import { + ClientEvent, + GroupCall, + MatrixClient, + MatrixEvent, + RoomStateEvent, +} from "matrix-js-sdk"; +import { CallEvent } from "matrix-js-sdk/src/webrtc/call"; +import { useCallback, useEffect, useState } from "react"; + +import { tracer } from "./otel"; + +/** + * Represent the span of time which we intend to be joined to a group call + */ +export class OTelGroupCallMembership { + private context: Context; + private callMembershipSpan: Span; + + constructor(private groupCall: GroupCall) { + const callIdContext = opentelemetry.context + .active() + .setValue(Symbol("confId"), groupCall.groupCallId); + + // Create the main span that tracks the time we intend to be in the call + this.callMembershipSpan = tracer.startSpan( + "otel_groupCallMembershipSpan", + undefined, + callIdContext + ); + + // Create a new call based on the callIdContext. This context also has a span assigned to it. + // Other spans can use this context to extract the parent span. + // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) + this.context = opentelemetry.trace.setSpan( + opentelemetry.context.active(), + this.callMembershipSpan + ); + } + + public onJoinCall() { + // Here we start a very short span. This is a hack to trigger the posthog exporter. + // Only ended spans are processed by the exporter. + // We want the exporter to know that a call has started + const joinCallSpan = tracer.startSpan( + "otel_joinCallSpan", + undefined, + this.context + ); + joinCallSpan.end(); + } + + public onLeaveCall() { + // A very short span to represent us leaving the call + const startCallSpan = tracer.startSpan( + "otel_leaveCallSpan", + undefined, + this.context + ); + startCallSpan.end(); + + // and end the main span to indicate we've left + this.callMembershipSpan.end(); + } + + public onSendStateEvent(stateEvent: MatrixEvent) {} + + public onSendToDeviceEvent(toDeviceEvent: Record) { + const eventType = toDeviceEvent.eventType as string; + if (!eventType.startsWith("m.call")) return; + + const span = tracer.startSpan( + `otel_sendToDeviceEvent_${toDeviceEvent.eventType}`, + undefined, + this.context + ); + + for (const [k, v] of Object.entries(toDeviceEvent)) { + if (["string", "number"].includes(typeof v)) + span.setAttribute(k, v as string | number); + } + } +} + +export const useCallEventInstrumentation = ( + client: MatrixClient, + groupCall: GroupCall +): void => { + const [groupCallSpan, setGroupCallSpan] = useState(); + const [groupCallId, setGroupCallId] = useState(); + + const startChildSpan = useCallback( + (name: string, groupCallId: string): Span => { + const traceId = "7b78c1f568312cb288e55a9bc3c28cc5"; + const spanId = "7d31f3e430d90882"; + + const ctx = opentelemetry.trace.setSpanContext(context.active(), { + traceId, + spanId, + traceFlags: 1, + isRemote: true, + }); + + console.log("LOG context", ctx); + console.log( + "LOG context valid", + trace.isSpanContextValid(trace.getSpan(ctx).spanContext()) + ); + console.log("LOG parent span", trace.getSpan(ctx)); + + return tracer.startSpan(name, undefined, ctx); + }, + [] + ); + + const onUpdateRoomState = useCallback((event?: MatrixEvent) => { + /*const callStateEvent = groupCall.room.currentState.getStateEvents( + "org.matrix.msc3401.call", + groupCall.groupCallId + );*/ + /*const memberStateEvents = groupCall.room.currentState.getStateEvents( + "org.matrix.msc3401.call.member" + );*/ + }, []); + + const onReceivedVoipEvent = (event: MatrixEvent) => {}; + + const onUndecryptableToDevice = (event: MatrixEvent) => {}; + + const onSendVoipEvent = useCallback( + (event: Record) => { + const span = startChildSpan( + `element-call:send-voip-event:${event.eventType}`, + groupCall.groupCallId + ); + span.setAttribute("groupCallId", groupCall.groupCallId); + + console.log("LOG span", span); + + span.end(); + }, + [groupCall.groupCallId, startChildSpan] + ); + + useEffect(() => { + return; + if (groupCallId === groupCall.groupCallId) return; + + console.log("LOG starting span", groupCall.groupCallId, groupCallId); + + groupCallSpan?.end(); + + const newSpan = tracer.startSpan("element-call:group-call"); + newSpan.setAttribute("groupCallId", groupCall.groupCallId); + setGroupCallSpan(newSpan); + setGroupCallId(groupCall.groupCallId); + }, [groupCallSpan, groupCallId, groupCall.groupCallId]); + + useEffect(() => () => { + console.log("LOG ending span"); + + groupCallSpan?.end(); + }); + + useEffect(() => { + client.on(RoomStateEvent.Events, onUpdateRoomState); + //groupCall.on("calls_changed", onCallsChanged); + groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent); + //client.on("state", onCallsChanged); + //client.on("hangup", onCallHangup); + client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); + client.on(ClientEvent.UndecryptableToDeviceEvent, onUndecryptableToDevice); + + onUpdateRoomState(); + + return () => { + client.removeListener(RoomStateEvent.Events, onUpdateRoomState); + //groupCall.removeListener("calls_changed", onCallsChanged); + groupCall.removeListener(CallEvent.SendVoipEvent, onSendVoipEvent); + //client.removeListener("state", onCallsChanged); + //client.removeListener("hangup", onCallHangup); + client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); + client.removeListener( + ClientEvent.UndecryptableToDeviceEvent, + onUndecryptableToDevice + ); + }; + }, [client, groupCall, onSendVoipEvent, onUpdateRoomState]); +}; diff --git a/src/otel/otel.ts b/src/otel/otel.ts new file mode 100644 index 0000000..6f546db --- /dev/null +++ b/src/otel/otel.ts @@ -0,0 +1,96 @@ +/* document-load.ts|js file - the code is the same for both the languages */ +import { + ConsoleSpanExporter, + SimpleSpanProcessor, +} from "@opentelemetry/sdk-trace-base"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; +import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; +import opentelemetry from "@opentelemetry/api"; +import { Context } from "@opentelemetry/api"; +import { Resource } from "@opentelemetry/resources"; +import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; + +import { PosthogSpanExporter } from "../analytics/OtelPosthogExporter"; + +const SERVICE_NAME = "element-call"; + +const otlpExporter = new OTLPTraceExporter(); +const consoleExporter = new ConsoleSpanExporter(); +const posthogExporter = new PosthogSpanExporter(); + +// This is how we can make Jaeger show a reaonsable service in the dropdown on the left. +const providerConfig = { + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, + }), +}; +const provider = new WebTracerProvider(providerConfig); + +provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); +provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); +provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); + +// This is not the serviceName shown in jaeger +export const tracer = opentelemetry.trace.getTracer( + "my-element-call-otl-tracer" +); + +class CallTracer { + // We create one tracer class for each main context. + // Even if differnt tracer classes overlap in time space, we might want to visulaize them seperately. + // The Call Tracer should only contain spans/events that are relevant to understand the procedure of the individual candidates. + // Another Tracer Class (for example a ConnectionTracer) can contain a very granular list of all steps to connect to a call. + + private callSpan; + private callContext; + private muteSpan?; + + public startGroupCall(groupCallId: string) {} + + public startCall(callId: string): Context { + // The main context will be set when initiating the main/parent span. + + // Create an initial context with the callId param + const callIdContext = opentelemetry.context + .active() + .setValue(Symbol("callId"), callId); + + // Create the main span that tracks the whole call + this.callSpan = tracer.startSpan("otel_callSpan", undefined, callIdContext); + + // Create a new call based on the callIdContext. This context also has a span assigned to it. + // Other spans can use this context to extract the parent span. + // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) + this.callContext = opentelemetry.trace.setSpan( + opentelemetry.context.active(), + this.callSpan + ); + + // Here we start a very short span. This is a hack to trigger the posthog exporter. + // Only ended spans are processed by the exporter. + // We want the exporter to know that a call has started + const startCallSpan = tracer.startSpan( + "otel_startCallSpan", + undefined, + this.callContext + ); + startCallSpan.end(); + } + public muteMic(muteState: boolean) { + if (muteState) { + this.muteSpan = tracer.startSpan( + "otel_muteSpan", + undefined, + this.callContext + ); + } else if (this.muteSpan) { + this.muteSpan.end(); + this.muteSpan = null; + } + } + public endCall() { + this.callSpan?.end(); + } +} + +export const callTracer = new CallTracer(); From 1e2cd9776403bbb479e5258a21650ee723f846e6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 15 Mar 2023 14:38:17 +0000 Subject: [PATCH 03/34] Include the arguably-obvious command line --- config/otel_dev/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/otel_dev/README.md b/config/otel_dev/README.md index 19bf00d..04690c7 100644 --- a/config/otel_dev/README.md +++ b/config/otel_dev/README.md @@ -5,3 +5,5 @@ with an in-memory database, along with a standalong OpenTelemetry collector that traces into the jaeger. Jaeger has a built-in OpenTelemetry collector, but it can't be configured to send CORS headers so can't be used from a browser. This sets the config on the collector to send CORS headers. + +Running `docker compose up` in this directory should be all you need. From c519e13885f72f52c0534f6d85c8af36fb9a7c54 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 15 Mar 2023 16:00:39 +0000 Subject: [PATCH 04/34] Version that does at least send some traces --- package.json | 1 - src/otel/OTelGroupCallMembership.ts | 176 ++++++++-------------------- src/otel/otel.ts | 6 +- src/room/GroupCallInspector.tsx | 23 +++- src/room/GroupCallView.tsx | 4 - src/room/useGroupCall.ts | 3 - src/telemetry/otel.ts | 128 -------------------- yarn.lock | 10 -- 8 files changed, 73 insertions(+), 278 deletions(-) delete mode 100644 src/telemetry/otel.ts diff --git a/package.json b/package.json index 0adbe94..a8f6ff0 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "@opentelemetry/context-zone": "^1.9.1", "@opentelemetry/exporter-jaeger": "^1.9.1", "@opentelemetry/exporter-trace-otlp-http": "^0.35.1", - "@opentelemetry/exporter-zipkin": "^1.9.1", "@opentelemetry/instrumentation-document-load": "^0.31.1", "@opentelemetry/instrumentation-user-interaction": "^0.32.1", "@opentelemetry/sdk-trace-web": "^1.9.1", diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 56a8cf8..3ea0266 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -15,18 +15,48 @@ limitations under the License. */ import opentelemetry, { Context, Span } from "@opentelemetry/api"; -import { - ClientEvent, - GroupCall, - MatrixClient, - MatrixEvent, - RoomStateEvent, -} from "matrix-js-sdk"; -import { CallEvent } from "matrix-js-sdk/src/webrtc/call"; -import { useCallback, useEffect, useState } from "react"; +import { GroupCall, MatrixEvent } from "matrix-js-sdk"; +import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; import { tracer } from "./otel"; +/** + * Recursively sets the contents of a todevice event object as attributes on a span + */ +function setNestedAttributesFromToDeviceEvent(span: Span, event: VoipEvent) { + setSpanEventAttributesRecursive( + span, + event as unknown as Record, // XXX Types + "matrix.", + 0 + ); +} + +function setSpanEventAttributesRecursive( + span: Span, + obj: Record, + prefix: string, + depth: number +) { + if (depth > 10) + throw new Error( + "Depth limit exceeded: aborting VoipEvent recursion. Prefix is " + prefix + ); + + for (const [k, v] of Object.entries(obj)) { + if (["string", "number"].includes(typeof v)) { + span.setAttribute(prefix + k, v as string | number); + } else if (typeof v === "object") { + setSpanEventAttributesRecursive( + span, + v as Record, + prefix + k + ".", + depth + 1 + ); + } + } +} + /** * Represent the span of time which we intend to be joined to a group call */ @@ -34,7 +64,7 @@ export class OTelGroupCallMembership { private context: Context; private callMembershipSpan: Span; - constructor(private groupCall: GroupCall) { + constructor(groupCall: GroupCall) { const callIdContext = opentelemetry.context .active() .setValue(Symbol("confId"), groupCall.groupCallId); @@ -82,125 +112,19 @@ export class OTelGroupCallMembership { public onSendStateEvent(stateEvent: MatrixEvent) {} - public onSendToDeviceEvent(toDeviceEvent: Record) { - const eventType = toDeviceEvent.eventType as string; + public onSendEvent(event: VoipEvent) { + const eventType = event.eventType as string; if (!eventType.startsWith("m.call")) return; - const span = tracer.startSpan( - `otel_sendToDeviceEvent_${toDeviceEvent.eventType}`, - undefined, - this.context - ); + if (event.type === "toDevice") { + const span = tracer.startSpan( + `otel_sendToDeviceEvent_${event.eventType}`, + undefined, + this.context + ); - for (const [k, v] of Object.entries(toDeviceEvent)) { - if (["string", "number"].includes(typeof v)) - span.setAttribute(k, v as string | number); + setNestedAttributesFromToDeviceEvent(span, event); + span.end(); } } } - -export const useCallEventInstrumentation = ( - client: MatrixClient, - groupCall: GroupCall -): void => { - const [groupCallSpan, setGroupCallSpan] = useState(); - const [groupCallId, setGroupCallId] = useState(); - - const startChildSpan = useCallback( - (name: string, groupCallId: string): Span => { - const traceId = "7b78c1f568312cb288e55a9bc3c28cc5"; - const spanId = "7d31f3e430d90882"; - - const ctx = opentelemetry.trace.setSpanContext(context.active(), { - traceId, - spanId, - traceFlags: 1, - isRemote: true, - }); - - console.log("LOG context", ctx); - console.log( - "LOG context valid", - trace.isSpanContextValid(trace.getSpan(ctx).spanContext()) - ); - console.log("LOG parent span", trace.getSpan(ctx)); - - return tracer.startSpan(name, undefined, ctx); - }, - [] - ); - - const onUpdateRoomState = useCallback((event?: MatrixEvent) => { - /*const callStateEvent = groupCall.room.currentState.getStateEvents( - "org.matrix.msc3401.call", - groupCall.groupCallId - );*/ - /*const memberStateEvents = groupCall.room.currentState.getStateEvents( - "org.matrix.msc3401.call.member" - );*/ - }, []); - - const onReceivedVoipEvent = (event: MatrixEvent) => {}; - - const onUndecryptableToDevice = (event: MatrixEvent) => {}; - - const onSendVoipEvent = useCallback( - (event: Record) => { - const span = startChildSpan( - `element-call:send-voip-event:${event.eventType}`, - groupCall.groupCallId - ); - span.setAttribute("groupCallId", groupCall.groupCallId); - - console.log("LOG span", span); - - span.end(); - }, - [groupCall.groupCallId, startChildSpan] - ); - - useEffect(() => { - return; - if (groupCallId === groupCall.groupCallId) return; - - console.log("LOG starting span", groupCall.groupCallId, groupCallId); - - groupCallSpan?.end(); - - const newSpan = tracer.startSpan("element-call:group-call"); - newSpan.setAttribute("groupCallId", groupCall.groupCallId); - setGroupCallSpan(newSpan); - setGroupCallId(groupCall.groupCallId); - }, [groupCallSpan, groupCallId, groupCall.groupCallId]); - - useEffect(() => () => { - console.log("LOG ending span"); - - groupCallSpan?.end(); - }); - - useEffect(() => { - client.on(RoomStateEvent.Events, onUpdateRoomState); - //groupCall.on("calls_changed", onCallsChanged); - groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent); - //client.on("state", onCallsChanged); - //client.on("hangup", onCallHangup); - client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); - client.on(ClientEvent.UndecryptableToDeviceEvent, onUndecryptableToDevice); - - onUpdateRoomState(); - - return () => { - client.removeListener(RoomStateEvent.Events, onUpdateRoomState); - //groupCall.removeListener("calls_changed", onCallsChanged); - groupCall.removeListener(CallEvent.SendVoipEvent, onSendVoipEvent); - //client.removeListener("state", onCallsChanged); - //client.removeListener("hangup", onCallHangup); - client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); - client.removeListener( - ClientEvent.UndecryptableToDeviceEvent, - onUndecryptableToDevice - ); - }; - }, [client, groupCall, onSendVoipEvent, onUpdateRoomState]); -}; diff --git a/src/otel/otel.ts b/src/otel/otel.ts index 6f546db..ecff690 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -6,7 +6,6 @@ import { import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; import opentelemetry from "@opentelemetry/api"; -import { Context } from "@opentelemetry/api"; import { Resource } from "@opentelemetry/resources"; import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; @@ -29,12 +28,14 @@ const provider = new WebTracerProvider(providerConfig); provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); +opentelemetry.trace.setGlobalTracerProvider(provider); // This is not the serviceName shown in jaeger export const tracer = opentelemetry.trace.getTracer( "my-element-call-otl-tracer" ); +/* class CallTracer { // We create one tracer class for each main context. // Even if differnt tracer classes overlap in time space, we might want to visulaize them seperately. @@ -47,7 +48,7 @@ class CallTracer { public startGroupCall(groupCallId: string) {} - public startCall(callId: string): Context { + public startCall(callId: string) { // The main context will be set when initiating the main/parent span. // Create an initial context with the callId param @@ -94,3 +95,4 @@ class CallTracer { } export const callTracer = new CallTracer(); +*/ diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index 648a0a1..a0b16a9 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -31,11 +31,12 @@ import { MatrixEvent, IContent } from "matrix-js-sdk/src/models/event"; import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; -import { CallEvent } from "matrix-js-sdk/src/webrtc/call"; +import { CallEvent, VoipEvent } from "matrix-js-sdk/src/webrtc/call"; import styles from "./GroupCallInspector.module.css"; import { SelectInput } from "../input/SelectInput"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; +import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; interface InspectorContextState { eventsByUserId?: { [userId: string]: SequenceDiagramMatrixEvent[] }; @@ -235,7 +236,7 @@ function reducer( action: { type?: CallEvent | ClientEvent | RoomStateEvent; event?: MatrixEvent; - rawEvent?: Record; + rawEvent?: VoipEvent; callStateEvent?: MatrixEvent; memberStateEvents?: MatrixEvent[]; } @@ -355,6 +356,18 @@ function useGroupCallState( groupCall: GroupCall, showPollCallStats: boolean ): InspectorContextState { + const [otelMembership] = useState( + () => new OTelGroupCallMembership(groupCall) + ); + + useEffect(() => { + otelMembership.onJoinCall(); + + return () => { + otelMembership.onLeaveCall(); + }; + }, [otelMembership]); + const [state, dispatch] = useReducer(reducer, { localUserId: client.getUserId(), localSessionId: client.getSessionId(), @@ -387,8 +400,10 @@ function useGroupCallState( dispatch({ type: ClientEvent.ReceivedVoipEvent, event }); } - function onSendVoipEvent(event: Record) { + function onSendVoipEvent(event: VoipEvent) { dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event }); + + otelMembership.onSendEvent(event); } function onUndecryptableToDevice(event: MatrixEvent) { @@ -422,7 +437,7 @@ function useGroupCallState( onUndecryptableToDevice ); }; - }, [client, groupCall]); + }, [client, groupCall, otelMembership]); return state; } diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 242e0e5..61e1eb5 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -35,7 +35,6 @@ import { useLocationNavigation } from "../useLocationNavigation"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { useMediaHandler } from "../settings/useMediaHandler"; import { findDeviceByName, getDevices } from "../media-utils"; -import { callTracer } from "../telemetry/otel"; declare global { interface Window { @@ -144,7 +143,6 @@ export function GroupCallView({ ]); await groupCall.enter(); - callTracer.startCall(groupCall.groupCallId); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); @@ -165,7 +163,6 @@ export function GroupCallView({ if (isEmbedded && !preload) { // In embedded mode, bypass the lobby and just enter the call straight away groupCall.enter(); - callTracer.startCall(groupCall.groupCallId); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); @@ -189,7 +186,6 @@ export function GroupCallView({ // In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent, // therefore we want the event to be sent instantly without getting queued/batched. - callTracer.endCall(); const sendInstantly = !!widget; PosthogAnalytics.instance.eventCallEnded.track( groupCall.groupCallId, diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index a325073..37484b4 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -32,7 +32,6 @@ import { usePageUnload } from "./usePageUnload"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { TranslatedError, translatedError } from "../TranslatedError"; import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget"; -import { callTracer } from "../telemetry/otel"; export enum ConnectionState { EstablishingCall = "establishing call", // call hasn't been established yet @@ -376,7 +375,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { ) { return; } - callTracer.startCall(groupCall.groupCallId); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); @@ -401,7 +399,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { const setMicrophoneMuted = useCallback( (setMuted) => { groupCall.setMicrophoneMuted(setMuted); - callTracer.muteMic(setMuted); PosthogAnalytics.instance.eventMuteMicrophone.track( setMuted, groupCall.groupCallId diff --git a/src/telemetry/otel.ts b/src/telemetry/otel.ts deleted file mode 100644 index c883669..0000000 --- a/src/telemetry/otel.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* document-load.ts|js file - the code is the same for both the languages */ -import { - ConsoleSpanExporter, - SimpleSpanProcessor, -} from "@opentelemetry/sdk-trace-base"; -import { ZipkinExporter } from "@opentelemetry/exporter-zipkin"; -// import { JaegerExporter } from "@opentelemetry/exporter-jaeger"; -import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; -import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; -import { ZoneContextManager } from "@opentelemetry/context-zone"; -import { registerInstrumentations } from "@opentelemetry/instrumentation"; -import opentelemetry from "@opentelemetry/api"; -import { Resource } from "@opentelemetry/resources"; -import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; - -import { PosthogSpanExporter } from "../analytics/OtelPosthogExporter"; - -const SERVICE_NAME = "element-call"; -// It is really important to set the correct content type here. Otherwise the Jaeger will crash and not accept the zipkin event -// Additionally jaeger needs to be started with zipkin on port 9411 -const optionsZipkin = { - // url: `http://localhost:9411/api/v2/spans`, - // serviceName: SERVICE_NAME, - headers: { - "Content-Type": "application/json", - }, -}; -// We DO NOT use the OTLPTraceExporter. This somehow does not hit the right endpoint and also causes issues with CORS -const collectorOptions = { - // url: `http://localhost:14268/api/v2/spans`, // url is optional and can be omitted - default is http://localhost:4318/v1/traces - headers: { "Access-Control-Allow-Origin": "*" }, // an optional object containing custom headers to be sent with each request - concurrencyLimit: 10, // an optional limit on pending requests -}; -const otlpExporter = new OTLPTraceExporter(collectorOptions); -const consoleExporter = new ConsoleSpanExporter(); -// The zipkin exporter is the actual exporter we need for web based otel applications -const zipkin = new ZipkinExporter(optionsZipkin); -const posthogExporter = new PosthogSpanExporter(); - -// This is how we can make Jaeger show a reaonsable service in the dropdown on the left. -const providerConfig = { - resource: new Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, - }), -}; -const provider = new WebTracerProvider(providerConfig); - -provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); -// We can add as many processors and exporters as we want to. The zipkin one is the important one for Jaeger -provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); -provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); -provider.addSpanProcessor(new SimpleSpanProcessor(zipkin)); - -// This is unecassary i think... -provider.register({ - // Changing default contextManager to use ZoneContextManager - supports asynchronous operations - optional - contextManager: new ZoneContextManager(), -}); - -// Registering instrumentations (These are automated span collectors for the Http request during page loading, switching) -registerInstrumentations({ - instrumentations: [ - // new DocumentLoadInstrumentation(), - // new UserInteractionInstrumentation(), - ], -}); - -// This is not the serviceName shown in jaeger -export const tracer = opentelemetry.trace.getTracer( - "my-element-call-otl-tracer" -); - -class CallTracer { - // We create one tracer class for each main context. - // Even if differnt tracer classes overlap in time space, we might want to visulaize them seperately. - // The Call Tracer should only contain spans/events that are relevant to understand the procedure of the individual candidates. - // Another Tracer Class (for example a ConnectionTracer) can contain a very granular list of all steps to connect to a call. - - private callSpan; - private callContext; - private muteSpan?; - public startCall(callId: string) { - // The main context will be set when initiating the main/parent span. - - // Create an initial context with the callId param - const callIdContext = opentelemetry.context - .active() - .setValue(Symbol("callId"), callId); - - // Create the main span that tracks the whole call - this.callSpan = tracer.startSpan("otel_callSpan", undefined, callIdContext); - - // Create a new call based on the callIdContext. This context also has a span assigned to it. - // Other spans can use this context to extract the parent span. - // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) - this.callContext = opentelemetry.trace.setSpan( - opentelemetry.context.active(), - this.callSpan - ); - - // Here we start a very short span. This is a hack to trigger the posthog exporter. - // Only ended spans are processed by the exporter. - // We want the exporter to know that a call has started - const startCallSpan = tracer.startSpan( - "otel_startCallSpan", - undefined, - this.callContext - ); - startCallSpan.end(); - } - public muteMic(muteState: boolean) { - if (muteState) { - this.muteSpan = tracer.startSpan( - "otel_muteSpan", - undefined, - this.callContext - ); - } else if (this.muteSpan) { - this.muteSpan.end(); - this.muteSpan = null; - } - } - public endCall() { - this.callSpan?.end(); - } -} - -export const callTracer = new CallTracer(); diff --git a/yarn.lock b/yarn.lock index 8e51837..2662e3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1956,16 +1956,6 @@ "@opentelemetry/resources" "1.9.1" "@opentelemetry/sdk-trace-base" "1.9.1" -"@opentelemetry/exporter-zipkin@^1.9.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.9.1.tgz#0bcddf2f3bcb1b26b94a090c953996a28087d21f" - integrity sha512-KBgf3w84luP5vWLlrqVFKmbwFK4lXM//t6K7H4nsg576htbz1RpBbQfybADjPdXTjGHqDTtLiC5MC90hxS7Z2w== - dependencies: - "@opentelemetry/core" "1.9.1" - "@opentelemetry/resources" "1.9.1" - "@opentelemetry/sdk-trace-base" "1.9.1" - "@opentelemetry/semantic-conventions" "1.9.1" - "@opentelemetry/instrumentation-document-load@^0.31.1": version "0.31.1" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-document-load/-/instrumentation-document-load-0.31.1.tgz#a535a5d1d71706701d3ff560a700b9dd03e4fb59" From 22d2404370c2ce1c72b6da3742c01f4c95df535b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 15 Mar 2023 16:04:15 +0000 Subject: [PATCH 05/34] Prettier --- config/otel_dev/collector-gateway.yaml | 66 +++++++++++++------------- config/otel_dev/docker-compose.yaml | 42 ++++++++-------- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml index 2c70fb4..6cc85a0 100644 --- a/config/otel_dev/collector-gateway.yaml +++ b/config/otel_dev/collector-gateway.yaml @@ -1,38 +1,38 @@ receivers: - otlp: - protocols: - http: - endpoint: 0.0.0.0:4318 - cors: - allowed_origins: - - "http://*" - allowed_headers: - - "*" + otlp: + protocols: + http: + endpoint: 0.0.0.0:4318 + cors: + allowed_origins: + - "http://*" + allowed_headers: + - "*" processors: - batch: - timeout: 1s - resource: - attributes: - - key: test.key - value: "test-value" - action: insert + batch: + timeout: 1s + resource: + attributes: + - key: test.key + value: "test-value" + action: insert exporters: - logging: - loglevel: info - jaeger: - endpoint: jaeger-all-in-one:14250 - tls: - insecure: true + logging: + loglevel: info + jaeger: + endpoint: jaeger-all-in-one:14250 + tls: + insecure: true extensions: - health_check: - pprof: - endpoint: :1888 - zpages: - endpoint: :55679 + health_check: + pprof: + endpoint: :1888 + zpages: + endpoint: :55679 service: - extensions: [pprof, zpages, health_check] - pipelines: - traces: - receivers: [otlp] - processors: [batch, resource] - exporters: [logging, jaeger] + extensions: [pprof, zpages, health_check] + pipelines: + traces: + receivers: [otlp] + processors: [batch, resource] + exporters: [logging, jaeger] diff --git a/config/otel_dev/docker-compose.yaml b/config/otel_dev/docker-compose.yaml index 478f80a..40de4a5 100644 --- a/config/otel_dev/docker-compose.yaml +++ b/config/otel_dev/docker-compose.yaml @@ -1,23 +1,23 @@ version: "2" services: - # Jaeger - jaeger-all-in-one: - image: jaegertracing/all-in-one:latest - ports: - - "16686:16686" - - "14268" - - "14250" - # Collector - collector-gateway: - image: otel/opentelemetry-collector:latest - volumes: - - ./collector-gateway.yaml:/etc/collector-gateway.yaml - command: [ "--config=/etc/collector-gateway.yaml" ] - ports: - - "1888:1888" # pprof extension - - "13133:13133" # health_check extension - - "4317:4317" # OTLP gRPC receiver - - "4318:4318" # OTLP HTTP receiver - - "55670:55679" # zpages extension - depends_on: - - jaeger-all-in-one + # Jaeger + jaeger-all-in-one: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "14268" + - "14250" + # Collector + collector-gateway: + image: otel/opentelemetry-collector:latest + volumes: + - ./collector-gateway.yaml:/etc/collector-gateway.yaml + command: ["--config=/etc/collector-gateway.yaml"] + ports: + - "1888:1888" # pprof extension + - "13133:13133" # health_check extension + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP HTTP receiver + - "55670:55679" # zpages extension + depends_on: + - jaeger-all-in-one From 31450219c87636677bcf8d1b5f60431e7cbe3918 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Mar 2023 14:41:55 +0000 Subject: [PATCH 06/34] More work on opentelemetry event reporting Moastly a re-org to avoid new contexts over React component unmounts/ remounts. --- src/analytics/OtelPosthogExporter.ts | 16 +++++++++++++ src/otel/OTelGroupCallMembership.ts | 35 ++++++++++++++-------------- src/otel/otel.ts | 17 +++++++++++++- src/room/GroupCallInspector.tsx | 22 +++++------------ src/room/GroupCallView.tsx | 5 +++- src/room/InCallView.tsx | 4 ++++ src/room/PTTCallView.tsx | 4 ++++ src/room/useGroupCall.ts | 28 ++++++++++++++++++++-- 8 files changed, 93 insertions(+), 38 deletions(-) diff --git a/src/analytics/OtelPosthogExporter.ts b/src/analytics/OtelPosthogExporter.ts index ee72155..d4f8b53 100644 --- a/src/analytics/OtelPosthogExporter.ts +++ b/src/analytics/OtelPosthogExporter.ts @@ -1,3 +1,19 @@ +/* +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 { SpanExporter } from "@opentelemetry/sdk-trace-base"; import { ReadableSpan } from "@opentelemetry/sdk-trace-base"; import { ExportResult, ExportResultCode } from "@opentelemetry/core"; diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 3ea0266..d8570b7 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -15,7 +15,7 @@ limitations under the License. */ import opentelemetry, { Context, Span } from "@opentelemetry/api"; -import { GroupCall, MatrixEvent } from "matrix-js-sdk"; +import { GroupCall, MatrixClient, MatrixEvent } from "matrix-js-sdk"; import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; import { tracer } from "./otel"; @@ -27,7 +27,7 @@ function setNestedAttributesFromToDeviceEvent(span: Span, event: VoipEvent) { setSpanEventAttributesRecursive( span, event as unknown as Record, // XXX Types - "matrix.", + "matrix.event.", 0 ); } @@ -64,28 +64,27 @@ export class OTelGroupCallMembership { private context: Context; private callMembershipSpan: Span; - constructor(groupCall: GroupCall) { - const callIdContext = opentelemetry.context - .active() - .setValue(Symbol("confId"), groupCall.groupCallId); + constructor(groupCall: GroupCall, client: MatrixClient) { + // Create a new call based on the callIdContext. This context also has a span assigned to it. + // Other spans can use this context to extract the parent span. + // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) + const myMember = groupCall.room.getMember(client.getUserId()); + this.context = opentelemetry.trace + .setSpan(opentelemetry.context.active(), this.callMembershipSpan) + .setValue(Symbol("confId"), groupCall.groupCallId) + .setValue(Symbol("matrix.userId"), client.getUserId()) + .setValue(Symbol("matrix.displayName"), myMember.name); + } + + public onJoinCall() { // Create the main span that tracks the time we intend to be in the call this.callMembershipSpan = tracer.startSpan( "otel_groupCallMembershipSpan", undefined, - callIdContext + this.context ); - // Create a new call based on the callIdContext. This context also has a span assigned to it. - // Other spans can use this context to extract the parent span. - // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) - this.context = opentelemetry.trace.setSpan( - opentelemetry.context.active(), - this.callMembershipSpan - ); - } - - public onJoinCall() { // Here we start a very short span. This is a hack to trigger the posthog exporter. // Only ended spans are processed by the exporter. // We want the exporter to know that a call has started @@ -107,7 +106,7 @@ export class OTelGroupCallMembership { startCallSpan.end(); // and end the main span to indicate we've left - this.callMembershipSpan.end(); + if (this.callMembershipSpan) this.callMembershipSpan.end(); } public onSendStateEvent(stateEvent: MatrixEvent) {} diff --git a/src/otel/otel.ts b/src/otel/otel.ts index ecff690..f7facba 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -1,4 +1,19 @@ -/* document-load.ts|js file - the code is the same for both the languages */ +/* +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 { ConsoleSpanExporter, SimpleSpanProcessor, diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index a0b16a9..18c5752 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -354,20 +354,8 @@ function reducer( function useGroupCallState( client: MatrixClient, groupCall: GroupCall, - showPollCallStats: boolean + otelGroupCallMembership: OTelGroupCallMembership ): InspectorContextState { - const [otelMembership] = useState( - () => new OTelGroupCallMembership(groupCall) - ); - - useEffect(() => { - otelMembership.onJoinCall(); - - return () => { - otelMembership.onLeaveCall(); - }; - }, [otelMembership]); - const [state, dispatch] = useReducer(reducer, { localUserId: client.getUserId(), localSessionId: client.getSessionId(), @@ -403,7 +391,7 @@ function useGroupCallState( function onSendVoipEvent(event: VoipEvent) { dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event }); - otelMembership.onSendEvent(event); + otelGroupCallMembership.onSendEvent(event); } function onUndecryptableToDevice(event: MatrixEvent) { @@ -437,7 +425,7 @@ function useGroupCallState( onUndecryptableToDevice ); }; - }, [client, groupCall, otelMembership]); + }, [client, groupCall, otelGroupCallMembership]); return state; } @@ -445,17 +433,19 @@ function useGroupCallState( interface GroupCallInspectorProps { client: MatrixClient; groupCall: GroupCall; + otelGroupCallMembership: OTelGroupCallMembership; show: boolean; } export function GroupCallInspector({ client, groupCall, + otelGroupCallMembership, show, }: GroupCallInspectorProps) { const [currentTab, setCurrentTab] = useState("sequence-diagrams"); const [selectedUserId, setSelectedUserId] = useState(); - const state = useGroupCallState(client, groupCall, show); + const state = useGroupCallState(client, groupCall, otelGroupCallMembership); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, setState] = useContext(InspectorContext); diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 61e1eb5..ad011ef 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -81,7 +81,8 @@ export function GroupCallView({ screenshareFeeds, participants, unencryptedEventsFromUsers, - } = useGroupCall(groupCall); + otelGroupCallMembership, + } = useGroupCall(groupCall, client); const { t } = useTranslation(); const { setAudioInput, setVideoInput } = useMediaHandler(); @@ -237,6 +238,7 @@ export function GroupCallView({ onLeave={onLeave} isEmbedded={isEmbedded} hideHeader={hideHeader} + otelGroupCallMembership={otelGroupCallMembership} /> ); } else { @@ -261,6 +263,7 @@ export function GroupCallView({ roomIdOrAlias={roomIdOrAlias} unencryptedEventsFromUsers={unencryptedEventsFromUsers} hideHeader={hideHeader} + otelGroupCallMembership={otelGroupCallMembership} /> ); } diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 48b1cbf..8639cba 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -73,6 +73,7 @@ import { TileDescriptor } from "../video-grid/TileDescriptor"; import { AudioSink } from "../video-grid/AudioSink"; import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; import { NewVideoGrid } from "../video-grid/NewVideoGrid"; +import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // There is currently a bug in Safari our our code with cloning and sending MediaStreams @@ -100,6 +101,7 @@ interface Props { roomIdOrAlias: string; unencryptedEventsFromUsers: Set; hideHeader: boolean; + otelGroupCallMembership: OTelGroupCallMembership; } export function InCallView({ @@ -122,6 +124,7 @@ export function InCallView({ roomIdOrAlias, unencryptedEventsFromUsers, hideHeader, + otelGroupCallMembership, }: Props) { const { t } = useTranslation(); usePreventScroll(); @@ -429,6 +432,7 @@ export function InCallView({ {rageshakeRequestModalState.isOpen && ( diff --git a/src/room/PTTCallView.tsx b/src/room/PTTCallView.tsx index a25f337..2781640 100644 --- a/src/room/PTTCallView.tsx +++ b/src/room/PTTCallView.tsx @@ -44,6 +44,7 @@ import { GroupCallInspector } from "./GroupCallInspector"; import { OverflowMenu } from "./OverflowMenu"; import { Size } from "../Avatar"; import { ParticipantInfo } from "./useGroupCall"; +import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; function getPromptText( networkWaiting: boolean, @@ -106,6 +107,7 @@ interface Props { onLeave: () => void; isEmbedded: boolean; hideHeader: boolean; + otelGroupCallMembership: OTelGroupCallMembership; } export const PTTCallView: React.FC = ({ @@ -119,6 +121,7 @@ export const PTTCallView: React.FC = ({ onLeave, isEmbedded, hideHeader, + otelGroupCallMembership, }) => { const { t } = useTranslation(); const { modalState: inviteModalState, modalProps: inviteModalProps } = @@ -192,6 +195,7 @@ export const PTTCallView: React.FC = ({ >; hasLocalParticipant: boolean; unencryptedEventsFromUsers: Set; + otelGroupCallMembership: OTelGroupCallMembership; } interface State { @@ -84,6 +87,13 @@ interface State { hasLocalParticipant: boolean; } +// This is a bit of a hack, but we keep the opentelemetry tracker object at the file +// level so that it doesn't pop in & out of existence as react mounts & unmounts +// components. The right solution is probably for this to live in the js-sdk and have +// the same lifetime as groupcalls themselves. +let groupCallOTelMembership: OTelGroupCallMembership; +let groupCallOTelMembershipGroupCallId: string; + function getParticipants( groupCall: GroupCall ): Map> { @@ -112,7 +122,10 @@ function getParticipants( return participants; } -export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { +export function useGroupCall( + groupCall: GroupCall, + client: MatrixClient +): UseGroupCallReturnType { const [ { state, @@ -146,6 +159,11 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { hasLocalParticipant: false, }); + if (groupCallOTelMembershipGroupCallId !== groupCall.groupCallId) { + groupCallOTelMembership = new OTelGroupCallMembership(groupCall, client); + groupCallOTelMembershipGroupCallId = groupCall.groupCallId; + } + const [unencryptedEventsFromUsers, addUnencryptedEventUser] = useReducer( (state: Set, newVal: string) => { return new Set(state).add(newVal); @@ -383,9 +401,14 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { console.error(error); updateState({ error }); }); + + groupCallOTelMembership.onJoinCall(); }, [groupCall, updateState]); - const leave = useCallback(() => groupCall.leave(), [groupCall]); + const leave = useCallback(() => { + groupCallOTelMembership.onLeaveCall(); + groupCall.leave(); + }, [groupCall]); const toggleLocalVideoMuted = useCallback(() => { const toggleToMute = !groupCall.isLocalVideoMuted(); @@ -525,5 +548,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { participants, hasLocalParticipant, unencryptedEventsFromUsers, + otelGroupCallMembership: groupCallOTelMembership, }; } From 521b0a857aa90ffdd4f162370bd6779acd482cc3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Mar 2023 18:08:28 +0000 Subject: [PATCH 07/34] Send spans for state events --- src/otel/OTelGroupCallMembership.ts | 31 ++++++++++++++++++++++++++--- src/room/GroupCallInspector.tsx | 2 ++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index d8570b7..5a8dae3 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -23,7 +23,7 @@ import { tracer } from "./otel"; /** * Recursively sets the contents of a todevice event object as attributes on a span */ -function setNestedAttributesFromToDeviceEvent(span: Span, event: VoipEvent) { +function setNestedAttributesFromEvent(span: Span, event: VoipEvent) { setSpanEventAttributesRecursive( span, event as unknown as Record, // XXX Types @@ -109,7 +109,23 @@ export class OTelGroupCallMembership { if (this.callMembershipSpan) this.callMembershipSpan.end(); } - public onSendStateEvent(stateEvent: MatrixEvent) {} + public onUpdateRoomState(event: MatrixEvent) { + if ( + !event || + (!event.getType().startsWith("m.call") && + !event.getType().startsWith("org.matrix.msc3401.call")) + ) + return; + + const span = tracer.startSpan( + `otel_onRoomStateEvent_${event.getType()}`, + undefined, + this.context + ); + + setNestedAttributesFromEvent(span, event.getContent()); + span.end(); + } public onSendEvent(event: VoipEvent) { const eventType = event.eventType as string; @@ -122,7 +138,16 @@ export class OTelGroupCallMembership { this.context ); - setNestedAttributesFromToDeviceEvent(span, event); + setNestedAttributesFromEvent(span, event); + span.end(); + } else if (event.type === "sendEvent") { + const span = tracer.startSpan( + `otel_sendToRoomEvent_${event.eventType}`, + undefined, + this.context + ); + + setNestedAttributesFromEvent(span, event); span.end(); } } diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index 18c5752..e1e12ea 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -382,6 +382,8 @@ function useGroupCallState( callStateEvent, memberStateEvents, }); + + otelGroupCallMembership.onUpdateRoomState(event); } function onReceivedVoipEvent(event: MatrixEvent) { From f8f5d2011d207c2c5e1add80b6c61b6fe3a28ae3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Mar 2023 17:01:59 +0000 Subject: [PATCH 08/34] Add CORS to jaeger query endpoint and make spans nested Adds an nginx in front of the query endpoint so we can use stalk without faffing with browser extension to bypass CORS. Also make the spans correctly have the call membership span as parent, which they didn't because we hadn't set the span at the point we made the context. --- config/otel_dev/README.md | 9 ++++++++ config/otel_dev/docker-compose.yaml | 6 +++++ src/otel/OTelGroupCallMembership.ts | 35 ++++++++++++++++------------- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/config/otel_dev/README.md b/config/otel_dev/README.md index 04690c7..8fe102d 100644 --- a/config/otel_dev/README.md +++ b/config/otel_dev/README.md @@ -6,4 +6,13 @@ traces into the jaeger. Jaeger has a built-in OpenTelemetry collector, but it ca configured to send CORS headers so can't be used from a browser. This sets the config on the collector to send CORS headers. +This also adds an nginx to add CORS headers to the jaeger query endpoint, such that it can +be used from webapps like stalk (https://deniz.co/stalk/). The CORS enabled endpoint is +exposed on port 16687. To use stalk, you should simply be able to navigate to it and add +http://127.0.0.1:16687/api as a data source. + +(Yes, we could enable the OTLP collector in jaeger all-in-one and passed this through +the nginx to enable CORS too, rather than running a separate collector. There's no reason +it's done this way other than that I'd already set up the separate collector.) + Running `docker compose up` in this directory should be all you need. diff --git a/config/otel_dev/docker-compose.yaml b/config/otel_dev/docker-compose.yaml index 40de4a5..0b43abe 100644 --- a/config/otel_dev/docker-compose.yaml +++ b/config/otel_dev/docker-compose.yaml @@ -21,3 +21,9 @@ services: - "55670:55679" # zpages extension depends_on: - jaeger-all-in-one + nginx: + image: nginxinc/nginx-unprivileged:latest + volumes: + - ./nginx_otel.conf:/etc/nginx/conf.d/default.conf:ro + ports: + - "16687:8080" diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 5a8dae3..886b4ec 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -15,7 +15,12 @@ limitations under the License. */ import opentelemetry, { Context, Span } from "@opentelemetry/api"; -import { GroupCall, MatrixClient, MatrixEvent } from "matrix-js-sdk"; +import { + GroupCall, + MatrixClient, + MatrixEvent, + RoomMember, +} from "matrix-js-sdk"; import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; import { tracer } from "./otel"; @@ -63,27 +68,27 @@ function setSpanEventAttributesRecursive( export class OTelGroupCallMembership { private context: Context; private callMembershipSpan: Span; + private myUserId: string; + private myMember: RoomMember; - constructor(groupCall: GroupCall, client: MatrixClient) { + constructor(private groupCall: GroupCall, client: MatrixClient) { + this.myUserId = client.getUserId(); + this.myMember = groupCall.room.getMember(client.getUserId()); + } + + public onJoinCall() { // Create a new call based on the callIdContext. This context also has a span assigned to it. // Other spans can use this context to extract the parent span. // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) - const myMember = groupCall.room.getMember(client.getUserId()); + // Create the main span that tracks the time we intend to be in the call + this.callMembershipSpan = tracer.startSpan("otel_groupCallMembershipSpan"); + this.context = opentelemetry.trace .setSpan(opentelemetry.context.active(), this.callMembershipSpan) - .setValue(Symbol("confId"), groupCall.groupCallId) - .setValue(Symbol("matrix.userId"), client.getUserId()) - .setValue(Symbol("matrix.displayName"), myMember.name); - } - - public onJoinCall() { - // Create the main span that tracks the time we intend to be in the call - this.callMembershipSpan = tracer.startSpan( - "otel_groupCallMembershipSpan", - undefined, - this.context - ); + .setValue(Symbol("confId"), this.groupCall.groupCallId) + .setValue(Symbol("matrix.userId"), this.myUserId) + .setValue(Symbol("matrix.displayName"), this.myMember.name); // Here we start a very short span. This is a hack to trigger the posthog exporter. // Only ended spans are processed by the exporter. From 2d91b43a7d78e3c6e35a4630d48e8069215b7e44 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Mar 2023 19:03:43 +0000 Subject: [PATCH 09/34] Set attributes on the root span Setting them on the context doesn't actually make them show up in jaeger, it's just a way to propagate the info around between different things. --- src/otel/OTelGroupCallMembership.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 886b4ec..ba06090 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -84,11 +84,20 @@ export class OTelGroupCallMembership { // Create the main span that tracks the time we intend to be in the call this.callMembershipSpan = tracer.startSpan("otel_groupCallMembershipSpan"); - this.context = opentelemetry.trace - .setSpan(opentelemetry.context.active(), this.callMembershipSpan) - .setValue(Symbol("confId"), this.groupCall.groupCallId) - .setValue(Symbol("matrix.userId"), this.myUserId) - .setValue(Symbol("matrix.displayName"), this.myMember.name); + this.callMembershipSpan.setAttribute( + "matrix.confId", + this.groupCall.groupCallId + ); + this.callMembershipSpan.setAttribute("matrix.userId", this.myUserId); + this.callMembershipSpan.setAttribute( + "matrix.displayName", + this.myMember.name + ); + + this.context = opentelemetry.trace.setSpan( + opentelemetry.context.active(), + this.callMembershipSpan + ); // Here we start a very short span. This is a hack to trigger the posthog exporter. // Only ended spans are processed by the exporter. From 63ede0b51aa86d0649fb0dd8262ca2ba9b706231 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Mar 2023 19:26:23 +0000 Subject: [PATCH 10/34] Version using events for call joins / leaves and matrix events This is probably conceptually nicer although isn't quite as nice in the jaeger / stalk UI. Also this may no loger work with the posthog exporter (unsure what it will do with events on spans). --- src/otel/OTelGroupCallMembership.ts | 78 +++++++++++------------------ 1 file changed, 28 insertions(+), 50 deletions(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index ba06090..a4e3bfb 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import opentelemetry, { Context, Span } from "@opentelemetry/api"; +import opentelemetry, { Span, Attributes } from "@opentelemetry/api"; import { GroupCall, MatrixClient, @@ -26,20 +26,25 @@ import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; import { tracer } from "./otel"; /** - * Recursively sets the contents of a todevice event object as attributes on a span + * Flattens out an object into a single layer with components + * of the key separated by dots */ -function setNestedAttributesFromEvent(span: Span, event: VoipEvent) { - setSpanEventAttributesRecursive( - span, +function flattenVoipEvent(event: VoipEvent): Attributes { + const flatObject = {}; + + flattenVoipEventRecursive( event as unknown as Record, // XXX Types + flatObject, "matrix.event.", 0 ); + + return flatObject; } -function setSpanEventAttributesRecursive( - span: Span, +function flattenVoipEventRecursive( obj: Record, + flatObject: Record, prefix: string, depth: number ) { @@ -50,11 +55,11 @@ function setSpanEventAttributesRecursive( for (const [k, v] of Object.entries(obj)) { if (["string", "number"].includes(typeof v)) { - span.setAttribute(prefix + k, v as string | number); + flatObject[prefix + k] = v; } else if (typeof v === "object") { - setSpanEventAttributesRecursive( - span, + flattenVoipEventRecursive( v as Record, + flatObject, prefix + k + ".", depth + 1 ); @@ -66,7 +71,6 @@ function setSpanEventAttributesRecursive( * Represent the span of time which we intend to be joined to a group call */ export class OTelGroupCallMembership { - private context: Context; private callMembershipSpan: Span; private myUserId: string; private myMember: RoomMember; @@ -83,7 +87,6 @@ export class OTelGroupCallMembership { // Create the main span that tracks the time we intend to be in the call this.callMembershipSpan = tracer.startSpan("otel_groupCallMembershipSpan"); - this.callMembershipSpan.setAttribute( "matrix.confId", this.groupCall.groupCallId @@ -94,30 +97,16 @@ export class OTelGroupCallMembership { this.myMember.name ); - this.context = opentelemetry.trace.setSpan( + opentelemetry.trace.setSpan( opentelemetry.context.active(), this.callMembershipSpan ); - // Here we start a very short span. This is a hack to trigger the posthog exporter. - // Only ended spans are processed by the exporter. - // We want the exporter to know that a call has started - const joinCallSpan = tracer.startSpan( - "otel_joinCallSpan", - undefined, - this.context - ); - joinCallSpan.end(); + this.callMembershipSpan.addEvent("matrix.joinCall"); } public onLeaveCall() { - // A very short span to represent us leaving the call - const startCallSpan = tracer.startSpan( - "otel_leaveCallSpan", - undefined, - this.context - ); - startCallSpan.end(); + this.callMembershipSpan.addEvent("matrix.leaveCall"); // and end the main span to indicate we've left if (this.callMembershipSpan) this.callMembershipSpan.end(); @@ -128,17 +117,14 @@ export class OTelGroupCallMembership { !event || (!event.getType().startsWith("m.call") && !event.getType().startsWith("org.matrix.msc3401.call")) - ) + ) { return; + } - const span = tracer.startSpan( + this.callMembershipSpan.addEvent( `otel_onRoomStateEvent_${event.getType()}`, - undefined, - this.context + flattenVoipEvent(event.getContent()) ); - - setNestedAttributesFromEvent(span, event.getContent()); - span.end(); } public onSendEvent(event: VoipEvent) { @@ -146,23 +132,15 @@ export class OTelGroupCallMembership { if (!eventType.startsWith("m.call")) return; if (event.type === "toDevice") { - const span = tracer.startSpan( - `otel_sendToDeviceEvent_${event.eventType}`, - undefined, - this.context + this.callMembershipSpan.addEvent( + `matrix.sendToDeviceEvent_${event.eventType}`, + flattenVoipEvent(event) ); - - setNestedAttributesFromEvent(span, event); - span.end(); } else if (event.type === "sendEvent") { - const span = tracer.startSpan( - `otel_sendToRoomEvent_${event.eventType}`, - undefined, - this.context + this.callMembershipSpan.addEvent( + `matrix.sendToRoomEvent_${event.eventType}`, + flattenVoipEvent(event) ); - - setNestedAttributesFromEvent(span, event); - span.end(); } } } From e7a7cf3eb8080b3dbe2067339ff3216bb68bc55d Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 20 Mar 2023 13:30:21 +0000 Subject: [PATCH 11/34] Export events to posthog too --- src/analytics/OtelPosthogExporter.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/analytics/OtelPosthogExporter.ts b/src/analytics/OtelPosthogExporter.ts index d4f8b53..3850e13 100644 --- a/src/analytics/OtelPosthogExporter.ts +++ b/src/analytics/OtelPosthogExporter.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { SpanExporter } from "@opentelemetry/sdk-trace-base"; -import { ReadableSpan } from "@opentelemetry/sdk-trace-base"; +import { SpanExporter, ReadableSpan } from "@opentelemetry/sdk-trace-base"; import { ExportResult, ExportResultCode } from "@opentelemetry/core"; import { PosthogAnalytics } from "./PosthogAnalytics"; @@ -34,11 +33,23 @@ export class PosthogSpanExporter implements SpanExporter { resultCallback: (result: ExportResult) => void ): Promise { console.log("POSTHOGEXPORTER", spans); - for (let i = 0; i < spans.length; i++) { - const span = spans[i]; - const sendInstantly = - span.name == "otel_callEnded" || - span.name == "otel_otherSentInstantlyEventName"; + for (const span of spans) { + const sendInstantly = [ + "otel_callEnded", + "otel_otherSentInstantlyEventName", + ].includes(span.name); + + for (const spanEvent of span.events) { + await PosthogAnalytics.instance.trackFromSpan( + { + eventName: spanEvent.name, + ...spanEvent.attributes, + }, + { + send_instantly: sendInstantly, + } + ); + } await PosthogAnalytics.instance.trackFromSpan( { eventName: span.name, ...span.attributes }, From ef9934ce6b6574fb2300551d25aedafd66dde427 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 20 Mar 2023 13:53:07 +0000 Subject: [PATCH 12/34] Commit nginx config file --- config/otel_dev/nginx_otel.conf | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 config/otel_dev/nginx_otel.conf diff --git a/config/otel_dev/nginx_otel.conf b/config/otel_dev/nginx_otel.conf new file mode 100644 index 0000000..90d6970 --- /dev/null +++ b/config/otel_dev/nginx_otel.conf @@ -0,0 +1,16 @@ +server { + listen 8080; + server_name localhost; + + location / { + proxy_pass http://jaeger-all-in-one:16686/; + add_header Access-Control-Allow-Origin *; + + if ($request_method = OPTIONS) { + add_header Access-Control-Allow-Origin *; + add_header Content-Type text/plain; + add_header Content-Length 0; + return 204; + } + } +} From 6b36604c8417879aacf0ae6a082b7dd30b34046d Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 20 Mar 2023 19:17:50 +0000 Subject: [PATCH 13/34] Update js-sdk --- package.json | 2 +- src/otel/OTelGroupCallMembership.ts | 43 ++++++++++++++++--- src/otel/otel.ts | 64 +---------------------------- src/room/useGroupCall.ts | 6 +++ yarn.lock | 16 ++++---- 5 files changed, 53 insertions(+), 78 deletions(-) diff --git a/package.json b/package.json index a8f6ff0..a32fd8c 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#64197bf4db6486d77708125d7fb2e8d7fe001f14", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#23837266fca5ee799b51a722f7b8eefb2f5ac140", "matrix-widget-api": "^1.0.0", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index a4e3bfb..65e5d6a 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -15,6 +15,7 @@ limitations under the License. */ import opentelemetry, { Span, Attributes } from "@opentelemetry/api"; +import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; import { GroupCall, MatrixClient, @@ -23,7 +24,7 @@ import { } from "matrix-js-sdk"; import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; -import { tracer } from "./otel"; +import { provider, tracer } from "./otel"; /** * Flattens out an object into a single layer with components @@ -78,15 +79,15 @@ export class OTelGroupCallMembership { constructor(private groupCall: GroupCall, client: MatrixClient) { this.myUserId = client.getUserId(); this.myMember = groupCall.room.getMember(client.getUserId()); + + provider.resource.attributes[ + SemanticResourceAttributes.SERVICE_NAME + ] = `element-call-${this.myUserId}-${client.getDeviceId()}`; } public onJoinCall() { - // Create a new call based on the callIdContext. This context also has a span assigned to it. - // Other spans can use this context to extract the parent span. - // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) - // Create the main span that tracks the time we intend to be in the call - this.callMembershipSpan = tracer.startSpan("otel_groupCallMembershipSpan"); + this.callMembershipSpan = tracer.startSpan("matrix.groupCallMembership"); this.callMembershipSpan.setAttribute( "matrix.confId", this.groupCall.groupCallId @@ -143,4 +144,34 @@ export class OTelGroupCallMembership { ); } } + + public onToggleMicrophoneMuted(newValue: boolean) { + this.callMembershipSpan.addEvent("matrix.toggleMicMuted", { + "matrix.microphone.muted": newValue, + }); + } + + public onSetMicrophoneMuted(setMuted: boolean) { + this.callMembershipSpan.addEvent("matrix.setMicMuted", { + "matrix.microphone.muted": setMuted, + }); + } + + public onToggleLocalVideoMuted(newValue: boolean) { + this.callMembershipSpan.addEvent("matrix.toggleVidMuted", { + "matrix.video.muted": newValue, + }); + } + + public onSetLocalVideoMuted(setMuted: boolean) { + this.callMembershipSpan.addEvent("matrix.setVidMuted", { + "matrix.video.muted": setMuted, + }); + } + + public onToggleScreensharing(newValue: boolean) { + this.callMembershipSpan.addEvent("matrix.setVidMuted", { + "matrix.screensharing.enabled": newValue, + }); + } } diff --git a/src/otel/otel.ts b/src/otel/otel.ts index f7facba..95e8197 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -38,7 +38,7 @@ const providerConfig = { [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, }), }; -const provider = new WebTracerProvider(providerConfig); +export const provider = new WebTracerProvider(providerConfig); provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); @@ -49,65 +49,3 @@ opentelemetry.trace.setGlobalTracerProvider(provider); export const tracer = opentelemetry.trace.getTracer( "my-element-call-otl-tracer" ); - -/* -class CallTracer { - // We create one tracer class for each main context. - // Even if differnt tracer classes overlap in time space, we might want to visulaize them seperately. - // The Call Tracer should only contain spans/events that are relevant to understand the procedure of the individual candidates. - // Another Tracer Class (for example a ConnectionTracer) can contain a very granular list of all steps to connect to a call. - - private callSpan; - private callContext; - private muteSpan?; - - public startGroupCall(groupCallId: string) {} - - public startCall(callId: string) { - // The main context will be set when initiating the main/parent span. - - // Create an initial context with the callId param - const callIdContext = opentelemetry.context - .active() - .setValue(Symbol("callId"), callId); - - // Create the main span that tracks the whole call - this.callSpan = tracer.startSpan("otel_callSpan", undefined, callIdContext); - - // Create a new call based on the callIdContext. This context also has a span assigned to it. - // Other spans can use this context to extract the parent span. - // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) - this.callContext = opentelemetry.trace.setSpan( - opentelemetry.context.active(), - this.callSpan - ); - - // Here we start a very short span. This is a hack to trigger the posthog exporter. - // Only ended spans are processed by the exporter. - // We want the exporter to know that a call has started - const startCallSpan = tracer.startSpan( - "otel_startCallSpan", - undefined, - this.callContext - ); - startCallSpan.end(); - } - public muteMic(muteState: boolean) { - if (muteState) { - this.muteSpan = tracer.startSpan( - "otel_muteSpan", - undefined, - this.callContext - ); - } else if (this.muteSpan) { - this.muteSpan.end(); - this.muteSpan = null; - } - } - public endCall() { - this.callSpan?.end(); - } -} - -export const callTracer = new CallTracer(); -*/ diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 0311ed9..44db212 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -413,6 +413,8 @@ export function useGroupCall( const toggleLocalVideoMuted = useCallback(() => { const toggleToMute = !groupCall.isLocalVideoMuted(); groupCall.setLocalVideoMuted(toggleToMute); + groupCallOTelMembership.onToggleLocalVideoMuted(toggleToMute); + // TODO: These explict posthog calls should be unnecessary now with the posthog otel exporter? PosthogAnalytics.instance.eventMuteCamera.track( toggleToMute, groupCall.groupCallId @@ -422,6 +424,7 @@ export function useGroupCall( const setMicrophoneMuted = useCallback( (setMuted) => { groupCall.setMicrophoneMuted(setMuted); + groupCallOTelMembership.onSetMicrophoneMuted(setMuted); PosthogAnalytics.instance.eventMuteMicrophone.track( setMuted, groupCall.groupCallId @@ -432,10 +435,13 @@ export function useGroupCall( const toggleMicrophoneMuted = useCallback(() => { const toggleToMute = !groupCall.isMicrophoneMuted(); + groupCallOTelMembership.onToggleMicrophoneMuted(toggleToMute); setMicrophoneMuted(toggleToMute); }, [groupCall, setMicrophoneMuted]); const toggleScreensharing = useCallback(async () => { + groupCallOTelMembership.onToggleScreensharing(!groupCall.isScreensharing); + if (!groupCall.isScreensharing()) { // toggling on updateState({ requestingScreenshare: true }); diff --git a/yarn.lock b/yarn.lock index 2662e3d..ddb3fc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1821,10 +1821,10 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== -"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.2": - version "0.1.0-alpha.2" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.2.tgz#a09d0fea858e817da971a3c9f904632ef7b49eb6" - integrity sha512-oVkBCh9YP7H9i4gAoQbZzswniczfo/aIptNa4dxRi4Ff9lSvUCFv6Hvzi7C+90c0/PWZLXjIDTIAWZYmwyd2fA== +"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.3": + version "0.1.0-alpha.5" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.5.tgz#60ede2c43b9d808ba8cf46085a3b347b290d9658" + integrity sha512-2KjAgWNGfuGLNjJwsrs6gGX157vmcTfNrA4u249utgnMPbJl7QwuUqh1bGxQ0PpK06yvZjgPlkna0lTbuwtuQw== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14" @@ -10545,12 +10545,12 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#64197bf4db6486d77708125d7fb2e8d7fe001f14": - version "23.1.1" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/64197bf4db6486d77708125d7fb2e8d7fe001f14" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#23837266fca5ee799b51a722f7b8eefb2f5ac140": + version "23.5.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/23837266fca5ee799b51a722f7b8eefb2f5ac140" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.2" + "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.3" another-json "^0.2.0" bs58 "^5.0.0" content-type "^1.0.4" From 6696af9b3ff5eb113dbef1530ecbb23a7166c2cf Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 20 Mar 2023 19:29:19 +0000 Subject: [PATCH 14/34] Experiment to try & stop vite OOMing --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index eee1406..654d609 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -23,6 +23,7 @@ jobs: SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} SENTRY_URL: ${{ secrets.SENTRY_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + NODE_OPTIONS: "--max-old-space-size=16384" - name: Upload Artifact uses: actions/upload-artifact@v2 with: From 359e055314123cbd0eea6d46fc1811c80868f6de Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 21 Mar 2023 12:13:51 +0000 Subject: [PATCH 15/34] Make callMembershipSpan optional --- src/otel/OTelGroupCallMembership.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 65e5d6a..8d9f03f 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -72,7 +72,7 @@ function flattenVoipEventRecursive( * Represent the span of time which we intend to be joined to a group call */ export class OTelGroupCallMembership { - private callMembershipSpan: Span; + private callMembershipSpan?: Span; private myUserId: string; private myMember: RoomMember; @@ -103,11 +103,11 @@ export class OTelGroupCallMembership { this.callMembershipSpan ); - this.callMembershipSpan.addEvent("matrix.joinCall"); + this.callMembershipSpan?.addEvent("matrix.joinCall"); } public onLeaveCall() { - this.callMembershipSpan.addEvent("matrix.leaveCall"); + this.callMembershipSpan?.addEvent("matrix.leaveCall"); // and end the main span to indicate we've left if (this.callMembershipSpan) this.callMembershipSpan.end(); @@ -122,7 +122,7 @@ export class OTelGroupCallMembership { return; } - this.callMembershipSpan.addEvent( + this.callMembershipSpan?.addEvent( `otel_onRoomStateEvent_${event.getType()}`, flattenVoipEvent(event.getContent()) ); @@ -133,12 +133,12 @@ export class OTelGroupCallMembership { if (!eventType.startsWith("m.call")) return; if (event.type === "toDevice") { - this.callMembershipSpan.addEvent( + this.callMembershipSpan?.addEvent( `matrix.sendToDeviceEvent_${event.eventType}`, flattenVoipEvent(event) ); } else if (event.type === "sendEvent") { - this.callMembershipSpan.addEvent( + this.callMembershipSpan?.addEvent( `matrix.sendToRoomEvent_${event.eventType}`, flattenVoipEvent(event) ); @@ -146,31 +146,31 @@ export class OTelGroupCallMembership { } public onToggleMicrophoneMuted(newValue: boolean) { - this.callMembershipSpan.addEvent("matrix.toggleMicMuted", { + this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", { "matrix.microphone.muted": newValue, }); } public onSetMicrophoneMuted(setMuted: boolean) { - this.callMembershipSpan.addEvent("matrix.setMicMuted", { + this.callMembershipSpan?.addEvent("matrix.setMicMuted", { "matrix.microphone.muted": setMuted, }); } public onToggleLocalVideoMuted(newValue: boolean) { - this.callMembershipSpan.addEvent("matrix.toggleVidMuted", { + this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", { "matrix.video.muted": newValue, }); } public onSetLocalVideoMuted(setMuted: boolean) { - this.callMembershipSpan.addEvent("matrix.setVidMuted", { + this.callMembershipSpan?.addEvent("matrix.setVidMuted", { "matrix.video.muted": setMuted, }); } public onToggleScreensharing(newValue: boolean) { - this.callMembershipSpan.addEvent("matrix.setVidMuted", { + this.callMembershipSpan?.addEvent("matrix.setVidMuted", { "matrix.screensharing.enabled": newValue, }); } From 3d6ae3fbc39ff892403e7fccf38c867ac4113172 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2023 11:55:21 +0000 Subject: [PATCH 16/34] Enable/disable opentelemetry based on config/user preference Add config to set collector URL, obey the same analytics setting as posthog. Also refactor into a class to make it easier to manage. --- src/config/ConfigOptions.ts | 7 +++ src/otel/OTelGroupCallMembership.ts | 9 ++- src/otel/otel.ts | 87 ++++++++++++++++++++++------- src/room/GroupCallInspector.tsx | 4 +- src/room/useGroupCall.ts | 24 +++++--- 5 files changed, 98 insertions(+), 33 deletions(-) diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index 5899d45..cfaaf77 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -36,6 +36,13 @@ export interface ConfigOptions { submit_url: string; }; + /** + * Controls whether to to send OpenTelemetry debugging data to collector + */ + opentelemetry?: { + collector_url: string; + }; + // Describes the default homeserver to use. The same format as Element Web // (without identity servers as we don't use them). default_server_config?: { diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 8d9f03f..764249f 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -24,7 +24,7 @@ import { } from "matrix-js-sdk"; import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; -import { provider, tracer } from "./otel"; +import { ElementCallOpenTelemetry } from "./otel"; /** * Flattens out an object into a single layer with components @@ -80,14 +80,17 @@ export class OTelGroupCallMembership { this.myUserId = client.getUserId(); this.myMember = groupCall.room.getMember(client.getUserId()); - provider.resource.attributes[ + ElementCallOpenTelemetry.instance.provider.resource.attributes[ SemanticResourceAttributes.SERVICE_NAME ] = `element-call-${this.myUserId}-${client.getDeviceId()}`; } public onJoinCall() { // Create the main span that tracks the time we intend to be in the call - this.callMembershipSpan = tracer.startSpan("matrix.groupCallMembership"); + this.callMembershipSpan = + ElementCallOpenTelemetry.instance.tracer.startSpan( + "matrix.groupCallMembership" + ); this.callMembershipSpan.setAttribute( "matrix.confId", this.groupCall.groupCallId diff --git a/src/otel/otel.ts b/src/otel/otel.ts index 95e8197..301c077 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -20,32 +20,79 @@ import { } from "@opentelemetry/sdk-trace-base"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; -import opentelemetry from "@opentelemetry/api"; +import opentelemetry, { Tracer } from "@opentelemetry/api"; import { Resource } from "@opentelemetry/resources"; import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; +import { logger } from "@sentry/utils"; import { PosthogSpanExporter } from "../analytics/OtelPosthogExporter"; +import { Anonymity } from "../analytics/PosthogAnalytics"; +import { Config } from "../config/Config"; +import { getSetting, settingsBus } from "../settings/useSetting"; -const SERVICE_NAME = "element-call"; +const SERVICE_NAME_BASE = "element-call"; -const otlpExporter = new OTLPTraceExporter(); -const consoleExporter = new ConsoleSpanExporter(); -const posthogExporter = new PosthogSpanExporter(); +let sharedInstance: ElementCallOpenTelemetry; -// This is how we can make Jaeger show a reaonsable service in the dropdown on the left. -const providerConfig = { - resource: new Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, - }), -}; -export const provider = new WebTracerProvider(providerConfig); +export class ElementCallOpenTelemetry { + private _provider: WebTracerProvider; + private _tracer: Tracer; + private _anonymity: Anonymity; -provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); -provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); -provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); -opentelemetry.trace.setGlobalTracerProvider(provider); + static get instance(): ElementCallOpenTelemetry { + return sharedInstance; + } -// This is not the serviceName shown in jaeger -export const tracer = opentelemetry.trace.getTracer( - "my-element-call-otl-tracer" -); + constructor(collectorUrl: string) { + const otlpExporter = new OTLPTraceExporter({ + url: collectorUrl, + }); + const consoleExporter = new ConsoleSpanExporter(); + const posthogExporter = new PosthogSpanExporter(); + + // This is how we can make Jaeger show a reaonsable service in the dropdown on the left. + const providerConfig = { + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: `${SERVICE_NAME_BASE}-unauthenticated`, + }), + }; + this._provider = new WebTracerProvider(providerConfig); + + this._provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); + this._provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); + this._provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); + opentelemetry.trace.setGlobalTracerProvider(this._provider); + + this._tracer = opentelemetry.trace.getTracer( + // This is not the serviceName shown in jaeger + "my-element-call-otl-tracer" + ); + } + + public get tracer(): Tracer { + return this._tracer; + } + + public get provider(): WebTracerProvider { + return this._provider; + } + + public get anonymity(): Anonymity { + return this._anonymity; + } +} + +function recheckOTelEnabledStatus(optInAnalayticsEnabled: boolean): void { + if (optInAnalayticsEnabled && !sharedInstance) { + logger.info("Starting OpenTelemetry debug reporting"); + sharedInstance = new ElementCallOpenTelemetry( + Config.get().opentelemetry?.collector_url + ); + } else if (!optInAnalayticsEnabled && sharedInstance) { + logger.info("Stopping OpenTelemetry debug reporting"); + sharedInstance = undefined; + } +} + +settingsBus.on("opt-in-analytics", recheckOTelEnabledStatus); +recheckOTelEnabledStatus(getSetting("opt-in-analytics", false)); diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index e1e12ea..c058297 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -383,7 +383,7 @@ function useGroupCallState( memberStateEvents, }); - otelGroupCallMembership.onUpdateRoomState(event); + otelGroupCallMembership?.onUpdateRoomState(event); } function onReceivedVoipEvent(event: MatrixEvent) { @@ -393,7 +393,7 @@ function useGroupCallState( function onSendVoipEvent(event: VoipEvent) { dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event }); - otelGroupCallMembership.onSendEvent(event); + otelGroupCallMembership?.onSendEvent(event); } function onUndecryptableToDevice(event: MatrixEvent) { diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 7d3f888..b8dfc0c 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -28,12 +28,14 @@ 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 { logger } from "@sentry/utils"; import { usePageUnload } from "./usePageUnload"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { TranslatedError, translatedError } from "../TranslatedError"; import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget"; import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; +import { ElementCallOpenTelemetry } from "../otel/otel"; export enum ConnectionState { EstablishingCall = "establishing call", // call hasn't been established yet @@ -172,8 +174,14 @@ export function useGroupCall( }); if (groupCallOTelMembershipGroupCallId !== groupCall.groupCallId) { - groupCallOTelMembership = new OTelGroupCallMembership(groupCall, client); - groupCallOTelMembershipGroupCallId = groupCall.groupCallId; + // If the user disables analytics, this will stay around until they leave the call + // so analytics will be disabled once they leave. + if (ElementCallOpenTelemetry.instance) { + groupCallOTelMembership = new OTelGroupCallMembership(groupCall, client); + groupCallOTelMembershipGroupCallId = groupCall.groupCallId; + } else { + groupCallOTelMembership = undefined; + } } const [unencryptedEventsFromUsers, addUnencryptedEventUser] = useReducer( @@ -414,18 +422,18 @@ export function useGroupCall( updateState({ error }); }); - groupCallOTelMembership.onJoinCall(); + groupCallOTelMembership?.onJoinCall(); }, [groupCall, updateState]); const leave = useCallback(() => { - groupCallOTelMembership.onLeaveCall(); + groupCallOTelMembership?.onLeaveCall(); groupCall.leave(); }, [groupCall]); const toggleLocalVideoMuted = useCallback(() => { const toggleToMute = !groupCall.isLocalVideoMuted(); groupCall.setLocalVideoMuted(toggleToMute); - groupCallOTelMembership.onToggleLocalVideoMuted(toggleToMute); + groupCallOTelMembership?.onToggleLocalVideoMuted(toggleToMute); // TODO: These explict posthog calls should be unnecessary now with the posthog otel exporter? PosthogAnalytics.instance.eventMuteCamera.track( toggleToMute, @@ -436,7 +444,7 @@ export function useGroupCall( const setMicrophoneMuted = useCallback( (setMuted) => { groupCall.setMicrophoneMuted(setMuted); - groupCallOTelMembership.onSetMicrophoneMuted(setMuted); + groupCallOTelMembership?.onSetMicrophoneMuted(setMuted); PosthogAnalytics.instance.eventMuteMicrophone.track( setMuted, groupCall.groupCallId @@ -447,12 +455,12 @@ export function useGroupCall( const toggleMicrophoneMuted = useCallback(() => { const toggleToMute = !groupCall.isMicrophoneMuted(); - groupCallOTelMembership.onToggleMicrophoneMuted(toggleToMute); + groupCallOTelMembership?.onToggleMicrophoneMuted(toggleToMute); setMicrophoneMuted(toggleToMute); }, [groupCall, setMicrophoneMuted]); const toggleScreensharing = useCallback(async () => { - groupCallOTelMembership.onToggleScreensharing(!groupCall.isScreensharing); + groupCallOTelMembership?.onToggleScreensharing(!groupCall.isScreensharing); if (!groupCall.isScreensharing()) { // toggling on From f6fb65be49c280be7ef0a4d4c8e3e28ae169121a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2023 11:58:41 +0000 Subject: [PATCH 17/34] Remove odd source mapping comment & unused commented code --- src/analytics/OtelPosthogExporter.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/analytics/OtelPosthogExporter.ts b/src/analytics/OtelPosthogExporter.ts index 3850e13..4fbfa90 100644 --- a/src/analytics/OtelPosthogExporter.ts +++ b/src/analytics/OtelPosthogExporter.ts @@ -69,16 +69,4 @@ export class PosthogSpanExporter implements SpanExporter { resolve(); }); } - /** - * converts span info into more readable format - * @param span - */ - // private _exportInfo; - /** - * Showing spans in console - * @param spans - * @param done - */ - // private _sendSpans; } -//# sourceMappingURL=ConsoleSpanExporter.d.ts.map From 9c0adfd32ebed5ccaed4955581db887460580fe6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2023 12:00:34 +0000 Subject: [PATCH 18/34] Unused import --- src/room/useGroupCall.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index b8dfc0c..0b54c82 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -28,7 +28,6 @@ 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 { logger } from "@sentry/utils"; import { usePageUnload } from "./usePageUnload"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; From ec88907981f3d517fe5fa5db4172c7d010093167 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2023 12:04:15 +0000 Subject: [PATCH 19/34] Comment the max old space workaround which seems to be working (so far) --- .github/workflows/build.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 654d609..ba27e26 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -23,6 +23,8 @@ jobs: SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} SENTRY_URL: ${{ secrets.SENTRY_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + # This appears to be necessary to stop Vite from OOMing + # https://github.com/vitejs/vite/issues/2433 NODE_OPTIONS: "--max-old-space-size=16384" - name: Upload Artifact uses: actions/upload-artifact@v2 From 48493a96e1707769ee168f4e0621d52bfd890fce Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2023 12:41:33 +0000 Subject: [PATCH 20/34] Wait until config is loaded to load otel --- src/initializer.tsx | 11 +++++++++++ src/otel/otel.ts | 8 +++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/initializer.tsx b/src/initializer.tsx index e10a4ec..37e659e 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -23,6 +23,7 @@ import * as Sentry from "@sentry/react"; import { getUrlParams } from "./UrlParams"; import { Config } from "./config/Config"; +import { ElementCallOpenTelemetry } from "./otel/otel"; enum LoadState { None, @@ -35,6 +36,7 @@ class DependencyLoadStates { // olm: LoadState = LoadState.None; config: LoadState = LoadState.None; sentry: LoadState = LoadState.None; + openTelemetry: LoadState = LoadState.None; allDepsAreLoaded() { return !Object.values(this).some((s) => s !== LoadState.Loaded); @@ -209,6 +211,15 @@ export class Initializer { this.loadStates.sentry = LoadState.Loaded; } + // OpenTelemetry (also only after config loaded) + if ( + this.loadStates.openTelemetry === LoadState.None && + this.loadStates.config === LoadState.Loaded + ) { + ElementCallOpenTelemetry.globalInit(); + this.loadStates.openTelemetry = LoadState.Loaded; + } + if (this.loadStates.allDepsAreLoaded()) { // resolve if there is no dependency that is not loaded resolve(); diff --git a/src/otel/otel.ts b/src/otel/otel.ts index 301c077..25de3ac 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -39,6 +39,11 @@ export class ElementCallOpenTelemetry { private _tracer: Tracer; private _anonymity: Anonymity; + static globalInit(): void { + settingsBus.on("opt-in-analytics", recheckOTelEnabledStatus); + recheckOTelEnabledStatus(getSetting("opt-in-analytics", false)); + } + static get instance(): ElementCallOpenTelemetry { return sharedInstance; } @@ -93,6 +98,3 @@ function recheckOTelEnabledStatus(optInAnalayticsEnabled: boolean): void { sharedInstance = undefined; } } - -settingsBus.on("opt-in-analytics", recheckOTelEnabledStatus); -recheckOTelEnabledStatus(getSetting("opt-in-analytics", false)); From d1ba5dff38f974574307778829f5ce66fa00a2c1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 23 Mar 2023 14:37:25 +0000 Subject: [PATCH 21/34] Allow all origins --- config/otel_dev/collector-gateway.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml index 6cc85a0..52f90c3 100644 --- a/config/otel_dev/collector-gateway.yaml +++ b/config/otel_dev/collector-gateway.yaml @@ -5,7 +5,7 @@ receivers: endpoint: 0.0.0.0:4318 cors: allowed_origins: - - "http://*" + - "*" allowed_headers: - "*" processors: From 40f5c53c052f77daf23e72810e13a63f952af356 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 24 Mar 2023 09:31:52 +0000 Subject: [PATCH 22/34] Put CORS header back to http://* with comment on why browsers are annoying --- config/otel_dev/collector-gateway.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml index 52f90c3..7b1fad0 100644 --- a/config/otel_dev/collector-gateway.yaml +++ b/config/otel_dev/collector-gateway.yaml @@ -5,7 +5,9 @@ receivers: endpoint: 0.0.0.0:4318 cors: allowed_origins: - - "*" + # This can't be '*' because opentelemetry-js uses sendBeacon which always operates + # in 'withCredentials' mode, which browsers don't allow with an allow-origin of '*' + - "http://*" allowed_headers: - "*" processors: From 77c6357b08b2f71a890e000a091536312d4b0648 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 29 Mar 2023 12:28:04 +0100 Subject: [PATCH 23/34] Use js-sdk from hangup refactor branch https://github.com/matrix-org/matrix-js-sdk/pull/3234 --- package.json | 2 +- src/otel/OTelGroupCallMembership.ts | 86 ++++++++++++++++++++++++++--- src/otel/otel.ts | 4 +- src/room/GroupCallInspector.tsx | 23 ++++++-- src/room/useGroupCall.ts | 2 + yarn.lock | 20 +++++-- 6 files changed, 114 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index a32fd8c..4ecc42d 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#23837266fca5ee799b51a722f7b8eefb2f5ac140", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#042f2ed76c501c10dde98a31732fd92d862e2187", "matrix-widget-api": "^1.0.0", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 764249f..4c6d308 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -14,15 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import opentelemetry, { Span, Attributes } from "@opentelemetry/api"; -import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; +import opentelemetry, { Span, Attributes, Context } from "@opentelemetry/api"; +import { logger } from "@sentry/utils"; import { GroupCall, MatrixClient, MatrixEvent, RoomMember, } from "matrix-js-sdk"; -import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; +import { + CallState, + MatrixCall, + VoipEvent, +} from "matrix-js-sdk/src/webrtc/call"; +import { + CallsByUserAndDevice, + GroupCallEvent, +} from "matrix-js-sdk/src/webrtc/groupCall"; import { ElementCallOpenTelemetry } from "./otel"; @@ -68,21 +76,37 @@ function flattenVoipEventRecursive( } } +interface CallTrackingInfo { + userId: string; + deviceId: string; + call: MatrixCall; + span: Span; +} + /** * Represent the span of time which we intend to be joined to a group call */ export class OTelGroupCallMembership { private callMembershipSpan?: Span; + private groupCallContext?: Context; private myUserId: string; + private myDeviceId: string; private myMember: RoomMember; + private callsByCallId = new Map(); constructor(private groupCall: GroupCall, client: MatrixClient) { this.myUserId = client.getUserId(); + this.myDeviceId = client.getDeviceId(); this.myMember = groupCall.room.getMember(client.getUserId()); - ElementCallOpenTelemetry.instance.provider.resource.attributes[ - SemanticResourceAttributes.SERVICE_NAME - ] = `element-call-${this.myUserId}-${client.getDeviceId()}`; + this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged); + } + + dispose() { + this.groupCall.removeListener( + GroupCallEvent.CallsChanged, + this.onCallsChanged + ); } public onJoinCall() { @@ -96,12 +120,13 @@ export class OTelGroupCallMembership { this.groupCall.groupCallId ); this.callMembershipSpan.setAttribute("matrix.userId", this.myUserId); + this.callMembershipSpan.setAttribute("matrix.deviceId", this.myDeviceId); this.callMembershipSpan.setAttribute( "matrix.displayName", this.myMember.name ); - opentelemetry.trace.setSpan( + this.groupCallContext = opentelemetry.trace.setSpan( opentelemetry.context.active(), this.callMembershipSpan ); @@ -126,12 +151,55 @@ export class OTelGroupCallMembership { } this.callMembershipSpan?.addEvent( - `otel_onRoomStateEvent_${event.getType()}`, + `matrix.roomStateEvent_${event.getType()}`, flattenVoipEvent(event.getContent()) ); } - public onSendEvent(event: VoipEvent) { + public onCallsChanged = (calls: CallsByUserAndDevice) => { + for (const [userId, userCalls] of calls.entries()) { + for (const [deviceId, call] of userCalls.entries()) { + if (!this.callsByCallId.has(call.callId)) { + const span = ElementCallOpenTelemetry.instance.tracer.startSpan( + `matrix.call`, + undefined, + this.groupCallContext + ); + // XXX: anonymity + span.setAttribute("matrix.call.target.userId", userId); + span.setAttribute("matrix.call.target.deviceId", deviceId); + this.callsByCallId.set(call.callId, { + userId, + deviceId, + call, + span, + }); + } + } + } + + for (const callTrackingInfo of this.callsByCallId.values()) { + const userCalls = calls.get(callTrackingInfo.userId); + if (!userCalls || !userCalls.has(callTrackingInfo.deviceId)) { + callTrackingInfo.span.end(); + this.callsByCallId.delete(callTrackingInfo.call.callId); + } + } + }; + + public onCallStateChange(call: MatrixCall, newState: CallState) { + const callTrackingInfo = this.callsByCallId.get(call.callId); + if (!callTrackingInfo) { + logger.error(`Got call state change for unknown call ID ${call.callId}`); + return; + } + + callTrackingInfo.span.addEvent("matrix.call.stateChange", { + state: newState, + }); + } + + public onSendEvent(call: MatrixCall, event: VoipEvent) { const eventType = event.eventType as string; if (!eventType.startsWith("m.call")) return; diff --git a/src/otel/otel.ts b/src/otel/otel.ts index 25de3ac..5d3e7d3 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -30,7 +30,7 @@ import { Anonymity } from "../analytics/PosthogAnalytics"; import { Config } from "../config/Config"; import { getSetting, settingsBus } from "../settings/useSetting"; -const SERVICE_NAME_BASE = "element-call"; +const SERVICE_NAME = "element-call"; let sharedInstance: ElementCallOpenTelemetry; @@ -58,7 +58,7 @@ export class ElementCallOpenTelemetry { // This is how we can make Jaeger show a reaonsable service in the dropdown on the left. const providerConfig = { resource: new Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: `${SERVICE_NAME_BASE}-unauthenticated`, + [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, }), }; this._provider = new WebTracerProvider(providerConfig); diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index c058297..ad838fb 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -31,7 +31,12 @@ import { MatrixEvent, IContent } from "matrix-js-sdk/src/models/event"; import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; -import { CallEvent, VoipEvent } from "matrix-js-sdk/src/webrtc/call"; +import { + CallEvent, + CallState, + MatrixCall, + VoipEvent, +} from "matrix-js-sdk/src/webrtc/call"; import styles from "./GroupCallInspector.module.css"; import { SelectInput } from "../input/SelectInput"; @@ -390,10 +395,18 @@ function useGroupCallState( dispatch({ type: ClientEvent.ReceivedVoipEvent, event }); } - function onSendVoipEvent(event: VoipEvent) { + function onSendVoipEvent(event: VoipEvent, call: MatrixCall) { dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event }); - otelGroupCallMembership?.onSendEvent(event); + otelGroupCallMembership?.onSendEvent(call, event); + } + + function onCallStateChange( + newState: CallState, + _: CallState, + call: MatrixCall + ) { + otelGroupCallMembership?.onCallStateChange(call, newState); } function onUndecryptableToDevice(event: MatrixEvent) { @@ -406,8 +419,8 @@ function useGroupCallState( } client.on(RoomStateEvent.Events, onUpdateRoomState); - //groupCall.on("calls_changed", onCallsChanged); groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent); + groupCall.on(CallEvent.State, onCallStateChange); //client.on("state", onCallsChanged); //client.on("hangup", onCallHangup); client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); @@ -417,8 +430,8 @@ function useGroupCallState( return () => { client.removeListener(RoomStateEvent.Events, onUpdateRoomState); - //groupCall.removeListener("calls_changed", onCallsChanged); groupCall.removeListener(CallEvent.SendVoipEvent, onSendVoipEvent); + groupCall.removeListener(CallEvent.State, onCallStateChange); //client.removeListener("state", onCallsChanged); //client.removeListener("hangup", onCallHangup); client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 0b54c82..553d418 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -173,6 +173,8 @@ export function useGroupCall( }); if (groupCallOTelMembershipGroupCallId !== groupCall.groupCallId) { + if (groupCallOTelMembership) groupCallOTelMembership.dispose(); + // If the user disables analytics, this will stay around until they leave the call // so analytics will be disabled once they leave. if (ElementCallOpenTelemetry.instance) { diff --git a/yarn.lock b/yarn.lock index ddb3fc6..58b450a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1821,7 +1821,7 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== -"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.3": +"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.5": version "0.1.0-alpha.5" resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.5.tgz#60ede2c43b9d808ba8cf46085a3b347b290d9658" integrity sha512-2KjAgWNGfuGLNjJwsrs6gGX157vmcTfNrA4u249utgnMPbJl7QwuUqh1bGxQ0PpK06yvZjgPlkna0lTbuwtuQw== @@ -10545,18 +10545,18 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#23837266fca5ee799b51a722f7b8eefb2f5ac140": - version "23.5.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/23837266fca5ee799b51a722f7b8eefb2f5ac140" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#042f2ed76c501c10dde98a31732fd92d862e2187": + version "24.0.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/042f2ed76c501c10dde98a31732fd92d862e2187" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.3" + "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.5" another-json "^0.2.0" bs58 "^5.0.0" content-type "^1.0.4" loglevel "^1.7.1" matrix-events-sdk "0.0.1" - matrix-widget-api "^1.0.0" + matrix-widget-api "^1.3.1" p-retry "4" sdp-transform "^2.14.1" unhomoglyph "^1.0.6" @@ -10570,6 +10570,14 @@ matrix-widget-api@^1.0.0: "@types/events" "^3.0.0" events "^3.2.0" +matrix-widget-api@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz#e38f404c76bb15c113909505c1c1a5b4d781c2f5" + integrity sha512-+rN6vGvnXm+fn0uq9r2KWSL/aPtehD6ObC50jYmUcEfgo8CUpf9eUurmjbRlwZkWq3XHXFuKQBUCI9UzqWg37Q== + dependencies: + "@types/events" "^3.0.0" + events "^3.2.0" + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" From 4bf1fbfd8ef4f79b56c235093f089ed74805c0b7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 29 Mar 2023 13:31:47 +0100 Subject: [PATCH 24/34] Gah, the sentry logger --- src/otel/OTelGroupCallMembership.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 4c6d308..432970b 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -15,13 +15,13 @@ limitations under the License. */ import opentelemetry, { Span, Attributes, Context } from "@opentelemetry/api"; -import { logger } from "@sentry/utils"; import { GroupCall, MatrixClient, MatrixEvent, RoomMember, } from "matrix-js-sdk"; +import { logger } from "matrix-js-sdk/src/logger"; import { CallState, MatrixCall, From 848e28ef925de880dfa98e384f5321da0a44b31d Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 29 Mar 2023 15:51:07 +0100 Subject: [PATCH 25/34] Change allowed origin to https://* as that allows the PR branches out-of-the-box --- config/otel_dev/collector-gateway.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml index 7b1fad0..9c1a9cd 100644 --- a/config/otel_dev/collector-gateway.yaml +++ b/config/otel_dev/collector-gateway.yaml @@ -7,7 +7,8 @@ receivers: allowed_origins: # This can't be '*' because opentelemetry-js uses sendBeacon which always operates # in 'withCredentials' mode, which browsers don't allow with an allow-origin of '*' - - "http://*" + #- "https://pr976--element-call.netlify.app" + - "https://*" allowed_headers: - "*" processors: From f96ce8985d261fecab22aed5b7be21c69ea6b9a9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 29 Mar 2023 16:04:11 +0100 Subject: [PATCH 26/34] Only enable otel if we have a collector URL --- src/otel/otel.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/otel/otel.ts b/src/otel/otel.ts index 5d3e7d3..eac7ce4 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -88,12 +88,16 @@ export class ElementCallOpenTelemetry { } function recheckOTelEnabledStatus(optInAnalayticsEnabled: boolean): void { - if (optInAnalayticsEnabled && !sharedInstance) { + const shouldEnable = + optInAnalayticsEnabled && + Boolean(Config.get().opentelemetry?.collector_url); + + if (shouldEnable && !sharedInstance) { logger.info("Starting OpenTelemetry debug reporting"); sharedInstance = new ElementCallOpenTelemetry( Config.get().opentelemetry?.collector_url ); - } else if (!optInAnalayticsEnabled && sharedInstance) { + } else if (!shouldEnable && sharedInstance) { logger.info("Stopping OpenTelemetry debug reporting"); sharedInstance = undefined; } From 21458c8840bdf0cd71b619e2a7911bfc0f93f4b0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 30 Mar 2023 13:03:58 +0100 Subject: [PATCH 27/34] Call the same leave method everywhere So we end the group call span whenever we leasve the call, including if we close the page. --- src/room/useGroupCall.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 392445f..b591629 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -202,6 +202,11 @@ export function useGroupCall( [] ); + const leaveCall = useCallback(() => { + groupCallOTelMembership?.onLeaveCall(); + groupCall.leave(); + }, [groupCall]); + useEffect(() => { // disable the media action keys, otherwise audio elements get paused when // the user presses media keys or unplugs headphones, etc. @@ -394,12 +399,12 @@ export function useGroupCall( onParticipantsChanged ); groupCall.removeListener(GroupCallEvent.Error, onError); - groupCall.leave(); + leaveCall(); }; - }, [groupCall, updateState]); + }, [groupCall, updateState, leaveCall]); usePageUnload(() => { - groupCall.leave(); + leaveCall(); }); const initLocalCallFeed = useCallback( @@ -426,11 +431,6 @@ export function useGroupCall( groupCallOTelMembership?.onJoinCall(); }, [groupCall, updateState]); - const leave = useCallback(() => { - groupCallOTelMembership?.onLeaveCall(); - groupCall.leave(); - }, [groupCall]); - const toggleLocalVideoMuted = useCallback(() => { const toggleToMute = !groupCall.isLocalVideoMuted(); groupCall.setLocalVideoMuted(toggleToMute); @@ -563,7 +563,7 @@ export function useGroupCall( error, initLocalCallFeed, enter, - leave, + leave: leaveCall, toggleLocalVideoMuted, toggleMicrophoneMuted, toggleScreensharing, From c2b78d59c6aba34f9cc1f9f98dbc778b2708680d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 30 Mar 2023 16:54:10 +0100 Subject: [PATCH 28/34] Add more events: * VoIP events received * Call errors * Group call errors * Undecryptable to-device events --- src/otel/OTelGroupCallMembership.ts | 53 +++++++++++++++++++++++++++++ src/room/GroupCallInspector.tsx | 24 ++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 432970b..53551b0 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -23,12 +23,14 @@ import { } from "matrix-js-sdk"; import { logger } from "matrix-js-sdk/src/logger"; import { + CallError, CallState, MatrixCall, VoipEvent, } from "matrix-js-sdk/src/webrtc/call"; import { CallsByUserAndDevice, + GroupCallError, GroupCallEvent, } from "matrix-js-sdk/src/webrtc/groupCall"; @@ -216,6 +218,37 @@ export class OTelGroupCallMembership { } } + public onReceivedVoipEvent(event: MatrixEvent) { + // These come straight from CallEventHandler so don't have + // a call already associated (in principle we could receive + // events for calls we don't know about). + const callId = event.getContent().call_id; + if (!callId) { + this.callMembershipSpan?.addEvent("matrix.receive_voip_event_no_callid", { + "sender.userId": event.getSender(), + }); + logger.error("Received call event with no call ID!"); + return; + } + + const call = this.callsByCallId.get(callId); + if (!call) { + this.callMembershipSpan?.addEvent( + "matrix.receive_voip_event_unknown_callid", + { + "sender.userId": event.getSender(), + } + ); + logger.error("Received call event for unknown call ID " + callId); + return; + } + + call.span.addEvent("matrix.receive_voip_event", { + "sender.userId": event.getSender(), + ...flattenVoipEvent(event.getContent()), + }); + } + public onToggleMicrophoneMuted(newValue: boolean) { this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", { "matrix.microphone.muted": newValue, @@ -245,4 +278,24 @@ export class OTelGroupCallMembership { "matrix.screensharing.enabled": newValue, }); } + + public onCallError(error: CallError, call: MatrixCall) { + const callTrackingInfo = this.callsByCallId.get(call.callId); + if (!callTrackingInfo) { + logger.error(`Got error for unknown call ID ${call.callId}`); + return; + } + + callTrackingInfo.span.recordException(error); + } + + public onGroupCallError(error: GroupCallError) { + this.callMembershipSpan?.recordException(error); + } + + public onUndecryptableToDevice(event: MatrixEvent) { + this.callMembershipSpan?.addEvent("matrix.toDevice.undecryptable", { + "sender.userId": event.getSender(), + }); + } } diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index ad838fb..ad67c7a 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -28,12 +28,17 @@ import ReactJson, { CollapsedFieldProps } from "react-json-view"; import mermaid from "mermaid"; import { Item } from "@react-stately/collections"; import { MatrixEvent, IContent } from "matrix-js-sdk/src/models/event"; -import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; +import { + GroupCall, + GroupCallError, + GroupCallEvent, +} from "matrix-js-sdk/src/webrtc/groupCall"; import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { CallEvent, CallState, + CallError, MatrixCall, VoipEvent, } from "matrix-js-sdk/src/webrtc/call"; @@ -393,6 +398,8 @@ function useGroupCallState( function onReceivedVoipEvent(event: MatrixEvent) { dispatch({ type: ClientEvent.ReceivedVoipEvent, event }); + + otelGroupCallMembership?.onReceivedVoipEvent(event); } function onSendVoipEvent(event: VoipEvent, call: MatrixCall) { @@ -409,18 +416,31 @@ function useGroupCallState( otelGroupCallMembership?.onCallStateChange(call, newState); } + function onCallError(error: CallError, call: MatrixCall) { + otelGroupCallMembership.onCallError(error, call); + } + + function onGroupCallError(error: GroupCallError) { + otelGroupCallMembership.onGroupCallError(error); + } + function onUndecryptableToDevice(event: MatrixEvent) { dispatch({ type: ClientEvent.ReceivedVoipEvent, event }); Sentry.captureMessage("Undecryptable to-device Event"); + // probably unnecessary if it's now captured via otel? PosthogAnalytics.instance.eventUndecryptableToDevice.track( groupCall.groupCallId ); + + otelGroupCallMembership.onUndecryptableToDevice(event); } client.on(RoomStateEvent.Events, onUpdateRoomState); groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent); groupCall.on(CallEvent.State, onCallStateChange); + groupCall.on(CallEvent.Error, onCallError); + groupCall.on(GroupCallEvent.Error, onGroupCallError); //client.on("state", onCallsChanged); //client.on("hangup", onCallHangup); client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); @@ -432,6 +452,8 @@ function useGroupCallState( client.removeListener(RoomStateEvent.Events, onUpdateRoomState); groupCall.removeListener(CallEvent.SendVoipEvent, onSendVoipEvent); groupCall.removeListener(CallEvent.State, onCallStateChange); + groupCall.removeListener(CallEvent.Error, onCallError); + groupCall.removeListener(GroupCallEvent.Error, onGroupCallError); //client.removeListener("state", onCallsChanged); //client.removeListener("hangup", onCallHangup); client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); From 74b218af8c436ff18e336db09901c91de095a56e Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 30 Mar 2023 17:19:13 +0100 Subject: [PATCH 29/34] Let otel know we're joining before trying to join Otherwise it starts getting calls being created before the group call span exists and we get call spans not associated with the group call span. --- config/otel_dev/collector-gateway.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml index 9c1a9cd..f9e3b90 100644 --- a/config/otel_dev/collector-gateway.yaml +++ b/config/otel_dev/collector-gateway.yaml @@ -8,7 +8,7 @@ receivers: # This can't be '*' because opentelemetry-js uses sendBeacon which always operates # in 'withCredentials' mode, which browsers don't allow with an allow-origin of '*' #- "https://pr976--element-call.netlify.app" - - "https://*" + - "http://*" allowed_headers: - "*" processors: From 72403d1aeac2bd17e363ea39439e8086de1919f7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Mar 2023 10:26:33 +0100 Subject: [PATCH 30/34] Revert 74b218af8c436ff18e336db09901c91de095a56e Comitted entirely the wrong thing --- config/otel_dev/collector-gateway.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml index f9e3b90..9c1a9cd 100644 --- a/config/otel_dev/collector-gateway.yaml +++ b/config/otel_dev/collector-gateway.yaml @@ -8,7 +8,7 @@ receivers: # This can't be '*' because opentelemetry-js uses sendBeacon which always operates # in 'withCredentials' mode, which browsers don't allow with an allow-origin of '*' #- "https://pr976--element-call.netlify.app" - - "http://*" + - "https://*" allowed_headers: - "*" processors: From 5e6c33b3b5aba9e776d217a3a33f3041e6f9fddf Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Mar 2023 10:30:01 +0100 Subject: [PATCH 31/34] Let otel know we're joining before trying to join Otherwise it starts getting calls being created before the group call span exists and we get call spans not associated with the group call span. (What 74b218af8c436ff18e336db09901c91de095a56e should have been) --- src/room/useGroupCall.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index b591629..7a9f69a 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -423,12 +423,14 @@ export function useGroupCall( PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); + // This must be called before we start trying to join the call, as we need to + // have started tracking by the time calls start getting created. + groupCallOTelMembership?.onJoinCall(); + groupCall.enter().catch((error) => { console.error(error); updateState({ error }); }); - - groupCallOTelMembership?.onJoinCall(); }, [groupCall, updateState]); const toggleLocalVideoMuted = useCallback(() => { From 773f2e009de5850ccce00d24e2f8c47d88391423 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Mar 2023 10:58:12 +0100 Subject: [PATCH 32/34] Typo --- config/otel_dev/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/otel_dev/README.md b/config/otel_dev/README.md index 8fe102d..ea6c09a 100644 --- a/config/otel_dev/README.md +++ b/config/otel_dev/README.md @@ -1,7 +1,7 @@ # OpenTelemetry Collector for development This directory contains a docker compose file that starts a jaeger all-in-one instance -with an in-memory database, along with a standalong OpenTelemetry collector that forwards +with an in-memory database, along with a standalone OpenTelemetry collector that forwards traces into the jaeger. Jaeger has a built-in OpenTelemetry collector, but it can't be configured to send CORS headers so can't be used from a browser. This sets the config on the collector to send CORS headers. From a1aca7bdf234214fc2dc144ccf5472eb0420f004 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Mar 2023 11:10:05 +0100 Subject: [PATCH 33/34] Fix lying comment --- src/analytics/OtelPosthogExporter.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/analytics/OtelPosthogExporter.ts b/src/analytics/OtelPosthogExporter.ts index 4fbfa90..8f9ba26 100644 --- a/src/analytics/OtelPosthogExporter.ts +++ b/src/analytics/OtelPosthogExporter.ts @@ -18,9 +18,10 @@ import { SpanExporter, ReadableSpan } from "@opentelemetry/sdk-trace-base"; import { ExportResult, ExportResultCode } from "@opentelemetry/core"; import { PosthogAnalytics } from "./PosthogAnalytics"; + /** - * This is implementation of {@link SpanExporter} that prints spans to the - * console. This class can be used for diagnostic purposes. + * This is implementation of {@link SpanExporter} that sends spans + * to Posthog */ export class PosthogSpanExporter implements SpanExporter { /** From dc725f90a9b8971e198cfb07c42ec6d5fbcb5ff5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Mar 2023 11:12:10 +0100 Subject: [PATCH 34/34] Fix confusing comment --- src/config/ConfigOptions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index cfaaf77..8b0fae1 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -37,7 +37,8 @@ export interface ConfigOptions { }; /** - * Controls whether to to send OpenTelemetry debugging data to collector + * Sets the URL to send opentelemetry data to. If unset, opentelemetry will + * be disabled. */ opentelemetry?: { collector_url: string;