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.
This commit is contained in:
David Baker 2023-03-22 11:55:21 +00:00
parent 359e055314
commit 3d6ae3fbc3
5 changed files with 98 additions and 33 deletions

View file

@ -36,6 +36,13 @@ export interface ConfigOptions {
submit_url: string; 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 // Describes the default homeserver to use. The same format as Element Web
// (without identity servers as we don't use them). // (without identity servers as we don't use them).
default_server_config?: { default_server_config?: {

View file

@ -24,7 +24,7 @@ import {
} from "matrix-js-sdk"; } from "matrix-js-sdk";
import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; 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 * Flattens out an object into a single layer with components
@ -80,14 +80,17 @@ export class OTelGroupCallMembership {
this.myUserId = client.getUserId(); this.myUserId = client.getUserId();
this.myMember = groupCall.room.getMember(client.getUserId()); this.myMember = groupCall.room.getMember(client.getUserId());
provider.resource.attributes[ ElementCallOpenTelemetry.instance.provider.resource.attributes[
SemanticResourceAttributes.SERVICE_NAME SemanticResourceAttributes.SERVICE_NAME
] = `element-call-${this.myUserId}-${client.getDeviceId()}`; ] = `element-call-${this.myUserId}-${client.getDeviceId()}`;
} }
public onJoinCall() { public onJoinCall() {
// Create the main span that tracks the time we intend to be in the call // 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( this.callMembershipSpan.setAttribute(
"matrix.confId", "matrix.confId",
this.groupCall.groupCallId this.groupCall.groupCallId

View file

@ -20,32 +20,79 @@ import {
} from "@opentelemetry/sdk-trace-base"; } from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
import opentelemetry from "@opentelemetry/api"; import opentelemetry, { Tracer } from "@opentelemetry/api";
import { Resource } from "@opentelemetry/resources"; import { Resource } from "@opentelemetry/resources";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
import { logger } from "@sentry/utils";
import { PosthogSpanExporter } from "../analytics/OtelPosthogExporter"; 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(); let sharedInstance: ElementCallOpenTelemetry;
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. export class ElementCallOpenTelemetry {
const providerConfig = { private _provider: WebTracerProvider;
resource: new Resource({ private _tracer: Tracer;
[SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, private _anonymity: Anonymity;
}),
};
export const provider = new WebTracerProvider(providerConfig);
provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); static get instance(): ElementCallOpenTelemetry {
provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); return sharedInstance;
provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); }
opentelemetry.trace.setGlobalTracerProvider(provider);
// This is not the serviceName shown in jaeger constructor(collectorUrl: string) {
export const tracer = opentelemetry.trace.getTracer( const otlpExporter = new OTLPTraceExporter({
"my-element-call-otl-tracer" 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));

View file

@ -383,7 +383,7 @@ function useGroupCallState(
memberStateEvents, memberStateEvents,
}); });
otelGroupCallMembership.onUpdateRoomState(event); otelGroupCallMembership?.onUpdateRoomState(event);
} }
function onReceivedVoipEvent(event: MatrixEvent) { function onReceivedVoipEvent(event: MatrixEvent) {
@ -393,7 +393,7 @@ function useGroupCallState(
function onSendVoipEvent(event: VoipEvent) { function onSendVoipEvent(event: VoipEvent) {
dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event }); dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event });
otelGroupCallMembership.onSendEvent(event); otelGroupCallMembership?.onSendEvent(event);
} }
function onUndecryptableToDevice(event: MatrixEvent) { function onUndecryptableToDevice(event: MatrixEvent) {

View file

@ -28,12 +28,14 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { IWidgetApiRequest } from "matrix-widget-api"; import { IWidgetApiRequest } from "matrix-widget-api";
import { MatrixClient } from "matrix-js-sdk"; import { MatrixClient } from "matrix-js-sdk";
import { logger } from "@sentry/utils";
import { usePageUnload } from "./usePageUnload"; import { usePageUnload } from "./usePageUnload";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { TranslatedError, translatedError } from "../TranslatedError"; import { TranslatedError, translatedError } from "../TranslatedError";
import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget"; import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
import { ElementCallOpenTelemetry } from "../otel/otel";
export enum ConnectionState { export enum ConnectionState {
EstablishingCall = "establishing call", // call hasn't been established yet EstablishingCall = "establishing call", // call hasn't been established yet
@ -172,8 +174,14 @@ export function useGroupCall(
}); });
if (groupCallOTelMembershipGroupCallId !== groupCall.groupCallId) { if (groupCallOTelMembershipGroupCallId !== groupCall.groupCallId) {
groupCallOTelMembership = new OTelGroupCallMembership(groupCall, client); // If the user disables analytics, this will stay around until they leave the call
groupCallOTelMembershipGroupCallId = groupCall.groupCallId; // 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( const [unencryptedEventsFromUsers, addUnencryptedEventUser] = useReducer(
@ -414,18 +422,18 @@ export function useGroupCall(
updateState({ error }); updateState({ error });
}); });
groupCallOTelMembership.onJoinCall(); groupCallOTelMembership?.onJoinCall();
}, [groupCall, updateState]); }, [groupCall, updateState]);
const leave = useCallback(() => { const leave = useCallback(() => {
groupCallOTelMembership.onLeaveCall(); groupCallOTelMembership?.onLeaveCall();
groupCall.leave(); groupCall.leave();
}, [groupCall]); }, [groupCall]);
const toggleLocalVideoMuted = useCallback(() => { const toggleLocalVideoMuted = useCallback(() => {
const toggleToMute = !groupCall.isLocalVideoMuted(); const toggleToMute = !groupCall.isLocalVideoMuted();
groupCall.setLocalVideoMuted(toggleToMute); groupCall.setLocalVideoMuted(toggleToMute);
groupCallOTelMembership.onToggleLocalVideoMuted(toggleToMute); groupCallOTelMembership?.onToggleLocalVideoMuted(toggleToMute);
// TODO: These explict posthog calls should be unnecessary now with the posthog otel exporter? // TODO: These explict posthog calls should be unnecessary now with the posthog otel exporter?
PosthogAnalytics.instance.eventMuteCamera.track( PosthogAnalytics.instance.eventMuteCamera.track(
toggleToMute, toggleToMute,
@ -436,7 +444,7 @@ export function useGroupCall(
const setMicrophoneMuted = useCallback( const setMicrophoneMuted = useCallback(
(setMuted) => { (setMuted) => {
groupCall.setMicrophoneMuted(setMuted); groupCall.setMicrophoneMuted(setMuted);
groupCallOTelMembership.onSetMicrophoneMuted(setMuted); groupCallOTelMembership?.onSetMicrophoneMuted(setMuted);
PosthogAnalytics.instance.eventMuteMicrophone.track( PosthogAnalytics.instance.eventMuteMicrophone.track(
setMuted, setMuted,
groupCall.groupCallId groupCall.groupCallId
@ -447,12 +455,12 @@ export function useGroupCall(
const toggleMicrophoneMuted = useCallback(() => { const toggleMicrophoneMuted = useCallback(() => {
const toggleToMute = !groupCall.isMicrophoneMuted(); const toggleToMute = !groupCall.isMicrophoneMuted();
groupCallOTelMembership.onToggleMicrophoneMuted(toggleToMute); groupCallOTelMembership?.onToggleMicrophoneMuted(toggleToMute);
setMicrophoneMuted(toggleToMute); setMicrophoneMuted(toggleToMute);
}, [groupCall, setMicrophoneMuted]); }, [groupCall, setMicrophoneMuted]);
const toggleScreensharing = useCallback(async () => { const toggleScreensharing = useCallback(async () => {
groupCallOTelMembership.onToggleScreensharing(!groupCall.isScreensharing); groupCallOTelMembership?.onToggleScreensharing(!groupCall.isScreensharing);
if (!groupCall.isScreensharing()) { if (!groupCall.isScreensharing()) {
// toggling on // toggling on