zeroclaw_log/macro.rs
1//! `record!` — the sole logging surface for the workspace.
2//!
3//! Call shape (compile-time-locked via the `Event` struct):
4//!
5//! ```ignore
6//! use zeroclaw_log::{record, Event, Action, EventOutcome};
7//!
8//! record!(INFO, Event::new(module_path!(), Action::Start), "starting step");
9//! record!(WARN, Event::new(module_path!(), Action::Fail).with_outcome(EventOutcome::Failure).with_attrs(serde_json::json!({"err": "timeout"})), "tool failed");
10//! ```
11//!
12//! Alias-bound attribution (channel, agent_alias, model_provider,
13//! tool, cron_job_id, …) is NOT a call-site argument — it is assembled
14//! automatically by the `LogCaptureLayer` from `attribution_span`s
15//! opened by entry points (channel listeners, the agent loop, cron
16//! tick handlers, the tool executor wrapper). To attach attribution
17//! to a region of work, wrap the work with [`crate::attribution_span`].
18
19/// Emit a structured ZeroClaw log event. The single positional `Event`
20/// expression carries the typed payload; the trailing literal is the
21/// human-readable message.
22#[macro_export]
23macro_rules! record {
24 ($level:ident, $event:expr, $msg:expr $(,)?) => {{
25 let __zc_event: $crate::Event = $event;
26 $crate::__private::tracing::event!(
27 target: "zeroclaw_log_event",
28 $crate::__private::tracing::Level::$level,
29 zc_name = %__zc_event.name,
30 zc_action = %__zc_event.action.as_str(),
31 zc_outcome = %__zc_event.outcome_str(),
32 zc_category = %__zc_event.category_str(),
33 zc_attrs = %__zc_event.attrs_str(),
34 zc_has_duration = %__zc_event.has_duration(),
35 zc_duration_ms = %__zc_event.duration_ms_or_zero(),
36 zc_file = %file!(),
37 zc_line = %line!(),
38 message = %$msg,
39 );
40 }};
41}
42
43/// Open an attribution span for the given `Attributable` thing. Every
44/// `record!` emitted while the returned span is entered inherits the
45/// thing's role + alias as alias-bound attribution on the resulting
46/// LogEvent. Wrap entry-point work with `.instrument(span)` (async) or
47/// `let _g = span.entered()` (sync).
48#[macro_export]
49macro_rules! attribution_span {
50 ($attributable:expr) => {{
51 let __zc_thing = $attributable;
52 let __zc_role = ::zeroclaw_api::attribution::Attributable::role(__zc_thing);
53 let __zc_alias = ::zeroclaw_api::attribution::Attributable::alias(__zc_thing);
54 $crate::__private::tracing::info_span!(
55 target: "zeroclaw_log_internal_attribution",
56 "zeroclaw_attribution",
57 zc_role_family = %__zc_role.family_str(),
58 zc_role_type = %__zc_role.composite_type().unwrap_or(""),
59 zc_attribution_field = %__zc_role.attribution_field().unwrap_or(""),
60 zc_composite_prefix = %__zc_role.composite_prefix().unwrap_or(""),
61 zc_default_category = %__zc_role.default_category(),
62 zc_alias = %__zc_alias,
63 )
64 }};
65}
66
67/// Open a free-form context span carrying ad-hoc fields (sender id,
68/// message id, turn id, etc.) for every `record!` inside its scope.
69/// Use sparingly — prefer `attribution_span!(thing)` for role-bearing
70/// attribution. This is for transient per-scope identifiers that
71/// aren't tied to an `Attributable`.
72///
73/// Field keys recognized by the layer: `agent_alias`, `tool`,
74/// `session_key`, `cron_job_id`, `risk_profile`, `runtime_profile`,
75/// `memory_namespace`, `skill_bundle`, `knowledge_bundle`, `mcp_bundle`,
76/// `peer_group`, `sop_name`, `model`, `embedding_provider`, `channel`,
77/// `model_provider`, `tts_provider`, `transcription_provider`,
78/// `tunnel_provider`. Anything else lands in event `attributes`.
79#[macro_export]
80macro_rules! scope {
81 ($($key:ident : $value:expr),+ $(,)? => $body:expr) => {{
82 use $crate::__private::tracing::Instrument;
83 ($body).instrument($crate::__private::tracing::info_span!(
84 target: "zeroclaw_log_internal_scope",
85 "zeroclaw_scope",
86 $($key = %($value)),+
87 ))
88 }};
89}
90
91/// `tokio::spawn` that propagates the caller's current span(s) into
92/// the spawned task. Use everywhere a per-message child task needs the
93/// parent's attribution.
94#[macro_export]
95macro_rules! spawn {
96 ($body:expr) => {{
97 #[allow(unused_imports)]
98 use $crate::__private::tracing::Instrument as _;
99 ::tokio::spawn(($body).in_current_span())
100 }};
101}
102
103#[cfg(test)]
104mod tests {
105 use crate::{Action, Event, EventOutcome};
106 use serde_json::json;
107
108 #[test]
109 fn record_compiles_minimal() {
110 record!(INFO, Event::new(module_path!(), Action::Note), "hello");
111 }
112
113 #[test]
114 fn record_compiles_with_attrs_and_outcome() {
115 record!(
116 WARN,
117 Event::new(module_path!(), Action::Fail)
118 .with_outcome(EventOutcome::Failure)
119 .with_attrs(json!({"code": 42})),
120 "failed"
121 );
122 }
123
124 #[test]
125 fn record_compiles_with_duration() {
126 record!(
127 INFO,
128 Event::new(module_path!(), Action::Complete)
129 .with_outcome(EventOutcome::Success)
130 .with_duration(123),
131 "done"
132 );
133 }
134}