obs_core/scope/guard.rs
1//! `ScopeGuard` — RAII guard returned by `obs::scope!` /
2//! `obs::context!`. Pops the frame on drop and, for `Scope` kind,
3//! flushes the tail buffer when `seen_error == true`.
4//!
5//! Spec 13 §§ 2 + 6.
6
7use std::sync::Arc;
8
9use obs_proto::obs::v1::{ObsEnvelope, SamplingReason as PSamplingReason};
10
11use super::{ScopeField, ScopeFrame, ScopeKind, pop_frame, push_frame};
12use crate::observer::{Observer, enter_emit_envelope};
13
14/// RAII guard returned by `obs::scope!` and `obs::context!`. Dropping
15/// pops the frame; for `Scope` kind frames where any `>= ERROR`
16/// envelope was observed, the tail buffer is flushed back through the
17/// active observer with `sampling_reason = TAIL_ERROR`.
18#[must_use = "the scope guard is popped on Drop; bind to a name like `_scope`"]
19pub struct ScopeGuard {
20 /// `None` after `into_inner` so the destructor knows not to pop.
21 armed: bool,
22}
23
24impl std::fmt::Debug for ScopeGuard {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.debug_struct("ScopeGuard")
27 .field("armed", &self.armed)
28 .finish()
29 }
30}
31
32impl ScopeGuard {
33 /// Push a `Scope` frame.
34 pub fn enter(fields: Vec<ScopeField>, tail_capacity: u16) -> Self {
35 let _ = push_frame(ScopeFrame::new(fields, ScopeKind::Scope, tail_capacity));
36 Self { armed: true }
37 }
38
39 /// Push a `Context` frame (no tail buffer).
40 pub fn enter_context(fields: Vec<ScopeField>) -> Self {
41 let _ = push_frame(ScopeFrame::new(fields, ScopeKind::Context, 0));
42 Self { armed: true }
43 }
44
45 /// Push a frame with explicit identity, used by the bridge to
46 /// stamp `(name, target)` for `obs::SpanTrace`.
47 pub fn enter_with_identity(
48 fields: Vec<ScopeField>,
49 kind: ScopeKind,
50 tail_capacity: u16,
51 name: &'static str,
52 target: &'static str,
53 ) -> Self {
54 let mut frame = ScopeFrame::new(fields, kind, tail_capacity);
55 frame.set_span_identity(name, target);
56 let _ = push_frame(frame);
57 Self { armed: true }
58 }
59
60 /// Push a fully-built [`ScopeFrame`] onto the active scope stack.
61 /// Used by [`super::ScopeFrameBuilder::push`] so external crates
62 /// can build a frame programmatically (instead of via the
63 /// `obs::scope!` macro) and own the resulting RAII guard.
64 /// Spec 94 D7-3.
65 pub fn enter_with_frame(frame: ScopeFrame) -> Self {
66 let _ = push_frame(frame);
67 Self { armed: true }
68 }
69
70 /// Detach the guard so the caller can layer it onto a
71 /// `Future::instrument(...)` (the future then re-applies the frame
72 /// on every poll). The frame is popped immediately so the caller
73 /// doesn't hold two copies.
74 #[must_use]
75 pub fn into_inner(mut self) -> ScopeFrame {
76 self.armed = false;
77 // Pop the live frame back out so the caller can transplant it
78 // onto an `Instrumented<F>`; if pop_frame returns None (because
79 // a child task already cleared its own stack), synthesise an
80 // empty frame so callers don't hit `Option::unwrap`.
81 pop_frame().unwrap_or_else(|| ScopeFrame::new(Vec::new(), ScopeKind::Scope, 64))
82 }
83}
84
85impl Drop for ScopeGuard {
86 fn drop(&mut self) {
87 if !self.armed {
88 return;
89 }
90 let Some(mut frame) = pop_frame() else {
91 return;
92 };
93 if frame.kind() != ScopeKind::Scope {
94 return;
95 }
96 if !frame.seen_error() {
97 return;
98 }
99 flush_tail_buffer(&mut frame);
100 }
101}
102
103fn flush_tail_buffer(frame: &mut ScopeFrame) {
104 let observer = crate::observer::observer();
105 flush_through(&observer, frame);
106}
107
108fn flush_through(observer: &Arc<dyn Observer>, frame: &mut ScopeFrame) {
109 for mut env in frame.drain_tail() {
110 env.sampling_reason =
111 ::buffa::EnumValue::Known(PSamplingReason::SAMPLING_REASON_TAIL_ERROR);
112 // Cannot recurse through enter_emit_envelope because the
113 // outer emit already cleared CAN_ENTER. Instead, dispatch
114 // directly: tail flush happens *after* the original error's
115 // emit completes so re-entry is safe.
116 flush_one(observer, env);
117 }
118}
119
120fn flush_one(observer: &Arc<dyn Observer>, env: ObsEnvelope) {
121 // Route through `enter_emit_envelope` so the CAN_ENTER re-entry
122 // guard (spec 11 § 3.1) is held during the dispatch — protects
123 // against a sink that synthesises events during tail flush.
124 enter_emit_envelope(observer, env);
125}