1mod span;
23
24use crate::span::v04::Span;
25use crate::span::TraceData;
26use span::LogSpan;
27use std::io::Write;
28
29const TRACE_PREFIX: &[u8] = b"{\"traces\":[[";
31const TRACE_SUFFIX: &[u8] = b"]]}\n";
33const TRACE_FORMAT_OVERHEAD: usize = TRACE_PREFIX.len() + TRACE_SUFFIX.len();
35
36#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
38pub struct EncodeStats {
39 pub spans_written: usize,
41 pub spans_dropped: usize,
43}
44
45pub fn encode_traces<T: TraceData>(
84 traces: &[Vec<Span<T>>],
85 out: &mut impl Write,
86 max_line_size: usize,
87) -> std::io::Result<EncodeStats> {
88 let mut stats = EncodeStats::default();
89
90 let mut span_buf: Vec<u8> = Vec::with_capacity(512);
95 let mut line: Vec<u8> = Vec::with_capacity(max_line_size.min(64 * 1024));
96 line.extend_from_slice(TRACE_PREFIX);
97 let mut line_span_count: usize = 0;
98
99 for trace in traces {
100 for span in trace {
101 span_buf.clear();
102 serde_json::to_writer(&mut span_buf, &LogSpan(span)).map_err(std::io::Error::other)?;
103 let span_len = span_buf.len();
104
105 if span_len + TRACE_FORMAT_OVERHEAD > max_line_size {
108 stats.spans_dropped += 1;
109 tracing::debug!(
110 span_len,
111 max_line_size,
112 "Span too large to send to logs, dropping"
113 );
114 continue;
115 }
116
117 let comma = usize::from(line_span_count > 0);
121 if line_span_count > 0
122 && line.len() + comma + span_len + TRACE_SUFFIX.len() > max_line_size
123 {
124 flush_line(out, &mut line)?;
125 line_span_count = 0;
126 }
127
128 if line_span_count > 0 {
129 line.push(b',');
130 }
131 line.extend_from_slice(&span_buf);
132 line_span_count += 1;
133 stats.spans_written += 1;
134 }
135 }
136
137 if line_span_count > 0 {
138 flush_line(out, &mut line)?;
139 }
140
141 Ok(stats)
142}
143
144fn flush_line(out: &mut impl Write, line: &mut Vec<u8>) -> std::io::Result<()> {
151 line.extend_from_slice(TRACE_SUFFIX);
152 out.write_all(line)?;
153 line.clear();
154 line.extend_from_slice(TRACE_PREFIX);
155 Ok(())
156}
157
158#[cfg(test)]
159#[allow(clippy::useless_conversion)]
164mod tests {
165 use super::*;
166 use crate::span::v04::SpanSlice;
167 use serde_json::Value;
168
169 const MAX: usize = 64 * 1024;
170
171 fn lines(out: &[u8]) -> Vec<String> {
172 String::from_utf8(out.to_vec())
173 .unwrap()
174 .lines()
175 .map(|s| s.to_string())
176 .collect()
177 }
178
179 #[test]
180 fn golden_known_span() {
181 let span = SpanSlice {
182 service: "my-fn".into(),
183 name: "aws.lambda".into(),
184 resource: "my-fn".into(),
185 r#type: "serverless".into(),
186 trace_id: 1,
187 span_id: 2,
188 parent_id: 0,
189 start: 1717200000000000000,
190 duration: 1500000,
191 error: 0,
192 meta: [("env".into(), "prod".into())].into_iter().collect(),
193 metrics: [("_sampling_priority_v1".into(), 1.0)]
194 .into_iter()
195 .collect(),
196 ..Default::default()
197 };
198 let mut out = Vec::new();
199 let stats = encode_traces(&[vec![span]], &mut out, MAX).unwrap();
200 assert_eq!(stats.spans_written, 1);
201 assert_eq!(stats.spans_dropped, 0);
202
203 let expected = concat!(
204 "{\"traces\":[[",
205 "{\"trace_id\":\"0000000000000001\",",
206 "\"span_id\":\"0000000000000002\",",
207 "\"parent_id\":\"0000000000000000\",",
208 "\"service\":\"my-fn\",",
209 "\"name\":\"aws.lambda\",",
210 "\"resource\":\"my-fn\",",
211 "\"type\":\"serverless\",",
212 "\"error\":0,",
213 "\"start\":1717200000000000000,",
214 "\"duration\":1500000,",
215 "\"meta\":{\"env\":\"prod\"},",
216 "\"metrics\":{\"_sampling_priority_v1\":1.0}",
217 "}]]}\n",
218 );
219 assert_eq!(String::from_utf8(out).unwrap(), expected);
220 }
221
222 #[test]
223 fn meta_struct_is_omitted() {
224 let span = SpanSlice {
225 trace_id: 1,
226 span_id: 2,
227 meta_struct: [("_dd.appsec.json".into(), [0x81u8, 0xa4].as_slice())]
228 .into_iter()
229 .collect(),
230 ..Default::default()
231 };
232 let mut out = Vec::new();
233 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
234 let text = String::from_utf8(out).unwrap();
235 assert!(
236 !text.contains("meta_struct"),
237 "meta_struct must not be emitted: {text}"
238 );
239 let v: Value = serde_json::from_str(text.trim_end()).unwrap();
241 assert!(v["traces"][0][0]["trace_id"].is_string());
242 }
243
244 #[test]
245 fn hex_high_bits_and_root_parent() {
246 let trace_id: u128 = (0xABCDu128 << 64) | 0x1;
248 let span = SpanSlice {
249 trace_id,
250 span_id: 0xFF,
251 parent_id: 0,
252 ..Default::default()
253 };
254 let mut out = Vec::new();
255 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
256 let text = String::from_utf8(out).unwrap();
257 assert!(
258 text.contains("\"trace_id\":\"000000000000abcd0000000000000001\""),
259 "got: {text}"
260 );
261 assert!(text.contains("\"span_id\":\"00000000000000ff\""));
262 assert!(text.contains("\"parent_id\":\"0000000000000000\""));
263 }
264
265 #[test]
266 fn error_is_integer() {
267 let span = SpanSlice {
268 error: 1,
269 ..Default::default()
270 };
271 let mut out = Vec::new();
272 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
273 let text = String::from_utf8(out).unwrap();
274 assert!(text.contains("\"error\":1,"), "got: {text}");
275 }
276
277 #[test]
278 fn empty_maps_and_type_omitted() {
279 let span = SpanSlice {
280 name: "op".into(),
281 ..Default::default()
282 };
283 let mut out = Vec::new();
284 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
285 let text = String::from_utf8(out).unwrap();
286 assert!(!text.contains("\"meta\""), "got: {text}");
287 assert!(!text.contains("\"metrics\""));
288 assert!(!text.contains("\"meta_struct\""));
289 assert!(!text.contains("\"span_links\""));
290 assert!(!text.contains("\"span_events\""));
291 assert!(!text.contains("\"type\""));
292 }
293
294 #[test]
295 fn string_escaping() {
296 let span = SpanSlice {
297 name: "say \"hi\"\n".into(),
298 ..Default::default()
299 };
300 let mut out = Vec::new();
301 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
302 for line in lines(&out) {
304 let parsed: Value = serde_json::from_str(&line).unwrap();
305 assert_eq!(
306 parsed["traces"][0][0]["name"].as_str().unwrap(),
307 "say \"hi\"\n"
308 );
309 }
310 }
311
312 #[test]
313 fn size_cap_batches_into_multiple_lines() {
314 let make = |id: u64| SpanSlice {
316 name: "x".into(),
317 span_id: id,
318 ..Default::default()
319 };
320 let trace: Vec<SpanSlice> = (1..=6).map(make).collect();
321
322 let mut one = Vec::new();
324 encode_traces(&[vec![make(1)]], &mut one, MAX).unwrap();
325 let span_len = one.len() - TRACE_FORMAT_OVERHEAD;
327 let cap = TRACE_FORMAT_OVERHEAD + span_len * 2 + 1;
329
330 let mut out = Vec::new();
331 let stats = encode_traces(&[trace], &mut out, cap).unwrap();
332 assert_eq!(stats.spans_written, 6);
333 assert_eq!(stats.spans_dropped, 0);
334
335 let emitted = lines(&out);
336 assert_eq!(emitted.len(), 3, "expected 3 lines, got {emitted:?}");
337 for line in &emitted {
338 assert!(line.len() <= cap, "line over cap: {} > {cap}", line.len());
339 let parsed: Value = serde_json::from_str(line).unwrap();
340 assert_eq!(parsed["traces"][0].as_array().unwrap().len(), 2);
341 }
342 }
343
344 #[test]
345 fn oversize_single_span_dropped() {
346 let big = "a".repeat(10_000);
347 let span = SpanSlice {
348 name: big.as_str().into(),
349 span_id: 1,
350 ..Default::default()
351 };
352 let small = SpanSlice {
353 name: "ok".into(),
354 span_id: 2,
355 ..Default::default()
356 };
357 let mut out = Vec::new();
358 let stats = encode_traces(&[vec![span, small]], &mut out, 1024).unwrap();
360 assert_eq!(stats.spans_dropped, 1);
361 assert_eq!(stats.spans_written, 1);
362
363 let emitted = lines(&out);
364 assert_eq!(emitted.len(), 1);
365 let parsed: Value = serde_json::from_str(&emitted[0]).unwrap();
366 assert_eq!(parsed["traces"][0][0]["name"].as_str().unwrap(), "ok");
367 }
368
369 #[test]
370 fn metric_non_finite_serializes_null() {
371 let span = SpanSlice {
374 span_id: 1,
375 metrics: [("nan".into(), f64::NAN), ("inf".into(), f64::INFINITY)]
376 .into_iter()
377 .collect(),
378 ..Default::default()
379 };
380 let mut out = Vec::new();
381 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
382 let emitted = lines(&out);
383 assert_eq!(emitted.len(), 1);
384 let parsed: Value = serde_json::from_str(&emitted[0]).unwrap();
385 let metrics = &parsed["traces"][0][0]["metrics"];
386 assert!(metrics["nan"].is_null(), "got: {metrics}");
387 assert!(metrics["inf"].is_null(), "got: {metrics}");
388 }
389
390 #[test]
391 fn multi_trace_flattened_into_one_line() {
392 let span_a = SpanSlice {
395 name: "a".into(),
396 span_id: 1,
397 ..Default::default()
398 };
399 let span_b = SpanSlice {
400 name: "b".into(),
401 span_id: 2,
402 ..Default::default()
403 };
404 let mut out = Vec::new();
405 let stats = encode_traces(&[vec![span_a], vec![span_b]], &mut out, MAX).unwrap();
406 assert_eq!(stats.spans_written, 2);
407 let emitted = lines(&out);
408 assert_eq!(emitted.len(), 1, "expected one line, got {emitted:?}");
409 let parsed: Value = serde_json::from_str(&emitted[0]).unwrap();
410 let inner = parsed["traces"][0].as_array().unwrap();
411 assert_eq!(inner.len(), 2);
412 assert_eq!(parsed["traces"].as_array().unwrap().len(), 1);
413 }
414
415 #[test]
416 fn span_links_and_events_emitted_when_present() {
417 use crate::span::v04::{SpanEvent, SpanLink};
418
419 let span = SpanSlice {
420 span_id: 1,
421 span_links: vec![SpanLink {
422 trace_id: 7,
423 span_id: 8,
424 ..Default::default()
425 }],
426 span_events: vec![SpanEvent {
427 time_unix_nano: 123,
428 name: "evt".into(),
429 ..Default::default()
430 }],
431 ..Default::default()
432 };
433 let mut out = Vec::new();
434 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
435 let emitted = lines(&out);
436 assert_eq!(emitted.len(), 1);
437 let parsed: Value = serde_json::from_str(&emitted[0]).unwrap();
438 let span_json = &parsed["traces"][0][0];
439 assert_eq!(span_json["span_links"][0]["trace_id"], 7);
441 assert_eq!(span_json["span_links"][0]["span_id"], 8);
442 assert_eq!(span_json["span_events"][0]["name"], "evt");
443 assert_eq!(span_json["span_events"][0]["time_unix_nano"], 123);
444 }
445
446 #[test]
447 fn span_link_flags_sentinel_bit_masked() {
448 use crate::span::v04::SpanLink;
452
453 fn encoded_flags(flags: u32) -> Value {
454 let span = SpanSlice {
455 span_id: 1,
456 span_links: vec![SpanLink {
457 trace_id: 7,
458 span_id: 8,
459 flags,
460 ..Default::default()
461 }],
462 ..Default::default()
463 };
464 let mut out = Vec::new();
465 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
466 let emitted = lines(&out);
467 let parsed: Value = serde_json::from_str(&emitted[0]).unwrap();
468 parsed["traces"][0][0]["span_links"][0]["flags"].clone()
469 }
470
471 assert_eq!(
472 encoded_flags(0x8000_0001),
473 1,
474 "the sentinel bit must not leak into the JSON log"
475 );
476 assert_eq!(
477 encoded_flags(0x8000_0000),
478 0,
479 "an explicit drop decision must still emit flags: 0, not omit the field"
480 );
481 }
482
483 #[test]
484 fn empty_inner_trace_writes_nothing() {
485 let traces: Vec<Vec<SpanSlice>> = vec![vec![]];
487 let mut out = Vec::new();
488 let stats = encode_traces(&traces, &mut out, MAX).unwrap();
489 assert!(out.is_empty());
490 assert_eq!(stats.spans_written, 0);
491 assert_eq!(stats.spans_dropped, 0);
492 }
493
494 #[test]
495 fn empty_input_writes_nothing() {
496 let traces: Vec<Vec<SpanSlice>> = vec![];
497 let mut out = Vec::new();
498 let stats = encode_traces(&traces, &mut out, MAX).unwrap();
499 assert_eq!(stats, EncodeStats::default());
500 assert!(out.is_empty());
501 }
502
503 #[test]
508 fn forwarder_is_trace_contract() {
509 let trace: Vec<SpanSlice> = (1u64..=5)
510 .map(|i| SpanSlice {
511 service: "svc".into(),
512 name: "op".into(),
513 trace_id: u128::from(i),
514 span_id: i + 100,
515 ..Default::default()
516 })
517 .collect();
518
519 let mut out = Vec::new();
520 encode_traces(&[trace], &mut out, 200).unwrap();
521
522 let text = String::from_utf8(out).unwrap();
523 assert!(!text.is_empty());
524 for chunk in text.split_inclusive('\n') {
526 assert!(chunk.ends_with('\n'), "line not newline-terminated");
527 let line = chunk.trim_end_matches('\n');
528 let parsed: Value = serde_json::from_str(line).unwrap();
529
530 let traces = parsed.get("traces").and_then(Value::as_array).unwrap();
531 assert!(!traces.is_empty());
532 let first = traces[0].as_array().unwrap();
533 assert!(!first.is_empty());
534 let trace_id = first[0].get("trace_id").unwrap();
535 assert!(trace_id.is_string());
536 assert!(!trace_id.as_str().unwrap().is_empty());
537 }
538 }
539}