1use std::marker::PhantomData;
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value};
5
6use crate::{ActionName, Diagnostic, DurationMs, ServiceName, Timestamp, TraceContext};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum SpanStatus {
11 Ok,
13 Error,
15 Unset,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21pub struct SpanStarted;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25pub struct SpanEnded;
26
27#[derive(Debug, Clone, PartialEq, Serialize)]
29pub struct SpanRecord<S> {
30 timestamp: Timestamp,
31 service: ServiceName,
32 name: ActionName,
33 trace: TraceContext,
34 status: SpanStatus,
35 diagnostic: Option<Diagnostic>,
36 attributes: Map<String, Value>,
37 duration_ms: Option<DurationMs>,
38 marker: PhantomData<S>,
39}
40
41impl SpanRecord<SpanStarted> {
42 #[must_use]
66 pub fn new(
67 timestamp: Timestamp,
68 service: ServiceName,
69 name: ActionName,
70 trace: TraceContext,
71 attributes: Map<String, Value>,
72 ) -> Self {
73 Self {
74 timestamp,
75 service,
76 name,
77 trace,
78 status: SpanStatus::Unset,
79 diagnostic: None,
80 attributes,
81 duration_ms: None,
82 marker: PhantomData,
83 }
84 }
85
86 #[must_use]
88 pub fn with_diagnostic(mut self, diagnostic: Diagnostic) -> Self {
89 self.diagnostic = Some(diagnostic);
90 self
91 }
92
93 #[must_use]
95 pub fn end(self, status: SpanStatus, duration: DurationMs) -> SpanRecord<SpanEnded> {
96 SpanRecord {
97 timestamp: self.timestamp,
98 service: self.service,
99 name: self.name,
100 trace: self.trace,
101 status,
102 diagnostic: self.diagnostic,
103 attributes: self.attributes,
104 duration_ms: Some(duration),
105 marker: PhantomData,
106 }
107 }
108}
109
110impl<S> SpanRecord<S> {
111 #[must_use]
113 pub fn timestamp(&self) -> Timestamp {
114 self.timestamp
115 }
116
117 #[must_use]
119 pub fn service(&self) -> &ServiceName {
120 &self.service
121 }
122
123 #[must_use]
125 pub fn name(&self) -> &ActionName {
126 &self.name
127 }
128
129 #[must_use]
131 pub fn trace(&self) -> &TraceContext {
132 &self.trace
133 }
134
135 #[must_use]
137 pub fn status(&self) -> SpanStatus {
138 self.status
139 }
140
141 #[must_use]
143 pub fn diagnostic(&self) -> Option<&Diagnostic> {
144 self.diagnostic.as_ref()
145 }
146
147 #[must_use]
149 pub fn attributes(&self) -> &Map<String, Value> {
150 &self.attributes
151 }
152}
153
154impl SpanRecord<SpanEnded> {
155 #[must_use]
162 pub fn duration_ms(&self) -> Option<DurationMs> {
163 self.duration_ms
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
169pub struct SpanEvent {
170 pub timestamp: Timestamp,
172 pub trace: TraceContext,
174 pub name: ActionName,
176 pub attributes: Map<String, Value>,
178 pub diagnostic: Option<Diagnostic>,
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize)]
184pub enum SpanSignal {
185 Started(SpanRecord<SpanStarted>),
187 Event(SpanEvent),
189 Ended(SpanRecord<SpanEnded>),
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use serde_json::json;
197
198 use crate::{Remediation, error_codes};
199 use crate::{SpanId, TraceId};
200
201 fn service_name() -> ServiceName {
202 ServiceName::new("sc-observability").expect("valid service name")
203 }
204
205 fn action_name() -> ActionName {
206 ActionName::new("observation.received").expect("valid action name")
207 }
208
209 fn trace_context() -> TraceContext {
210 TraceContext {
211 trace_id: TraceId::new("0123456789abcdef0123456789abcdef").expect("valid trace id"),
212 span_id: SpanId::new("0123456789abcdef").expect("valid span id"),
213 parent_span_id: Some(SpanId::new("fedcba9876543210").expect("valid parent span id")),
214 }
215 }
216
217 fn diagnostic() -> Diagnostic {
218 Diagnostic {
219 timestamp: Timestamp::UNIX_EPOCH,
220 code: error_codes::DIAGNOSTIC_INVALID,
221 message: "diagnostic invalid".to_string(),
222 cause: Some("invalid example".to_string()),
223 remediation: Remediation::recoverable(
224 "fix the input",
225 ["rerun the command", "review the docs"],
226 ),
227 docs: Some("https://example.test/docs".to_string()),
228 details: Map::from_iter([("key".to_string(), json!("value"))]),
229 }
230 }
231
232 #[test]
233 fn span_signal_round_trips_through_serde() {
234 let mut attributes = Map::new();
235 attributes.insert("tool".to_string(), json!("rg"));
236
237 let started = SpanRecord::<SpanStarted>::new(
238 Timestamp::UNIX_EPOCH,
239 service_name(),
240 action_name(),
241 trace_context(),
242 attributes.clone(),
243 )
244 .with_diagnostic(diagnostic());
245
246 let ended = started.clone().end(SpanStatus::Ok, DurationMs::from(123));
247 let signal = SpanSignal::Ended(ended);
248 let encoded = serde_json::to_value(&signal).expect("serialize span signal");
249
250 assert_eq!(encoded["Ended"]["status"], "Ok");
251 assert_eq!(encoded["Ended"]["duration_ms"], 123);
252 }
253
254 #[test]
255 fn span_record_end_transitions_to_span_ended() {
256 let span = SpanRecord::<SpanStarted>::new(
257 Timestamp::UNIX_EPOCH,
258 service_name(),
259 action_name(),
260 trace_context(),
261 Map::new(),
262 );
263
264 let ended = span.end(SpanStatus::Error, DurationMs::from(88));
265
266 assert_eq!(ended.status(), SpanStatus::Error);
267 assert_eq!(ended.duration_ms(), Some(DurationMs::from(88)));
268 assert_eq!(ended.service().as_str(), "sc-observability");
269 }
270
271 #[test]
272 fn span_record_accessors_preserve_started_values() {
273 let mut attributes = Map::new();
274 attributes.insert("count".to_string(), json!(2));
275 let span = SpanRecord::<SpanStarted>::new(
276 Timestamp::UNIX_EPOCH,
277 service_name(),
278 action_name(),
279 trace_context(),
280 attributes.clone(),
281 )
282 .with_diagnostic(diagnostic());
283
284 assert_eq!(span.timestamp(), Timestamp::UNIX_EPOCH);
285 assert_eq!(span.name().as_str(), "observation.received");
286 assert_eq!(
287 span.trace().trace_id.as_str(),
288 "0123456789abcdef0123456789abcdef"
289 );
290 assert_eq!(span.status(), SpanStatus::Unset);
291 assert_eq!(span.diagnostic(), Some(&diagnostic()));
292 assert_eq!(span.attributes(), &attributes);
293 }
294}