confium_coordinator/coordinator/
otlp.rs1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct OtlpSpan {
12 pub trace_id: String,
13 pub span_id: String,
14 pub parent_span_id: Option<String>,
15 pub name: String,
16 pub start_time: DateTime<Utc>,
17 pub end_time: DateTime<Utc>,
18 pub attributes: Vec<OtlpAttribute>,
19 pub status: OtlpStatus,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct OtlpAttribute {
25 pub key: String,
26 pub value: String,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub struct OtlpStatus {
33 pub code: String,
34 pub message: Option<String>,
35}
36
37impl OtlpStatus {
38 pub fn ok() -> Self {
39 Self {
40 code: "ok".into(),
41 message: None,
42 }
43 }
44 pub fn error(msg: &str) -> Self {
45 Self {
46 code: "error".into(),
47 message: Some(msg.into()),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct SpanBatch {
55 pub resource_spans: Vec<ResourceSpans>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ResourceSpans {
61 pub resource: ResourceAttributes,
62 pub scope_spans: Vec<ScopeSpans>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ResourceAttributes {
68 pub attributes: Vec<OtlpAttribute>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ScopeSpans {
74 pub scope: ScopeInfo,
75 pub spans: Vec<OtlpSpan>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct ScopeInfo {
81 pub name: String,
82 pub version: String,
83}
84
85pub fn build_span(
87 trace_id: &str,
88 span_id: &str,
89 name: &str,
90 start: DateTime<Utc>,
91 end: DateTime<Utc>,
92 attributes: Vec<(&str, &str)>,
93) -> OtlpSpan {
94 OtlpSpan {
95 trace_id: trace_id.into(),
96 span_id: span_id.into(),
97 parent_span_id: None,
98 name: name.into(),
99 start_time: start,
100 end_time: end,
101 attributes: attributes
102 .into_iter()
103 .map(|(k, v)| OtlpAttribute {
104 key: k.into(),
105 value: v.into(),
106 })
107 .collect(),
108 status: OtlpStatus::ok(),
109 }
110}
111
112pub fn build_export_batch(
114 service_name: &str,
115 service_version: &str,
116 spans: Vec<OtlpSpan>,
117) -> SpanBatch {
118 SpanBatch {
119 resource_spans: vec![ResourceSpans {
120 resource: ResourceAttributes {
121 attributes: vec![
122 OtlpAttribute {
123 key: "service.name".into(),
124 value: service_name.into(),
125 },
126 OtlpAttribute {
127 key: "service.version".into(),
128 value: service_version.into(),
129 },
130 ],
131 },
132 scope_spans: vec![ScopeSpans {
133 scope: ScopeInfo {
134 name: "confium-tc".into(),
135 version: service_version.into(),
136 },
137 spans,
138 }],
139 }],
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn span_has_trace_id() {
149 let span = build_span("trace-1", "span-1", "test", Utc::now(), Utc::now(), vec![]);
150 assert_eq!(span.trace_id, "trace-1");
151 assert_eq!(span.span_id, "span-1");
152 }
153
154 #[test]
155 fn span_attributes_preserved() {
156 let span = build_span(
157 "t",
158 "s",
159 "n",
160 Utc::now(),
161 Utc::now(),
162 vec![("key1", "val1"), ("key2", "val2")],
163 );
164 assert_eq!(span.attributes.len(), 2);
165 assert_eq!(span.attributes[0].key, "key1");
166 }
167
168 #[test]
169 fn status_ok_has_no_message() {
170 let status = OtlpStatus::ok();
171 assert_eq!(status.code, "ok");
172 assert!(status.message.is_none());
173 }
174
175 #[test]
176 fn status_error_has_message() {
177 let status = OtlpStatus::error("failed");
178 assert_eq!(status.code, "error");
179 assert_eq!(status.message.as_deref(), Some("failed"));
180 }
181
182 #[test]
183 fn export_batch_serializes() {
184 let span = build_span("t", "s", "n", Utc::now(), Utc::now(), vec![]);
185 let batch = build_export_batch("confium-coord", "0.3.0", vec![span]);
186 let json = serde_json::to_string(&batch).unwrap();
187 assert!(json.contains("confium-coord"));
188 assert!(json.contains("resource_spans"));
189 }
190
191 #[test]
192 fn export_batch_has_resource_attributes() {
193 let batch = build_export_batch("svc", "1.0", vec![]);
194 assert_eq!(batch.resource_spans.len(), 1);
195 let attrs = &batch.resource_spans[0].resource.attributes;
196 assert!(
197 attrs
198 .iter()
199 .any(|a| a.key == "service.name" && a.value == "svc")
200 );
201 }
202
203 #[test]
204 fn span_round_trips_json() {
205 let span = build_span(
206 "t1",
207 "s1",
208 "operation",
209 Utc::now(),
210 Utc::now(),
211 vec![("a", "b")],
212 );
213 let json = serde_json::to_string(&span).unwrap();
214 let recovered: OtlpSpan = serde_json::from_str(&json).unwrap();
215 assert_eq!(recovered.trace_id, "t1");
216 assert_eq!(recovered.attributes.len(), 1);
217 }
218}