Skip to main content

obs_core/scope/
frame.rs

1//! `ScopeFrame` — one entry on the per-task / per-thread scope stack.
2
3use std::collections::VecDeque;
4
5use obs_proto::obs::v1::ObsEnvelope;
6
7use crate::codegen_helpers::SpanFrame;
8
9/// A field declared on `obs::scope!` / `obs::context!`. Field types
10/// match the envelope projection contract: trace ids land on the typed
11/// envelope slots, labels go into `env.labels`. Spec 13 § 2.1.
12#[derive(Debug, Clone)]
13pub enum ScopeField {
14    /// Pushes onto `env.trace_id` when missing.
15    TraceId(String),
16    /// Pushes onto `env.span_id` when missing.
17    SpanId(String),
18    /// Pushes onto `env.parent_span_id` when missing.
19    ParentSpanId(String),
20    /// Pushes onto `env.labels[name]` when the key is absent.
21    Label(&'static str, String),
22}
23
24impl ScopeField {
25    /// Field name as it would appear on a schema (for diagnostics).
26    #[must_use]
27    pub fn name(&self) -> &'static str {
28        match self {
29            Self::TraceId(_) => "trace_id",
30            Self::SpanId(_) => "span_id",
31            Self::ParentSpanId(_) => "parent_span_id",
32            Self::Label(k, _) => k,
33        }
34    }
35
36    /// Borrow the value's bytes for diagnostics or `SpanCtx` rendering.
37    #[must_use]
38    pub fn value(&self) -> &str {
39        match self {
40            Self::TraceId(v) | Self::SpanId(v) | Self::ParentSpanId(v) | Self::Label(_, v) => v,
41        }
42    }
43}
44
45/// Whether a frame carries a tail-on-error buffer (`Scope`) or is a
46/// pure broadcast frame (`Context`). Spec 13 § 2.2.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48#[non_exhaustive]
49pub enum ScopeKind {
50    /// `obs::scope!` — fields + 64-deep tail-on-error buffer.
51    Scope,
52    /// `obs::context!` — fields only; no buffer cost.
53    Context,
54}
55
56/// One frame on the per-task / per-thread scope stack.
57#[derive(Debug, Clone)]
58pub struct ScopeFrame {
59    fields: Vec<ScopeField>,
60    kind: ScopeKind,
61    tail_capacity: u16,
62    tail_buffer: VecDeque<ObsEnvelope>,
63    seen_error: bool,
64    traceparent_sampled: Option<bool>,
65    span_name: Option<&'static str>,
66    span_target: Option<&'static str>,
67}
68
69impl ScopeFrame {
70    /// Construct a frame with the supplied declared fields and tail
71    /// buffer capacity.
72    #[must_use]
73    pub fn new(fields: Vec<ScopeField>, kind: ScopeKind, tail_capacity: u16) -> Self {
74        let cap = match kind {
75            ScopeKind::Scope => tail_capacity as usize,
76            ScopeKind::Context => 0,
77        };
78        Self {
79            fields,
80            kind,
81            tail_capacity,
82            tail_buffer: VecDeque::with_capacity(cap),
83            seen_error: false,
84            traceparent_sampled: None,
85            span_name: None,
86            span_target: None,
87        }
88    }
89
90    /// Update the inbound `traceparent.sampled` decision (set by the
91    /// HTTP middleware at request entry). Spec 13 § 6.
92    pub fn set_traceparent_sampled(&mut self, sampled: bool) {
93        self.traceparent_sampled = Some(sampled);
94    }
95
96    /// Set the bridged tracing span identity for `obs::SpanTrace`
97    /// rendering. Spec 13 § 9.
98    pub fn set_span_identity(&mut self, name: &'static str, target: &'static str) {
99        self.span_name = Some(name);
100        self.span_target = Some(target);
101    }
102
103    /// Inbound `traceparent.sampled` decision, when set.
104    #[must_use]
105    pub fn traceparent_sampled(&self) -> Option<bool> {
106        self.traceparent_sampled
107    }
108
109    /// Read-only view of the declared fields.
110    #[must_use]
111    pub fn fields(&self) -> &[ScopeField] {
112        &self.fields
113    }
114
115    /// Frame kind.
116    #[must_use]
117    pub fn kind(&self) -> ScopeKind {
118        self.kind
119    }
120
121    /// `true` if the scope has observed an `>= ERROR` envelope.
122    #[must_use]
123    pub fn seen_error(&self) -> bool {
124        self.seen_error
125    }
126
127    /// Mark this frame's tail buffer as needing flush.
128    pub fn mark_error(&mut self) {
129        self.seen_error = true;
130    }
131
132    /// Capacity of the tail buffer (in envelopes).
133    #[must_use]
134    pub fn tail_capacity(&self) -> u16 {
135        self.tail_capacity
136    }
137
138    /// Push an envelope onto the ring buffer. No-op for `Context`.
139    pub fn push_tail(&mut self, env: ObsEnvelope) {
140        if self.kind == ScopeKind::Context {
141            return;
142        }
143        if self.tail_capacity == 0 {
144            return;
145        }
146        if self.tail_buffer.len() >= self.tail_capacity as usize {
147            self.tail_buffer.pop_front();
148        }
149        self.tail_buffer.push_back(env);
150    }
151
152    /// Drain and return the buffered envelopes (callers flush on
153    /// scope-end-with-error).
154    #[must_use]
155    pub fn drain_tail(&mut self) -> Vec<ObsEnvelope> {
156        self.tail_buffer.drain(..).collect()
157    }
158
159    /// Snapshot of currently-buffered envelopes (test-only).
160    #[must_use]
161    pub fn tail_snapshot(&self) -> Vec<ObsEnvelope> {
162        self.tail_buffer.iter().cloned().collect()
163    }
164
165    /// Render the frame as a `SpanFrame` for `SpanTrace`.
166    #[must_use]
167    pub fn as_span_frame(&self) -> Option<SpanFrame<'_>> {
168        Some(SpanFrame {
169            name: self.span_name?,
170            target: self.span_target?,
171        })
172    }
173}