7.4 KiB
Design
Context
- Telemetry stats reach the writer from two sources: per-track media stats, built from the RTP buffers of the server's
media path, and the signal (
TR_SG…) and data (TR_DT…) pseudo-tracks, built from WebSocket and data channel byte counters. A media engine that does not move RTP through those buffers produces only the second kind. projection/stats.goadds every stat to the participant (bytes,lastStatsAt) and skips pseudo-tracks before the room delta, so the room getslastStatsAt, bytes and quality only from media stats.nodesdocuments are written bystore.Heartbeaton every batch tick and expire 30 days afterlastSeenAt. Node ids are generated per process, so a restarted node writes a new document and the old one expires on its own.- Rooms take
nodeIdfromROOM_CREATED(and from the first event of any kind while it is unknown); participant sessions take it from their first event. Entities are kept forretention.entities(90 days by default). analytics.Node{ID, Region}is built inopenvidu/openvidu.go; the projection options inanalytics.go. A server edition adjusts analytics defaults throughopenviduconfigpackage variables before the analytics start (DefaultStatsDetail).
Goals / Non-Goals
Goals:
- Every view that shows bytes or quality can tell "not reported" apart from "zero", for the whole entity retention.
- The CE code and specs stay engine-agnostic: the server states a capability, not an engine name.
Non-Goals:
- Hiding views or features: only the values and charts that depend on media statistics change.
- Marking track or subscription documents: views reach them through their participant.
Decisions
D1. A capability flag set by the server
openviduconfig.MediaStats (default true) says whether this server's media engine reports media statistics. A server
whose engine does not sets it to false before the analytics start, like DefaultStatsDetail. openvidu.Start passes
it as analytics.Node.MediaStats. There is no configuration key: the value is a fact of the build and the engine, not
an operator choice.
Rejected: the engine name in the data (rtcEngine: "…"). It ties the CE contract to an edition's engines and must be
reinterpreted when an engine gains statistics; the capability flips by itself.
D2. Mark both the node and the entities
The heartbeat $sets mediaStats (true or false) on every tick. Rooms and participant sessions created by a node with
MediaStats: false get mediaStats: false; others get no field. Views of rooms and sessions read the entity, so the
mark lives as long as the entity; analytics read the nodes, since rollups have no per-engine dimension.
Rejected: nodes only, with the dashboard joining nodeId. Node documents expire 30 days after their last heartbeat and
every restart creates a new node id, so rooms between 30 and 90 days old would fall back to "0 B".
Rejected: entities only. A range or scope of the Analytics page has no entity to read the mark from.
D3. Only false is stored on entities
Absent means "reports media statistics", which is also what every document written before the change says. No
backfill, no extra bytes on the common case, and the API omits the property unless it is false.
D4. The mark follows nodeId
The mark is computed where nodeId is: ROOM_CREATED sets nodeId and sets mediaStats: false or removes it
($$REMOVE); the touch of other events sets mediaStats only when the document has no nodeId yet, in the same stage,
so the first writer of the room decides until ROOM_CREATED arrives. Participants follow their own nodeId rule. The
legacy migration replays with MediaStats: true: the engine of legacy nodes is unknown.
D5. Pseudo-track stats advance the room's lastStatsAt
A pseudo-track stat creates the room delta with statsAt and zero bytes and quality, exactly as a media stat of zero
bytes would. Every connected participant flushes signal stats every 30 s, so the room's lastStatsAt becomes a
liveness signal under any engine: the room stale flag, the stale hints of the overview and the reconciler's
empty-listing guard (Untrusted live listing) cover these rooms. With media stats nothing changes, since the room
already gets the same flush time from them.
D6. Analytics coverage from heartbeat spans, on the summary only
The summary reads nodes documents whose span overlaps the range (firstSeenAt <= to, lastSeenAt >= from, plus
_id for a node scope) with a mediaStats projection and folds them into none, partial or nothing. The page
applies the summary's coverage to the Data up / down tile and the Bandwidth section, and exposes it to the cards of the
page's extension slots as AnalyticsPageContext.mediaStats (a signal of 'partial' | 'none' | undefined), so the
series and breakdown responses do not change.
Rejected: a coverage field on every series response (the page already holds the summary of the same range and scope). Rejected: inferring coverage from rollup bytes at 0 (a quiet range reads the same).
D7. Values stay as stored
The rooms list keeps bytes next to mediaStats: false, and the summary keeps bytesUp and bytesDown; only the UI
replaces them. API clients see the same shapes plus one optional property.
D8. A generator option for nodes without media statistics
--nodes-without-media-stats <n> (default 0) marks the last n generated nodes: their node documents carry
mediaStats: false, their rooms and sessions are stamped and get no media track_stats, room bytes or quality, and their
rollup contributions lose bytes and quality. The other nodes get mediaStats: true. The keep-alive preserves the flag.
Data model
nodes.mediaStats: bool, written on every heartbeat.rooms.mediaStatsandparticipants.mediaStats: bool, onlyfalse, set by the projections. Contract schemas of the three collections gain the optional property; the dashboard mirror and its typings follow.- No index, retention or schema version change (the v2 schema has not shipped). No mapping from legacy events (D4).
API
RoomListItem.mediaStats?: false(GET /api/v1/rooms).AnalyticsSummary.mediaStats?: "partial" | "none"(GET /api/v1/analytics/summary).- The coverage query runs on
nodes, backed by thelastSeenAtTTL index; the collection holds one document per node process of the last 30 days.
Performance budget
One extra query per summary on a collection of tens of documents: under 5 ms, verified by an integration test with an
explain() plan (IXSCAN on lastSeenAt) next to the existing analytics plan tests. The summary p95 budget (150 ms at
scenario L) is unchanged. The pseudo-track room delta adds at most one room update per room and batch, and only for
rooms whose flush carries no media stat; the writer benchmark of the batch path checks it stays within its budget.
Risks / Trade-offs
- [A room touched first by an egress or ingress event of another node] → it takes that node's mark until its
ROOM_CREATEDis applied, which then sets the host's mark (D4). - [Coverage misses nodes whose documents expired] → ranges older than 30 days may read as fully reported; the entity marks keep rooms and sessions right.
- [A pseudo-track-only room delta on a room not written yet] → the same upsert as a media stat delta today.
Migration Plan
Ships with the v2 writer and dashboard. Rollback: dashboards ignore the property and writers without it never write it.