Skip to main content

obs_macros/
lib.rs

1#![forbid(unsafe_code)]
2#![warn(rust_2024_compatibility, missing_docs, missing_debug_implementations)]
3// Proc-macro crate: runs at build time only, so the runtime hot-path
4// lints are not relevant here.
5#![allow(
6    clippy::expect_used,
7    clippy::unwrap_used,
8    clippy::indexing_slicing,
9    clippy::panic
10)]
11
12//! Procedural macros for the obs SDK.
13//!
14//! Phase-1 surface (impl-plan task 1.9):
15//!
16//! - [`Event`] (`#[derive(Event)]`) — emits `EventSchema` impl, `EventSchemaErased` impl,
17//!   `linkme::distributed_slice` registration, typed builder, and the const-eval lint block
18//!   (L001/L002/L003/L011).
19//! - [`emit`] — terse `obs::emit!(MyEvent { … })` shorthand.
20//! - [`scope`] — placeholder (full impl in Phase 3 task 3.3).
21//!
22//! See spec 12 § 1.2 (Rust-first authoring) and spec 13 § 2 (`obs::scope!`).
23
24use proc_macro::TokenStream;
25
26mod derive_event;
27mod emit_macro;
28mod forensic_macro;
29mod include_schemas;
30mod instrument_attr;
31mod scope_macro;
32mod test_attr;
33
34/// Derive macro for the Rust-first authoring path.
35///
36/// Container attributes:
37///
38/// - `#[event(tier = "log" | "metric" | "trace" | "audit")]`
39/// - `#[event(default_sev = "trace" | "debug" | "info" | "warn" | "error" | "fatal")]`
40/// - `#[event(full_name = "myapp.v1.ObsXxx")]` (defaults to `<crate>.v1.<TypeName>` derived from
41///   `module_path!`).
42///
43/// Field attributes:
44///
45/// - `#[obs(label, cardinality = "low" | "medium" | "high" | "unbounded")]`
46/// - `#[obs(attribute, classification = "internal" | "pii" | "secret")]`
47/// - `#[obs(measurement)]`
48/// - `#[obs(trace_id)]`, `#[obs(span_id)]`, `#[obs(parent_span_id)]`
49/// - `#[obs(forensic)]`
50///
51/// Lints (compile-time `const _: () = { assert!(...) }` blocks):
52///
53/// - **L001** — every `LABEL` field must declare a `Low` or `Medium` cardinality.
54/// - **L002** — `PII`-classified fields must not be `LABEL`.
55/// - **L003** — `SECRET`-classified fields must not exist on `LOG` or `AUDIT` tier events.
56/// - **L011** — the type name must start with the workspace event prefix (default `Obs`).
57///
58/// See spec 12 § 3.4.
59#[proc_macro_derive(Event, attributes(event, obs))]
60pub fn derive_event(item: TokenStream) -> TokenStream {
61    derive_event::expand(item.into())
62        .unwrap_or_else(syn::Error::into_compile_error)
63        .into()
64}
65
66/// Function-like emit macro: `obs::emit!(MyEvent { field: value })`
67/// or `obs::emit!(WARN, MyEvent { field: value })` to escalate.
68///
69/// Spec 13 § 1.
70#[proc_macro]
71pub fn emit(item: TokenStream) -> TokenStream {
72    emit_macro::expand(item.into())
73        .unwrap_or_else(syn::Error::into_compile_error)
74        .into()
75}
76
77/// `obs::include_schemas!("myapp.v1")` — wire up every file
78/// `obs-build` emits into the user's crate. Expands to four
79/// `include!` calls under `OUT_DIR/obs/`. Spec 12 § 3.1.
80#[proc_macro]
81pub fn include_schemas(item: TokenStream) -> TokenStream {
82    include_schemas::expand(item.into())
83        .unwrap_or_else(syn::Error::into_compile_error)
84        .into()
85}
86
87/// `obs::scope!(name = value, ...)` — push an `obs::scope!` frame
88/// onto the active task's scope stack. Returns a `ScopeGuard` that
89/// pops the frame on drop and flushes the tail-on-error buffer when
90/// `>= ERROR` was observed inside.
91///
92/// Spec 13 § 2.
93#[proc_macro]
94pub fn scope(item: TokenStream) -> TokenStream {
95    scope_macro::expand_scope(item.into())
96        .unwrap_or_else(syn::Error::into_compile_error)
97        .into()
98}
99
100/// `obs::context!(name = value, ...)` — like `obs::scope!` but without
101/// the per-scope tail buffer. Spec 13 § 2.2.
102#[proc_macro]
103pub fn context(item: TokenStream) -> TokenStream {
104    scope_macro::expand_context(item.into())
105        .unwrap_or_else(syn::Error::into_compile_error)
106        .into()
107}
108
109/// `obs::forensic!(site = "...", message = "...", { "k" => v, ... })`
110/// — emergency escape hatch. Always emits, regardless of sampling.
111/// Spec 13 § 8.
112#[proc_macro]
113pub fn forensic(item: TokenStream) -> TokenStream {
114    forensic_macro::expand(item.into())
115        .unwrap_or_else(syn::Error::into_compile_error)
116        .into()
117}
118
119/// `#[obs::instrument]` — wraps a function body in an `obs::scope!`
120/// and emits one `ObsFnExecuted` event on exit (default) or two
121/// (`ObsFnEntered` + `ObsFnExecuted`) when `enter = true`.
122///
123/// Spec 13 § 5.
124#[proc_macro_attribute]
125pub fn instrument(attr: TokenStream, item: TokenStream) -> TokenStream {
126    instrument_attr::expand(attr.into(), item.into())
127        .unwrap_or_else(syn::Error::into_compile_error)
128        .into()
129}
130
131/// `#[obs::test]` — drop-in replacement for `#[test]` /
132/// `#[tokio::test]` that installs an `InMemoryObserver` on the
133/// current thread (sync) or current task (async) for the duration of
134/// the test. The body's emits land in a thread-local /
135/// task-local handle that `obs::test::assert_emitted!` reads.
136///
137/// Sync example:
138///
139/// ```ignore
140/// #[obs::test]
141/// fn login_emits_event() -> anyhow::Result<()> {
142///     login("alice")?;
143///     obs::test::assert_emitted!(ObsLoggedIn { user: "alice", .. });
144///     Ok(())
145/// }
146/// ```
147///
148/// Async example:
149///
150/// ```ignore
151/// #[obs::test]
152/// async fn billing_emits_charge_event() -> anyhow::Result<()> {
153///     charge_card("4242…").await?;
154///     obs::test::assert_emitted!(ObsChargeAttempted { outcome: "approved", .. });
155///     Ok(())
156/// }
157/// ```
158///
159/// Spec 60 § 8 + spec 72 § 3.
160#[proc_macro_attribute]
161pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
162    test_attr::expand(attr.into(), item.into())
163        .unwrap_or_else(syn::Error::into_compile_error)
164        .into()
165}