use std::error::Error;
use std::fmt::{self, Display, Formatter};
use lodestone_types::{
ProfileBatchV1, ProfileFunctionV1, ProfileInstrumentationScopeV1, ProfileLabelV1, ProfileLineV1, ProfileLocationV1,
ProfileMappingV1, ProfileNumberV1, ProfilePrivacyActionV1, ProfilePrivacyFieldV1, ProfilePrivacyPolicyV1,
ProfilePrivacyRuleV1, ProfileSampleTypeV1, ProfileSampleV1, ProfileStackV1,
};
use crate::{
I64, MetricNumber, ProfileBatch, ProfileFunction, ProfileInstrumentationScope, ProfileLabel, ProfileLine,
ProfileLocation, ProfileMapping, ProfilePrivacyAction, ProfilePrivacyField, ProfilePrivacyPolicy,
ProfilePrivacyRule, ProfileSample, ProfileSampleType, ProfileStack, U64,
};
pub fn profile_batch_from_wire(value: &ProfileBatch) -> Result<ProfileBatchV1, NativeProfileWireError> {
Ok(ProfileBatchV1 {
format_version: u16::try_from(value.format_version).map_err(|_| NativeProfileWireError::Integer)?,
profile_id: value.profile_id.clone(),
batch_id: value.batch_id.clone(),
start_unix_nanos: parse_u64(&value.start_unix_nanos)?,
end_unix_nanos: parse_u64(&value.end_unix_nanos)?,
duration_nanos: parse_u64(&value.duration_nanos)?,
period_nanos: parse_u64(&value.period_nanos)?,
sample_types: value
.sample_types
.iter()
.map(|item| ProfileSampleTypeV1 {
sample_type: item.sample_type.clone(),
unit: item.unit.clone(),
})
.collect(),
resource_attributes: labels_from_wire(&value.resource_attributes),
instrumentation_scope: ProfileInstrumentationScopeV1 {
name: value.instrumentation_scope.name.clone(),
version: value.instrumentation_scope.version.clone(),
attributes: labels_from_wire(&value.instrumentation_scope.attributes),
},
mappings: value
.mappings
.iter()
.map(|item| ProfileMappingV1 {
mapping_id: item.mapping_id.clone(),
build_id: item.build_id.clone(),
module_id: item.module_id.clone(),
filename: item.filename.clone(),
})
.collect(),
functions: value
.functions
.iter()
.map(|item| {
Ok(ProfileFunctionV1 {
function_id: item.function_id.clone(),
name: item.name.clone(),
system_name: item.system_name.clone(),
filename: item.filename.clone(),
start_line: item.start_line.as_ref().map(parse_u64).transpose()?,
symbol_observation_id: item.symbol_observation_id.clone(),
})
})
.collect::<Result<Vec<_>, NativeProfileWireError>>()?,
locations: value
.locations
.iter()
.map(|item| {
Ok(ProfileLocationV1 {
location_id: item.location_id.clone(),
mapping_id: item.mapping_id.clone(),
address: parse_u64(&item.address)?,
lines: item
.lines
.iter()
.map(|line| {
Ok(ProfileLineV1 {
function_id: line.function_id.clone(),
line: parse_u64(&line.line)?,
})
})
.collect::<Result<Vec<_>, NativeProfileWireError>>()?,
})
})
.collect::<Result<Vec<_>, NativeProfileWireError>>()?,
stacks: value
.stacks
.iter()
.map(|item| ProfileStackV1 {
stack_id: item.stack_id.clone(),
parent_stack_id: item.parent_stack_id.clone(),
location_id: item.location_id.clone(),
})
.collect(),
samples: value
.samples
.iter()
.map(|item| {
Ok(ProfileSampleV1 {
sample_id: item.sample_id.clone(),
stack_id: item.stack_id.clone(),
values: item
.values
.iter()
.map(profile_number_from_wire)
.collect::<Result<Vec<_>, _>>()?,
labels: labels_from_wire(&item.labels),
trace_id: item.trace_id.clone(),
span_id: item.span_id.clone(),
})
})
.collect::<Result<Vec<_>, NativeProfileWireError>>()?,
})
}
#[must_use]
pub fn profile_batch_to_wire(value: ProfileBatchV1) -> ProfileBatch {
ProfileBatch {
format_version: u32::from(value.format_version),
profile_id: value.profile_id,
batch_id: value.batch_id,
start_unix_nanos: wire_u64(value.start_unix_nanos),
end_unix_nanos: wire_u64(value.end_unix_nanos),
duration_nanos: wire_u64(value.duration_nanos),
period_nanos: wire_u64(value.period_nanos),
sample_types: value
.sample_types
.into_iter()
.map(|item| ProfileSampleType {
sample_type: item.sample_type,
unit: item.unit,
})
.collect(),
resource_attributes: labels_to_wire(value.resource_attributes),
instrumentation_scope: ProfileInstrumentationScope {
name: value.instrumentation_scope.name,
version: value.instrumentation_scope.version,
attributes: labels_to_wire(value.instrumentation_scope.attributes),
},
mappings: value
.mappings
.into_iter()
.map(|item| ProfileMapping {
mapping_id: item.mapping_id,
build_id: item.build_id,
module_id: item.module_id,
filename: item.filename,
})
.collect(),
functions: value
.functions
.into_iter()
.map(|item| ProfileFunction {
function_id: item.function_id,
name: item.name,
system_name: item.system_name,
filename: item.filename,
start_line: item.start_line.map(wire_u64),
symbol_observation_id: item.symbol_observation_id,
})
.collect(),
locations: value
.locations
.into_iter()
.map(|item| ProfileLocation {
location_id: item.location_id,
mapping_id: item.mapping_id,
address: wire_u64(item.address),
lines: item
.lines
.into_iter()
.map(|line| ProfileLine {
function_id: line.function_id,
line: wire_u64(line.line),
})
.collect(),
})
.collect(),
stacks: value
.stacks
.into_iter()
.map(|item| ProfileStack {
stack_id: item.stack_id,
parent_stack_id: item.parent_stack_id,
location_id: item.location_id,
})
.collect(),
samples: value
.samples
.into_iter()
.map(|item| ProfileSample {
sample_id: item.sample_id,
stack_id: item.stack_id,
values: item.values.into_iter().map(profile_number_to_wire).collect(),
labels: labels_to_wire(item.labels),
trace_id: item.trace_id,
span_id: item.span_id,
})
.collect(),
}
}
fn labels_to_wire(values: Vec<ProfileLabelV1>) -> Vec<ProfileLabel> {
values
.into_iter()
.map(|item| ProfileLabel {
key: item.key,
value: item.value,
})
.collect()
}
fn wire_u64(value: u64) -> U64 {
U64 {
value: value.to_string(),
}
}
pub fn profile_number_from_wire(value: &MetricNumber) -> Result<ProfileNumberV1, NativeProfileWireError> {
match value {
MetricNumber::I64 { value } => Ok(ProfileNumberV1::I64 {
value: parse_i64(value)?,
}),
MetricNumber::F64Bits { value } => Ok(ProfileNumberV1::F64Bits {
value: parse_u64(value)?,
}),
}
}
#[must_use]
pub fn profile_number_to_wire(value: ProfileNumberV1) -> MetricNumber {
match value {
ProfileNumberV1::I64 { value } => MetricNumber::I64 {
value: I64 {
value: value.to_string(),
},
},
ProfileNumberV1::F64Bits { value } => MetricNumber::F64Bits {
value: U64 {
value: value.to_string(),
},
},
}
}
pub fn profile_privacy_policy_from_wire(
value: &ProfilePrivacyPolicy,
) -> Result<ProfilePrivacyPolicyV1, NativeProfileWireError> {
Ok(ProfilePrivacyPolicyV1 {
format_version: u16::try_from(value.format_version).map_err(|_| NativeProfileWireError::Integer)?,
rules: value
.rules
.iter()
.map(|rule| ProfilePrivacyRuleV1 {
field: match &rule.field {
ProfilePrivacyField::ResourceAttribute { key } => {
ProfilePrivacyFieldV1::ResourceAttribute { key: key.clone() }
}
ProfilePrivacyField::ScopeAttribute { key } => {
ProfilePrivacyFieldV1::ScopeAttribute { key: key.clone() }
}
ProfilePrivacyField::SampleLabel { key } => ProfilePrivacyFieldV1::SampleLabel { key: key.clone() },
ProfilePrivacyField::FunctionName => ProfilePrivacyFieldV1::FunctionName,
ProfilePrivacyField::FunctionSystemName => ProfilePrivacyFieldV1::FunctionSystemName,
ProfilePrivacyField::FunctionFilename => ProfilePrivacyFieldV1::FunctionFilename,
ProfilePrivacyField::MappingFilename => ProfilePrivacyFieldV1::MappingFilename,
},
action: match rule.action {
ProfilePrivacyAction::Allow => ProfilePrivacyActionV1::Allow,
ProfilePrivacyAction::Drop => ProfilePrivacyActionV1::Drop,
ProfilePrivacyAction::Redact => ProfilePrivacyActionV1::Redact,
ProfilePrivacyAction::Reject => ProfilePrivacyActionV1::Reject,
},
})
.collect(),
})
}
#[must_use]
pub fn profile_privacy_policy_to_wire(value: &ProfilePrivacyPolicyV1) -> ProfilePrivacyPolicy {
ProfilePrivacyPolicy {
format_version: u32::from(value.format_version),
rules: value
.rules
.iter()
.map(|rule| ProfilePrivacyRule {
field: match &rule.field {
ProfilePrivacyFieldV1::ResourceAttribute { key } => {
ProfilePrivacyField::ResourceAttribute { key: key.clone() }
}
ProfilePrivacyFieldV1::ScopeAttribute { key } => {
ProfilePrivacyField::ScopeAttribute { key: key.clone() }
}
ProfilePrivacyFieldV1::SampleLabel { key } => ProfilePrivacyField::SampleLabel { key: key.clone() },
ProfilePrivacyFieldV1::FunctionName => ProfilePrivacyField::FunctionName,
ProfilePrivacyFieldV1::FunctionSystemName => ProfilePrivacyField::FunctionSystemName,
ProfilePrivacyFieldV1::FunctionFilename => ProfilePrivacyField::FunctionFilename,
ProfilePrivacyFieldV1::MappingFilename => ProfilePrivacyField::MappingFilename,
},
action: match rule.action {
ProfilePrivacyActionV1::Allow => ProfilePrivacyAction::Allow,
ProfilePrivacyActionV1::Drop => ProfilePrivacyAction::Drop,
ProfilePrivacyActionV1::Redact => ProfilePrivacyAction::Redact,
ProfilePrivacyActionV1::Reject => ProfilePrivacyAction::Reject,
},
})
.collect(),
}
}
fn labels_from_wire(values: &[ProfileLabel]) -> Vec<ProfileLabelV1> {
values
.iter()
.map(|item| ProfileLabelV1 {
key: item.key.clone(),
value: item.value.clone(),
})
.collect()
}
fn parse_u64(value: &U64) -> Result<u64, NativeProfileWireError> {
value.value.parse().map_err(|_| NativeProfileWireError::Integer)
}
fn parse_i64(value: &I64) -> Result<i64, NativeProfileWireError> {
value.value.parse().map_err(|_| NativeProfileWireError::Integer)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NativeProfileWireError {
Integer,
}
impl Display for NativeProfileWireError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str("native profile wire contains a noncanonical integer")
}
}
impl Error for NativeProfileWireError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn model_to_wire_round_trips_complete_profile_shape() -> Result<(), Box<dyn Error>> {
let expected = ProfileBatchV1 {
format_version: 1,
profile_id: "profile".to_owned(),
batch_id: "batch".to_owned(),
start_unix_nanos: 1,
end_unix_nanos: u64::MAX,
duration_nanos: u64::MAX - 1,
period_nanos: 10,
sample_types: vec![ProfileSampleTypeV1 {
sample_type: "cpu".to_owned(),
unit: "nanoseconds".to_owned(),
}],
resource_attributes: vec![ProfileLabelV1 {
key: "service".to_owned(),
value: "api".to_owned(),
}],
instrumentation_scope: ProfileInstrumentationScopeV1 {
name: "scope".to_owned(),
version: Some("1".to_owned()),
attributes: vec![ProfileLabelV1 {
key: "language".to_owned(),
value: "rust".to_owned(),
}],
},
mappings: vec![ProfileMappingV1 {
mapping_id: "mapping".to_owned(),
build_id: "build".to_owned(),
module_id: "module".to_owned(),
filename: Some("module.rs".to_owned()),
}],
functions: vec![ProfileFunctionV1 {
function_id: "function".to_owned(),
name: Some("handle".to_owned()),
system_name: None,
filename: Some("module.rs".to_owned()),
start_line: Some(u64::MAX),
symbol_observation_id: Some("symbol".to_owned()),
}],
locations: vec![ProfileLocationV1 {
location_id: "location".to_owned(),
mapping_id: Some("mapping".to_owned()),
address: u64::MAX,
lines: vec![ProfileLineV1 {
function_id: "function".to_owned(),
line: u64::MAX,
}],
}],
stacks: vec![ProfileStackV1 {
stack_id: "stack".to_owned(),
parent_stack_id: None,
location_id: "location".to_owned(),
}],
samples: vec![ProfileSampleV1 {
sample_id: "sample".to_owned(),
stack_id: "stack".to_owned(),
values: vec![
ProfileNumberV1::I64 { value: i64::MIN },
ProfileNumberV1::F64Bits {
value: 3.5_f64.to_bits(),
},
],
labels: vec![ProfileLabelV1 {
key: "thread".to_owned(),
value: "main".to_owned(),
}],
trace_id: Some("trace".to_owned()),
span_id: Some("span".to_owned()),
}],
};
let wire = profile_batch_to_wire(expected.clone());
assert_eq!(profile_batch_from_wire(&wire)?, expected);
Ok(())
}
}