use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use crate::{
event::{attributes_size_hint, now_unix_nanos},
scope::ScopeState,
Attribute, AttributeValue,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SpanKind {
Internal,
Client,
}
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpanOutcome {
Ok,
Error,
Cancelled,
}
impl SpanOutcome {
pub(crate) fn as_str(self) -> &'static str {
match self {
SpanOutcome::Ok => "ok",
SpanOutcome::Error => "error",
SpanOutcome::Cancelled => "cancelled",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct SpanEvent {
pub name: String,
pub time_ns: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct SpanRecord {
pub span_id: u64,
pub parent_span_id: Option<u64>,
pub name: String,
pub kind: SpanKind,
pub start_ns: u64,
pub end_ns: u64,
pub outcome: SpanOutcome,
pub error_type: Option<String>,
pub attributes: Vec<Attribute>,
pub dropped_attributes: u32,
pub events: Vec<SpanEvent>,
pub dropped_events: u32,
pub session: Arc<ScopeState>,
pub route: Option<String>,
}
const SPAN_OVERHEAD_BYTES: usize = 94;
const SPAN_EVENT_OVERHEAD_BYTES: usize = 13;
const MAX_EVENTS_PER_SPAN: usize = 128;
const MAX_ATTRIBUTES_PER_SPAN: usize = 128;
const MAX_OPEN_SPANS: usize = 256;
pub(crate) struct Spans {
open: HashMap<u64, SpanRecord>,
open_order: VecDeque<u64>,
finished: VecDeque<SpanRecord>,
finished_capacity: usize,
sessions: HashMap<u64, Arc<ScopeState>>,
session_order: VecDeque<u64>,
next_id: u64,
dropped: u64,
warned: Warned,
revoked: Arc<std::sync::atomic::AtomicBool>,
}
#[derive(Default)]
struct Warned {
full: bool,
attributes: bool,
events: bool,
}
fn warn_once(warned: &mut bool, warning: std::fmt::Arguments<'_>) {
if !*warned {
*warned = true;
log::warn!("{warning}");
}
}
const REMEMBERED_SPANS: usize = 1024;
impl Spans {
pub fn new(finished_capacity: usize) -> Self {
Self {
open: HashMap::new(),
open_order: VecDeque::new(),
finished: VecDeque::new(),
finished_capacity: finished_capacity.max(1),
sessions: HashMap::new(),
session_order: VecDeque::new(),
next_id: (rand::random::<u64>() >> 1) | 1,
dropped: 0,
warned: Warned::default(),
revoked: Arc::default(),
}
}
pub fn with_consent(mut self, revoked: Arc<std::sync::atomic::AtomicBool>) -> Self {
self.revoked = revoked;
self
}
fn revoked(&self) -> bool {
self.revoked.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn begin_in(
&mut self,
name: &str,
kind: SpanKind,
parent: Option<u64>,
session: Arc<ScopeState>,
) -> u64 {
if self.revoked() {
return 0;
}
let id = self.next_id;
self.next_id = ((self.next_id + 1) & (u64::MAX >> 1)).max(1);
if self.open.len() >= MAX_OPEN_SPANS {
if let Some(oldest) = self.open_order.pop_front() {
self.open.remove(&oldest);
self.retire(oldest);
self.count_drop("open", MAX_OPEN_SPANS);
}
}
self.sessions.insert(id, session.clone());
let record = SpanRecord {
span_id: id,
parent_span_id: parent.filter(|p| *p != 0),
name: name.to_owned(),
kind,
start_ns: now_unix_nanos(),
end_ns: 0,
outcome: SpanOutcome::Ok,
error_type: None,
attributes: Vec::new(),
dropped_attributes: 0,
events: Vec::new(),
dropped_events: 0,
session,
route: None,
};
self.open.insert(id, record);
self.open_order.push_back(id);
id
}
pub fn scope_of(&self, id: u64) -> Option<Arc<ScopeState>> {
self.sessions.get(&id).cloned()
}
#[cfg(test)]
pub fn begin(&mut self, name: &str, kind: SpanKind, parent: Option<u64>) -> u64 {
self.begin_in(name, kind, parent, ScopeState::new())
}
#[cfg(test)]
pub fn open_count(&self) -> usize {
self.open.len()
}
pub fn any_open(&self, names: &[&str]) -> bool {
self.open.values().any(|span| names.contains(&span.name.as_str()))
}
pub fn add_event(&mut self, id: u64, name: &str) {
if self.revoked() {
return;
}
let Some(span) = self.open.get_mut(&id) else { return };
if span.events.len() >= MAX_EVENTS_PER_SPAN {
span.dropped_events = span.dropped_events.saturating_add(1);
warn_once(
&mut self.warned.events,
format_args!(
"{} over {MAX_EVENTS_PER_SPAN} checkpoints: dropping the rest",
span.name
),
);
return;
}
span.events.push(SpanEvent { name: name.to_owned(), time_ns: now_unix_nanos() });
}
pub fn end(
&mut self,
id: u64,
outcome: SpanOutcome,
error_type: Option<String>,
mut attributes: Vec<Attribute>,
) {
if self.revoked() {
return;
}
let Some(mut span) = self.open.remove(&id) else { return };
self.open_order.retain(|open| *open != id);
span.end_ns = now_unix_nanos().max(span.start_ns);
span.outcome = outcome;
span.error_type = error_type;
span.session.snapshot_custom(&mut attributes);
span.route = span.session.route();
let excess = attributes.len().saturating_sub(MAX_ATTRIBUTES_PER_SPAN);
if excess > 0 {
attributes.truncate(MAX_ATTRIBUTES_PER_SPAN);
span.dropped_attributes = u32::try_from(excess).unwrap_or(u32::MAX);
warn_once(
&mut self.warned.attributes,
format_args!(
"{} over {MAX_ATTRIBUTES_PER_SPAN} attributes: dropping {excess}",
span.name
),
);
}
span.session.decorate(&mut attributes);
span.attributes = attributes;
self.retire(id);
if self.finished.len() >= self.finished_capacity {
self.finished.pop_front();
self.count_drop("finished", self.finished_capacity);
}
self.finished.push_back(span);
}
fn retire(&mut self, id: u64) {
self.session_order.push_back(id);
if self.session_order.len() > REMEMBERED_SPANS {
if let Some(old) = self.session_order.pop_front() {
self.sessions.remove(&old);
}
}
}
fn count_drop(&mut self, buffer: &str, capacity: usize) {
self.dropped += 1;
warn_once(
&mut self.warned.full,
format_args!("{buffer} spans full ({capacity}): dropping the oldest"),
);
}
pub fn drain(&mut self, max: usize, max_bytes: usize) -> Vec<SpanRecord> {
self.warned = Warned::default();
let (mut n, mut bytes) = (0, 0);
for span in self.finished.iter().take(max) {
let size = span.size_hint();
if n > 0 && bytes + size > max_bytes {
break;
}
bytes += size;
n += 1;
}
self.finished.drain(..n).collect()
}
pub fn clear(&mut self) -> u64 {
let n = self.open.len() + self.finished.len();
self.open.clear();
self.open_order.clear();
self.finished.clear();
self.sessions.clear();
self.session_order.clear();
self.warned = Warned::default();
n as u64
}
pub fn take_dropped(&mut self) -> u64 {
std::mem::take(&mut self.dropped)
}
}
impl SpanRecord {
pub(crate) fn size_hint(&self) -> usize {
SPAN_OVERHEAD_BYTES
+ self.name.len()
+ self.error_type.as_ref().map_or(0, String::len)
+ attributes_size_hint(&self.attributes)
+ self.events.iter().map(|e| SPAN_EVENT_OVERHEAD_BYTES + e.name.len()).sum::<usize>()
}
pub(crate) fn outcome_attributes(&self) -> Vec<Attribute> {
let mut attributes = vec![Attribute::new("lk.outcome", self.outcome.as_str())];
if let (SpanOutcome::Error, Some(error_type)) = (self.outcome, &self.error_type) {
attributes.push(Attribute::new("error.type", AttributeValue::Str(error_type.clone())));
}
attributes
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spans_open_record_events_and_finish_in_order() {
let mut spans = Spans::new(8);
let parent = spans.begin("lk.connect", SpanKind::Client, None);
let child = spans.begin("lk.publish", SpanKind::Internal, Some(parent));
spans.add_event(parent, "ws_open");
spans.end(child, SpanOutcome::Cancelled, None, vec![]);
spans.end(
parent,
SpanOutcome::Error,
Some("timeout".into()),
vec![Attribute::new("lk.connect.attempt", 1i64)],
);
spans.end(parent, SpanOutcome::Ok, None, vec![]);
let finished = spans.drain(10, usize::MAX);
assert_eq!(finished.len(), 2);
assert_eq!(finished[0].name, "lk.publish");
assert_eq!(finished[0].parent_span_id, Some(parent));
assert_eq!(finished[0].outcome, SpanOutcome::Cancelled);
assert_eq!(finished[1].events[0].name, "ws_open");
assert_eq!(finished[1].error_type.as_deref(), Some("timeout"));
assert!(finished[1].end_ns >= finished[1].start_ns);
assert_eq!(spans.take_dropped(), 0);
}
#[test]
fn open_spans_keep_their_session_however_many_spans_end_after_them() {
let mut spans = Spans::new(REMEMBERED_SPANS + 1);
let room = ScopeState::new();
let connect = spans.begin_in("lk.connect", SpanKind::Client, None, room.clone());
let first = spans.begin("lk.publish", SpanKind::Internal, None);
spans.end(first, SpanOutcome::Ok, None, vec![]);
for _ in 0..REMEMBERED_SPANS {
let id = spans.begin("lk.publish", SpanKind::Internal, None);
spans.end(id, SpanOutcome::Ok, None, vec![]);
}
assert!(spans.scope_of(first).is_none(), "ended spans age out");
assert_eq!(spans.scope_of(connect), Some(room.clone()));
spans.end(connect, SpanOutcome::Error, Some("timeout".into()), vec![]);
assert_eq!(spans.scope_of(connect), Some(room), "and stays resolvable once it ended");
assert_eq!(spans.sessions.len(), REMEMBERED_SPANS);
}
#[test]
fn abandoned_spans_are_counted_warned_once_and_age_out() {
crate::test_log::capture();
let mut spans = Spans::new(8);
let ids: Vec<_> = (0..MAX_OPEN_SPANS + REMEMBERED_SPANS + 1)
.map(|_| spans.begin("lk.publish", SpanKind::Internal, None))
.collect();
assert_eq!(spans.open_count(), MAX_OPEN_SPANS);
assert_eq!(spans.take_dropped(), REMEMBERED_SPANS as u64 + 1);
let warning = "open spans full (256): dropping the oldest";
assert_eq!(crate::test_log::take(), [warning], "the first drop logged, the rest counted");
assert!(spans.scope_of(ids[0]).is_none());
assert_eq!(spans.sessions.len(), MAX_OPEN_SPANS + REMEMBERED_SPANS);
spans.drain(10, usize::MAX);
spans.begin("lk.publish", SpanKind::Internal, None);
assert_eq!(crate::test_log::take(), [warning], "a drain starts a new episode");
}
#[test]
fn zero_finished_capacity_keeps_one_span_instead_of_panicking() {
let mut spans = Spans::new(0);
for _ in 0..2 {
let id = spans.begin("lk.publish", SpanKind::Internal, None);
spans.end(id, SpanOutcome::Ok, None, vec![]);
}
assert_eq!(spans.drain(10, usize::MAX).len(), 1);
assert_eq!(spans.take_dropped(), 1);
}
#[test]
fn clear_starts_a_new_warning_episode() {
let mut spans = Spans::new(1);
for _ in 0..2 {
let id = spans.begin("lk.publish", SpanKind::Internal, None);
spans.end(id, SpanOutcome::Ok, None, vec![]);
}
assert!(spans.warned.full);
assert_eq!(spans.clear(), 1);
assert!(!spans.warned.full);
}
fn encoded(spans: &mut Spans) -> Vec<crate::proto::opentelemetry::proto::trace::v1::Span> {
use crate::proto::opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest;
use prost::Message;
let bytes = crate::otlp::encode_spans(&[], &[], spans.drain(10, usize::MAX));
let mut decoded = ExportTraceServiceRequest::decode(&bytes[..]).expect("valid OTLP");
std::mem::take(&mut decoded.resource_spans[0].scope_spans[0].spans)
}
#[test]
fn attributes_over_the_cap_are_counted_warned_once_and_spare_the_sessions() {
crate::test_log::capture();
let mut spans = Spans::new(8);
let session = ScopeState::new();
session.set_attribute("lk.room.sid", Some("RM_1".into()));
assert!(session.set_custom("app.call_id", Some("c1".into())));
let overflow = |spans: &mut Spans| {
let id = spans.begin_in("lk.publish", SpanKind::Internal, None, session.clone());
let own = (0..MAX_ATTRIBUTES_PER_SPAN)
.map(|i| Attribute::new(format!("lk.publish.k{i}"), i as i64))
.collect();
spans.end(id, SpanOutcome::Ok, None, own);
};
overflow(&mut spans);
overflow(&mut spans);
let warning = "lk.publish over 128 attributes: dropping 1";
assert_eq!(crate::test_log::take(), [warning], "two overflows, one warning");
for span in encoded(&mut spans) {
assert_eq!(span.dropped_attributes_count, 1, "the app's call id");
let keys: Vec<_> = span.attributes.iter().map(|kv| kv.key.as_str()).collect();
assert!(!keys.contains(&"app.call_id"), "the app's own go first");
for key in ["lk.publish.k127", "lk.room.sid", "session.id", "lk.outcome"] {
assert!(keys.contains(&key), "{key} survives the cap");
}
}
overflow(&mut spans);
assert_eq!(crate::test_log::take(), [warning], "a drain starts a new episode");
assert_eq!(spans.take_dropped(), 0, "no span was lost");
}
#[test]
fn an_attribute_overflow_does_not_silence_a_full_buffer() {
crate::test_log::capture();
let mut spans = Spans::new(1);
let first = spans.begin("lk.publish", SpanKind::Internal, None);
spans.end(first, SpanOutcome::Ok, None, vec![]);
let id = spans.begin("lk.connect", SpanKind::Client, None);
let own = (0..=MAX_ATTRIBUTES_PER_SPAN).map(|i| Attribute::new(format!("k{i}"), 1i64));
spans.end(id, SpanOutcome::Ok, None, own.collect());
assert_eq!(
crate::test_log::take(),
[
"lk.connect over 128 attributes: dropping 1",
"finished spans full (1): dropping the oldest"
],
"one `end`, two limits, two warnings"
);
}
#[test]
fn checkpoints_over_the_cap_are_counted_and_warned_once() {
crate::test_log::capture();
let mut spans = Spans::new(8);
let overflow = |spans: &mut Spans| {
let id = spans.begin("lk.connect", SpanKind::Client, None);
for i in 0..=MAX_EVENTS_PER_SPAN {
spans.add_event(id, &format!("step {i}"));
}
spans.end(id, SpanOutcome::Ok, None, vec![]);
};
overflow(&mut spans);
overflow(&mut spans);
let warning = "lk.connect over 128 checkpoints: dropping the rest";
assert_eq!(crate::test_log::take(), [warning], "two overflows, one warning");
for span in encoded(&mut spans) {
assert_eq!(span.dropped_events_count, 1);
assert_eq!(span.events.len(), MAX_EVENTS_PER_SPAN);
assert_eq!(span.events.last().map(|e| e.name.as_str()), Some("step 127"));
}
overflow(&mut spans);
assert_eq!(crate::test_log::take(), [warning], "a drain starts a new episode");
}
#[test]
fn finished_spans_are_bounded() {
let mut spans = Spans::new(1);
for _ in 0..2 {
let id = spans.begin("lk.publish", SpanKind::Internal, None);
spans.end(id, SpanOutcome::Ok, None, vec![]);
}
assert_eq!(spans.drain(10, usize::MAX).len(), 1);
assert_eq!(spans.take_dropped(), 1);
}
}