Skip to content

Commit dc578d3

Browse files
authored
add span_parents to span components format (#42)
1 parent 9c03643 commit dc578d3

5 files changed

Lines changed: 232 additions & 4 deletions

File tree

src/log_queue/queue.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ impl LogQueueCore {
401401
merge_paths: None,
402402
span_id,
403403
root_span_id,
404-
span_parents: None,
404+
span_parents: span_components.span_parents.clone(),
405405
destination,
406406
org_id,
407407
org_name,
@@ -470,6 +470,7 @@ impl LogQueueCore {
470470
compute_object_metadata_args,
471471
span_id: parent_span_id,
472472
root_span_id: parent_root_span_id,
473+
span_parents: _,
473474
propagated_event: _,
474475
}) => {
475476
let span_parents = Some(vec![parent_span_id]);
@@ -804,7 +805,9 @@ impl LogQueueCore {
804805
is_merge: if payload.is_merge { Some(true) } else { None },
805806
merge_paths: None,
806807
root_span_id,
807-
span_parents: None,
808+
span_parents: span_components
809+
.as_ref()
810+
.and_then(|components| components.span_parents.clone()),
808811
destination,
809812
org_id: payload.org_id,
810813
org_name: payload.org_name,

src/span.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,10 @@ impl<S: SpanSubmitter> SpanHandle<S> {
477477
Some(ParentSpanInfo::FullSpan { root_span_id, .. }) => Some(root_span_id.clone()),
478478
_ => Some(inner.span_id.clone()),
479479
};
480+
let span_parents = match &self.parent_info {
481+
Some(ParentSpanInfo::FullSpan { span_id, .. }) => Some(vec![span_id.clone()]),
482+
_ => None,
483+
};
480484

481485
let compute_object_metadata_args = if object_id.is_none() {
482486
inherited_compute_object_metadata_args.or_else(|| {
@@ -500,6 +504,7 @@ impl<S: SpanSubmitter> SpanHandle<S> {
500504
row_id: Some(inner.row_id.clone()),
501505
span_id: Some(inner.span_id.clone()),
502506
root_span_id,
507+
span_parents,
503508
propagated_event: inner.propagated_event.clone(),
504509
})
505510
}
@@ -876,6 +881,7 @@ mod tests {
876881
span_id: "parent-span-id".to_string(),
877882
root_span_id: "root-span-id".to_string(),
878883
compute_object_metadata_args: None,
884+
span_parents: None,
879885
propagated_event: Some(parent_propagated),
880886
};
881887

@@ -924,6 +930,7 @@ mod tests {
924930
span_id: "span-456".to_string(),
925931
root_span_id: "root-789".to_string(),
926932
compute_object_metadata_args: None,
933+
span_parents: None,
927934
propagated_event: Some(propagated),
928935
};
929936

@@ -940,6 +947,7 @@ mod tests {
940947
event.get("test_key").and_then(|v| v.as_str()),
941948
Some("test_value")
942949
);
950+
assert_eq!(exported.span_parents, Some(vec!["span-456".to_string()]));
943951
}
944952

945953
#[tokio::test]

src/span_components.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ pub struct SpanComponents {
7070
#[serde(skip_serializing_if = "Option::is_none")]
7171
pub root_span_id: Option<String>,
7272

73+
/// Direct parent span IDs for this span.
74+
#[serde(skip_serializing_if = "Option::is_none")]
75+
pub span_parents: Option<Vec<String>>,
76+
7377
/// Event data to propagate to child spans (e.g., prompt versions, metadata)
7478
#[serde(skip_serializing_if = "Option::is_none")]
7579
pub propagated_event: Option<Map<String, Value>>,
@@ -85,6 +89,7 @@ impl SpanComponents {
8589
row_id: None,
8690
span_id: None,
8791
root_span_id: None,
92+
span_parents: None,
8893
propagated_event: None,
8994
}
9095
}
@@ -194,6 +199,12 @@ impl SpanComponents {
194199
if let Some(ref event) = self.propagated_event {
195200
json_obj.insert("propagated_event".to_string(), Value::Object(event.clone()));
196201
}
202+
if let Some(ref span_parents) = self.span_parents {
203+
json_obj.insert(
204+
"span_parents".to_string(),
205+
Value::Array(span_parents.iter().cloned().map(Value::String).collect()),
206+
);
207+
}
197208

198209
if !json_obj.is_empty() {
199210
let json_str = serde_json::to_string(&json_obj).unwrap();
@@ -365,6 +376,25 @@ impl SpanComponents {
365376
root_span_id: json_obj
366377
.remove("root_span_id")
367378
.and_then(|v| v.as_str().map(String::from)),
379+
span_parents: match json_obj.remove("span_parents") {
380+
None => None,
381+
Some(Value::Array(values)) => Some(
382+
values
383+
.into_iter()
384+
.map(|value| match value {
385+
Value::String(value) => Ok(value),
386+
_ => Err(BraintrustError::InvalidConfig(
387+
"span_parents must be an array of strings".to_string(),
388+
)),
389+
})
390+
.collect::<Result<Vec<_>>>()?,
391+
),
392+
Some(_) => {
393+
return Err(BraintrustError::InvalidConfig(
394+
"span_parents must be an array of strings".to_string(),
395+
))
396+
}
397+
},
368398
propagated_event: json_obj
369399
.remove("propagated_event")
370400
.and_then(|v| v.as_object().cloned()),
@@ -404,6 +434,7 @@ impl SpanComponents {
404434
compute_object_metadata_args: self.compute_object_metadata_args.clone(),
405435
span_id,
406436
root_span_id,
437+
span_parents: self.span_parents.clone(),
407438
propagated_event: self.propagated_event.clone(),
408439
})
409440
}
@@ -417,6 +448,7 @@ impl SpanComponents {
417448
compute_object_metadata_args,
418449
span_id,
419450
root_span_id,
451+
span_parents,
420452
propagated_event,
421453
} => Some(Self {
422454
object_type: *object_type,
@@ -425,6 +457,7 @@ impl SpanComponents {
425457
row_id: None,
426458
span_id: Some(span_id.clone()),
427459
root_span_id: Some(root_span_id.clone()),
460+
span_parents: span_parents.clone(),
428461
propagated_event: propagated_event.clone(),
429462
}),
430463
_ => None,
@@ -529,6 +562,7 @@ mod tests {
529562
components.object_id = Some("550e8400-e29b-41d4-a716-446655440000".to_string());
530563
components.span_id = Some("0123456789abcdef".to_string());
531564
components.root_span_id = Some("0123456789abcdef0123456789abcdef".to_string());
565+
components.span_parents = Some(vec!["parent-a".to_string()]);
532566

533567
let encoded = components.to_str();
534568
let decoded = SpanComponents::parse(&encoded).unwrap();
@@ -537,6 +571,7 @@ mod tests {
537571
assert_eq!(decoded.object_id, components.object_id);
538572
assert_eq!(decoded.span_id, components.span_id);
539573
assert_eq!(decoded.root_span_id, components.root_span_id);
574+
assert_eq!(decoded.span_parents, components.span_parents);
540575
}
541576

542577
#[test]
@@ -584,6 +619,7 @@ mod tests {
584619
components.object_id = Some("project-123".to_string());
585620
components.span_id = Some("span-456".to_string());
586621
components.root_span_id = Some("root-789".to_string());
622+
components.span_parents = Some(vec!["parent-a".to_string()]);
587623

588624
let mut propagated = Map::new();
589625
propagated.insert(
@@ -600,13 +636,15 @@ mod tests {
600636
object_id,
601637
span_id,
602638
root_span_id,
639+
span_parents,
603640
propagated_event,
604641
..
605642
} => {
606643
assert_eq!(object_type, SpanObjectType::ProjectLogs);
607644
assert_eq!(object_id, Some("project-123".to_string()));
608645
assert_eq!(span_id, "span-456");
609646
assert_eq!(root_span_id, "root-789");
647+
assert_eq!(span_parents, Some(vec!["parent-a".to_string()]));
610648
assert!(propagated_event.is_some());
611649
let event = propagated_event.unwrap();
612650
assert_eq!(
@@ -629,6 +667,7 @@ mod tests {
629667
span_id: "span-456".to_string(),
630668
root_span_id: "root-789".to_string(),
631669
compute_object_metadata_args: None,
670+
span_parents: Some(vec!["parent-a".to_string()]),
632671
propagated_event: Some(propagated),
633672
};
634673

@@ -638,6 +677,48 @@ mod tests {
638677
assert_eq!(components.object_id, Some("exp-123".to_string()));
639678
assert_eq!(components.span_id, Some("span-456".to_string()));
640679
assert_eq!(components.root_span_id, Some("root-789".to_string()));
680+
assert_eq!(components.span_parents, Some(vec!["parent-a".to_string()]));
641681
assert!(components.propagated_event.is_some());
642682
}
683+
684+
#[test]
685+
fn test_parse_v3_json_remainder_preserves_span_parents() {
686+
let payload = serde_json::json!({
687+
"object_type": SpanObjectType::ProjectLogs as u8,
688+
"object_id": "project-123",
689+
"row_id": "row-123",
690+
"span_id": "span-123",
691+
"root_span_id": "root-123",
692+
"span_parents": ["parent-a"],
693+
});
694+
let encoded = BASE64.encode(
695+
[
696+
vec![ENCODING_VERSION_V3, SpanObjectType::ProjectLogs as u8, 0],
697+
serde_json::to_vec(&payload).unwrap(),
698+
]
699+
.concat(),
700+
);
701+
702+
let decoded = SpanComponents::parse(&encoded).unwrap();
703+
704+
assert_eq!(decoded.span_parents, Some(vec!["parent-a".to_string()]));
705+
}
706+
707+
#[test]
708+
fn test_parse_rejects_invalid_span_parents() {
709+
let payload = serde_json::json!({
710+
"object_type": SpanObjectType::ProjectLogs as u8,
711+
"span_parents": [123],
712+
});
713+
let encoded = BASE64.encode(
714+
[
715+
vec![ENCODING_VERSION_V4, SpanObjectType::ProjectLogs as u8, 0],
716+
serde_json::to_vec(&payload).unwrap(),
717+
]
718+
.concat(),
719+
);
720+
721+
let err = SpanComponents::parse(&encoded).unwrap_err();
722+
assert!(matches!(err, BraintrustError::InvalidConfig(_)));
723+
}
643724
}

src/types.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,8 @@ pub enum ParentSpanInfo {
364364
span_id: String,
365365
root_span_id: String,
366366
#[serde(skip_serializing_if = "Option::is_none")]
367+
span_parents: Option<Vec<String>>,
368+
#[serde(skip_serializing_if = "Option::is_none")]
367369
propagated_event: Option<Map<String, Value>>,
368370
},
369371
}
@@ -876,6 +878,7 @@ mod tests {
876878
span_id: "span-1".to_string(),
877879
root_span_id: "root-1".to_string(),
878880
compute_object_metadata_args: None,
881+
span_parents: Some(vec!["parent-1".to_string()]),
879882
propagated_event: None,
880883
};
881884

@@ -884,6 +887,7 @@ mod tests {
884887

885888
// SpanObjectType serializes as u8 for wire compatibility
886889
assert_eq!(obj.get("object_type").unwrap(), 1);
890+
assert_eq!(obj.get("span_parents").unwrap(), &json!(["parent-1"]));
887891
}
888892

889893
#[test]
@@ -894,15 +898,21 @@ mod tests {
894898
"object_type": 1,
895899
"object_id": "exp-123",
896900
"span_id": "span-1",
897-
"root_span_id": "root-1"
901+
"root_span_id": "root-1",
902+
"span_parents": ["parent-1"]
898903
}
899904
});
900905

901906
let parent: ParentSpanInfo = serde_json::from_value(json).unwrap();
902907

903908
match parent {
904-
ParentSpanInfo::FullSpan { object_type, .. } => {
909+
ParentSpanInfo::FullSpan {
910+
object_type,
911+
span_parents,
912+
..
913+
} => {
905914
assert_eq!(object_type, SpanObjectType::Experiment);
915+
assert_eq!(span_parents, Some(vec!["parent-1".to_string()]));
906916
}
907917
_ => panic!("Expected FullSpan variant"),
908918
}
@@ -916,6 +926,7 @@ mod tests {
916926
span_id: "span-1".to_string(),
917927
root_span_id: "root-1".to_string(),
918928
compute_object_metadata_args: None,
929+
span_parents: Some(vec!["parent-1".to_string()]),
919930
propagated_event: Some(Map::from_iter([(
920931
"metrics".to_string(),
921932
json!({ "foo": 0.1 }),
@@ -928,6 +939,7 @@ mod tests {
928939
obj.get("propagated_event").unwrap(),
929940
&json!({ "metrics": { "foo": 0.1 } })
930941
);
942+
assert_eq!(obj.get("span_parents").unwrap(), &json!(["parent-1"]));
931943
}
932944

933945
#[test]

0 commit comments

Comments
 (0)