1mod span;
23mod span_v1;
24
25use crate::span::v04::Span;
26use crate::span::v1::TracerPayload;
27use crate::span::TraceData;
28use span::LogSpan;
29use span_v1::{ChunkContextV1, LogSpanV1};
30use std::io::Write;
31
32const TRACE_PREFIX: &[u8] = b"{\"traces\":[[";
34const TRACE_SUFFIX: &[u8] = b"]]}\n";
36const TRACE_FORMAT_OVERHEAD: usize = TRACE_PREFIX.len() + TRACE_SUFFIX.len();
38
39#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
41pub struct EncodeStats {
42 pub spans_written: usize,
44 pub spans_dropped: usize,
46}
47
48pub fn encode_traces<T: TraceData>(
87 traces: &[Vec<Span<T>>],
88 out: &mut impl Write,
89 max_line_size: usize,
90) -> std::io::Result<EncodeStats> {
91 let mut stats = EncodeStats::default();
92
93 let mut span_buf: Vec<u8> = Vec::with_capacity(512);
98 let mut line: Vec<u8> = Vec::with_capacity(max_line_size.min(64 * 1024));
99 line.extend_from_slice(TRACE_PREFIX);
100 let mut line_span_count: usize = 0;
101
102 for trace in traces {
103 for span in trace {
104 span_buf.clear();
105 serde_json::to_writer(&mut span_buf, &LogSpan(span)).map_err(std::io::Error::other)?;
106 let span_len = span_buf.len();
107
108 if span_len + TRACE_FORMAT_OVERHEAD > max_line_size {
111 stats.spans_dropped += 1;
112 tracing::debug!(
113 span_len,
114 max_line_size,
115 "Span too large to send to logs, dropping"
116 );
117 continue;
118 }
119
120 let comma = usize::from(line_span_count > 0);
124 if line_span_count > 0
125 && line.len() + comma + span_len + TRACE_SUFFIX.len() > max_line_size
126 {
127 flush_line(out, &mut line)?;
128 line_span_count = 0;
129 }
130
131 if line_span_count > 0 {
132 line.push(b',');
133 }
134 line.extend_from_slice(&span_buf);
135 line_span_count += 1;
136 stats.spans_written += 1;
137 }
138 }
139
140 if line_span_count > 0 {
141 flush_line(out, &mut line)?;
142 }
143
144 Ok(stats)
145}
146
147pub fn encode_traces_v1<T: TraceData>(
162 payload: &TracerPayload<T>,
163 out: &mut impl Write,
164 max_line_size: usize,
165) -> std::io::Result<EncodeStats> {
166 let mut stats = EncodeStats::default();
167
168 let mut span_buf: Vec<u8> = Vec::with_capacity(512);
173 let mut line: Vec<u8> = Vec::with_capacity(max_line_size.min(64 * 1024));
174 line.extend_from_slice(TRACE_PREFIX);
175 let mut line_span_count: usize = 0;
176
177 for chunk in &payload.chunks {
178 let ctx = ChunkContextV1::new(
179 chunk,
180 &payload.env,
181 &payload.app_version,
182 &payload.attributes,
183 );
184 for span in &chunk.spans {
185 span_buf.clear();
186 serde_json::to_writer(&mut span_buf, &LogSpanV1(span, &ctx))
187 .map_err(std::io::Error::other)?;
188 let span_len = span_buf.len();
189
190 if span_len + TRACE_FORMAT_OVERHEAD > max_line_size {
193 stats.spans_dropped += 1;
194 tracing::debug!(
195 span_len,
196 max_line_size,
197 "Span too large to send to logs, dropping"
198 );
199 continue;
200 }
201
202 let comma = usize::from(line_span_count > 0);
206 if line_span_count > 0
207 && line.len() + comma + span_len + TRACE_SUFFIX.len() > max_line_size
208 {
209 flush_line(out, &mut line)?;
210 line_span_count = 0;
211 }
212
213 if line_span_count > 0 {
214 line.push(b',');
215 }
216 line.extend_from_slice(&span_buf);
217 line_span_count += 1;
218 stats.spans_written += 1;
219 }
220 }
221
222 if line_span_count > 0 {
223 flush_line(out, &mut line)?;
224 }
225
226 Ok(stats)
227}
228
229fn flush_line(out: &mut impl Write, line: &mut Vec<u8>) -> std::io::Result<()> {
236 line.extend_from_slice(TRACE_SUFFIX);
237 out.write_all(line)?;
238 line.clear();
239 line.extend_from_slice(TRACE_PREFIX);
240 Ok(())
241}
242
243#[cfg(test)]
244#[allow(clippy::useless_conversion)]
249mod tests {
250 use super::*;
251 use crate::span::v04::SpanSlice;
252 use serde_json::Value;
253
254 const MAX: usize = 64 * 1024;
255
256 fn lines(out: &[u8]) -> Vec<String> {
257 String::from_utf8(out.to_vec())
258 .unwrap()
259 .lines()
260 .map(|s| s.to_string())
261 .collect()
262 }
263
264 #[test]
265 fn golden_known_span() {
266 let span = SpanSlice {
267 service: "my-fn".into(),
268 name: "aws.lambda".into(),
269 resource: "my-fn".into(),
270 r#type: "serverless".into(),
271 trace_id: 1,
272 span_id: 2,
273 parent_id: 0,
274 start: 1717200000000000000,
275 duration: 1500000,
276 error: 0,
277 meta: [("env".into(), "prod".into())].into_iter().collect(),
278 metrics: [("_sampling_priority_v1".into(), 1.0)]
279 .into_iter()
280 .collect(),
281 ..Default::default()
282 };
283 let mut out = Vec::new();
284 let stats = encode_traces(&[vec![span]], &mut out, MAX).unwrap();
285 assert_eq!(stats.spans_written, 1);
286 assert_eq!(stats.spans_dropped, 0);
287
288 let expected = concat!(
289 "{\"traces\":[[",
290 "{\"trace_id\":\"0000000000000001\",",
291 "\"span_id\":\"0000000000000002\",",
292 "\"parent_id\":\"0000000000000000\",",
293 "\"service\":\"my-fn\",",
294 "\"name\":\"aws.lambda\",",
295 "\"resource\":\"my-fn\",",
296 "\"type\":\"serverless\",",
297 "\"error\":0,",
298 "\"start\":1717200000000000000,",
299 "\"duration\":1500000,",
300 "\"meta\":{\"env\":\"prod\"},",
301 "\"metrics\":{\"_sampling_priority_v1\":1.0}",
302 "}]]}\n",
303 );
304 assert_eq!(String::from_utf8(out).unwrap(), expected);
305 }
306
307 #[test]
308 fn meta_struct_is_omitted() {
309 let span = SpanSlice {
310 trace_id: 1,
311 span_id: 2,
312 meta_struct: [("_dd.appsec.json".into(), [0x81u8, 0xa4].as_slice())]
313 .into_iter()
314 .collect(),
315 ..Default::default()
316 };
317 let mut out = Vec::new();
318 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
319 let text = String::from_utf8(out).unwrap();
320 assert!(
321 !text.contains("meta_struct"),
322 "meta_struct must not be emitted: {text}"
323 );
324 let v: Value = serde_json::from_str(text.trim_end()).unwrap();
326 assert!(v["traces"][0][0]["trace_id"].is_string());
327 }
328
329 #[test]
330 fn hex_high_bits_and_root_parent() {
331 let trace_id: u128 = (0xABCDu128 << 64) | 0x1;
333 let span = SpanSlice {
334 trace_id,
335 span_id: 0xFF,
336 parent_id: 0,
337 ..Default::default()
338 };
339 let mut out = Vec::new();
340 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
341 let text = String::from_utf8(out).unwrap();
342 assert!(
343 text.contains("\"trace_id\":\"000000000000abcd0000000000000001\""),
344 "got: {text}"
345 );
346 assert!(text.contains("\"span_id\":\"00000000000000ff\""));
347 assert!(text.contains("\"parent_id\":\"0000000000000000\""));
348 }
349
350 #[test]
351 fn error_is_integer() {
352 let span = SpanSlice {
353 error: 1,
354 ..Default::default()
355 };
356 let mut out = Vec::new();
357 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
358 let text = String::from_utf8(out).unwrap();
359 assert!(text.contains("\"error\":1,"), "got: {text}");
360 }
361
362 #[test]
363 fn empty_maps_and_type_omitted() {
364 let span = SpanSlice {
365 name: "op".into(),
366 ..Default::default()
367 };
368 let mut out = Vec::new();
369 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
370 let text = String::from_utf8(out).unwrap();
371 assert!(!text.contains("\"meta\""), "got: {text}");
372 assert!(!text.contains("\"metrics\""));
373 assert!(!text.contains("\"meta_struct\""));
374 assert!(!text.contains("\"span_links\""));
375 assert!(!text.contains("\"span_events\""));
376 assert!(!text.contains("\"type\""));
377 }
378
379 #[test]
380 fn string_escaping() {
381 let span = SpanSlice {
382 name: "say \"hi\"\n".into(),
383 ..Default::default()
384 };
385 let mut out = Vec::new();
386 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
387 for line in lines(&out) {
389 let parsed: Value = serde_json::from_str(&line).unwrap();
390 assert_eq!(
391 parsed["traces"][0][0]["name"].as_str().unwrap(),
392 "say \"hi\"\n"
393 );
394 }
395 }
396
397 #[test]
398 fn size_cap_batches_into_multiple_lines() {
399 let make = |id: u64| SpanSlice {
401 name: "x".into(),
402 span_id: id,
403 ..Default::default()
404 };
405 let trace: Vec<SpanSlice> = (1..=6).map(make).collect();
406
407 let mut one = Vec::new();
409 encode_traces(&[vec![make(1)]], &mut one, MAX).unwrap();
410 let span_len = one.len() - TRACE_FORMAT_OVERHEAD;
412 let cap = TRACE_FORMAT_OVERHEAD + span_len * 2 + 1;
414
415 let mut out = Vec::new();
416 let stats = encode_traces(&[trace], &mut out, cap).unwrap();
417 assert_eq!(stats.spans_written, 6);
418 assert_eq!(stats.spans_dropped, 0);
419
420 let emitted = lines(&out);
421 assert_eq!(emitted.len(), 3, "expected 3 lines, got {emitted:?}");
422 for line in &emitted {
423 assert!(line.len() <= cap, "line over cap: {} > {cap}", line.len());
424 let parsed: Value = serde_json::from_str(line).unwrap();
425 assert_eq!(parsed["traces"][0].as_array().unwrap().len(), 2);
426 }
427 }
428
429 #[test]
430 fn oversize_single_span_dropped() {
431 let big = "a".repeat(10_000);
432 let span = SpanSlice {
433 name: big.as_str().into(),
434 span_id: 1,
435 ..Default::default()
436 };
437 let small = SpanSlice {
438 name: "ok".into(),
439 span_id: 2,
440 ..Default::default()
441 };
442 let mut out = Vec::new();
443 let stats = encode_traces(&[vec![span, small]], &mut out, 1024).unwrap();
445 assert_eq!(stats.spans_dropped, 1);
446 assert_eq!(stats.spans_written, 1);
447
448 let emitted = lines(&out);
449 assert_eq!(emitted.len(), 1);
450 let parsed: Value = serde_json::from_str(&emitted[0]).unwrap();
451 assert_eq!(parsed["traces"][0][0]["name"].as_str().unwrap(), "ok");
452 }
453
454 #[test]
455 fn metric_non_finite_serializes_null() {
456 let span = SpanSlice {
459 span_id: 1,
460 metrics: [("nan".into(), f64::NAN), ("inf".into(), f64::INFINITY)]
461 .into_iter()
462 .collect(),
463 ..Default::default()
464 };
465 let mut out = Vec::new();
466 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
467 let emitted = lines(&out);
468 assert_eq!(emitted.len(), 1);
469 let parsed: Value = serde_json::from_str(&emitted[0]).unwrap();
470 let metrics = &parsed["traces"][0][0]["metrics"];
471 assert!(metrics["nan"].is_null(), "got: {metrics}");
472 assert!(metrics["inf"].is_null(), "got: {metrics}");
473 }
474
475 #[test]
476 fn multi_trace_flattened_into_one_line() {
477 let span_a = SpanSlice {
480 name: "a".into(),
481 span_id: 1,
482 ..Default::default()
483 };
484 let span_b = SpanSlice {
485 name: "b".into(),
486 span_id: 2,
487 ..Default::default()
488 };
489 let mut out = Vec::new();
490 let stats = encode_traces(&[vec![span_a], vec![span_b]], &mut out, MAX).unwrap();
491 assert_eq!(stats.spans_written, 2);
492 let emitted = lines(&out);
493 assert_eq!(emitted.len(), 1, "expected one line, got {emitted:?}");
494 let parsed: Value = serde_json::from_str(&emitted[0]).unwrap();
495 let inner = parsed["traces"][0].as_array().unwrap();
496 assert_eq!(inner.len(), 2);
497 assert_eq!(parsed["traces"].as_array().unwrap().len(), 1);
498 }
499
500 #[test]
501 fn span_links_and_events_emitted_when_present() {
502 use crate::span::v04::{SpanEvent, SpanLink};
503
504 let span = SpanSlice {
505 span_id: 1,
506 span_links: vec![SpanLink {
507 trace_id: 7,
508 span_id: 8,
509 ..Default::default()
510 }],
511 span_events: vec![SpanEvent {
512 time_unix_nano: 123,
513 name: "evt".into(),
514 ..Default::default()
515 }],
516 ..Default::default()
517 };
518 let mut out = Vec::new();
519 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
520 let emitted = lines(&out);
521 assert_eq!(emitted.len(), 1);
522 let parsed: Value = serde_json::from_str(&emitted[0]).unwrap();
523 let span_json = &parsed["traces"][0][0];
524 assert_eq!(span_json["span_links"][0]["trace_id"], 7);
526 assert_eq!(span_json["span_links"][0]["span_id"], 8);
527 assert_eq!(span_json["span_events"][0]["name"], "evt");
528 assert_eq!(span_json["span_events"][0]["time_unix_nano"], 123);
529 }
530
531 #[test]
532 fn span_link_flags_sentinel_bit_masked() {
533 use crate::span::v04::SpanLink;
537
538 fn encoded_flags(flags: u32) -> Value {
539 let span = SpanSlice {
540 span_id: 1,
541 span_links: vec![SpanLink {
542 trace_id: 7,
543 span_id: 8,
544 flags,
545 ..Default::default()
546 }],
547 ..Default::default()
548 };
549 let mut out = Vec::new();
550 encode_traces(&[vec![span]], &mut out, MAX).unwrap();
551 let emitted = lines(&out);
552 let parsed: Value = serde_json::from_str(&emitted[0]).unwrap();
553 parsed["traces"][0][0]["span_links"][0]["flags"].clone()
554 }
555
556 assert_eq!(
557 encoded_flags(0x8000_0001),
558 1,
559 "the sentinel bit must not leak into the JSON log"
560 );
561 assert_eq!(
562 encoded_flags(0x8000_0000),
563 0,
564 "an explicit drop decision must still emit flags: 0, not omit the field"
565 );
566 }
567
568 #[test]
569 fn empty_inner_trace_writes_nothing() {
570 let traces: Vec<Vec<SpanSlice>> = vec![vec![]];
572 let mut out = Vec::new();
573 let stats = encode_traces(&traces, &mut out, MAX).unwrap();
574 assert!(out.is_empty());
575 assert_eq!(stats.spans_written, 0);
576 assert_eq!(stats.spans_dropped, 0);
577 }
578
579 #[test]
580 fn empty_input_writes_nothing() {
581 let traces: Vec<Vec<SpanSlice>> = vec![];
582 let mut out = Vec::new();
583 let stats = encode_traces(&traces, &mut out, MAX).unwrap();
584 assert_eq!(stats, EncodeStats::default());
585 assert!(out.is_empty());
586 }
587
588 #[test]
593 fn forwarder_is_trace_contract() {
594 let trace: Vec<SpanSlice> = (1u64..=5)
595 .map(|i| SpanSlice {
596 service: "svc".into(),
597 name: "op".into(),
598 trace_id: u128::from(i),
599 span_id: i + 100,
600 ..Default::default()
601 })
602 .collect();
603
604 let mut out = Vec::new();
605 encode_traces(&[trace], &mut out, 200).unwrap();
606
607 let text = String::from_utf8(out).unwrap();
608 assert!(!text.is_empty());
609 for chunk in text.split_inclusive('\n') {
611 assert!(chunk.ends_with('\n'), "line not newline-terminated");
612 let line = chunk.trim_end_matches('\n');
613 let parsed: Value = serde_json::from_str(line).unwrap();
614
615 let traces = parsed.get("traces").and_then(Value::as_array).unwrap();
616 assert!(!traces.is_empty());
617 let first = traces[0].as_array().unwrap();
618 assert!(!first.is_empty());
619 let trace_id = first[0].get("trace_id").unwrap();
620 assert!(trace_id.is_string());
621 assert!(!trace_id.as_str().unwrap().is_empty());
622 }
623 }
624}