mod common;
pub use common::*;
use opentelemetry::trace::{SpanId, SpanKind};
use opentelemetry::{KeyValue, Value};
use opentelemetry_aws::xray_exporter::{SegmentTranslator, XrayExporter};
use opentelemetry_sdk::trace::SpanExporter;
#[tokio::test]
async fn test_http_attributes_translation() {
let mock_exporter = MockExporter::new();
let exporter = XrayExporter::new(mock_exporter.clone());
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0x1111111111111111u64.to_be_bytes());
let parent_span_id = SpanId::from_bytes(0x2222222222222222u64.to_be_bytes());
let mut span = create_basic_span(
"http-request",
SpanKind::Client,
trace_id,
span_id,
Some(parent_span_id),
);
span.attributes = vec![
KeyValue::new("http.method", "POST"),
KeyValue::new("http.url", "https://api.example.com/v1/users"),
KeyValue::new("http.status_code", Value::I64(201)),
KeyValue::new("net.peer.name", "api.example.com"),
KeyValue::new("net.peer.port", Value::I64(443)),
];
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_exists(json, "http");
assert_field_eq(json, "http.request.method", "POST");
assert_field_exists(json, "http.request.url");
assert_field_eq(json, "http.response.status", 201);
}
#[tokio::test]
async fn test_aws_attributes_translation() {
let mock_exporter = MockExporter::new();
let translator = SegmentTranslator::new()
.with_indexed_attr("rpc.service".to_string())
.with_indexed_attr("rpc.method".to_string());
let exporter = XrayExporter::new(mock_exporter.clone()).with_translator(translator);
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0x1111111111111111u64.to_be_bytes());
let parent_span_id = SpanId::from_bytes(0x2222222222222222u64.to_be_bytes());
let mut span = create_basic_span(
"aws-call",
SpanKind::Client,
trace_id,
span_id,
Some(parent_span_id),
);
span.attributes = vec![
KeyValue::new("rpc.service", "DynamoDB"),
KeyValue::new("rpc.method", "GetItem"),
KeyValue::new("rpc.system", "aws-api"),
KeyValue::new("aws.request_id", "ABCD1234EFGH5678"),
KeyValue::new("aws.region", "us-west-2"),
KeyValue::new(
"aws.dynamodb.table_names",
Value::Array(opentelemetry::Array::String(vec![
opentelemetry::StringValue::from("Users"),
])),
),
];
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_exists(json, "aws");
assert_field_eq(json, "namespace", "aws");
assert_field_eq(json, "annotations.rpc_service", "DynamoDB");
assert_field_eq(json, "annotations.rpc_method", "GetItem");
}
#[tokio::test]
async fn test_sql_attributes_translation() {
let mock_exporter = MockExporter::new();
let exporter = XrayExporter::new(mock_exporter.clone());
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0x1111111111111111u64.to_be_bytes());
let parent_span_id = SpanId::from_bytes(0x2222222222222222u64.to_be_bytes());
let mut span = create_basic_span(
"db-query",
SpanKind::Client,
trace_id,
span_id,
Some(parent_span_id),
);
span.attributes = vec![
KeyValue::new("db.system", "postgresql"),
KeyValue::new("db.name", "mydb"),
KeyValue::new("db.statement", "SELECT * FROM users WHERE id = $1"),
KeyValue::new("db.user", "app_user"),
KeyValue::new("net.peer.name", "db.example.com"),
KeyValue::new("net.peer.port", Value::I64(5432)),
];
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_exists(json, "sql");
assert_field_eq(json, "sql.database_type", "postgresql");
assert_field_exists(json, "sql.sanitized_query");
}
#[tokio::test]
async fn test_indexed_attributes_as_annotations() {
let mock_exporter = MockExporter::new();
let translator = SegmentTranslator::new()
.with_indexed_attr("custom.user_id".to_string())
.with_indexed_attr("custom.request_type".to_string());
let exporter = XrayExporter::new(mock_exporter.clone()).with_translator(translator);
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0x1111111111111111u64.to_be_bytes());
let mut span = create_basic_span("custom-span", SpanKind::Server, trace_id, span_id, None);
span.attributes = vec![
KeyValue::new("custom.user_id", "user123"),
KeyValue::new("custom.request_type", "payment"),
KeyValue::new("custom.other_data", "not_indexed"),
];
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_eq(
json,
["annotations", "custom_user_id"].as_slice(),
"user123",
);
assert_field_eq(
json,
["annotations", "custom_request_type"].as_slice(),
"payment",
);
assert_field_not_exists(json, ["annotations", "custom_other_data"].as_slice());
}
#[tokio::test]
async fn test_index_all_attributes() {
let mock_exporter = MockExporter::new();
let translator = SegmentTranslator::new().index_all_attrs();
let exporter = XrayExporter::new(mock_exporter.clone()).with_translator(translator);
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0x1111111111111111u64.to_be_bytes());
let mut span = create_basic_span("test-span", SpanKind::Server, trace_id, span_id, None);
span.attributes = vec![
KeyValue::new("attr1", "value1"),
KeyValue::new("attr2", Value::I64(42)),
KeyValue::new("attr3", Value::Bool(true)),
];
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_eq(json, "annotations.attr1", "value1");
assert_field_eq(json, "annotations.attr2", 42);
assert_field_eq(json, "annotations.attr3", true);
}
#[tokio::test]
async fn test_attribute_types_conversion() {
let mock_exporter = MockExporter::new();
let translator = SegmentTranslator::new().index_all_attrs();
let exporter = XrayExporter::new(mock_exporter.clone()).with_translator(translator);
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0x1111111111111111u64.to_be_bytes());
let mut span = create_basic_span("test-span", SpanKind::Server, trace_id, span_id, None);
span.attributes = vec![
KeyValue::new("string_attr", "hello"),
KeyValue::new("int_attr", Value::I64(123)),
KeyValue::new("float_attr", Value::F64(45.67)),
KeyValue::new("bool_attr", Value::Bool(true)),
KeyValue::new(
"array_attr",
Value::Array(opentelemetry::Array::String(vec![
opentelemetry::StringValue::from("item1"),
opentelemetry::StringValue::from("item2"),
])),
),
];
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_eq(json, "annotations.string_attr", "hello");
assert_field_eq(json, "annotations.int_attr", 123);
assert_field_eq(json, "annotations.float_attr", 45.67);
assert_field_eq(json, "annotations.bool_attr", true);
}
#[tokio::test]
async fn test_aws_xray_annotations_attribute() {
let mock_exporter = MockExporter::new();
let exporter = XrayExporter::new(mock_exporter.clone());
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0x1111111111111111u64.to_be_bytes());
let mut span = create_basic_span("test-span", SpanKind::Server, trace_id, span_id, None);
span.attributes = vec![
KeyValue::new("annotation.custom.field1", "value1"),
KeyValue::new("custom.field2", "value2"),
];
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_eq(json, "annotations.custom_field1", "value1");
}
#[tokio::test]
async fn test_resource_attributes_translation() {
let mock_exporter = MockExporter::new();
let mut exporter = XrayExporter::new(mock_exporter.clone());
use opentelemetry_sdk::Resource;
let resource = Resource::builder()
.with_attributes(vec![
KeyValue::new("cloud.provider", "aws"),
KeyValue::new("cloud.platform", "aws_ec2"),
KeyValue::new("cloud.region", "us-east-1"),
KeyValue::new("cloud.availability_zone", "us-east-1a"),
KeyValue::new("host.id", "i-1234567890abcdef0"),
KeyValue::new("host.type", "t3.medium"),
])
.build();
exporter.set_resource(&resource);
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0x1111111111111111u64.to_be_bytes());
let span = create_basic_span("test-span", SpanKind::Server, trace_id, span_id, None);
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_eq(json, "origin", "AWS::EC2::Instance");
}
#[tokio::test]
async fn test_ecs_resource_attributes() {
let mock_exporter = MockExporter::new();
let mut exporter = XrayExporter::new(mock_exporter.clone());
use opentelemetry_sdk::Resource;
let resource = Resource::builder()
.with_attributes(vec![
KeyValue::new("cloud.provider", "aws"),
KeyValue::new("cloud.platform", "aws_ecs"),
KeyValue::new("cloud.region", "us-west-2"),
KeyValue::new(
"aws.ecs.cluster.arn",
"arn:aws:ecs:us-west-2:123456789012:cluster/my-cluster",
),
KeyValue::new(
"aws.ecs.task.arn",
"arn:aws:ecs:us-west-2:123456789012:task/my-task",
),
KeyValue::new("container.id", "abc123"),
])
.build();
exporter.set_resource(&resource);
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0x1111111111111111u64.to_be_bytes());
let span = create_basic_span("test-span", SpanKind::Server, trace_id, span_id, None);
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_eq(json, "origin", "AWS::ECS::Container");
}
#[tokio::test]
async fn test_lambda_resource_attributes() {
let mock_exporter = MockExporter::new();
let mut exporter = XrayExporter::new(mock_exporter.clone());
use opentelemetry_sdk::Resource;
let resource = Resource::builder()
.with_attributes(vec![
KeyValue::new("cloud.provider", "aws"),
KeyValue::new("cloud.platform", "aws_lambda"),
KeyValue::new("faas.name", "my-function"),
KeyValue::new("faas.version", "1"),
KeyValue::new("cloud.region", "eu-west-1"),
])
.build();
exporter.set_resource(&resource);
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0x1111111111111111u64.to_be_bytes());
let span = create_basic_span("test-span", SpanKind::Server, trace_id, span_id, None);
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_eq(json, "origin", "AWS::Lambda::Function");
}
#[tokio::test]
async fn test_many_attributes_annotation_limit() {
let mock_exporter = MockExporter::new();
let translator = SegmentTranslator::new().index_all_attrs();
let exporter = XrayExporter::new(mock_exporter.clone()).with_translator(translator);
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0x1111111111111111u64.to_be_bytes());
let mut span = create_basic_span("test-span", SpanKind::Server, trace_id, span_id, None);
let mut attributes = Vec::new();
for i in 0..60 {
attributes.push(KeyValue::new(format!("attr_{i}"), format!("value_{i}")));
}
span.attributes = attributes;
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_exists(json, "annotations");
assert_field_exists(json, "metadata");
let annotations = get_nested_value(json, "annotations")
.and_then(|v| v.as_object())
.expect("annotations should be an object");
let metadata = get_nested_value(json, "metadata")
.and_then(|v| v.as_object())
.expect("metadata should be an object");
assert_eq!(
annotations.len(),
50,
"Should have exactly 50 annotations (the limit)"
);
assert!(
metadata.len() >= 10,
"Should have at least 10 items in metadata (overflow)"
);
}
#[tokio::test]
async fn test_subsegment_preserves_timing_and_parent() {
use std::time::{Duration, UNIX_EPOCH};
let mock_exporter = MockExporter::new();
let exporter = XrayExporter::new(mock_exporter.clone());
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0x3333333333333333u64.to_be_bytes());
let parent_span_id = SpanId::from_bytes(0x4444444444444444u64.to_be_bytes());
let mut span = create_basic_span(
"test-subsegment",
SpanKind::Client,
trace_id,
span_id,
Some(parent_span_id),
);
let start_timestamp = UNIX_EPOCH + Duration::from_millis(1234567890123);
let end_timestamp = UNIX_EPOCH + Duration::from_millis(1234567890456);
span.start_time = start_timestamp;
span.end_time = end_timestamp;
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_eq(json, "type", "subsegment");
assert_field_eq(json, "parent_id", "4444444444444444");
assert_field_eq(json, "id", "3333333333333333");
let expected_start = start_timestamp
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs_f64();
assert_field_eq(json, "start_time", expected_start);
let expected_end = end_timestamp
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs_f64();
assert_field_eq(json, "end_time", expected_end);
assert_field_exists(json, "trace_id");
assert_field_eq(json, "name", "test-subsegment");
}
#[tokio::test]
async fn test_metadata_prefix_routing() {
let mock_exporter = MockExporter::new();
let exporter = XrayExporter::new(mock_exporter.clone());
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0xaaaaaaaaaaaaaaaa_u64.to_be_bytes());
let mut span = create_basic_span("test-span", SpanKind::Server, trace_id, span_id, None);
span.attributes = vec![
KeyValue::new("metadata.debug_info", "some debug data"),
KeyValue::new("metadata.trace_context", "ctx-12345"),
];
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_eq(json, "metadata.debug_info", "some debug data");
assert_field_eq(json, "metadata.trace_context", "ctx-12345");
assert_field_not_exists(json, "annotations.debug_info");
assert_field_not_exists(json, "annotations.trace_context");
}
#[tokio::test]
async fn test_metadata_prefix_with_index_all_attrs_still_indexes() {
let mock_exporter = MockExporter::new();
let translator = SegmentTranslator::new().index_all_attrs();
let exporter = XrayExporter::new(mock_exporter.clone()).with_translator(translator);
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0xbbbbbbbbbbbbbbbb_u64.to_be_bytes());
let mut span = create_basic_span("test-span", SpanKind::Server, trace_id, span_id, None);
span.attributes = vec![KeyValue::new("metadata.some_key", "some_value")];
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_eq(json, "annotations.some_key", "some_value");
assert_field_not_exists(json, "metadata.some_key");
}
#[tokio::test]
async fn test_with_metadata_attr_explicit_routing() {
let mock_exporter = MockExporter::new();
let translator = SegmentTranslator::new().with_metadata_attr("custom.field".to_string());
let exporter = XrayExporter::new(mock_exporter.clone()).with_translator(translator);
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0xcccccccccccccccc_u64.to_be_bytes());
let mut span = create_basic_span("test-span", SpanKind::Server, trace_id, span_id, None);
span.attributes = vec![
KeyValue::new("custom.field", "routed_value"),
KeyValue::new("other.field", "not_routed"),
];
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_eq(
json,
["metadata", "custom.field"].as_slice(),
"routed_value",
);
assert_field_not_exists(json, ["metadata", "other.field"].as_slice());
}
#[tokio::test]
async fn test_with_metadata_attrs_multiple_keys() {
let mock_exporter = MockExporter::new();
let translator =
SegmentTranslator::new().with_metadata_attrs(vec!["key1".to_string(), "key2".to_string()]);
let exporter = XrayExporter::new(mock_exporter.clone()).with_translator(translator);
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0xdddddddddddddddd_u64.to_be_bytes());
let mut span = create_basic_span("test-span", SpanKind::Server, trace_id, span_id, None);
span.attributes = vec![
KeyValue::new("key1", "value1"),
KeyValue::new("key2", "value2"),
KeyValue::new("key3", "value3"),
];
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_eq(json, "metadata.key1", "value1");
assert_field_eq(json, "metadata.key2", "value2");
assert_field_not_exists(json, "metadata.key3");
}
#[tokio::test]
async fn test_unrecognized_attrs_without_metadata_optin_are_dropped() {
let mock_exporter = MockExporter::new();
let exporter = XrayExporter::new(mock_exporter.clone());
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0xeeeeeeeeeeeeeeee_u64.to_be_bytes());
let mut span = create_basic_span("test-span", SpanKind::Server, trace_id, span_id, None);
span.attributes = vec![
KeyValue::new("custom.field1", "value1"),
KeyValue::new("custom.field2", "value2"),
];
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_not_exists(json, ["annotations", "custom_field1"].as_slice());
assert_field_not_exists(json, ["annotations", "custom_field2"].as_slice());
assert_field_not_exists(json, ["metadata", "custom.field1"].as_slice());
assert_field_not_exists(json, ["metadata", "custom.field2"].as_slice());
assert_field_not_exists(json, "metadata");
}
#[tokio::test]
async fn test_annotation_prefix_with_array_value_falls_back_to_metadata() {
let mock_exporter = MockExporter::new();
let translator = SegmentTranslator::new().metadata_all_attrs();
let exporter = XrayExporter::new(mock_exporter.clone()).with_translator(translator);
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0xabababababababab_u64.to_be_bytes());
let mut span = create_basic_span("test-span", SpanKind::Server, trace_id, span_id, None);
span.attributes = vec![KeyValue::new(
"annotation.my_array",
Value::Array(opentelemetry::Array::String(vec![
opentelemetry::StringValue::from("item1"),
opentelemetry::StringValue::from("item2"),
])),
)];
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_not_exists(json, "annotations.my_array");
assert_field_exists(json, "metadata.my_array");
}
#[tokio::test]
async fn test_annotation_and_metadata_prefix_coexistence() {
let mock_exporter = MockExporter::new();
let exporter = XrayExporter::new(mock_exporter.clone());
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0xbcbcbcbcbcbcbcbc_u64.to_be_bytes());
let mut span = create_basic_span("test-span", SpanKind::Server, trace_id, span_id, None);
span.attributes = vec![
KeyValue::new("annotation.indexed_key", "indexed_value"),
KeyValue::new("metadata.meta_key", "meta_value"),
];
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_eq(json, "annotations.indexed_key", "indexed_value");
assert_field_eq(json, "metadata.meta_key", "meta_value");
assert_field_not_exists(json, "metadata.indexed_key");
assert_field_not_exists(json, "annotations.meta_key");
}
#[tokio::test]
async fn test_metadata_all_and_index_all_combined() {
let mock_exporter = MockExporter::new();
let translator = SegmentTranslator::new()
.index_all_attrs()
.metadata_all_attrs();
let exporter = XrayExporter::new(mock_exporter.clone()).with_translator(translator);
let trace_id = create_valid_trace_id();
let span_id = SpanId::from_bytes(0xcdcdcdcdcdcdcdcd_u64.to_be_bytes());
let mut span = create_basic_span("test-span", SpanKind::Server, trace_id, span_id, None);
span.attributes = vec![
KeyValue::new("str_attr1", "value1"),
KeyValue::new("str_attr2", "value2"),
KeyValue::new("str_attr3", "value3"),
KeyValue::new(
"array_attr",
Value::Array(opentelemetry::Array::String(vec![
opentelemetry::StringValue::from("a"),
opentelemetry::StringValue::from("b"),
])),
),
];
exporter.export(vec![span]).await.unwrap();
let documents = mock_exporter.get_documents();
let json = &documents[0];
assert_field_eq(json, "annotations.str_attr1", "value1");
assert_field_eq(json, "annotations.str_attr2", "value2");
assert_field_eq(json, "annotations.str_attr3", "value3");
assert_field_exists(json, "metadata.array_attr");
assert_field_not_exists(json, "metadata.str_attr1");
assert_field_not_exists(json, "metadata.str_attr2");
assert_field_not_exists(json, "metadata.str_attr3");
}