obs_core/scope/builder.rs
1//! `ScopeFrameBuilder` — public, programmatic API for pushing a scope
2//! frame from outside `obs-core`.
3//!
4//! `obs::scope!` is the canonical user-facing API and resolves to a
5//! macro that captures the call site at compile time. External crates
6//! that need to push a frame from a generic context (the tracing
7//! bridge, `obs-tower`, user-defined middleware) cannot use the macro
8//! because the field set is computed at runtime. This builder fills
9//! that gap. Spec 13 § 4 (D7-3 in spec 94).
10
11use super::{ScopeField, ScopeFrame, ScopeGuard, ScopeKind};
12
13/// Programmatic builder for pushing an `obs::scope!`-shaped frame.
14///
15/// External crates use this when they need to push a frame whose
16/// fields are decided at runtime (e.g. the tracing bridge stamps
17/// `(trace_id, span_id, parent_span_id)` from a span extension; the
18/// HTTP middleware stamps the same fields from extracted W3C
19/// `traceparent` headers).
20///
21/// The builder is consumed by [`Self::push`], which returns a
22/// [`ScopeGuard`] that pops the frame on drop. To carry the frame
23/// across an async boundary, use [`Self::into_frame`] and feed the
24/// resulting [`ScopeFrame`] to
25/// [`crate::instrumented::Instrument::instrument`].
26#[derive(Debug)]
27pub struct ScopeFrameBuilder {
28 fields: Vec<ScopeField>,
29 kind: ScopeKind,
30 tail_capacity: u16,
31 traceparent_sampled: Option<bool>,
32 span_identity: Option<(&'static str, &'static str)>,
33}
34
35impl Default for ScopeFrameBuilder {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl ScopeFrameBuilder {
42 /// New builder defaulting to a `Scope` frame with a 64-deep
43 /// tail-on-error buffer (matches the `obs::scope!` defaults).
44 #[must_use]
45 pub fn new() -> Self {
46 Self {
47 fields: Vec::new(),
48 kind: ScopeKind::Scope,
49 tail_capacity: 64,
50 traceparent_sampled: None,
51 span_identity: None,
52 }
53 }
54
55 /// Switch to a `Context` frame (no tail-on-error buffer cost).
56 /// Equivalent to `obs::context!`.
57 #[must_use]
58 pub fn context(mut self) -> Self {
59 self.kind = ScopeKind::Context;
60 self.tail_capacity = 0;
61 self
62 }
63
64 /// Override the tail-on-error capacity (only meaningful for
65 /// `Scope` kind; ignored for `Context`).
66 #[must_use]
67 pub fn tail_capacity(mut self, capacity: u16) -> Self {
68 self.tail_capacity = capacity;
69 self
70 }
71
72 /// Set `trace_id` on the frame so emitted envelopes inherit it
73 /// via [`super::auto_fill_envelope`].
74 #[must_use]
75 pub fn trace_id(mut self, value: impl Into<String>) -> Self {
76 self.fields.push(ScopeField::TraceId(value.into()));
77 self
78 }
79
80 /// Set `span_id` on the frame.
81 #[must_use]
82 pub fn span_id(mut self, value: impl Into<String>) -> Self {
83 self.fields.push(ScopeField::SpanId(value.into()));
84 self
85 }
86
87 /// Set `parent_span_id` on the frame.
88 #[must_use]
89 pub fn parent_span_id(mut self, value: impl Into<String>) -> Self {
90 self.fields.push(ScopeField::ParentSpanId(value.into()));
91 self
92 }
93
94 /// Add a `(name, value)` label pair. The `name` must be a static
95 /// `&'static str` so it can round-trip through the envelope's
96 /// `labels` map without an allocation. Spec 13 § 2.1.
97 #[must_use]
98 pub fn label(mut self, name: &'static str, value: impl Into<String>) -> Self {
99 self.fields.push(ScopeField::Label(name, value.into()));
100 self
101 }
102
103 /// Inbound `traceparent.sampled` decision. Spec 13 § 6.
104 #[must_use]
105 pub fn traceparent_sampled(mut self, sampled: bool) -> Self {
106 self.traceparent_sampled = Some(sampled);
107 self
108 }
109
110 /// Bridged tracing-span identity for `obs::SpanTrace` rendering.
111 /// Spec 13 § 9.
112 #[must_use]
113 pub fn span_identity(mut self, name: &'static str, target: &'static str) -> Self {
114 self.span_identity = Some((name, target));
115 self
116 }
117
118 /// Push the frame onto the active task's scope stack and return
119 /// the RAII guard. Drop the guard to pop the frame.
120 pub fn push(self) -> ScopeGuard {
121 let frame = self.into_frame();
122 ScopeGuard::enter_with_frame(frame)
123 }
124
125 /// Build the frame without pushing it. Useful when the caller
126 /// wants to attach it to a future via
127 /// [`crate::instrumented::Instrument::instrument`] so the frame
128 /// is re-entered on every poll.
129 #[must_use]
130 pub fn into_frame(self) -> ScopeFrame {
131 let mut frame = ScopeFrame::new(self.fields, self.kind, self.tail_capacity);
132 if let Some(sampled) = self.traceparent_sampled {
133 frame.set_traceparent_sampled(sampled);
134 }
135 if let Some((name, target)) = self.span_identity {
136 frame.set_span_identity(name, target);
137 }
138 frame
139 }
140}