Event streams use a bounded channel with DropOldest, which is the right default: a slow consumer must never block the receive loop. But the drop is invisible. EventStream.cs:
FullMode = BoundedChannelFullMode.DropOldest,
...
void Handler(object? sender, TEventArgs e) => channel.Writer.TryWrite(e);
TryWrite returns true even when it evicts, so nothing in the current code can tell that events were lost. A consumer watching InputVolumeMeters on a small capacity silently misses data with no signal.
Shape
Channel.CreateBounded<T>(options, itemDropped) has a drop callback, which is the exact hook needed and costs nothing when no drop happens.
Add a counter on the existing ObsWebSocketMetrics meter, tagged by event type, alongside EventsReceived. Low cardinality: the event type is a fixed set of 60.
Behaviour must not change. Dropping the oldest stays the default and the receive loop must never block.
Test by filling a capacity-1 stream and asserting the count.
A public per-stream drop count is probably not worth the API surface; the metric is enough unless someone asks.
Event streams use a bounded channel with
DropOldest, which is the right default: a slow consumer must never block the receive loop. But the drop is invisible.EventStream.cs:TryWritereturnstrueeven when it evicts, so nothing in the current code can tell that events were lost. A consumer watchingInputVolumeMeterson a small capacity silently misses data with no signal.Shape
Channel.CreateBounded<T>(options, itemDropped)has a drop callback, which is the exact hook needed and costs nothing when no drop happens.Add a counter on the existing
ObsWebSocketMetricsmeter, tagged by event type, alongsideEventsReceived. Low cardinality: the event type is a fixed set of 60.Behaviour must not change. Dropping the oldest stays the default and the receive loop must never block.
Test by filling a capacity-1 stream and asserting the count.
A public per-stream drop count is probably not worth the API surface; the metric is enough unless someone asks.