From 3d6ae3fbc39ff892403e7fccf38c867ac4113172 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2023 11:55:21 +0000 Subject: [PATCH] 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