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
127fn sanitize_optional_arc_str(field: &mut Option<Arc<str>>, max_len: usize) {
133 if field
134 .as_deref()
135 .is_some_and(crate::config::has_control_char)
136 {
137 *field = None;
138 return;
139 }
140 if let Some(s) = field.as_ref()
141 && s.len() > max_len
142 {
143 let mut tmp = s.to_string();
144 truncate_field(&mut tmp, max_len);
145 *field = Some(Arc::from(tmp));
146 }
147}
148
149fn truncate_arc_str(field: &mut Arc<str>, max_len: usize) {
155 if field.len() <= max_len {
156 return;
157 }
158 let mut tmp = field.to_string();
159 truncate_field(&mut tmp, max_len);
160 *field = Arc::from(tmp);
161}
162
163fn sanitize_arc_str_vec(field: &mut Vec<Arc<str>>, max_len: usize, max_count: usize) {
172 field.retain(|s| !crate::config::has_control_char(s));
173 if field.len() > max_count {
174 field.truncate(max_count);
175 }
176 for s in field.iter_mut() {
177 truncate_arc_str(s, max_len);
178 }
179}
180
181const MAX_TIMESTAMP_LENGTH: usize = 64;
186
187pub fn sanitize_span_event(event: &mut SpanEvent) {
193 truncate_field(&mut event.timestamp, MAX_TIMESTAMP_LENGTH);
194 truncate_field(&mut event.trace_id, MAX_ID_LENGTH);
195 truncate_field(&mut event.span_id, MAX_ID_LENGTH);
196 if let Some(ref mut pid) = event.parent_span_id {
197 truncate_field(pid, MAX_ID_LENGTH);
198 }
199 sanitize_optional_arc_str(&mut event.cloud_region, MAX_ID_LENGTH);
204 event.grouping.retain_mut(|attr| {
210 let mut key = Some(Arc::clone(&attr.key));
211 let mut value = Some(Arc::clone(&attr.value));
212 sanitize_optional_arc_str(&mut key, MAX_SERVICE_LENGTH);
213 sanitize_optional_arc_str(&mut value, MAX_SERVICE_LENGTH);
214 match (key, value) {
215 (Some(k), Some(v)) if !k.is_empty() && !v.is_empty() => {
216 attr.key = k;
217 attr.value = v;
218 true
219 }
220 _ => false,
221 }
222 });
223 event
224 .grouping
225 .truncate(crate::config::MAX_GROUPING_ATTRIBUTES);
226 sanitize_optional_arc_str(&mut event.link_trace_id, MAX_ID_LENGTH);
227 truncate_arc_str(&mut event.service, MAX_SERVICE_LENGTH);
228 truncate_field(&mut event.operation, MAX_OPERATION_LENGTH);
229 if event.event_type == EventType::Messaging && event.target.contains("://") {
233 strip_endpoint_secrets(&mut event.target);
234 }
235 truncate_field(&mut event.target, MAX_TARGET_LENGTH);
236 strip_endpoint_secrets(&mut event.source.endpoint);
237 truncate_field(&mut event.source.endpoint, MAX_SOURCE_LENGTH);
238 truncate_field(&mut event.source.method, MAX_SOURCE_LENGTH);
239 sanitize_optional_arc_str(&mut event.code_function, MAX_CODE_FUNCTION_LENGTH);
240 sanitize_optional_arc_str(&mut event.code_filepath, MAX_CODE_FILEPATH_LENGTH);
241 sanitize_optional_arc_str(&mut event.code_namespace, MAX_CODE_NAMESPACE_LENGTH);
242 sanitize_arc_str_vec(
243 &mut event.instrumentation_scopes,
244 MAX_SCOPE_NAME_LENGTH,
245 MAX_INSTRUMENTATION_SCOPES,
246 );
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255pub struct CodeLocation {
256 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub function: Option<String>,
258 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub filepath: Option<String>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub lineno: Option<u32>,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub namespace: Option<String>,
264}
265
266impl CodeLocation {
267 #[must_use]
269 pub fn is_empty(&self) -> bool {
270 self.function.is_none()
271 && self.filepath.is_none()
272 && self.lineno.is_none()
273 && self.namespace.is_none()
274 }
275
276 #[must_use]
284 pub fn display_string(&self) -> String {
285 let mut src = String::new();
286 if let Some(ref ns) = self.namespace {
287 src.push_str(ns);
288 src.push('.');
289 }
290 if let Some(ref func) = self.function {
291 src.push_str(func);
292 }
293 let has_name = !src.is_empty();
294 if let Some(ref fp) = self.filepath {
295 if has_name {
296 src.push_str(" (");
297 }
298 src.push_str(fp);
299 if let Some(ln) = self.lineno {
300 src.push(':');
301 src.push_str(&ln.to_string());
302 }
303 if has_name {
304 src.push(')');
305 }
306 }
307 src
308 }
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
314pub struct GroupingAttribute {
315 pub key: Arc<str>,
316 pub value: Arc<str>,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
324pub struct SpanEvent {
325 pub timestamp: String,
326 pub trace_id: String,
327 pub span_id: String,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub parent_span_id: Option<String>,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub link_trace_id: Option<Arc<str>>,
335 pub service: Arc<str>,
336 #[serde(default, skip_serializing_if = "Vec::is_empty")]
341 pub grouping: Vec<GroupingAttribute>,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub cloud_region: Option<Arc<str>>,
351 #[serde(rename = "type")]
352 pub event_type: EventType,
353 pub operation: String,
356 pub target: String,
357 pub duration_us: u64,
358 pub source: EventSource,
359 #[serde(skip_serializing_if = "Option::is_none")]
360 pub status_code: Option<u16>,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
367 pub response_size_bytes: Option<u64>,
368 #[serde(default, skip_serializing_if = "Option::is_none")]
370 pub code_function: Option<Arc<str>>,
371 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub code_filepath: Option<Arc<str>>,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub code_lineno: Option<u32>,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub code_namespace: Option<Arc<str>>,
380 #[serde(default, skip_serializing_if = "Vec::is_empty")]
387 pub instrumentation_scopes: Vec<Arc<str>>,
388}
389
390impl SpanEvent {
391 #[must_use]
393 pub fn effective_grouping(&self) -> Option<&GroupingAttribute> {
394 self.grouping.first()
395 }
396
397 #[must_use]
399 pub fn grouping_value(&self) -> Option<&str> {
400 self.grouping.first().map(|g| g.value.as_ref())
401 }
402
403 #[must_use]
407 pub fn grouping_identity(&self) -> Option<(&str, &str)> {
408 self.grouping
409 .first()
410 .map(|g| (g.key.as_ref(), g.value.as_ref()))
411 }
412
413 #[must_use]
417 pub fn code_location(&self) -> Option<CodeLocation> {
418 if self.code_function.is_none()
419 && self.code_filepath.is_none()
420 && self.code_lineno.is_none()
421 && self.code_namespace.is_none()
422 {
423 return None;
424 }
425 Some(CodeLocation {
426 function: self.code_function.as_deref().map(String::from),
427 filepath: self.code_filepath.as_deref().map(String::from),
428 lineno: self.code_lineno,
429 namespace: self.code_namespace.as_deref().map(String::from),
430 })
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[test]
439 fn code_location_display_string_full() {
440 let loc = CodeLocation {
441 function: Some("OrderItemRepository.findByOrderId".to_string()),
442 filepath: Some("order-service/src/main/java/OrderItemRepository.java".to_string()),
443 lineno: Some(42),
444 namespace: Some("com.example.order.repository".to_string()),
445 };
446 assert_eq!(
447 loc.display_string(),
448 "com.example.order.repository.OrderItemRepository.findByOrderId \
449 (order-service/src/main/java/OrderItemRepository.java:42)"
450 );
451 }
452
453 #[test]
454 fn code_location_display_string_function_only() {
455 let loc = CodeLocation {
456 function: Some("fetchUser".to_string()),
457 filepath: None,
458 lineno: None,
459 namespace: None,
460 };
461 assert_eq!(loc.display_string(), "fetchUser");
462 }
463
464 #[test]
465 fn code_location_display_string_filepath_only() {
466 let loc = CodeLocation {
467 function: None,
468 filepath: Some("src/main.rs".to_string()),
469 lineno: Some(7),
470 namespace: None,
471 };
472 assert_eq!(loc.display_string(), "src/main.rs:7");
475 }
476
477 #[test]
478 fn code_location_display_string_empty_when_all_none() {
479 let loc = CodeLocation {
480 function: None,
481 filepath: None,
482 lineno: None,
483 namespace: None,
484 };
485 assert_eq!(loc.display_string(), "");
486 assert!(loc.is_empty());
487 }
488
489 fn sample_sql_json() -> &'static str {
490 r#"{
491 "timestamp": "2025-07-10T14:32:01.123Z",
492 "trace_id": "abc123-def456",
493 "span_id": "span-789",
494 "service": "order-svc",
495 "type": "sql",
496 "operation": "SELECT",
497 "target": "SELECT * FROM order_item WHERE order_id = 42",
498 "duration_us": 1200,
499 "source": {
500 "endpoint": "POST /api/orders/42/submit",
501 "method": "OrderService::create_order"
502 }
503 }"#
504 }
505
506 fn sample_http_json() -> &'static str {
507 r#"{
508 "timestamp": "2025-07-10T14:32:01.456Z",
509 "trace_id": "abc123-def456",
510 "span_id": "span-790",
511 "service": "order-svc",
512 "type": "http_out",
513 "operation": "GET",
514 "target": "http://user-svc:5000/api/users/user-123",
515 "duration_us": 15000,
516 "status_code": 200,
517 "source": {
518 "endpoint": "POST /api/orders/42/submit",
519 "method": "OrderService::create_order"
520 }
521 }"#
522 }
523
524 #[test]
525 fn deserialize_sql_event() {
526 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
527 assert_eq!(event.event_type, EventType::Sql);
528 assert_eq!(event.trace_id, "abc123-def456");
529 assert_eq!(&*event.service, "order-svc");
530 assert_eq!(event.target, "SELECT * FROM order_item WHERE order_id = 42");
531 assert_eq!(event.duration_us, 1200);
532 assert!(event.status_code.is_none());
533 }
534
535 #[test]
536 fn deserialize_http_event() {
537 let event: SpanEvent = serde_json::from_str(sample_http_json()).unwrap();
538 assert_eq!(event.event_type, EventType::HttpOut);
539 assert_eq!(event.status_code, Some(200));
540 assert_eq!(event.source.endpoint, "POST /api/orders/42/submit");
541 }
542
543 #[test]
544 fn serde_roundtrip_sql() {
545 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
546 let json = serde_json::to_string(&event).unwrap();
547 let back: SpanEvent = serde_json::from_str(&json).unwrap();
548 assert_eq!(event, back);
549 }
550
551 #[test]
552 fn serde_roundtrip_http() {
553 let event: SpanEvent = serde_json::from_str(sample_http_json()).unwrap();
554 let json = serde_json::to_string(&event).unwrap();
555 let back: SpanEvent = serde_json::from_str(&json).unwrap();
556 assert_eq!(event, back);
557 }
558
559 #[test]
560 fn sql_event_omits_status_code_in_json() {
561 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
562 let json = serde_json::to_string(&event).unwrap();
563 assert!(!json.contains("status_code"));
564 }
565
566 #[test]
567 fn deserialize_event_without_cloud_region_defaults_to_none() {
568 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
569 assert!(event.cloud_region.is_none());
570 }
571
572 #[test]
573 fn deserialize_event_without_grouping_defaults_to_empty() {
574 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
575 assert!(event.grouping.is_empty());
576 assert_eq!(event.grouping_value(), None);
577 }
578
579 #[test]
582 fn grouping_value_is_the_first_captured_attribute() {
583 let mut event = make_event_with_field("service", "svc");
584 event.grouping = vec![
585 GroupingAttribute {
586 key: Arc::from("k8s.namespace.name"),
587 value: Arc::from("prod-eu"),
588 },
589 GroupingAttribute {
590 key: Arc::from("service.namespace"),
591 value: Arc::from("payments"),
592 },
593 ];
594 assert_eq!(event.grouping_value(), Some("prod-eu"));
595 assert_eq!(
596 event.effective_grouping().map(|g| g.key.as_ref()),
597 Some("k8s.namespace.name"),
598 "the operator must be able to tell which attribute won"
599 );
600 assert_eq!(event.grouping[1].value.as_ref(), "payments");
601
602 event.grouping.clear();
603 assert_eq!(event.grouping_value(), None);
604 }
605
606 #[test]
607 fn serde_roundtrip_with_grouping() {
608 let mut value: serde_json::Value = serde_json::from_str(sample_sql_json()).unwrap();
609 value["grouping"] = serde_json::json!([
610 {"key": "tenant.id", "value": "acme"},
611 {"key": "k8s.namespace.name", "value": "shared-cluster"},
612 ]);
613
614 let event: SpanEvent = serde_json::from_value(value).unwrap();
615 assert_eq!(event.grouping_value(), Some("acme"));
616 assert_eq!(event.grouping.len(), 2);
617
618 let serialized = serde_json::to_string(&event).unwrap();
619 let back: SpanEvent = serde_json::from_str(&serialized).unwrap();
620 assert_eq!(back, event);
621 }
622
623 #[test]
624 fn serde_roundtrip_with_cloud_region() {
625 let json = r#"{
626 "timestamp": "2025-07-10T14:32:01.123Z",
627 "trace_id": "abc123-def456",
628 "span_id": "span-789",
629 "service": "order-svc",
630 "cloud_region": "eu-west-3",
631 "type": "sql",
632 "operation": "SELECT",
633 "target": "SELECT 1",
634 "duration_us": 1200,
635 "source": {
636 "endpoint": "POST /api/orders/42/submit",
637 "method": "OrderService::create_order"
638 }
639 }"#;
640 let event: SpanEvent = serde_json::from_str(json).unwrap();
641 assert_eq!(event.cloud_region.as_deref(), Some("eu-west-3"));
642 let serialized = serde_json::to_string(&event).unwrap();
643 assert!(serialized.contains("\"cloud_region\":\"eu-west-3\""));
644 let back: SpanEvent = serde_json::from_str(&serialized).unwrap();
645 assert_eq!(event, back);
646 }
647
648 #[test]
649 fn cloud_region_omitted_when_none() {
650 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
651 let json = serde_json::to_string(&event).unwrap();
652 assert!(!json.contains("cloud_region"));
653 }
654
655 #[test]
656 fn sanitize_id_short_unchanged() {
657 assert_eq!(sanitize_id("abc-123"), "abc-123");
658 }
659
660 #[test]
661 fn sanitize_id_truncates_long() {
662 let long = "a".repeat(200);
663 let result = sanitize_id(&long);
664 assert_eq!(result.len(), MAX_ID_LENGTH);
665 }
666
667 #[test]
668 fn sanitize_id_exact_length_unchanged() {
669 let exact = "b".repeat(MAX_ID_LENGTH);
670 assert_eq!(sanitize_id(&exact), exact);
671 }
672
673 #[test]
674 fn sanitize_id_multibyte_no_panic() {
675 let id = "\u{1F600}".repeat(50);
677 assert!(id.len() > MAX_ID_LENGTH);
678 let result = sanitize_id(&id);
679 assert!(result.len() <= MAX_ID_LENGTH);
680 assert!(result.is_char_boundary(result.len()));
682 }
683
684 #[test]
685 fn sanitize_id_two_byte_chars_no_panic() {
686 let id = "é".repeat(100); let result = sanitize_id(&id);
689 assert!(result.len() <= MAX_ID_LENGTH);
690 assert_eq!(result.len() % 2, 0);
692 }
693
694 fn make_event_with_field(field: &str, value: &str) -> SpanEvent {
699 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
700 match field {
701 "service" => event.service = Arc::from(value),
702 "operation" => event.operation = value.to_string(),
703 "target" => event.target = value.to_string(),
704 "endpoint" => event.source.endpoint = value.to_string(),
705 "method" => event.source.method = value.to_string(),
706 _ => panic!("unknown field: {field}"),
707 }
708 event
709 }
710
711 #[test]
712 fn sanitize_bounds_link_trace_id() {
713 let mut event = make_event_with_field("service", "svc");
714 event.link_trace_id = Some(Arc::from("x".repeat(500)));
715 sanitize_span_event(&mut event);
716 assert_eq!(
717 event.link_trace_id.as_deref().map(str::len),
718 Some(MAX_ID_LENGTH)
719 );
720
721 event.link_trace_id = Some(Arc::from("trace\u{7}id"));
722 sanitize_span_event(&mut event);
723 assert_eq!(event.link_trace_id, None);
724 }
725
726 #[test]
727 fn sanitize_strips_credentials_from_a_messaging_destination() {
728 let mut event = make_event_with_field("target", "amqp://svc:hunter2@rabbit:5672/orders");
731 event.event_type = EventType::Messaging;
732 sanitize_span_event(&mut event);
733 assert!(
734 !event.target.contains("hunter2"),
735 "credential survived: {}",
736 event.target
737 );
738 assert!(event.target.contains("rabbit"), "host must survive");
739 }
740
741 #[test]
742 fn sanitize_keeps_an_sqs_arn_intact() {
743 let arn = "arn:aws:sqs:eu-west-3:123456789012:orders";
746 let mut event = make_event_with_field("target", arn);
747 event.event_type = EventType::Messaging;
748 sanitize_span_event(&mut event);
749 assert_eq!(event.target, arn);
750 }
751
752 #[test]
753 fn sanitize_keeps_scheme_less_destinations_verbatim() {
754 for name in [
757 "ORDERS@QM1", "logs.#", "my.topic?v=2", "orders",
761 ] {
762 let mut event = make_event_with_field("target", name);
763 event.event_type = EventType::Messaging;
764 sanitize_span_event(&mut event);
765 assert_eq!(event.target, name, "rewrote a legitimate destination");
766 }
767 }
768
769 #[test]
770 fn sanitize_truncates_long_service() {
771 let mut event = make_event_with_field("service", &"x".repeat(500));
772 sanitize_span_event(&mut event);
773 assert!(event.service.len() <= MAX_SERVICE_LENGTH);
774 }
775
776 #[test]
780 fn sanitize_bounds_and_rejects_grouping_values() {
781 let mut event = make_event_with_field("service", "svc");
782 event.grouping = vec![
783 GroupingAttribute {
784 key: Arc::from("k8s.namespace.name"),
785 value: Arc::from("s".repeat(500)),
786 },
787 GroupingAttribute {
788 key: Arc::from("tenant.id"),
789 value: Arc::from("prod\u{7}hidden"),
790 },
791 GroupingAttribute {
792 key: Arc::from("service.namespace"),
793 value: Arc::from(""),
794 },
795 ];
796
797 sanitize_span_event(&mut event);
798
799 assert_eq!(event.grouping.len(), 1, "{:?}", event.grouping);
800 assert_eq!(
801 event.grouping_value().map(str::len),
802 Some(MAX_SERVICE_LENGTH),
803 "an over-long value is truncated, not dropped"
804 );
805 }
806
807 #[test]
808 fn sanitize_caps_oversize_grouping_vec() {
809 let mut event = make_event_with_field("service", "svc");
810 event.grouping = (0..32)
811 .map(|i| GroupingAttribute {
812 key: Arc::from(format!("tenant.dimension.{i}")),
813 value: Arc::from(format!("value-{i}")),
814 })
815 .collect();
816
817 sanitize_span_event(&mut event);
818
819 assert_eq!(event.grouping.len(), crate::config::MAX_GROUPING_ATTRIBUTES);
820 assert_eq!(event.grouping[0].key.as_ref(), "tenant.dimension.0");
821 }
822
823 #[test]
824 fn sanitize_truncates_long_operation() {
825 let mut event = make_event_with_field("operation", &"x".repeat(500));
826 sanitize_span_event(&mut event);
827 assert!(event.operation.len() <= MAX_OPERATION_LENGTH);
828 }
829
830 #[test]
831 fn sanitize_truncates_long_target() {
832 let mut event = make_event_with_field("target", &"x".repeat(100_000));
833 sanitize_span_event(&mut event);
834 assert!(event.target.len() <= MAX_TARGET_LENGTH);
835 }
836
837 #[test]
838 fn sanitize_truncates_long_endpoint() {
839 let mut event = make_event_with_field("endpoint", &"x".repeat(1000));
840 sanitize_span_event(&mut event);
841 assert!(event.source.endpoint.len() <= MAX_SOURCE_LENGTH);
842 }
843
844 #[test]
845 fn sanitize_truncates_long_method() {
846 let mut event = make_event_with_field("method", &"x".repeat(1000));
847 sanitize_span_event(&mut event);
848 assert!(event.source.method.len() <= MAX_SOURCE_LENGTH);
849 }
850
851 #[test]
852 fn sanitize_strips_endpoint_query_and_userinfo() {
853 for (raw, expected) in [
854 (
855 "https://svc/api/users?token=SECRET",
856 "https://svc/api/users",
857 ),
858 ("http://user:pass@host/cb?code=abc#frag", "http://host/cb"),
859 ("/api/reset?token=SECRET", "/api/reset"),
860 ("http://svc?filter=a@b&token=SECRET", "http://svc"),
862 ("http://host?a=x@y", "http://host"),
863 ("//user:tok@bmc/cb?redirect=https://app", "//bmc/cb"),
864 ("user:pass@host/cb?next=https://x", "host/cb"),
865 ("user:pass@host/path", "host/path"),
867 ("//svc-user:tok@order-svc/cb", "//order-svc/cb"),
868 ("/users/a@b.example/orders", "/users/a@b.example/orders"),
870 ("POST /api/orders/{id}", "POST /api/orders/{id}"),
872 (
873 "http://order-svc/api/orders/42",
874 "http://order-svc/api/orders/42",
875 ),
876 ] {
877 let mut event = make_event_with_field("endpoint", raw);
878 sanitize_span_event(&mut event);
879 assert_eq!(event.source.endpoint, expected, "input: {raw}");
880 }
881 }
882
883 #[test]
884 fn sanitize_short_fields_unchanged() {
885 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
886 let before = event.clone();
887 sanitize_span_event(&mut event);
888 assert_eq!(event, before);
889 }
890
891 #[test]
892 fn sanitize_multibyte_char_boundary() {
893 let mut event = make_event_with_field("service", &"\u{1F600}".repeat(100));
895 sanitize_span_event(&mut event);
896 assert!(event.service.len() <= MAX_SERVICE_LENGTH);
897 assert!(event.service.is_char_boundary(event.service.len()));
899 }
900
901 #[test]
906 fn code_location_is_empty_when_all_none() {
907 let loc = CodeLocation {
908 function: None,
909 filepath: None,
910 lineno: None,
911 namespace: None,
912 };
913 assert!(loc.is_empty());
914 }
915
916 #[test]
917 fn code_location_not_empty_with_function() {
918 let loc = CodeLocation {
919 function: Some("processItems".to_string()),
920 filepath: None,
921 lineno: None,
922 namespace: None,
923 };
924 assert!(!loc.is_empty());
925 }
926
927 #[test]
928 fn span_event_code_location_none_when_all_absent() {
929 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
930 assert!(event.code_location().is_none());
931 }
932
933 #[test]
934 fn span_event_code_location_some_when_present() {
935 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
936 event.code_function = Some(Arc::from("processItems"));
937 event.code_filepath = Some(Arc::from("src/OrderService.java"));
938 event.code_lineno = Some(42);
939 event.code_namespace = Some(Arc::from("com.example"));
940 let loc = event.code_location().unwrap();
941 assert_eq!(loc.function.as_deref(), Some("processItems"));
942 assert_eq!(loc.filepath.as_deref(), Some("src/OrderService.java"));
943 assert_eq!(loc.lineno, Some(42));
944 assert_eq!(loc.namespace.as_deref(), Some("com.example"));
945 }
946
947 #[test]
948 fn serde_roundtrip_with_code_fields() {
949 let json = r#"{
950 "timestamp": "2025-07-10T14:32:01.123Z",
951 "trace_id": "abc123",
952 "span_id": "span-1",
953 "service": "svc",
954 "type": "sql",
955 "operation": "SELECT",
956 "target": "SELECT 1",
957 "duration_us": 100,
958 "source": { "endpoint": "GET /test", "method": "test" },
959 "code_function": "processItems",
960 "code_filepath": "src/OrderService.java",
961 "code_lineno": 42,
962 "code_namespace": "com.example"
963 }"#;
964 let event: SpanEvent = serde_json::from_str(json).unwrap();
965 assert_eq!(event.code_function.as_deref(), Some("processItems"));
966 assert_eq!(event.code_lineno, Some(42));
967 let serialized = serde_json::to_string(&event).unwrap();
968 let back: SpanEvent = serde_json::from_str(&serialized).unwrap();
969 assert_eq!(event, back);
970 }
971
972 #[test]
973 fn code_fields_omitted_when_none() {
974 let event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
975 let json = serde_json::to_string(&event).unwrap();
976 assert!(!json.contains("code_function"));
977 assert!(!json.contains("code_filepath"));
978 assert!(!json.contains("code_lineno"));
979 assert!(!json.contains("code_namespace"));
980 }
981
982 #[test]
983 fn sanitize_truncates_long_code_function() {
984 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
985 event.code_function = Some(Arc::from("x".repeat(1000)));
986 sanitize_span_event(&mut event);
987 assert!(event.code_function.as_ref().unwrap().len() <= MAX_CODE_FUNCTION_LENGTH);
988 }
989
990 #[test]
991 fn sanitize_truncates_long_code_filepath() {
992 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
993 event.code_filepath = Some(Arc::from("x".repeat(2000)));
994 sanitize_span_event(&mut event);
995 assert!(event.code_filepath.as_ref().unwrap().len() <= MAX_CODE_FILEPATH_LENGTH);
996 }
997
998 #[test]
999 fn sanitize_drops_code_function_with_control_char() {
1000 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
1001 event.code_function = Some(Arc::from("findItems\x1b[31m"));
1002 sanitize_span_event(&mut event);
1003 assert!(event.code_function.is_none());
1004 }
1005
1006 #[test]
1007 fn sanitize_drops_code_filepath_with_newline() {
1008 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
1009 event.code_filepath = Some(Arc::from("src/main.rs\nINJECT"));
1010 sanitize_span_event(&mut event);
1011 assert!(event.code_filepath.is_none());
1012 }
1013
1014 #[test]
1015 fn sanitize_drops_code_namespace_with_del() {
1016 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
1017 event.code_namespace = Some(Arc::from("com.foo\x7fX"));
1018 sanitize_span_event(&mut event);
1019 assert!(event.code_namespace.is_none());
1020 }
1021
1022 #[test]
1023 fn sanitize_keeps_clean_code_fields() {
1024 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
1025 event.code_function = Some(Arc::from("findItems"));
1026 event.code_filepath = Some(Arc::from("src/main/java/com/foo/Repo.java"));
1027 event.code_namespace = Some(Arc::from("com.foo.Repo"));
1028 sanitize_span_event(&mut event);
1029 assert_eq!(event.code_function.as_deref(), Some("findItems"));
1030 assert_eq!(
1031 event.code_filepath.as_deref(),
1032 Some("src/main/java/com/foo/Repo.java")
1033 );
1034 assert_eq!(event.code_namespace.as_deref(), Some("com.foo.Repo"));
1035 }
1036
1037 #[test]
1040 fn sanitize_truncates_long_instrumentation_scope() {
1041 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
1042 event.instrumentation_scopes = vec![Arc::from("x".repeat(1024))];
1043 sanitize_span_event(&mut event);
1044 assert_eq!(event.instrumentation_scopes.len(), 1);
1045 assert!(event.instrumentation_scopes[0].len() <= MAX_SCOPE_NAME_LENGTH);
1046 }
1047
1048 #[test]
1049 fn sanitize_drops_instrumentation_scope_with_control_char() {
1050 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
1051 event.instrumentation_scopes = vec![
1052 Arc::from("io.opentelemetry.spring-data"),
1053 Arc::from("\x1b[31mio.opentelemetry.evil\x1b[0m"),
1054 Arc::from("io.opentelemetry.hibernate"),
1055 ];
1056 sanitize_span_event(&mut event);
1057 let scopes: Vec<&str> = event
1058 .instrumentation_scopes
1059 .iter()
1060 .map(AsRef::as_ref)
1061 .collect();
1062 assert_eq!(
1063 scopes,
1064 vec!["io.opentelemetry.spring-data", "io.opentelemetry.hibernate"]
1065 );
1066 }
1067
1068 #[test]
1069 fn sanitize_caps_oversize_instrumentation_scopes_vec() {
1070 let mut event: SpanEvent = serde_json::from_str(sample_sql_json()).unwrap();
1071 event.instrumentation_scopes = (0..32)
1072 .map(|i| Arc::from(format!("io.opentelemetry.scope-{i}")))
1073 .collect();
1074 sanitize_span_event(&mut event);
1075 assert_eq!(
1076 event.instrumentation_scopes.len(),
1077 MAX_INSTRUMENTATION_SCOPES
1078 );
1079 assert_eq!(
1080 &*event.instrumentation_scopes[0],
1081 "io.opentelemetry.scope-0"
1082 );
1083 }
1084}