1use std::sync::atomic::{AtomicU64, Ordering};
9use std::time::{SystemTime, UNIX_EPOCH};
10
11use crate::error::Error;
12use crate::event::{EventKind, ObservabilityEvent, SCHEMA_VERSION};
13
14pub const EVENT_TARGET: &str = "rig_tap";
16
17static TICK: AtomicU64 = AtomicU64::new(0);
18
19pub fn next_tick() -> u64 {
21 TICK.fetch_add(1, Ordering::Relaxed)
22}
23
24pub fn now_millis() -> u64 {
27 SystemTime::now()
28 .duration_since(UNIX_EPOCH)
29 .map(|d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
30 .unwrap_or(0)
31}
32
33pub fn current_span_id() -> Option<u64> {
41 tracing::Span::current().id().map(|id| id.into_u64())
42}
43
44pub fn build_event(conversation_id: impl Into<String>, kind: EventKind) -> ObservabilityEvent {
47 ObservabilityEvent {
48 version: SCHEMA_VERSION,
49 occurred_at_millis: now_millis(),
50 tick: next_tick(),
51 conversation_id: conversation_id.into(),
52 span_id: current_span_id(),
53 kind,
54 }
55}
56
57pub fn try_emit(event: &ObservabilityEvent) -> Result<(), Error> {
64 let json = serde_json::to_string(event)?;
65 let fields = event.kind.scalar_fields();
66 tracing::info!(
67 target: EVENT_TARGET,
68 event = %json,
69 rig_tap.version = event.version,
70 rig_tap.kind = event.kind.discriminant(),
71 rig_tap.conversation_id = %event.conversation_id,
72 rig_tap.tick = event.tick,
73 rig_tap.occurred_at_millis = event.occurred_at_millis,
74 rig_tap.span_id = event.span_id.unwrap_or(0),
80 rig_tap.kernel_id = fields.kernel_id,
84 rig_tap.tool_name = fields.tool_name,
85 rig_tap.call_id = fields.call_id,
86 rig_tap.skill_id = fields.skill_id,
87 rig_tap.model = fields.model,
88 rig_tap.response_id = fields.response_id,
89 rig_tap.previous_response_id = fields.previous_response_id,
90 rig_tap.dataset = fields.dataset,
91 rig_tap.metric = fields.metric,
92 rig_tap.verdict = fields.verdict,
93 rig_tap.error_class = fields.error_class,
94 );
95 Ok(())
96}
97
98pub fn emit(event: &ObservabilityEvent) {
102 if let Err(err) = try_emit(event) {
103 tracing::warn!(
104 target: EVENT_TARGET,
105 error = %err,
106 error_kind = err.kind(),
107 "rig-tap: failed to emit event",
108 );
109 }
110}
111
112pub fn emit_kind(conversation_id: impl Into<String>, kind: EventKind) {
114 let event = build_event(conversation_id, kind);
115 emit(&event);
116}
117
118#[cfg(test)]
119#[allow(
120 clippy::unwrap_used,
121 clippy::panic,
122 clippy::indexing_slicing,
123 clippy::expect_used
124)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn tick_is_monotonic() {
130 let a = next_tick();
131 let b = next_tick();
132 assert!(b > a);
133 }
134
135 #[test]
136 fn build_event_stamps_envelope() {
137 let evt = build_event(
138 "c",
139 EventKind::PromptStarted {
140 model: "m".into(),
141 messages_in: 0,
142 },
143 );
144 assert_eq!(evt.version, SCHEMA_VERSION);
145 assert_eq!(evt.conversation_id, "c");
146 assert!(evt.occurred_at_millis > 0);
149 }
150}