1mod builder;
8mod frame;
9mod guard;
10
11use std::cell::RefCell;
12
13use obs_proto::obs::v1::ObsEnvelope;
14
15pub use self::{
16 builder::ScopeFrameBuilder,
17 frame::{ScopeField, ScopeFrame, ScopeKind},
18 guard::ScopeGuard,
19};
20
21thread_local! {
22 static THREAD_STACK: RefCell<Vec<ScopeFrame>> = const { RefCell::new(Vec::new()) };
23}
24
25tokio::task_local! {
26 static TASK_STACK: RefCell<Vec<ScopeFrame>>;
27}
28
29pub(crate) fn push_frame(frame: ScopeFrame) -> usize {
32 if let Ok(depth) = TASK_STACK.try_with(|cell| {
33 let mut v = cell.borrow_mut();
34 v.push(frame.clone());
35 v.len()
36 }) {
37 return depth;
38 }
39 THREAD_STACK.with(|cell| {
40 let mut v = cell.borrow_mut();
41 v.push(frame);
42 v.len()
43 })
44}
45
46pub(crate) fn pop_frame() -> Option<ScopeFrame> {
49 if let Ok(frame) = TASK_STACK.try_with(|cell| cell.borrow_mut().pop()) {
50 return frame;
51 }
52 THREAD_STACK.with(|cell| cell.borrow_mut().pop())
53}
54
55pub fn push_frame_pub(frame: ScopeFrame) {
63 let _ = push_frame(frame);
64}
65
66pub fn pop_frame_pub() -> Option<ScopeFrame> {
68 pop_frame()
69}
70
71pub fn with_frames_innermost_first<F, R>(f: F) -> R
74where
75 F: FnOnce(&[ScopeFrame]) -> R,
76{
77 let snapshot = collect_active_stack();
81 f(snapshot.as_slice())
82}
83
84fn collect_active_stack() -> Vec<ScopeFrame> {
85 if let Ok(v) = TASK_STACK.try_with(|cell| cell.borrow().clone()) {
86 return v;
87 }
88 THREAD_STACK.with(|cell| cell.borrow().clone())
89}
90
91pub fn auto_fill_envelope(env: &mut ObsEnvelope) {
95 let frames = collect_active_stack();
96 for frame in frames.iter().rev() {
97 for field in frame.fields() {
98 match field {
99 ScopeField::TraceId(v) if env.trace_id.is_empty() => {
100 env.trace_id.clone_from(v);
101 }
102 ScopeField::SpanId(v) if env.span_id.is_empty() => {
103 env.span_id.clone_from(v);
104 }
105 ScopeField::ParentSpanId(v) if env.parent_span_id.is_empty() => {
106 env.parent_span_id.clone_from(v);
107 }
108 ScopeField::Label(k, v) if !env.labels.contains_key(*k) => {
109 env.labels.insert((*k).to_string(), v.clone());
110 }
111 _ => {}
112 }
113 }
114 }
115}
116
117#[must_use]
120pub fn inbound_traceparent_sampled() -> Option<bool> {
121 let frames = collect_active_stack();
122 frames.iter().find_map(|f| f.traceparent_sampled())
123}
124
125#[must_use]
135pub fn active_correlation() -> Option<(String, String)> {
136 let frames = collect_active_stack();
137 let mut trace_id: Option<String> = None;
138 let mut span_id: Option<String> = None;
139 for frame in frames.iter().rev() {
140 for field in frame.fields() {
141 match field {
142 ScopeField::TraceId(v) if trace_id.is_none() => trace_id = Some(v.clone()),
143 ScopeField::SpanId(v) if span_id.is_none() => span_id = Some(v.clone()),
144 _ => {}
145 }
146 }
147 if trace_id.is_some() && span_id.is_some() {
148 break;
149 }
150 }
151 match (trace_id, span_id) {
152 (Some(t), Some(s)) => Some((t, s)),
153 _ => None,
154 }
155}
156
157#[must_use]
161pub fn active_sampled() -> Option<bool> {
162 inbound_traceparent_sampled()
163}
164
165pub fn push_tail_buffer(env: &ObsEnvelope) {
169 if let Ok(()) = TASK_STACK.try_with(|cell| push_to_top(cell.borrow_mut().last_mut(), env)) {
170 return;
171 }
172 THREAD_STACK.with(|cell| push_to_top(cell.borrow_mut().last_mut(), env));
173}
174
175pub fn mark_error_on_active_scopes() {
179 if let Ok(()) = TASK_STACK.try_with(|cell| {
180 for f in cell.borrow_mut().iter_mut() {
181 f.mark_error();
182 }
183 }) {
184 return;
185 }
186 THREAD_STACK.with(|cell| {
187 for f in cell.borrow_mut().iter_mut() {
188 f.mark_error();
189 }
190 });
191}
192
193#[allow(dead_code)]
197pub(crate) async fn scope_task<F, R>(fut: F) -> R
198where
199 F: std::future::Future<Output = R>,
200{
201 TASK_STACK.scope(RefCell::new(Vec::new()), fut).await
202}
203
204fn push_to_top(top: Option<&mut ScopeFrame>, env: &ObsEnvelope) {
205 if let Some(frame) = top {
206 frame.push_tail(env.clone());
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use obs_types::Severity;
213
214 use super::*;
215
216 fn make_frame(fields: Vec<ScopeField>, kind: ScopeKind) -> ScopeFrame {
217 ScopeFrame::new(fields, kind, 64)
218 }
219
220 #[test]
221 fn test_should_inject_label_when_envelope_missing() {
222 let frame = make_frame(
223 vec![ScopeField::Label("tenant", "alpha".to_string())],
224 ScopeKind::Scope,
225 );
226 let _depth = push_frame(frame);
227 let mut env = ObsEnvelope::default();
228 auto_fill_envelope(&mut env);
229 assert_eq!(env.labels.get("tenant"), Some(&"alpha".to_string()));
230 let _ = pop_frame();
231 }
232
233 #[test]
234 fn test_should_not_overwrite_explicit_label() {
235 let frame = make_frame(
236 vec![ScopeField::Label("tenant", "alpha".to_string())],
237 ScopeKind::Scope,
238 );
239 let _depth = push_frame(frame);
240 let mut env = ObsEnvelope::default();
241 env.labels.insert("tenant".to_string(), "beta".to_string());
242 auto_fill_envelope(&mut env);
243 assert_eq!(env.labels.get("tenant"), Some(&"beta".to_string()));
244 let _ = pop_frame();
245 }
246
247 #[test]
248 fn test_should_inject_trace_id() {
249 let frame = make_frame(
250 vec![ScopeField::TraceId("abc".to_string())],
251 ScopeKind::Scope,
252 );
253 let _depth = push_frame(frame);
254 let mut env = ObsEnvelope::default();
255 auto_fill_envelope(&mut env);
256 assert_eq!(env.trace_id, "abc");
257 let _ = pop_frame();
258 }
259
260 #[test]
261 fn test_should_return_active_correlation_when_present() {
262 let frame = make_frame(
263 vec![
264 ScopeField::TraceId("trc".to_string()),
265 ScopeField::SpanId("spn".to_string()),
266 ],
267 ScopeKind::Scope,
268 );
269 let _ = push_frame(frame);
270 assert_eq!(
271 active_correlation(),
272 Some(("trc".to_string(), "spn".to_string())),
273 );
274 let _ = pop_frame();
275 }
276
277 #[test]
278 fn test_should_return_none_when_no_correlation() {
279 assert_eq!(active_correlation(), None);
281 }
282
283 #[test]
284 fn test_should_walk_stack_for_correlation() {
285 let outer = make_frame(
286 vec![ScopeField::TraceId("outer-trc".to_string())],
287 ScopeKind::Scope,
288 );
289 let inner = make_frame(
290 vec![ScopeField::SpanId("inner-spn".to_string())],
291 ScopeKind::Scope,
292 );
293 let _ = push_frame(outer);
294 let _ = push_frame(inner);
295 assert_eq!(
297 active_correlation(),
298 Some(("outer-trc".to_string(), "inner-spn".to_string())),
299 );
300 let _ = pop_frame();
301 let _ = pop_frame();
302 }
303
304 #[test]
305 fn test_should_push_tail_buffer_only_for_scope_kind() {
306 let frame = make_frame(vec![], ScopeKind::Context);
307 let _ = push_frame(frame);
308 let env = ObsEnvelope {
309 full_name: "test.v1.X".to_string(),
310 sev: ::buffa::EnumValue::Known(obs_proto::obs::v1::Severity::SEVERITY_DEBUG),
311 ..Default::default()
312 };
313 push_tail_buffer(&env);
314 let frame = pop_frame().unwrap();
315 assert!(frame.tail_snapshot().is_empty());
317 let _ = Severity::Debug;
318 }
319}