use std::{
fmt,
sync::{Arc, Mutex},
};
use crate::{Attribute, AttributeValue};
pub(crate) struct ScopeState {
pub trace_id: [u8; 16],
attributes: Mutex<Vec<Attribute>>,
custom: Mutex<Vec<Attribute>>,
route: Mutex<Option<String>>,
server: Mutex<Option<(String, String)>>,
}
impl ScopeState {
pub fn new() -> Arc<Self> {
Self::with_trace_id(rand::random::<u128>().max(1).to_be_bytes())
}
pub fn with_trace_id(trace_id: [u8; 16]) -> Arc<Self> {
Arc::new(Self {
trace_id,
attributes: Mutex::new(Vec::new()),
custom: Mutex::new(Vec::new()),
route: Mutex::new(None),
server: Mutex::new(None),
})
}
pub fn in_call(&self) -> bool {
self.server.lock().unwrap_or_else(|e| e.into_inner()).is_some()
}
pub fn same_server(&self, url: &str, token: &str) -> bool {
let mut server = self.server.lock().unwrap_or_else(|e| e.into_inner());
if server.as_ref().is_some_and(|(u, t)| u == url && t == token) {
return true;
}
*server = Some((url.to_owned(), token.to_owned()));
false
}
pub fn route(&self) -> Option<String> {
self.route.lock().unwrap_or_else(|e| e.into_inner()).clone()
}
pub fn set_route(&self, host: String) {
*self.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(host);
}
pub fn hex(&self) -> String {
format!("{:032x}", u128::from_be_bytes(self.trace_id))
}
pub fn set_attribute(&self, key: &str, value: Option<AttributeValue>) {
let mut attributes = self.attributes.lock().unwrap_or_else(|e| e.into_inner());
attributes.retain(|a| a.key != key);
if let Some(value) = value {
attributes.push(Attribute::new(key, value));
}
}
pub fn accepts_custom(&self, key: &str, value: Option<&AttributeValue>) -> bool {
crate::event::valid_custom(key, value)
&& fits(&self.custom.lock().unwrap_or_else(|e| e.into_inner()), key, value)
}
pub fn set_custom(&self, key: &str, value: Option<AttributeValue>) -> bool {
if !crate::event::valid_custom(key, value.as_ref()) {
return false;
}
let mut custom = self.custom.lock().unwrap_or_else(|e| e.into_inner());
if !fits(&custom, key, value.as_ref()) {
return false;
}
custom.retain(|a| a.key != key);
if let Some(value) = value {
custom.push(Attribute::new(key, value));
}
true
}
pub fn custom_snapshot(&self) -> Vec<Attribute> {
self.custom.lock().unwrap_or_else(|e| e.into_inner()).clone()
}
pub fn snapshot_custom(&self, own: &mut Vec<Attribute>) {
let custom = self.custom.lock().unwrap_or_else(|e| e.into_inner());
for attribute in custom.iter() {
if !own.iter().any(|a| a.key == attribute.key) {
own.push(attribute.clone());
}
}
}
pub fn decorate(&self, own: &mut Vec<Attribute>) {
let session = self.attributes.lock().unwrap_or_else(|e| e.into_inner());
for attribute in session.iter() {
own.retain(|a| a.key != attribute.key);
own.push(attribute.clone());
}
drop(session);
own.retain(|a| a.key != "session.id");
own.push(Attribute::new("session.id", self.hex()));
}
}
fn fits(custom: &[Attribute], key: &str, value: Option<&AttributeValue>) -> bool {
value.is_none()
|| custom.iter().any(|a| a.key == key)
|| custom.len() < crate::event::MAX_CUSTOM_ATTRIBUTES
}
impl PartialEq for ScopeState {
fn eq(&self, other: &Self) -> bool {
self.trace_id == other.trace_id
}
}
impl fmt::Debug for ScopeState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Scope({})", self.hex())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::MAX_CUSTOM_ATTRIBUTES;
#[test]
fn custom_attributes_reject_sdk_keys_and_fit_replacements_at_capacity() {
for key in [
"lk.room.sid",
"otel.event.name",
"otel.scope.name",
"code.function.name",
"session.id",
"error.type",
] {
assert!(!crate::event::valid_custom(key, None), "{key} is the SDK's");
}
let scope = ScopeState::new();
for i in 0..MAX_CUSTOM_ATTRIBUTES {
assert!(scope.set_custom(&format!("key.{i}"), Some(1i64.into())));
}
assert!(!scope.set_custom("extra", Some(1i64.into())));
assert!(scope.set_custom("key.0", Some(2i64.into())), "replacing an existing key fits");
assert!(scope.custom_snapshot().contains(&Attribute::new("key.0", 2i64)));
assert!(scope.set_custom("key.0", None));
assert!(scope.set_custom("extra", Some(1i64.into())));
assert_eq!(scope.custom_snapshot().len(), MAX_CUSTOM_ATTRIBUTES);
}
#[test]
fn concurrent_custom_insertions_respect_capacity() {
const WRITERS: usize = 16;
for _ in 0..64 {
let scope = ScopeState::new();
for i in 0..MAX_CUSTOM_ATTRIBUTES - 1 {
assert!(scope.set_custom(&format!("key.{i}"), Some(1i64.into())));
}
let barrier = std::sync::Barrier::new(WRITERS);
let accepted = std::thread::scope(|threads| {
let handles: Vec<_> = (0..WRITERS)
.map(|i| {
let (scope, barrier) = (&scope, &barrier);
threads.spawn(move || {
barrier.wait();
scope.set_custom(&format!("race.{i}"), Some(1i64.into()))
})
})
.collect();
handles.into_iter().map(|h| h.join().unwrap()).filter(|a| *a).count()
});
assert_eq!(accepted, 1, "one slot left, one writer wins");
assert_eq!(scope.custom_snapshot().len(), MAX_CUSTOM_ATTRIBUTES);
}
}
}