Skip to main content

saddle_core/
request_context.rs

1//! Request facts only. Allocation permission and cancellation remain with Runtime/Admission.
2//! Root creation and each local allocation must be preceded by their storage reservation.
3use crate::{CallContext, DbScopeDiagnosticIdentity};
4use serde::{Serialize, Serializer, ser::SerializeStruct};
5use std::sync::{
6    Arc, OnceLock,
7    atomic::{AtomicU64, Ordering},
8};
9
10static NEXT_ROOT: AtomicU64 = AtomicU64::new(1);
11
12/// Closed absence vocabulary, separate from output availability.
13#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
14#[serde(tag = "state", content = "value", rename_all = "snake_case")]
15pub enum ContextFact<T> {
16    Present(T),
17    NotApplicable,
18    NotEstablished,
19    Unavailable,
20}
21
22/// Exact bounded protocol identity. Not an authorization or a user-data container.
23#[derive(Clone, Copy, Eq, PartialEq)]
24pub struct ContextIdentity {
25    bytes: [u8; 256],
26    len: u16,
27}
28impl ContextIdentity {
29    pub fn checked(value: &str) -> Result<Self, ContextConflict> {
30        if value.is_empty() || value.len() > 256 || value.chars().any(char::is_control) {
31            return Err(ContextConflict::InvalidIdentity);
32        }
33        let mut out = Self {
34            bytes: [0; 256],
35            len: value.len() as u16,
36        };
37        out.bytes[..value.len()].copy_from_slice(value.as_bytes());
38        Ok(out)
39    }
40    fn as_str(&self) -> &str {
41        std::str::from_utf8(&self.bytes[..usize::from(self.len)]).expect("validated UTF-8")
42    }
43}
44impl Serialize for ContextIdentity {
45    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
46        s.serialize_str(self.as_str())
47    }
48}
49
50/// Safe metadata, bounded and exact; credentials/URL/query syntax is rejected.
51#[derive(Clone, Copy, Eq, PartialEq, Serialize)]
52#[serde(transparent)]
53pub struct ContextLabel(ContextIdentity);
54impl ContextLabel {
55    pub fn checked(value: &str) -> Result<Self, ContextConflict> {
56        let value = ContextIdentity::checked(value)?;
57        if value.as_str().contains("://")
58            || !value
59                .as_str()
60                .chars()
61                .all(|c| c.is_alphanumeric() || "_.:/{}*-".contains(c))
62        {
63            return Err(ContextConflict::UnsafeMetadata);
64        }
65        Ok(Self(value))
66    }
67}
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub enum ContextConflict {
71    InvalidIdentity,
72    UnsafeMetadata,
73    IdentityGroup,
74    ForeignRoot,
75    ChildRelation,
76    CounterExhausted,
77}
78
79/// Complete validated identity group. Consumed atomically by the entry publisher.
80/// No partial-field setters; rejected publication returns this same group.
81#[derive(Eq, PartialEq)]
82pub struct RequestIdentityGroup {
83    application: ContextLabel,
84    module: ContextLabel,
85    service: ContextLabel,
86    operation: ContextLabel,
87    trace: ContextIdentity,
88    rpc: ContextFact<ContextIdentity>,
89    span: u64,
90    request: ContextIdentity,
91    route: ContextLabel,
92    attempt: u32,
93    zone: ContextFact<ContextLabel>,
94}
95impl RequestIdentityGroup {
96    pub fn from_validated(
97        call: &CallContext,
98        request: &str,
99        route: &str,
100        attempt: u32,
101        zone: ContextFact<ContextLabel>,
102    ) -> Result<Self, ContextConflict> {
103        if attempt == 0 {
104            return Err(ContextConflict::InvalidIdentity);
105        }
106        Ok(Self {
107            application: ContextLabel::checked(call.application().as_str())?,
108            module: ContextLabel::checked(call.module().as_str())?,
109            service: ContextLabel::checked(call.service().as_str())?,
110            operation: ContextLabel::checked(call.operation().as_str())?,
111            trace: ContextIdentity::checked(call.trace_correlation_id().as_str())?,
112            rpc: match call.rpc_correlation_id() {
113                Some(id) => ContextFact::Present(ContextIdentity::checked(id.as_str())?),
114                None => ContextFact::Unavailable,
115            },
116            span: call.span_id().as_u64(),
117            request: ContextIdentity::checked(request)?,
118            route: ContextLabel::checked(route)?,
119            attempt,
120            zone,
121        })
122    }
123}
124
125struct RequestRoot {
126    local: u64,
127    application: ContextLabel,
128    initial: ContextFact<()>,
129    identity: OnceLock<RequestIdentityGroup>,
130}
131impl Drop for RequestRoot {
132    fn drop(&mut self) {
133        observe(self.local, "root_drop");
134    }
135}
136
137/// Unique entry binding capability. Does not own an account or output handle.
138///
139/// ```compile_fail
140/// use saddle_core::RequestRootPublisher;
141/// fn duplicate(p: RequestRootPublisher) { let _ = p.clone(); }
142/// ```
143pub struct RequestRootPublisher {
144    root: Arc<RequestRoot>,
145}
146
147/// Read-only shared reference; cloning shares one allocation, never root fields.
148pub struct RequestRootRef {
149    root: Arc<RequestRoot>,
150}
151impl Clone for RequestRootRef {
152    fn clone(&self) -> Self {
153        observe(self.root.local, "root_share");
154        Self {
155            root: Arc::clone(&self.root),
156        }
157    }
158}
159impl Drop for RequestRootRef {
160    fn drop(&mut self) {
161        observe(self.root.local, "root_release");
162    }
163}
164
165impl RequestRootPublisher {
166    /// Storage-only constructor, not admission. Caller must reserve before calling;
167    /// C supplies layout, R0/A/R supply and enforce the actual reservation seam.
168    pub fn create(
169        application: ContextLabel,
170        initial: ContextFact<()>,
171    ) -> Result<Self, ContextConflict> {
172        if matches!(initial, ContextFact::Present(())) {
173            return Err(ContextConflict::InvalidIdentity);
174        }
175        let local = NEXT_ROOT
176            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_add(1))
177            .map_err(|_| ContextConflict::CounterExhausted)?;
178        let root = Arc::new(RequestRoot {
179            local,
180            application,
181            initial,
182            identity: OnceLock::new(),
183        });
184        observe(local, "root_create");
185        Ok(Self { root })
186    }
187    pub fn reference(&self) -> RequestRootRef {
188        observe(self.root.local, "root_share");
189        RequestRootRef {
190            root: Arc::clone(&self.root),
191        }
192    }
193    #[allow(clippy::result_large_err)] // Recover the fixed input without allocating on rejection.
194    pub fn publish(
195        &mut self,
196        group: RequestIdentityGroup,
197    ) -> Result<(), (ContextConflict, RequestIdentityGroup)> {
198        if group.application != self.root.application {
199            return Err((ContextConflict::IdentityGroup, group));
200        }
201        if let Some(old) = self.root.identity.get() {
202            return if old == &group {
203                Ok(())
204            } else {
205                Err((ContextConflict::IdentityGroup, group))
206            };
207        }
208        self.root
209            .identity
210            .set(group)
211            .map_err(|group| (ContextConflict::IdentityGroup, group))
212    }
213}
214
215/// Closed lifecycle metadata, not a state machine controlling the request.
216#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
217#[serde(rename_all = "snake_case")]
218pub enum RequestViewPhase {
219    SocketAccepted,
220    Reading,
221    Admitted,
222    Dispatch,
223    Handler,
224    Database,
225    Outbound,
226    Response,
227    Finalizing,
228    Finished,
229}
230
231/// Static registered operation labels are borrowed forever, never copied as text.
232#[derive(Clone, Copy, Eq, PartialEq, Serialize)]
233pub struct RegisteredContextOperation(&'static str);
234impl RegisteredContextOperation {
235    pub fn checked(value: &'static str) -> Result<Self, ContextConflict> {
236        ContextLabel::checked(value)?;
237        Ok(Self(value))
238    }
239}
240
241#[derive(Clone, Copy)]
242pub struct RequestLocalFacts {
243    task: ContextFact<u64>,
244    scope: ContextFact<DbScopeDiagnosticIdentity>,
245    db_operation: ContextFact<RegisteredContextOperation>,
246    phase: RequestViewPhase,
247}
248impl RequestLocalFacts {
249    pub fn new(phase: RequestViewPhase) -> Self {
250        Self {
251            task: ContextFact::NotEstablished,
252            scope: ContextFact::NotEstablished,
253            db_operation: ContextFact::NotApplicable,
254            phase,
255        }
256    }
257    /// Numeric observation supplied by the real task owner; not execution authority.
258    pub fn with_task(mut self, task: ContextFact<u64>) -> Self {
259        self.task = task;
260        self
261    }
262    pub fn without_scope(mut self, state: ContextFact<()>) -> Self {
263        self.scope = absent(state);
264        self
265    }
266    pub fn with_db_operation(mut self, operation: RegisteredContextOperation) -> Self {
267        self.db_operation = ContextFact::Present(operation);
268        self
269    }
270}
271
272// Only a real child needs variable local identities. Sibling views share this
273// allocation; the root's original request/trace/zone never appear in this object.
274struct ChildCall {
275    #[cfg(test)]
276    observation: LocalObservation,
277    application: ContextLabel,
278    module: ContextLabel,
279    service: ContextLabel,
280    operation: ContextLabel,
281    rpc: ContextIdentity,
282    span: u64,
283    route: ContextLabel,
284    attempt: u32,
285}
286struct LocalView {
287    #[cfg(test)]
288    observation: LocalObservation,
289    root: Arc<RequestRoot>,
290    published: bool,
291    facts: RequestLocalFacts,
292    child: Option<Arc<ChildCall>>,
293}
294impl Drop for LocalView {
295    fn drop(&mut self) {
296        observe(self.root.local, "view_drop");
297        #[cfg(test)]
298        self.observation.record("destroy");
299    }
300}
301impl Drop for ChildCall {
302    fn drop(&mut self) {
303        #[cfg(test)]
304        self.observation.record("destroy");
305    }
306}
307
308/// Immutable local facts + a frozen visibility stage, sharing the single root.
309/// Source receipts retain this object, not another projection or diagnostic body.
310///
311/// ```compile_fail
312/// use saddle_core::RequestExecutionView;
313/// fn rewrite(view: RequestExecutionView) { view.inner.published = false; }
314/// ```
315pub struct RequestExecutionView {
316    inner: Arc<LocalView>,
317}
318
319/// Fixed diagnostic facts, with no root reference or resource authority.
320/// Capturing this value cannot extend an account's lifetime.
321#[derive(Clone, Copy, Serialize)]
322pub struct RequestContextSnapshot {
323    schema_version: u8,
324    local_request: u64,
325    publication: u8,
326    application: ContextFact<ContextLabel>,
327    call_application: ContextFact<ContextLabel>,
328    module: ContextFact<ContextLabel>,
329    service: ContextFact<ContextLabel>,
330    operation: ContextFact<ContextLabel>,
331    trace_id: ContextFact<ContextIdentity>,
332    request: ContextFact<ContextIdentity>,
333    span_id: ContextFact<SpanProjection>,
334    route: ContextFact<ContextLabel>,
335    attempt: ContextFact<u32>,
336    rpc_id: ContextFact<ContextIdentity>,
337    zone: ContextFact<ContextLabel>,
338    db_operation: ContextFact<RegisteredContextOperation>,
339    scope: ContextFact<DbScopeDiagnosticIdentity>,
340    task: ContextFact<u64>,
341    lifecycle: ContextFact<RequestViewPhase>,
342    target: ContextFact<ContextLabel>,
343}
344/// A borrowed source-record projection of the actual outbound call target.
345/// It does not change the request identity or retain the target.
346pub struct OutboundSourceContext<'a> {
347    view: &'a RequestExecutionView,
348    business_unit: &'a str,
349}
350impl Clone for RequestExecutionView {
351    fn clone(&self) -> Self {
352        #[cfg(test)]
353        self.inner.observation.record("share");
354        Self {
355            inner: Arc::clone(&self.inner),
356        }
357    }
358}
359impl Drop for RequestExecutionView {
360    fn drop(&mut self) {
361        #[cfg(test)]
362        self.inner.observation.record("release");
363    }
364}
365impl RequestRootRef {
366    pub fn view(&self, facts: RequestLocalFacts) -> RequestExecutionView {
367        RequestExecutionView::allocate(
368            Arc::clone(&self.root),
369            self.root.identity.get().is_some(),
370            facts,
371            None,
372        )
373    }
374    pub fn same_request(&self, view: &RequestExecutionView) -> bool {
375        Arc::ptr_eq(&self.root, &view.inner.root)
376    }
377}
378impl RequestExecutionView {
379    pub fn diagnostic_snapshot(&self) -> RequestContextSnapshot {
380        let identity = self.identity();
381        let child = self.inner.child.as_deref();
382        let initial = self.inner.root.initial;
383        macro_rules! root {
384            ($field:ident) => { identity.map_or_else(|| absent(initial), |g| ContextFact::Present(g.$field)) };
385        }
386        macro_rules! call {
387            ($field:ident) => { child.map(|c| c.$field).or_else(|| identity.map(|g| g.$field))
388                .map_or_else(|| absent(initial), ContextFact::Present) };
389        }
390        RequestContextSnapshot {
391            schema_version: 2,
392            local_request: self.inner.root.local,
393            publication: u8::from(self.inner.published),
394            application: ContextFact::Present(self.inner.root.application),
395            call_application: ContextFact::Present(child.map_or(self.inner.root.application, |c| c.application)),
396            module: call!(module), service: call!(service), operation: call!(operation),
397            trace_id: root!(trace), request: root!(request),
398            span_id: child.map(|c| c.span).or_else(|| identity.map(|g| g.span))
399                .map_or_else(|| absent(initial), |span| ContextFact::Present(SpanProjection(span))),
400            route: call!(route), attempt: call!(attempt),
401            rpc_id: child.map(|c| ContextFact::Present(c.rpc)).or_else(|| identity.map(|g| g.rpc))
402                .unwrap_or_else(|| absent(initial)),
403            zone: identity.map_or_else(|| absent(initial), |g| g.zone),
404            db_operation: self.inner.facts.db_operation, scope: self.inner.facts.scope,
405            task: self.inner.facts.task, lifecycle: ContextFact::Present(self.inner.facts.phase),
406            target: child.map_or(ContextFact::NotApplicable, |c| ContextFact::Present(c.route)),
407        }
408    }
409    pub fn outbound_source_context<'a>(&'a self, business_unit: &'a str) -> OutboundSourceContext<'a> {
410        OutboundSourceContext { view: self, business_unit }
411    }
412    fn allocate(
413        root: Arc<RequestRoot>,
414        published: bool,
415        facts: RequestLocalFacts,
416        child: Option<Arc<ChildCall>>,
417    ) -> Self {
418        observe(root.local, "view_create");
419        Self {
420            inner: Arc::new(LocalView {
421                #[cfg(test)]
422                observation: LocalObservation::create(root.local, "view"),
423                root,
424                published,
425                facts,
426                child,
427            }),
428        }
429    }
430    /// New operation, no root-text copy or mutation of an earlier view.
431    fn local(&self, facts: RequestLocalFacts) -> Self {
432        Self::allocate(
433            Arc::clone(&self.inner.root),
434            self.inner.published,
435            facts,
436            self.inner.child.clone(),
437        )
438    }
439    pub fn with_phase(&self, phase: RequestViewPhase) -> Self {
440        let mut facts = self.inner.facts;
441        facts.phase = phase;
442        self.local(facts)
443    }
444    pub fn with_db_operation(&self, operation: RegisteredContextOperation) -> Self {
445        self.local(self.inner.facts.with_db_operation(operation))
446    }
447    /// Derive the observation for the actual task selected by the runtime. The
448    /// numeric task is metadata, never a task permit or a sequence minted here.
449    pub fn in_task(&self, task: u64) -> Self {
450        self.local(self.inner.facts.with_task(ContextFact::Present(task)))
451    }
452    pub fn in_db_scope(
453        &self,
454        scope: &crate::DbScopeDiagnosticContext<Self>,
455    ) -> Result<Self, ContextConflict> {
456        let (original, identity) = scope.diagnostic_context();
457        if !self.same_request(original) {
458            return Err(ContextConflict::ForeignRoot);
459        }
460        let mut facts = self.inner.facts;
461        facts.scope = ContextFact::Present(identity);
462        Ok(self.local(facts))
463    }
464    pub fn observed_db_scope(
465        &self,
466        scope: &crate::DbScopeObservation<Self>,
467    ) -> Result<Self, ContextConflict> {
468        let (original, identity) = scope.diagnostic_context();
469        if !self.same_request(original) {
470            return Err(ContextConflict::ForeignRoot);
471        }
472        let mut facts = self.inner.facts;
473        facts.scope = ContextFact::Present(identity);
474        Ok(self.local(facts))
475    }
476    pub fn same_request(&self, other: &Self) -> bool {
477        Arc::ptr_eq(&self.inner.root, &other.inner.root)
478    }
479    pub fn same_view(&self, other: &Self) -> bool {
480        Arc::ptr_eq(&self.inner, &other.inner)
481    }
482    /// Only the existing entry root can authorize a later visibility stage.
483    pub fn refresh(&self, root: &RequestRootRef) -> Result<Self, ContextConflict> {
484        if !root.same_request(self) {
485            return Err(ContextConflict::ForeignRoot);
486        }
487        Ok(Self::allocate(
488            Arc::clone(&self.inner.root),
489            self.inner.root.identity.get().is_some(),
490            self.inner.facts,
491            self.inner.child.clone(),
492        ))
493    }
494    pub fn child(
495        &self,
496        call: &CallContext,
497        request: &str,
498        route: &str,
499        attempt: u32,
500    ) -> Result<Self, ContextConflict> {
501        let identity = self.identity().ok_or(ContextConflict::ChildRelation)?;
502        if identity.trace.as_str() != call.trace_correlation_id().as_str()
503            || identity.request.as_str() != request
504        {
505            return Err(ContextConflict::ForeignRoot);
506        }
507        let parent_rpc = if let Some(child) = &self.inner.child {
508            &child.rpc
509        } else if let ContextFact::Present(rpc) = &identity.rpc {
510            rpc
511        } else {
512            return Err(ContextConflict::ChildRelation);
513        };
514        let rpc = call
515            .rpc_correlation_id()
516            .ok_or(ContextConflict::ChildRelation)?;
517        let suffix = rpc
518            .as_str()
519            .strip_prefix(parent_rpc.as_str())
520            .and_then(|s| s.strip_prefix('.'))
521            .ok_or(ContextConflict::ChildRelation)?;
522        let span = self
523            .inner
524            .child
525            .as_ref()
526            .map_or(identity.span, |child| child.span);
527        if suffix.is_empty()
528            || !suffix.bytes().all(|b| b.is_ascii_digit())
529            || span == call.span_id().as_u64()
530            || attempt == 0
531        {
532            return Err(ContextConflict::ChildRelation);
533        }
534        let child = Arc::new(ChildCall {
535            application: ContextLabel::checked(call.application().as_str())?,
536            module: ContextLabel::checked(call.module().as_str())?,
537            service: ContextLabel::checked(call.service().as_str())?,
538            operation: ContextLabel::checked(call.operation().as_str())?,
539            rpc: ContextIdentity::checked(rpc.as_str())?,
540            span: call.span_id().as_u64(),
541            route: ContextLabel::checked(route)?,
542            attempt,
543            #[cfg(test)]
544            observation: LocalObservation::create(self.inner.root.local, "child"),
545        });
546        Ok(Self::allocate(
547            Arc::clone(&self.inner.root),
548            self.inner.published,
549            self.inner.facts,
550            Some(child),
551        ))
552    }
553    fn identity(&self) -> Option<&RequestIdentityGroup> {
554        self.inner
555            .published
556            .then(|| self.inner.root.identity.get())
557            .flatten()
558    }
559}
560
561fn absent<T>(state: ContextFact<()>) -> ContextFact<T> {
562    match state {
563        ContextFact::NotApplicable => ContextFact::NotApplicable,
564        ContextFact::NotEstablished => ContextFact::NotEstablished,
565        _ => ContextFact::Unavailable,
566    }
567}
568impl Serialize for RequestExecutionView {
569    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
570        self.serialize_context(s, None)
571    }
572}
573impl Serialize for OutboundSourceContext<'_> {
574    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
575        self.view.serialize_context(s, Some(self.business_unit))
576    }
577}
578impl RequestExecutionView {
579    fn serialize_context<S: Serializer>(&self, s: S, business_unit: Option<&str>) -> Result<S::Ok, S::Error> {
580        let mut out = s.serialize_struct("RequestContext", 21 + usize::from(business_unit.is_some()))?;
581        let identity = self.identity();
582        let child = self.inner.child.as_deref();
583        let initial = self.inner.root.initial;
584        out.serialize_field("schema_version", &2u8)?;
585        out.serialize_field("local_request", &self.inner.root.local)?;
586        out.serialize_field("publication", &u8::from(self.inner.published))?;
587        out.serialize_field(
588            "application",
589            &ContextFact::Present(&self.inner.root.application),
590        )?;
591        out.serialize_field(
592            "call_application",
593            &ContextFact::Present(child.map_or(&self.inner.root.application, |c| &c.application)),
594        )?;
595        macro_rules! root_field {
596            ($name:literal, $field:ident) => {
597                out.serialize_field(
598                    $name,
599                    &identity.map_or_else(|| absent(initial), |g| ContextFact::Present(&g.$field)),
600                )?;
601            };
602        }
603        macro_rules! call_field {
604            ($name:literal, $field:ident) => {
605                out.serialize_field(
606                    $name,
607                    &child
608                        .map(|c| &c.$field)
609                        .or_else(|| identity.map(|g| &g.$field))
610                        .map_or_else(|| absent(initial), ContextFact::Present),
611                )?;
612            };
613        }
614        call_field!("module", module);
615        call_field!("service", service);
616        call_field!("operation", operation);
617        root_field!("trace_id", trace);
618        root_field!("request", request);
619        let span = child.map(|c| c.span).or_else(|| identity.map(|g| g.span));
620        out.serialize_field(
621            "span_id",
622            &span.map_or_else(
623                || absent(initial),
624                |s| ContextFact::Present(SpanProjection(s)),
625            ),
626        )?;
627        call_field!("route", route);
628        call_field!("attempt", attempt);
629        let rpc = child
630            .map(|c| ContextFact::Present(c.rpc))
631            .or_else(|| identity.map(|g| g.rpc))
632            .unwrap_or_else(|| absent(initial));
633        out.serialize_field("rpc_id", &rpc)?;
634        out.serialize_field(
635            "zone",
636            &identity.map_or_else(|| absent(initial), |g| g.zone),
637        )?;
638        out.serialize_field("db_operation", &self.inner.facts.db_operation)?;
639        out.serialize_field("scope", &self.inner.facts.scope)?;
640        out.serialize_field("task", &self.inner.facts.task)?;
641        out.serialize_field("lifecycle", &ContextFact::Present(self.inner.facts.phase))?;
642        // Target is the registered route, never endpoint/URL or credentials.
643        out.serialize_field(
644            "target",
645            &child.map_or(ContextFact::NotApplicable, |c| {
646                ContextFact::Present(&c.route)
647            }),
648        )?;
649        if let Some(business_unit) = business_unit {
650            out.serialize_field("business_unit", &ContextFact::Present(business_unit))?;
651        }
652        out.end()
653    }
654}
655
656/// Payload and shared allocation layouts, not a reservation or full task charge.
657/// Arc control block calculation follows std's two-AtomicUsize header, with
658/// Layout::extend padding. Allocator metadata is NOT included; R0 must account it.
659pub fn request_context_layouts() -> [(std::alloc::Layout, std::alloc::Layout); 3] {
660    fn pair<T>() -> (std::alloc::Layout, std::alloc::Layout) {
661        let payload = std::alloc::Layout::new::<T>();
662        let header = std::alloc::Layout::new::<[std::sync::atomic::AtomicUsize; 2]>();
663        (
664            payload,
665            header
666                .extend(payload)
667                .expect("fixed layout")
668                .0
669                .pad_to_align(),
670        )
671    }
672    [
673        pair::<RequestRoot>(),
674        pair::<LocalView>(),
675        pair::<ChildCall>(),
676    ]
677}
678
679#[cfg(not(test))]
680fn observe(_: u64, _: &'static str) {}
681#[cfg(test)]
682fn observe(root: u64, event: &'static str) {
683    EVENTS.lock().unwrap().push((root, event));
684    record_observation(root, root, "root", event);
685}
686#[cfg(test)]
687static EVENTS: std::sync::Mutex<Vec<(u64, &'static str)>> = std::sync::Mutex::new(Vec::new());
688
689#[derive(Clone, Copy)]
690struct SpanProjection(u64);
691impl Serialize for SpanProjection {
692    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
693        let mut bytes = [b'0'; 16];
694        for (i, byte) in bytes.iter_mut().enumerate() {
695            *byte = b"0123456789abcdef"[((self.0 >> ((15 - i) * 4)) & 15) as usize];
696        }
697        serializer.serialize_str(std::str::from_utf8(&bytes).expect("hex"))
698    }
699}
700
701// Private observation holds numbers only, never Arc/Weak, callbacks or objects.
702// Instrumentation storage is test-only and excluded from production layouts.
703#[cfg(test)]
704struct LocalObservation {
705    id: u64,
706    root: u64,
707    kind: &'static str,
708}
709#[cfg(test)]
710static LOCAL_EVENTS: std::sync::Mutex<Vec<(u64, u64, &'static str, &'static str)>> =
711    std::sync::Mutex::new(Vec::new());
712#[cfg(test)]
713impl LocalObservation {
714    fn create(root: u64, kind: &'static str) -> Self {
715        static NEXT: AtomicU64 = AtomicU64::new(1);
716        let observation = Self {
717            id: NEXT.fetch_add(1, Ordering::Relaxed),
718            root,
719            kind,
720        };
721        observation.record("create");
722        observation
723    }
724    fn record(&self, event: &'static str) {
725        LOCAL_EVENTS
726            .lock()
727            .unwrap()
728            .push((self.id, self.root, self.kind, event));
729        record_observation(self.root, self.id, self.kind, event);
730    }
731}
732
733#[cfg(test)]
734type ObservationEvent = (u64, u64, u64, &'static str, &'static str);
735#[cfg(test)]
736static ORDERED_EVENTS: std::sync::Mutex<Vec<ObservationEvent>> = std::sync::Mutex::new(Vec::new());
737#[cfg(test)]
738fn record_observation(root: u64, object: u64, kind: &'static str, event: &'static str) {
739    static SEQUENCE: AtomicU64 = AtomicU64::new(1);
740    let sequence = SEQUENCE.fetch_add(1, Ordering::Relaxed);
741    ORDERED_EVENTS
742        .lock()
743        .unwrap()
744        .push((sequence, root, object, kind, event));
745}
746
747#[cfg(test)]
748mod tests;