harn_vm/observability/request_id.rs
1//! Ambient request_id scope threaded into `.harn` callees by hosts that
2//! mint a request id at ingress (today: `harn-serve`'s HTTP/ACP/MCP/A2A
3//! adapters via `http_codec::fresh_request_id`; future: in-process
4//! callers that already track one).
5//!
6//! Exposed to scripts as the `harness.obs.request_id()` method:
7//!
8//! ```harn
9//! let id = harness.obs.request_id()
10//! ```
11//!
12//! Like [`crate::harness_tenant`], the scope is a stack so nested
13//! dispatches restore the outer id on return. The host pushes once per
14//! incoming request; the .harn callee sees a stable id for the entire
15//! span tree of its dispatch.
16//!
17//! When no host has pushed a request id, `request_id()` returns `nil` —
18//! callers in tests / standalone `harn run` contexts shouldn't have to
19//! mint one to call obs methods.
20
21use std::cell::RefCell;
22
23thread_local! {
24 static ACTIVE_REQUEST_ID_STACK: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
25}
26
27/// RAII guard returned by [`enter_request_id`]. Popping the stack on
28/// drop keeps the ambient balanced even when the dispatched callable
29/// panics or returns an error.
30#[must_use = "dropping the guard immediately pops the request_id scope"]
31pub struct RequestIdScopeGuard {
32 _private: (),
33}
34
35impl Drop for RequestIdScopeGuard {
36 fn drop(&mut self) {
37 ACTIVE_REQUEST_ID_STACK.with(|stack| {
38 stack.borrow_mut().pop();
39 });
40 }
41}
42
43/// Push `request_id` onto the ambient stack for the lifetime of the
44/// returned guard. The innermost entry wins for [`current_request_id`].
45pub fn enter_request_id(request_id: impl Into<String>) -> RequestIdScopeGuard {
46 ACTIVE_REQUEST_ID_STACK.with(|stack| stack.borrow_mut().push(request_id.into()));
47 RequestIdScopeGuard { _private: () }
48}
49
50/// Currently-active request id, or `None` when the host did not bind
51/// one (e.g. `harn run` against a local script with no ingress).
52pub fn current_request_id() -> Option<String> {
53 ACTIVE_REQUEST_ID_STACK.with(|stack| stack.borrow().last().cloned())
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn current_returns_none_when_nothing_pushed() {
62 assert_eq!(current_request_id(), None);
63 }
64
65 #[test]
66 fn guard_pops_on_drop_and_inner_scope_shadows_outer() {
67 let outer = enter_request_id("req_outer");
68 assert_eq!(current_request_id().as_deref(), Some("req_outer"));
69 {
70 let _inner = enter_request_id("req_inner");
71 assert_eq!(current_request_id().as_deref(), Some("req_inner"));
72 }
73 assert_eq!(current_request_id().as_deref(), Some("req_outer"));
74 drop(outer);
75 assert_eq!(current_request_id(), None);
76 }
77}