mod builder;
mod frame;
mod guard;
use std::cell::RefCell;
use obs_proto::obs::v1::ObsEnvelope;
pub(crate) use self::guard::finish_scope_frame;
pub use self::{
builder::ScopeFrameBuilder,
frame::{ScopeField, ScopeFrame, ScopeKind},
guard::ScopeGuard,
};
thread_local! {
static THREAD_STACK: RefCell<Vec<ScopeFrame>> = const { RefCell::new(Vec::new()) };
}
tokio::task_local! {
static TASK_STACK: RefCell<Vec<ScopeFrame>>;
}
pub(crate) fn push_frame(frame: ScopeFrame) -> usize {
if let Ok(depth) = TASK_STACK.try_with(|cell| {
let mut v = cell.borrow_mut();
v.push(frame.clone());
v.len()
}) {
return depth;
}
THREAD_STACK.with(|cell| {
let mut v = cell.borrow_mut();
v.push(frame);
v.len()
})
}
pub(crate) fn pop_frame() -> Option<ScopeFrame> {
if let Ok(frame) = TASK_STACK.try_with(|cell| cell.borrow_mut().pop()) {
return frame;
}
THREAD_STACK.with(|cell| cell.borrow_mut().pop())
}
pub fn push_frame_pub(frame: ScopeFrame) {
let _ = push_frame(frame);
}
pub fn pop_frame_pub() -> Option<ScopeFrame> {
pop_frame()
}
pub fn with_frames_innermost_first<F, R>(f: F) -> R
where
F: FnOnce(&[ScopeFrame]) -> R,
{
let snapshot = collect_active_stack();
f(snapshot.as_slice())
}
fn collect_active_stack() -> Vec<ScopeFrame> {
if let Ok(v) = TASK_STACK.try_with(|cell| cell.borrow().clone()) {
return v;
}
THREAD_STACK.with(|cell| cell.borrow().clone())
}
pub fn auto_fill_envelope(env: &mut ObsEnvelope) {
let frames = collect_active_stack();
for frame in frames.iter().rev() {
for field in frame.fields() {
match field {
ScopeField::TraceId(v) if env.trace_id.is_empty() => {
env.trace_id.clone_from(v);
}
ScopeField::SpanId(v) if env.span_id.is_empty() => {
env.span_id.clone_from(v);
}
ScopeField::ParentSpanId(v) if env.parent_span_id.is_empty() => {
env.parent_span_id.clone_from(v);
}
ScopeField::Label(k, v) if !env.labels.contains_key(*k) => {
env.labels.insert((*k).to_string(), v.clone());
}
_ => {}
}
}
}
}
#[must_use]
pub fn inbound_traceparent_sampled() -> Option<bool> {
let frames = collect_active_stack();
frames.iter().find_map(|f| f.traceparent_sampled())
}
#[must_use]
pub fn active_correlation() -> Option<(String, String)> {
let frames = collect_active_stack();
let mut trace_id: Option<String> = None;
let mut span_id: Option<String> = None;
for frame in frames.iter().rev() {
for field in frame.fields() {
match field {
ScopeField::TraceId(v) if trace_id.is_none() => trace_id = Some(v.clone()),
ScopeField::SpanId(v) if span_id.is_none() => span_id = Some(v.clone()),
_ => {}
}
}
if trace_id.is_some() && span_id.is_some() {
break;
}
}
match (trace_id, span_id) {
(Some(t), Some(s)) => Some((t, s)),
_ => None,
}
}
#[must_use]
pub fn active_sampled() -> Option<bool> {
inbound_traceparent_sampled()
}
pub fn push_tail_buffer(env: &ObsEnvelope) {
if let Ok(()) = TASK_STACK.try_with(|cell| push_to_top(cell.borrow_mut().last_mut(), env)) {
return;
}
THREAD_STACK.with(|cell| push_to_top(cell.borrow_mut().last_mut(), env));
}
pub fn mark_error_on_active_scopes() {
if let Ok(()) = TASK_STACK.try_with(|cell| {
for f in cell.borrow_mut().iter_mut() {
f.mark_error();
}
}) {
return;
}
THREAD_STACK.with(|cell| {
for f in cell.borrow_mut().iter_mut() {
f.mark_error();
}
});
}
#[allow(dead_code)]
pub(crate) async fn scope_task<F, R>(fut: F) -> R
where
F: std::future::Future<Output = R>,
{
TASK_STACK.scope(RefCell::new(Vec::new()), fut).await
}
fn push_to_top(top: Option<&mut ScopeFrame>, env: &ObsEnvelope) {
if let Some(frame) = top {
frame.push_tail(env.clone());
}
}
#[cfg(test)]
mod tests {
use obs_proto::obs::v1::Severity;
use super::*;
fn make_frame(fields: Vec<ScopeField>, kind: ScopeKind) -> ScopeFrame {
ScopeFrame::new(fields, kind, 64)
}
#[test]
fn test_should_inject_label_when_envelope_missing() {
let frame = make_frame(
vec![ScopeField::Label("tenant", "alpha".to_string())],
ScopeKind::Scope,
);
let _depth = push_frame(frame);
let mut env = ObsEnvelope::default();
auto_fill_envelope(&mut env);
assert_eq!(env.labels.get("tenant"), Some(&"alpha".to_string()));
let _ = pop_frame();
}
#[test]
fn test_should_not_overwrite_explicit_label() {
let frame = make_frame(
vec![ScopeField::Label("tenant", "alpha".to_string())],
ScopeKind::Scope,
);
let _depth = push_frame(frame);
let mut env = ObsEnvelope::default();
env.labels.insert("tenant".to_string(), "beta".to_string());
auto_fill_envelope(&mut env);
assert_eq!(env.labels.get("tenant"), Some(&"beta".to_string()));
let _ = pop_frame();
}
#[test]
fn test_should_inject_trace_id() {
let frame = make_frame(
vec![ScopeField::TraceId("abc".to_string())],
ScopeKind::Scope,
);
let _depth = push_frame(frame);
let mut env = ObsEnvelope::default();
auto_fill_envelope(&mut env);
assert_eq!(env.trace_id, "abc");
let _ = pop_frame();
}
#[test]
fn test_should_return_active_correlation_when_present() {
let frame = make_frame(
vec![
ScopeField::TraceId("trc".to_string()),
ScopeField::SpanId("spn".to_string()),
],
ScopeKind::Scope,
);
let _ = push_frame(frame);
assert_eq!(
active_correlation(),
Some(("trc".to_string(), "spn".to_string())),
);
let _ = pop_frame();
}
#[test]
fn test_should_return_none_when_no_correlation() {
assert_eq!(active_correlation(), None);
}
#[test]
fn test_should_walk_stack_for_correlation() {
let outer = make_frame(
vec![ScopeField::TraceId("outer-trc".to_string())],
ScopeKind::Scope,
);
let inner = make_frame(
vec![ScopeField::SpanId("inner-spn".to_string())],
ScopeKind::Scope,
);
let _ = push_frame(outer);
let _ = push_frame(inner);
assert_eq!(
active_correlation(),
Some(("outer-trc".to_string(), "inner-spn".to_string())),
);
let _ = pop_frame();
let _ = pop_frame();
}
#[test]
fn test_should_push_tail_buffer_only_for_scope_kind() {
let frame = make_frame(vec![], ScopeKind::Context);
let _ = push_frame(frame);
let env = ObsEnvelope {
full_name: "test.v1.X".to_string(),
sev: ::buffa::EnumValue::Known(obs_proto::obs::v1::Severity::SEVERITY_DEBUG),
..Default::default()
};
push_tail_buffer(&env);
let frame = pop_frame().unwrap();
assert!(frame.tail_snapshot().is_empty());
let _ = Severity::Debug;
}
}