use std::convert::Infallible;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use opentelemetry::{
trace::{SpanContext, SpanId, SpanKind, Status, TraceFlags, TraceId, TraceState},
InstrumentationScope, KeyValue, Value,
};
use opentelemetry_aws::xray_exporter::{SegmentDocument, SegmentDocumentExporter};
use opentelemetry_sdk::trace::{SpanData, SpanEvents, SpanLinks};
use serde::Serialize;
use rand::{
distr::{Distribution, StandardUniform},
rngs::StdRng,
Rng, SeedableRng,
};
fn rng_gen<T>() -> T
where
StandardUniform: Distribution<T>,
{
thread_local! {
static RNG : Mutex<StdRng> = Mutex::new(StdRng::seed_from_u64(42));
}
RNG.with(|rng| rng.lock().unwrap().random())
}
#[derive(Debug, Clone)]
pub struct MockExporter {
pub documents: Arc<Mutex<Vec<serde_json::Value>>>,
}
impl MockExporter {
pub fn new() -> Self {
Self {
documents: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn get_documents(&self) -> Vec<serde_json::Value> {
self.documents.lock().unwrap().clone()
}
pub fn clear(&self) {
self.documents.lock().unwrap().clear()
}
pub fn count(&self) -> usize {
self.documents.lock().unwrap().len()
}
}
impl Default for MockExporter {
fn default() -> Self {
Self::new()
}
}
impl SegmentDocumentExporter for MockExporter {
type Error = Infallible;
async fn export_segment_documents(
&self,
batch: Vec<SegmentDocument<'_>>,
) -> Result<(), Self::Error> {
let mut docs = self.documents.lock().unwrap();
for document in batch {
docs.push(serde_json::to_value(&document).unwrap());
}
Ok(())
}
}
pub enum JsonPath<'a> {
Split(core::str::Split<'a, char>),
Slice(core::slice::Iter<'a, &'a str>),
}
impl<'a> Iterator for JsonPath<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
match self {
JsonPath::Split(iter) => iter.next(),
JsonPath::Slice(iter) => iter.next().copied(),
}
}
}
impl<'a> From<&'a str> for JsonPath<'a> {
fn from(value: &'a str) -> Self {
Self::Split(value.split('.'))
}
}
impl<'a> From<&'a [&'a str]> for JsonPath<'a> {
fn from(value: &'a [&'a str]) -> Self {
Self::Slice(value.iter())
}
}
pub fn get_nested_value<'a, 'p, F: Into<JsonPath<'p>>>(
mut json: &'a serde_json::Value,
field: F,
) -> Option<&'a serde_json::Value> {
for part in field.into() {
json = json.get(part)?;
}
Some(json)
}
pub fn helper_eq<V: Serialize>(expected: V) -> impl FnOnce(&serde_json::Value) -> bool {
move |v| *v == serde_json::json!(&expected)
}
pub fn assert_field_eq<
'p,
F: Into<JsonPath<'p>> + core::fmt::Debug + Copy,
V: Serialize + core::fmt::Debug,
>(
json: &serde_json::Value,
field: F,
expected: V,
) {
let value = get_nested_value(json, field);
assert!(
value.is_some_and(helper_eq(&expected)),
"Field '{:?}' should exist and equal {:?}, but was {:?}: {}",
field,
expected,
value,
serde_json::to_string_pretty(json).unwrap()
);
}
pub fn assert_field_exists<'p, F: Into<JsonPath<'p>> + core::fmt::Debug + Copy>(
json: &serde_json::Value,
field: F,
) {
assert!(
get_nested_value(json, field).is_some(),
"Field '{:?}' should exist: {}",
field,
serde_json::to_string_pretty(json).unwrap()
)
}
pub fn assert_field_not_exists<'p, F: Into<JsonPath<'p>> + core::fmt::Debug + Copy>(
json: &serde_json::Value,
field: F,
) {
assert!(
get_nested_value(json, field).is_none(),
"Field '{:?}' should not exist: {}",
field,
serde_json::to_string_pretty(json).unwrap()
)
}
pub fn create_valid_trace_id() -> TraceId {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as u128;
let random_part: u128 = rng_gen();
let trace_id = (timestamp << 96) | (random_part >> 32);
TraceId::from_bytes(trace_id.to_be_bytes())
}
pub fn create_basic_span(
name: &'static str,
kind: SpanKind,
trace_id: TraceId,
span_id: SpanId,
parent_span_id: Option<SpanId>,
) -> SpanData {
let span_context = SpanContext::new(
trace_id,
span_id,
TraceFlags::SAMPLED,
false,
TraceState::default(),
);
let start_time = UNIX_EPOCH + Duration::from_secs(1700000000);
let end_time = start_time + Duration::from_millis(100);
SpanData {
span_context,
parent_span_id: parent_span_id.unwrap_or(SpanId::INVALID),
parent_span_is_remote: parent_span_id.is_some(),
span_kind: kind,
name: name.into(),
start_time,
end_time,
attributes: vec![],
dropped_attributes_count: 0,
events: SpanEvents::default(),
links: SpanLinks::default(),
status: Status::Unset,
instrumentation_scope: InstrumentationScope::builder("test").build(),
}
}
pub fn create_lambda_handler_span(
trace_id: TraceId,
span_id: SpanId,
parent_span_id: Option<SpanId>,
) -> SpanData {
let span_context = SpanContext::new(
trace_id,
span_id,
TraceFlags::SAMPLED,
false,
TraceState::default(),
);
let start_time = UNIX_EPOCH + Duration::from_millis(1700000000000);
let end_time = start_time + Duration::from_millis(130);
let attributes = vec![
KeyValue::new("faas.trigger", "http"),
KeyValue::new(
"cloud.resource_id",
"arn:aws:lambda:us-east-1:123456789012:function:my-function",
),
KeyValue::new("faas.invocation_id", "784851f1-0490-42ca-a093-8bbe57fc1e04"),
KeyValue::new("cloud.account.id", "123456789012"),
KeyValue::new("faas.coldstart", Value::Bool(true)),
];
SpanData {
span_context,
parent_span_id: parent_span_id.unwrap_or(SpanId::INVALID),
parent_span_is_remote: parent_span_id.is_some(),
span_kind: SpanKind::Server,
name: "my-lambda-function".into(),
start_time,
end_time,
attributes,
dropped_attributes_count: 0,
events: SpanEvents::default(),
links: SpanLinks::default(),
status: Status::Unset,
instrumentation_scope: InstrumentationScope::builder("lambda-runtime").build(),
}
}
pub fn create_dynamodb_span(
trace_id: TraceId,
span_id: SpanId,
parent_span_id: SpanId,
) -> SpanData {
let span_context = SpanContext::new(
trace_id,
span_id,
TraceFlags::SAMPLED,
false,
TraceState::default(),
);
let start_time = UNIX_EPOCH + Duration::from_millis(1700000000010);
let end_time = start_time + Duration::from_millis(50);
let attributes = vec![
KeyValue::new("rpc.service", "DynamoDB"),
KeyValue::new("rpc.method", "PutItem"),
KeyValue::new("rpc.system", "aws-api"),
KeyValue::new("cloud.region", "us-east-1"),
KeyValue::new("db.system", "dynamodb"),
KeyValue::new(
"aws.dynamodb.table_names",
Value::Array(opentelemetry::Array::String(vec![
opentelemetry::StringValue::from("my-table"),
])),
),
KeyValue::new("aws.request_id", "ABCD1234EFGH5678"),
];
SpanData {
span_context,
parent_span_id,
parent_span_is_remote: false,
span_kind: SpanKind::Client,
name: "DynamoDB.PutItem".into(),
start_time,
end_time,
attributes,
dropped_attributes_count: 0,
events: SpanEvents::default(),
links: SpanLinks::default(),
status: Status::Ok,
instrumentation_scope: InstrumentationScope::builder("aws-sdk").build(),
}
}
pub fn create_http_client_span(
trace_id: TraceId,
span_id: SpanId,
parent_span_id: SpanId,
status_code: i64,
) -> SpanData {
let span_context = SpanContext::new(
trace_id,
span_id,
TraceFlags::SAMPLED,
false,
TraceState::default(),
);
let start_time = UNIX_EPOCH + Duration::from_millis(1700000000020);
let end_time = start_time + Duration::from_millis(75);
let attributes = vec![
KeyValue::new("http.method", "GET"),
KeyValue::new("http.url", "https://api.example.com/users/123"),
KeyValue::new("http.status_code", Value::I64(status_code)),
KeyValue::new("http.request.header.user_agent", "MyApp/1.0"),
KeyValue::new("http.response.header.content_type", "application/json"),
KeyValue::new("net.peer.name", "api.example.com"),
KeyValue::new("net.peer.port", Value::I64(443)),
];
SpanData {
span_context,
parent_span_id,
parent_span_is_remote: false,
span_kind: SpanKind::Client,
name: "GET /users/:id".into(),
start_time,
end_time,
attributes,
dropped_attributes_count: 0,
events: SpanEvents::default(),
links: SpanLinks::default(),
status: if status_code >= 400 {
Status::error("")
} else {
Status::Ok
},
instrumentation_scope: InstrumentationScope::builder("http-client").build(),
}
}