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