1use std::sync::Arc;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum EventType {
11 Sql,
12 HttpOut,
13 Messaging,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct EventSource {
21 pub endpoint: String,
22 pub method: String,
23}
24
25pub const MAX_ID_LENGTH: usize = 128;
30
31#[must_use]
37pub fn sanitize_id(id: &str) -> String {
38 let mut s = id.to_string();
39 truncate_field(&mut s, MAX_ID_LENGTH);
40 s
41}
42
43pub const MAX_SERVICE_LENGTH: usize = 256;
45
46pub const MAX_OPERATION_LENGTH: usize = 256;
48
49pub const MAX_TARGET_LENGTH: usize = 65_536;
53
54pub const MAX_SOURCE_LENGTH: usize = 512;
56
57pub const MAX_CODE_FUNCTION_LENGTH: usize = 512;
59
60pub const MAX_CODE_FILEPATH_LENGTH: usize = 1024;
62
63pub const MAX_CODE_NAMESPACE_LENGTH: usize = 512;
65
66pub const MAX_SCOPE_NAME_LENGTH: usize = 256;
71
72pub const MAX_INSTRUMENTATION_SCOPES: usize = 8;
76
77pub(crate) fn truncate_field(s: &mut String, max_len: usize) {
83 if s.len() <= max_len {
84 return;
85 }
86 let mut end = max_len;
87 while end > 0 && !s.is_char_boundary(end) {
88 end -= 1;
89 }
90 s.truncate(end);
91}
92
93fn strip_endpoint_secrets(endpoint: &mut String) {
101 if let Some(cut) = endpoint.find(['?', '#']) {
104 endpoint.truncate(cut);
105 }
106 let authority_start = if endpoint.starts_with("//") {
110 2
111 } else if let Some(i) = endpoint
112 .find("://")
113 .filter(|&i| !endpoint[..i].contains('/'))
114 {
115 i + 3
116 } else {
117 0
118 };
119 let authority_end = endpoint[authority_start..]
120 .find('/')
121 .map_or(endpoint.len(), |i| authority_start + i);
122 if let Some(at) = endpoint[authority_start..authority_end].rfind('@') {
123 endpoint.replace_range(authority_start..=authority_start + at, "");
124 }
125}
126
127pub(crate) fn sanitize_source_endpoint(endpoint: &mut String) {
128 strip_endpoint_secrets(endpoint);
129 truncate_field(endpoint, MAX_SOURCE_LENGTH);
130}
131
132fn sanitize_optional_arc_str(field: &mut Option<Arc<str>>, max_len: usize) {
138 if field
139 .as_deref()
140 .is_some_and(crate::config::has_control_char)
141 {
142 *field = None;
143 return;
144 }
145 if let Some(s) = field.as_ref()
146 && s.len() > max_len
147 {
148 let mut tmp = s.to_string();
149 truncate_field(&mut tmp, max_len);
150 *field = Some(Arc::from(tmp));
151 }
152}
153
154fn truncate_arc_str(field: &mut Arc<str>, max_len: usize) {
160 if field.len() <= max_len {
161 return;
162 }
163 let mut tmp = field.to_string();
164 truncate_field(&mut tmp, max_len);
165 *field = Arc::from(tmp);
166}
167
168fn sanitize_arc_str_vec(field: &mut Vec<Arc<str>>, max_len: usize, max_count: usize) {
177 field.retain(|s| !crate::config::has_control_char(s));
178 if field.len() > max_count {
179 field.truncate(max_count);
180 }
181 for s in field.iter_mut() {
182 truncate_arc_str(s, max_len);
183 }
184}
185
186const MAX_TIMESTAMP_LENGTH: usize = 64;
191
192pub fn sanitize_span_event(event: &mut SpanEvent) {
198 truncate_field(&mut event.timestamp, MAX_TIMESTAMP_LENGTH);
199 truncate_field(&mut event.trace_id, MAX_ID_LENGTH);
200 truncate_field(&mut event.span_id, MAX_ID_LENGTH);
201 if let Some(ref mut pid) = event.parent_span_id {
202 truncate_field(pid, MAX_ID_LENGTH);
203 }
204 sanitize_optional_arc_str(&mut event.cloud_region, MAX_ID_LENGTH);
209 event.grouping.retain_mut(|attr| {
215 let mut key = Some(Arc::clone(&attr.key));
216 let mut value = Some(Arc::clone(&attr.value));
217 sanitize_optional_arc_str(&mut key, MAX_SERVICE_LENGTH);
218 sanitize_optional_arc_str(&mut value, MAX_SERVICE_LENGTH);
219 match (key, value) {
220 (Some(k), Some(v)) if !k.is_empty() && !v.is_empty() => {
221 attr.key = k;
222 attr.value = v;
223 true
224 }
225 _ => false,
226 }
227 });
228 event
229 .grouping
230 .truncate(crate::config::MAX_GROUPING_ATTRIBUTES);
231 sanitize_optional_arc_str(&mut event.link_trace_id, MAX_ID_LENGTH);
232 truncate_arc_str(&mut event.service, MAX_SERVICE_LENGTH);
233 truncate_field(&mut event.operation, MAX_OPERATION_LENGTH);
234 if event.event_type == EventType::Messaging && event.target.contains("://") {
238 strip_endpoint_secrets(&mut event.target);
239 }
240 truncate_field(&mut event.target, MAX_TARGET_LENGTH);
241 sanitize_source_endpoint(&mut event.source.endpoint);
242 truncate_field(&mut event.source.method, MAX_SOURCE_LENGTH);
243 sanitize_optional_arc_str(&mut event.code_function, MAX_CODE_FUNCTION_LENGTH);
244 sanitize_optional_arc_str(&mut event.code_filepath, MAX_CODE_FILEPATH_LENGTH);
245 sanitize_optional_arc_str(&mut event.code_namespace, MAX_CODE_NAMESPACE_LENGTH);
246 sanitize_arc_str_vec(
247 &mut event.instrumentation_scopes,
248 MAX_SCOPE_NAME_LENGTH,
249 MAX_INSTRUMENTATION_SCOPES,
250 );
251}
252
253#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
259pub struct CodeLocation {
260 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub function: Option<String>,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub filepath: Option<String>,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub lineno: Option<u32>,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub namespace: Option<String>,
268}
269
270impl CodeLocation {
271 #[must_use]
273 pub fn is_empty(&self) -> bool {
274 self.function.is_none()
275 && self.filepath.is_none()
276 && self.lineno.is_none()
277 && self.namespace.is_none()
278 }
279
280 #[must_use]
288 pub fn display_string(&self) -> String {
289 let mut src = String::new();
290 if let Some(ref ns) = self.namespace {
291 src.push_str(ns);
292 src.push('.');
293 }
294 if let Some(ref func) = self.function {
295 src.push_str(func);
296 }
297 let has_name = !src.is_empty();
298 if let Some(ref fp) = self.filepath {
299 if has_name {
300 src.push_str(" (");
301 }
302 src.push_str(fp);
303 if let Some(ln) = self.lineno {
304 src.push(':');
305 src.push_str(&ln.to_string());
306 }
307 if has_name {
308 src.push(')');
309 }
310 }
311 src
312 }
313}
314
315#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
318pub struct GroupingAttribute {
319 pub key: Arc<str>,
320 pub value: Arc<str>,
321}
322
323impl GroupingAttribute {
324 #[must_use]
328 pub fn identity(&self) -> (&str, &str) {
329 (self.key.as_ref(), self.value.as_ref())
330 }
331}
332
333#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
338pub struct SpanEvent {
339 pub timestamp: String,
340 pub trace_id: String,
341 pub span_id: String,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub parent_span_id: Option<String>,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub link_trace_id: Option<Arc<str>>,
349 pub service: Arc<str>,
350 #[serde(default, skip_serializing_if = "Vec::is_empty")]
355 pub grouping: Vec<GroupingAttribute>,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub cloud_region: Option<Arc<str>>,
365 #[serde(rename = "type")]
366 pub event_type: EventType,
367 pub operation: String,
370 pub target: String,
371 pub duration_us: u64,
372 pub source: EventSource,
373 #[serde(skip_serializing_if = "Option::is_none")]
374 pub status_code: Option<u16>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
381 pub response_size_bytes: Option<u64>,
382 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub code_function: Option<Arc<str>>,
385 #[serde(default, skip_serializing_if = "Option::is_none")]
387 pub code_filepath: Option<Arc<str>>,
388 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub code_lineno: Option<u32>,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
393 pub code_namespace: Option<Arc<str>>,
394 #[serde(default, skip_serializing_if = "Vec::is_empty")]
401 pub instrumentation_scopes: Vec<Arc<str>>,
402}
403
404impl SpanEvent {
405 #[must_use]
407 pub fn effective_grouping(&self) -> Option<&GroupingAttribute> {
408 self.grouping.first()
409 }
410
411 #[must_use]
413 pub fn grouping_value(&self) -> Option<&str> {
414 self.grouping.first().map(|g| g.value.as_ref())
415 }
416
417 #[must_use]
419 pub fn grouping_identity(&self) -> Option<(&str, &str)> {
420 self.grouping.first().map(GroupingAttribute::identity)
421 }
422
423 #[must_use]
427 pub fn code_location(&self) -> Option<CodeLocation> {
428 if self.code_function.is_none()
429 && self.code_filepath.is_none()
430 && self.code_lineno.is_none()
431 && self.code_namespace.is_none()
432 {
433 return None;
434 }
435 Some(CodeLocation {
436 function: self.code_function.as_deref().map(String::from),
437 filepath: self.code_filepath.as_deref().map(String::from),
438 lineno: self.code_lineno,
439 namespace: self.code_namespace.as_deref().map(String::from),
440 })
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 fn code_location_display_string_full() {
450 let loc = CodeLocation {
451 function: Some("OrderItemRepository.findByOrderId".to_string()),
452 filepath: Some("order-service/src/main/java/OrderItemRepository.java".to_string()),
453 lineno: Some(42),
454 namespace: Some("com.example.order.repository".to_string()),
455 };
456 assert_eq!(
457 loc.display_string(),
458 "com.example.order.repository.OrderItemRepository.findByOrderId \
459 (order-service/src/main/java/OrderItemRepository.java:42)"
460 );
461 }
462
463 #[test]
464 fn code_location_display_string_function_only() {
465 let loc = CodeLocation {
466 function: Some("fetchUser".to_string()),
467 filepath: None,
468 lineno: None,
469 namespace: None,
470 };
471 assert_eq!(loc.display_string(), "fetchUser");
472 }
473
474 #[test]
475 fn code_location_display_string_filepath_only() {
476 let loc = CodeLocation {
477 function: None,
478 filepath: Some("src/main.rs".to_string()),
479 lineno: Some(7),
480 namespace: None,
481 };
482 assert_eq!(loc.display_string(), "src/main.rs:7");
485 }
486
487 #[test]
488 fn code_location_display_string_empty_when_all_none() {
489 let loc = CodeLocation {
490 function: None,
491 filepath: None,
492 lineno: None,
493 namespace: None,
494 };
495 assert_eq!(loc.display_string(), "");
496 assert!(loc.is_empty());
497 }
498
499 fn sample_sql_json() -> &'static str {
500 r#"{
501 "timestamp": "2025-07-10T14:32:01.123Z",
502 "trace_id": "abc123-def456",
503 "span_id": "span-789",
504 "service": "order-svc",
505 "type": "sql",
506 "operation": "SELECT",
507 "target": "SELECT * FROM order_item WHERE order_id = 42",
508 "duration_us": 1200,
509 "source": {
510 "endpoint": "POST /api/orders/42/submit",
511 "method": "OrderService::create_order"
512 }
513 }"#
514 }
515
516 fn sample_http_json() -> &'static str {
517 r#"{
518 "timestamp": "2025-07-10T14:32:01.456Z",
519 "trace_id": "abc123-def456",
520 "span_id": "span-790",
521 "service": "order-svc",
522 "type": "http_out",
523 "operation": "GET",
524 "target": "http://user-svc:5000/api/users/user-123",
525 "duration_us": 15000,
526 "status_code": 200,
527 "source": {
528 "endpoint": "POST /api/orders/42/submit",
529 "method": "OrderService::create_order"
530 }
531 }"#
532 }
533
534 #[test]
535 fn deserialize_sql_event() {
536 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
537 assert_eq!(event.event_type, EventType::Sql);
538 assert_eq!(event.trace_id, "abc123-def456");
539 assert_eq!(&*event.service, "order-svc");
540 assert_eq!(event.target, "SELECT * FROM order_item WHERE order_id = 42");
541 assert_eq!(event.duration_us, 1200);
542 assert!(event.status_code.is_none());
543 }
544
545 #[test]
546 fn deserialize_http_event() {
547 let event: SpanEvent = serde_json::from_str(sample_http_json()).unwrap();
548 assert_eq!(event.event_type, EventType::HttpOut);
549 assert_eq!(event.status_code, Some(200));
550 assert_eq!(event.source.endpoint, "POST /api/orders/42/submit");
551 }
552
553 #[test]
554 fn serde_roundtrip_sql() {
555 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
556 let json = serde_json::to_string(&event).unwrap();
557 let back: SpanEvent = serde_json::from_str(&json).unwrap();
558 assert_eq!(event, back);
559 }
560
561 #[test]
562 fn serde_roundtrip_http() {
563 let event: SpanEvent = serde_json::from_str(sample_http_json()).unwrap();
564 let json = serde_json::to_string(&event).unwrap();
565 let back: SpanEvent = serde_json::from_str(&json).unwrap();
566 assert_eq!(event, back);
567 }
568
569 #[test]
570 fn sql_event_omits_status_code_in_json() {
571 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
572 let json = serde_json::to_string(&event).unwrap();
573 assert!(!json.contains("status_code"));
574 }
575
576 #[test]
577 fn deserialize_event_without_cloud_region_defaults_to_none() {
578 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
579 assert!(event.cloud_region.is_none());
580 }
581
582 #[test]
583 fn deserialize_event_without_grouping_defaults_to_empty() {
584 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
585 assert!(event.grouping.is_empty());
586 assert_eq!(event.grouping_value(), None);
587 }
588
589 #[test]
592 fn grouping_value_is_the_first_captured_attribute() {
593 let mut event = make_event_with_field("service", "svc");
594 event.grouping = vec![
595 GroupingAttribute {
596 key: Arc::from("k8s.namespace.name"),
597 value: Arc::from("prod-eu"),
598 },
599 GroupingAttribute {
600 key: Arc::from("service.namespace"),
601 value: Arc::from("payments"),
602 },
603 ];
604 assert_eq!(event.grouping_value(), Some("prod-eu"));
605 assert_eq!(
606 event.effective_grouping().map(|g| g.key.as_ref()),
607 Some("k8s.namespace.name"),
608 "the operator must be able to tell which attribute won"
609 );
610 assert_eq!(event.grouping[1].value.as_ref(), "payments");
611
612 event.grouping.clear();
613 assert_eq!(event.grouping_value(), None);
614 }
615
616 #[test]
617 fn serde_roundtrip_with_grouping() {
618 let mut value: serde_json::Value = serde_json::from_str(sample_sql_json()).unwrap();
619 value["grouping"] = serde_json::json!([
620 {"key": "tenant.id", "value": "acme"},
621 {"key": "k8s.namespace.name", "value": "shared-cluster"},
622 ]);
623
624 let event: SpanEvent = serde_json::from_value(value).unwrap();
625 assert_eq!(event.grouping_value(), Some("acme"));
626 assert_eq!(event.grouping.len(), 2);
627
628 let serialized = serde_json::to_string(&event).unwrap();
629 let back: SpanEvent = serde_json::from_str(&serialized).unwrap();
630 assert_eq!(back, event);
631 }
632
633 #[test]
634 fn serde_roundtrip_with_cloud_region() {
635 let json = r#"{
636 "timestamp": "2025-07-10T14:32:01.123Z",
637 "trace_id": "abc123-def456",
638 "span_id": "span-789",
639 "service": "order-svc",
640 "cloud_region": "eu-west-3",
641 "type": "sql",
642 "operation": "SELECT",
643 "target": "SELECT 1",
644 "duration_us": 1200,
645 "source": {
646 "endpoint": "POST /api/orders/42/submit",
647 "method": "OrderService::create_order"
648 }
649 }"#;
650 let event: SpanEvent = serde_json::from_str(json).unwrap();
651 assert_eq!(event.cloud_region.as_deref(), Some("eu-west-3"));
652 let serialized = serde_json::to_string(&event).unwrap();
653 assert!(serialized.contains("\"cloud_region\":\"eu-west-3\""));
654 let back: SpanEvent = serde_json::from_str(&serialized).unwrap();
655 assert_eq!(event, back);
656 }
657
658 #[test]
659 fn cloud_region_omitted_when_none() {
660 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
661 let json = serde_json::to_string(&event).unwrap();
662 assert!(!json.contains("cloud_region"));
663 }
664
665 #[test]
666 fn sanitize_id_short_unchanged() {
667 assert_eq!(sanitize_id("abc-123"), "abc-123");
668 }
669
670 #[test]
671 fn sanitize_id_truncates_long() {
672 let long = "a".repeat(200);
673 let result = sanitize_id(&long);
674 assert_eq!(result.len(), MAX_ID_LENGTH);
675 }
676
677 #[test]
678 fn sanitize_id_exact_length_unchanged() {
679 let exact = "b".repeat(MAX_ID_LENGTH);
680 assert_eq!(sanitize_id(&exact), exact);
681 }
682
683 #[test]
684 fn sanitize_id_multibyte_no_panic() {
685 let id = "\u{1F600}".repeat(50);
687 assert!(id.len() > MAX_ID_LENGTH);
688 let result = sanitize_id(&id);
689 assert!(result.len() <= MAX_ID_LENGTH);
690 assert!(result.is_char_boundary(result.len()));
692 }
693
694 #[test]
695 fn sanitize_id_two_byte_chars_no_panic() {
696 let id = "é".repeat(100); let result = sanitize_id(&id);
699 assert!(result.len() <= MAX_ID_LENGTH);
700 assert_eq!(result.len() % 2, 0);
702 }
703
704 fn make_event_with_field(field: &str, value: &str) -> SpanEvent {
709 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
710 match field {
711 "service" => event.service = Arc::from(value),
712 "operation" => event.operation = value.to_string(),
713 "target" => event.target = value.to_string(),
714 "endpoint" => event.source.endpoint = value.to_string(),
715 "method" => event.source.method = value.to_string(),
716 _ => panic!("unknown field: {field}"),
717 }
718 event
719 }
720
721 #[test]
722 fn sanitize_bounds_link_trace_id() {
723 let mut event = make_event_with_field("service", "svc");
724 event.link_trace_id = Some(Arc::from("x".repeat(500)));
725 sanitize_span_event(&mut event);
726 assert_eq!(
727 event.link_trace_id.as_deref().map(str::len),
728 Some(MAX_ID_LENGTH)
729 );
730
731 event.link_trace_id = Some(Arc::from("trace\u{7}id"));
732 sanitize_span_event(&mut event);
733 assert_eq!(event.link_trace_id, None);
734 }
735
736 #[test]
737 fn sanitize_strips_credentials_from_a_messaging_destination() {
738 let mut event = make_event_with_field("target", "amqp://svc:hunter2@rabbit:5672/orders");
741 event.event_type = EventType::Messaging;
742 sanitize_span_event(&mut event);
743 assert!(
744 !event.target.contains("hunter2"),
745 "credential survived: {}",
746 event.target
747 );
748 assert!(event.target.contains("rabbit"), "host must survive");
749 }
750
751 #[test]
752 fn sanitize_keeps_an_sqs_arn_intact() {
753 let arn = "arn:aws:sqs:eu-west-3:123456789012:orders";
756 let mut event = make_event_with_field("target", arn);
757 event.event_type = EventType::Messaging;
758 sanitize_span_event(&mut event);
759 assert_eq!(event.target, arn);
760 }
761
762 #[test]
763 fn sanitize_keeps_scheme_less_destinations_verbatim() {
764 for name in [
767 "ORDERS@QM1", "logs.#", "my.topic?v=2", "orders",
771 ] {
772 let mut event = make_event_with_field("target", name);
773 event.event_type = EventType::Messaging;
774 sanitize_span_event(&mut event);
775 assert_eq!(event.target, name, "rewrote a legitimate destination");
776 }
777 }
778
779 #[test]
780 fn sanitize_truncates_long_service() {
781 let mut event = make_event_with_field("service", &"x".repeat(500));
782 sanitize_span_event(&mut event);
783 assert!(event.service.len() <= MAX_SERVICE_LENGTH);
784 }
785
786 #[test]
790 fn sanitize_bounds_and_rejects_grouping_values() {
791 let mut event = make_event_with_field("service", "svc");
792 event.grouping = vec![
793 GroupingAttribute {
794 key: Arc::from("k8s.namespace.name"),
795 value: Arc::from("s".repeat(500)),
796 },
797 GroupingAttribute {
798 key: Arc::from("tenant.id"),
799 value: Arc::from("prod\u{7}hidden"),
800 },
801 GroupingAttribute {
802 key: Arc::from("service.namespace"),
803 value: Arc::from(""),
804 },
805 ];
806
807 sanitize_span_event(&mut event);
808
809 assert_eq!(event.grouping.len(), 1, "{:?}", event.grouping);
810 assert_eq!(
811 event.grouping_value().map(str::len),
812 Some(MAX_SERVICE_LENGTH),
813 "an over-long value is truncated, not dropped"
814 );
815 }
816
817 #[test]
818 fn sanitize_caps_oversize_grouping_vec() {
819 let mut event = make_event_with_field("service", "svc");
820 event.grouping = (0..32)
821 .map(|i| GroupingAttribute {
822 key: Arc::from(format!("tenant.dimension.{i}")),
823 value: Arc::from(format!("value-{i}")),
824 })
825 .collect();
826
827 sanitize_span_event(&mut event);
828
829 assert_eq!(event.grouping.len(), crate::config::MAX_GROUPING_ATTRIBUTES);
830 assert_eq!(event.grouping[0].key.as_ref(), "tenant.dimension.0");
831 }
832
833 #[test]
834 fn sanitize_truncates_long_operation() {
835 let mut event = make_event_with_field("operation", &"x".repeat(500));
836 sanitize_span_event(&mut event);
837 assert!(event.operation.len() <= MAX_OPERATION_LENGTH);
838 }
839
840 #[test]
841 fn sanitize_truncates_long_target() {
842 let mut event = make_event_with_field("target", &"x".repeat(100_000));
843 sanitize_span_event(&mut event);
844 assert!(event.target.len() <= MAX_TARGET_LENGTH);
845 }
846
847 #[test]
848 fn sanitize_truncates_long_endpoint() {
849 let mut event = make_event_with_field("endpoint", &"x".repeat(1000));
850 sanitize_span_event(&mut event);
851 assert!(event.source.endpoint.len() <= MAX_SOURCE_LENGTH);
852 }
853
854 #[test]
855 fn sanitize_truncates_long_method() {
856 let mut event = make_event_with_field("method", &"x".repeat(1000));
857 sanitize_span_event(&mut event);
858 assert!(event.source.method.len() <= MAX_SOURCE_LENGTH);
859 }
860
861 #[test]
862 fn sanitize_strips_endpoint_query_and_userinfo() {
863 for (raw, expected) in [
864 (
865 "https://svc/api/users?token=SECRET",
866 "https://svc/api/users",
867 ),
868 ("http://user:pass@host/cb?code=abc#frag", "http://host/cb"),
869 ("/api/reset?token=SECRET", "/api/reset"),
870 ("http://svc?filter=a@b&token=SECRET", "http://svc"),
872 ("http://host?a=x@y", "http://host"),
873 ("//user:tok@bmc/cb?redirect=https://app", "//bmc/cb"),
874 ("user:pass@host/cb?next=https://x", "host/cb"),
875 ("user:pass@host/path", "host/path"),
877 ("//svc-user:tok@order-svc/cb", "//order-svc/cb"),
878 ("/users/a@b.example/orders", "/users/a@b.example/orders"),
880 ("POST /api/orders/{id}", "POST /api/orders/{id}"),
882 (
883 "http://order-svc/api/orders/42",
884 "http://order-svc/api/orders/42",
885 ),
886 ] {
887 let mut event = make_event_with_field("endpoint", raw);
888 sanitize_span_event(&mut event);
889 assert_eq!(event.source.endpoint, expected, "input: {raw}");
890 }
891 }
892
893 #[test]
894 fn sanitize_short_fields_unchanged() {
895 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
896 let before = event.clone();
897 sanitize_span_event(&mut event);
898 assert_eq!(event, before);
899 }
900
901 #[test]
902 fn sanitize_multibyte_char_boundary() {
903 let mut event = make_event_with_field("service", &"\u{1F600}".repeat(100));
905 sanitize_span_event(&mut event);
906 assert!(event.service.len() <= MAX_SERVICE_LENGTH);
907 assert!(event.service.is_char_boundary(event.service.len()));
909 }
910
911 #[test]
916 fn code_location_is_empty_when_all_none() {
917 let loc = CodeLocation {
918 function: None,
919 filepath: None,
920 lineno: None,
921 namespace: None,
922 };
923 assert!(loc.is_empty());
924 }
925
926 #[test]
927 fn code_location_not_empty_with_function() {
928 let loc = CodeLocation {
929 function: Some("processItems".to_string()),
930 filepath: None,
931 lineno: None,
932 namespace: None,
933 };
934 assert!(!loc.is_empty());
935 }
936
937 #[test]
938 fn span_event_code_location_none_when_all_absent() {
939 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
940 assert!(event.code_location().is_none());
941 }
942
943 #[test]
944 fn span_event_code_location_some_when_present() {
945 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
946 event.code_function = Some(Arc::from("processItems"));
947 event.code_filepath = Some(Arc::from("src/OrderService.java"));
948 event.code_lineno = Some(42);
949 event.code_namespace = Some(Arc::from("com.example"));
950 let loc = event.code_location().unwrap();
951 assert_eq!(loc.function.as_deref(), Some("processItems"));
952 assert_eq!(loc.filepath.as_deref(), Some("src/OrderService.java"));
953 assert_eq!(loc.lineno, Some(42));
954 assert_eq!(loc.namespace.as_deref(), Some("com.example"));
955 }
956
957 #[test]
958 fn serde_roundtrip_with_code_fields() {
959 let json = r#"{
960 "timestamp": "2025-07-10T14:32:01.123Z",
961 "trace_id": "abc123",
962 "span_id": "span-1",
963 "service": "svc",
964 "type": "sql",
965 "operation": "SELECT",
966 "target": "SELECT 1",
967 "duration_us": 100,
968 "source": { "endpoint": "GET /test", "method": "test" },
969 "code_function": "processItems",
970 "code_filepath": "src/OrderService.java",
971 "code_lineno": 42,
972 "code_namespace": "com.example"
973 }"#;
974 let event: SpanEvent = serde_json::from_str(json).unwrap();
975 assert_eq!(event.code_function.as_deref(), Some("processItems"));
976 assert_eq!(event.code_lineno, Some(42));
977 let serialized = serde_json::to_string(&event).unwrap();
978 let back: SpanEvent = serde_json::from_str(&serialized).unwrap();
979 assert_eq!(event, back);
980 }
981
982 #[test]
983 fn code_fields_omitted_when_none() {
984 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
985 let json = serde_json::to_string(&event).unwrap();
986 assert!(!json.contains("code_function"));
987 assert!(!json.contains("code_filepath"));
988 assert!(!json.contains("code_lineno"));
989 assert!(!json.contains("code_namespace"));
990 }
991
992 #[test]
993 fn sanitize_truncates_long_code_function() {
994 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
995 event.code_function = Some(Arc::from("x".repeat(1000)));
996 sanitize_span_event(&mut event);
997 assert!(event.code_function.as_ref().unwrap().len() <= MAX_CODE_FUNCTION_LENGTH);
998 }
999
1000 #[test]
1001 fn sanitize_truncates_long_code_filepath() {
1002 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
1003 event.code_filepath = Some(Arc::from("x".repeat(2000)));
1004 sanitize_span_event(&mut event);
1005 assert!(event.code_filepath.as_ref().unwrap().len() <= MAX_CODE_FILEPATH_LENGTH);
1006 }
1007
1008 #[test]
1009 fn sanitize_drops_code_function_with_control_char() {
1010 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
1011 event.code_function = Some(Arc::from("findItems\x1b[31m"));
1012 sanitize_span_event(&mut event);
1013 assert!(event.code_function.is_none());
1014 }
1015
1016 #[test]
1017 fn sanitize_drops_code_filepath_with_newline() {
1018 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
1019 event.code_filepath = Some(Arc::from("src/main.rs\nINJECT"));
1020 sanitize_span_event(&mut event);
1021 assert!(event.code_filepath.is_none());
1022 }
1023
1024 #[test]
1025 fn sanitize_drops_code_namespace_with_del() {
1026 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
1027 event.code_namespace = Some(Arc::from("com.foo\x7fX"));
1028 sanitize_span_event(&mut event);
1029 assert!(event.code_namespace.is_none());
1030 }
1031
1032 #[test]
1033 fn sanitize_keeps_clean_code_fields() {
1034 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
1035 event.code_function = Some(Arc::from("findItems"));
1036 event.code_filepath = Some(Arc::from("src/main/java/com/foo/Repo.java"));
1037 event.code_namespace = Some(Arc::from("com.foo.Repo"));
1038 sanitize_span_event(&mut event);
1039 assert_eq!(event.code_function.as_deref(), Some("findItems"));
1040 assert_eq!(
1041 event.code_filepath.as_deref(),
1042 Some("src/main/java/com/foo/Repo.java")
1043 );
1044 assert_eq!(event.code_namespace.as_deref(), Some("com.foo.Repo"));
1045 }
1046
1047 #[test]
1050 fn sanitize_truncates_long_instrumentation_scope() {
1051 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
1052 event.instrumentation_scopes = vec![Arc::from("x".repeat(1024))];
1053 sanitize_span_event(&mut event);
1054 assert_eq!(event.instrumentation_scopes.len(), 1);
1055 assert!(event.instrumentation_scopes[0].len() <= MAX_SCOPE_NAME_LENGTH);
1056 }
1057
1058 #[test]
1059 fn sanitize_drops_instrumentation_scope_with_control_char() {
1060 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
1061 event.instrumentation_scopes = vec![
1062 Arc::from("io.opentelemetry.spring-data"),
1063 Arc::from("\x1b[31mio.opentelemetry.evil\x1b[0m"),
1064 Arc::from("io.opentelemetry.hibernate"),
1065 ];
1066 sanitize_span_event(&mut event);
1067 let scopes: Vec<&str> = event
1068 .instrumentation_scopes
1069 .iter()
1070 .map(AsRef::as_ref)
1071 .collect();
1072 assert_eq!(
1073 scopes,
1074 vec!["io.opentelemetry.spring-data", "io.opentelemetry.hibernate"]
1075 );
1076 }
1077
1078 #[test]
1079 fn sanitize_caps_oversize_instrumentation_scopes_vec() {
1080 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
1081 event.instrumentation_scopes = (0..32)
1082 .map(|i| Arc::from(format!("io.opentelemetry.scope-{i}")))
1083 .collect();
1084 sanitize_span_event(&mut event);
1085 assert_eq!(
1086 event.instrumentation_scopes.len(),
1087 MAX_INSTRUMENTATION_SCOPES
1088 );
1089 assert_eq!(
1090 &*event.instrumentation_scopes[0],
1091 "io.opentelemetry.scope-0"
1092 );
1093 }
1094}