Skip to content

Commit 2d5a8b5

Browse files
committed
convert labels to key-values
1 parent c05fc8b commit 2d5a8b5

File tree

2 files changed

+203
-28
lines changed

2 files changed

+203
-28
lines changed

datadog-profiling/src/internal/profile/otel_emitter/label.rs

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@
44
use crate::collections::identifiable::Id;
55
use crate::internal::Label as InternalLabel;
66
use anyhow::{Result, Context};
7+
use std::collections::HashMap;
78

89
/// Converts a datadog-profiling internal Label to an OpenTelemetry KeyValue
910
///
1011
/// # Arguments
1112
/// * `label` - The internal label to convert
1213
/// * `string_table` - A slice of strings where StringIds index into
14+
/// * `key_to_unit_map` - A mutable map from key string index to unit string index for numeric labels
1315
///
1416
/// # Returns
1517
/// * `Ok(KeyValue)` if the conversion is successful
1618
/// * `Err` with context if the StringIds are out of bounds of the string table
1719
pub fn convert_label_to_key_value(
1820
label: &InternalLabel,
1921
string_table: &[String],
22+
key_to_unit_map: &mut HashMap<usize, usize>,
2023
) -> Result<datadog_profiling_otel::KeyValue> {
2124
// Get the key string
2225
let key_id = label.get_key().to_raw_id() as usize;
@@ -38,8 +41,17 @@ pub fn convert_label_to_key_value(
3841
value: Some(datadog_profiling_otel::key_value::Value::StringValue(str_value)),
3942
})
4043
}
41-
crate::internal::LabelValue::Num { num, num_unit: _ } => {
44+
crate::internal::LabelValue::Num { num, num_unit } => {
4245
// Note: OpenTelemetry KeyValue doesn't support units, so we only store the numeric value
46+
// But we track the mapping for building the attribute_units table
47+
let key_index = label.get_key().to_raw_id() as usize;
48+
let unit_index = num_unit.to_raw_id() as usize;
49+
50+
// Only add to the map if the unit is not the default empty string (index 0)
51+
if unit_index > 0 {
52+
key_to_unit_map.insert(key_index, unit_index);
53+
}
54+
4355
Ok(datadog_profiling_otel::KeyValue {
4456
key,
4557
value: Some(datadog_profiling_otel::key_value::Value::IntValue(*num)),
@@ -66,7 +78,8 @@ mod tests {
6678
StringId::from_offset(2), // "main"
6779
);
6880

69-
let result = convert_label_to_key_value(&label, &string_table);
81+
let mut key_to_unit_map = HashMap::new();
82+
let result = convert_label_to_key_value(&label, &string_table, &mut key_to_unit_map);
7083
assert!(result.is_ok());
7184

7285
let key_value = result.unwrap();
@@ -93,7 +106,8 @@ mod tests {
93106
StringId::from_offset(2), // "bytes"
94107
);
95108

96-
let result = convert_label_to_key_value(&label, &string_table);
109+
let mut key_to_unit_map = HashMap::new();
110+
let result = convert_label_to_key_value(&label, &string_table, &mut key_to_unit_map);
97111
assert!(result.is_ok());
98112

99113
let key_value = result.unwrap();
@@ -115,7 +129,8 @@ mod tests {
115129
StringId::from_offset(0), // This index exists
116130
);
117131

118-
let result = convert_label_to_key_value(&label, &string_table);
132+
let mut key_to_unit_map = HashMap::new();
133+
let result = convert_label_to_key_value(&label, &string_table, &mut key_to_unit_map);
119134
assert!(result.is_err());
120135
}
121136

@@ -128,7 +143,71 @@ mod tests {
128143
StringId::from_offset(0),
129144
);
130145

131-
let result = convert_label_to_key_value(&label, &string_table);
146+
let mut key_to_unit_map = HashMap::new();
147+
let result = convert_label_to_key_value(&label, &string_table, &mut key_to_unit_map);
132148
assert!(result.is_err());
133149
}
150+
151+
#[test]
152+
fn test_convert_numeric_label_with_unit_mapping() {
153+
let string_table = vec![
154+
"".to_string(), // index 0
155+
"memory_usage".to_string(), // index 1
156+
"megabytes".to_string(), // index 2
157+
];
158+
159+
let label = InternalLabel::num(
160+
StringId::from_offset(1), // "memory_usage"
161+
512, // 512 MB
162+
StringId::from_offset(2), // "megabytes"
163+
);
164+
165+
let mut key_to_unit_map = HashMap::new();
166+
let result = convert_label_to_key_value(&label, &string_table, &mut key_to_unit_map);
167+
assert!(result.is_ok());
168+
169+
// Verify the KeyValue conversion
170+
let key_value = result.unwrap();
171+
assert_eq!(key_value.key, "memory_usage");
172+
match key_value.value {
173+
Some(datadog_profiling_otel::key_value::Value::IntValue(n)) => {
174+
assert_eq!(n, 512);
175+
}
176+
_ => panic!("Expected IntValue"),
177+
}
178+
179+
// Verify the unit mapping was added
180+
assert_eq!(key_to_unit_map.get(&1), Some(&2)); // key index 1 maps to unit index 2
181+
}
182+
183+
#[test]
184+
fn test_convert_numeric_label_without_unit_mapping() {
185+
let string_table = vec![
186+
"".to_string(), // index 0
187+
"counter".to_string(), // index 1
188+
];
189+
190+
let label = InternalLabel::num(
191+
StringId::from_offset(1), // "counter"
192+
42, // 42
193+
StringId::from_offset(0), // empty string (default)
194+
);
195+
196+
let mut key_to_unit_map = HashMap::new();
197+
let result = convert_label_to_key_value(&label, &string_table, &mut key_to_unit_map);
198+
assert!(result.is_ok());
199+
200+
// Verify the KeyValue conversion
201+
let key_value = result.unwrap();
202+
assert_eq!(key_value.key, "counter");
203+
match key_value.value {
204+
Some(datadog_profiling_otel::key_value::Value::IntValue(n)) => {
205+
assert_eq!(n, 42);
206+
}
207+
_ => panic!("Expected IntValue"),
208+
}
209+
210+
// Verify no unit mapping was added for default empty string
211+
assert!(key_to_unit_map.is_empty());
212+
}
134213
}

datadog-profiling/src/internal/profile/otel_emitter/profile.rs

Lines changed: 119 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,48 @@
33

44
use crate::internal::Profile as InternalProfile;
55
use crate::iter::{IntoLendingIterator, LendingIterator};
6+
use crate::internal::profile::otel_emitter::label::convert_label_to_key_value;
7+
use crate::collections::identifiable::Id;
8+
use anyhow::{Result, Context};
9+
use std::collections::HashMap;
610

7-
impl From<InternalProfile> for datadog_profiling_otel::ProfilesData {
8-
fn from(internal_profile: InternalProfile) -> Self {
11+
impl TryFrom<InternalProfile> for datadog_profiling_otel::ProfilesData {
12+
type Error = anyhow::Error;
13+
14+
fn try_from(internal_profile: InternalProfile) -> Result<Self> {
15+
// Convert string table using into_lending_iter
16+
// Note: We can't use .map().collect() here because LendingIterator doesn't implement
17+
// the standard Iterator trait. LendingIterator is designed for yielding references
18+
// with lifetimes tied to the iterator itself, so we need to manually iterate and
19+
// convert each string reference to an owned String.
20+
let string_table = {
21+
let mut strings = Vec::with_capacity(internal_profile.strings.len());
22+
let mut iter = internal_profile.strings.into_lending_iter();
23+
while let Some(s) = iter.next() {
24+
strings.push(s.to_string());
25+
}
26+
strings
27+
};
28+
29+
// Convert labels to KeyValues for the attribute table
30+
let mut key_to_unit_map = HashMap::new();
31+
let mut attribute_table = Vec::with_capacity(internal_profile.labels.len());
32+
33+
for label in internal_profile.labels.iter() {
34+
let key_value = convert_label_to_key_value(label, &string_table, &mut key_to_unit_map)
35+
.with_context(|| format!("Failed to convert label with key index {}", label.get_key().to_raw_id()))?;
36+
attribute_table.push(key_value);
37+
}
38+
39+
// Build attribute units from the key-to-unit mapping
40+
let attribute_units = key_to_unit_map
41+
.into_iter()
42+
.map(|(key_index, unit_index)| datadog_profiling_otel::AttributeUnit {
43+
attribute_key_strindex: key_index as i32,
44+
unit_strindex: unit_index as i32,
45+
})
46+
.collect();
47+
948
// Convert the ProfilesDictionary components
1049
let dictionary = datadog_profiling_otel::ProfilesDictionary {
1150
// Convert mappings
@@ -36,24 +75,12 @@ impl From<InternalProfile> for datadog_profiling_otel::ProfilesData {
3675
.map(|stack_trace| stack_trace.into())
3776
.collect(),
3877

39-
// Convert string table using into_lending_iter
40-
// Note: We can't use .map().collect() here because LendingIterator doesn't implement
41-
// the standard Iterator trait. LendingIterator is designed for yielding references
42-
// with lifetimes tied to the iterator itself, so we need to manually iterate and
43-
// convert each string reference to an owned String.
44-
string_table: {
45-
let mut strings = Vec::with_capacity(internal_profile.strings.len());
46-
let mut iter = internal_profile.strings.into_lending_iter();
47-
while let Some(s) = iter.next() {
48-
strings.push(s.to_string());
49-
}
50-
strings
51-
},
78+
string_table,
79+
attribute_table,
80+
attribute_units,
5281

53-
// For now, set empty defaults for fields we're not handling yet
54-
link_table: vec![], // TODO: Implement when we handle links
55-
attribute_table: vec![], // TODO: Implement when we handle attributes
56-
attribute_units: vec![], // TODO: Implement when we handle attribute units
82+
// For now, set empty default for links (they represent trace span connections)
83+
link_table: vec![], // TODO: Implement when we handle trace links
5784
};
5885

5986
// Create a basic ResourceProfiles structure
@@ -68,10 +95,10 @@ impl From<InternalProfile> for datadog_profiling_otel::ProfilesData {
6895
schema_url: String::new(), // TODO: Implement when we handle schema URLs
6996
}];
7097

71-
Self {
98+
Ok(Self {
7299
resource_profiles,
73100
dictionary: Some(dictionary),
74-
}
101+
})
75102
}
76103
}
77104

@@ -86,7 +113,7 @@ mod tests {
86113
let internal_profile = InternalProfile::new(&[], None);
87114

88115
// Convert to OpenTelemetry ProfilesData
89-
let otel_profiles_data = ProfilesData::from(internal_profile);
116+
let otel_profiles_data = ProfilesData::try_from(internal_profile).unwrap();
90117

91118
// Verify the conversion
92119
assert!(otel_profiles_data.dictionary.is_some());
@@ -125,7 +152,7 @@ mod tests {
125152
let _function2_id = internal_profile.add_function(&function2);
126153

127154
// Convert to OpenTelemetry ProfilesData
128-
let otel_profiles_data = ProfilesData::from(internal_profile);
155+
let otel_profiles_data = ProfilesData::try_from(internal_profile).unwrap();
129156

130157
// Verify the conversion
131158
assert!(otel_profiles_data.dictionary.is_some());
@@ -146,4 +173,73 @@ mod tests {
146173
assert_eq!(otel_function2.system_name_strindex, 8);
147174
assert_eq!(otel_function2.filename_strindex, 9);
148175
}
176+
177+
#[test]
178+
fn test_from_internal_profile_with_labels() {
179+
// Create an internal profile with some data
180+
let mut internal_profile = InternalProfile::new(&[], None);
181+
182+
// Add some labels using the API
183+
let label1 = crate::api::Label {
184+
key: "thread_id",
185+
str: "main",
186+
num: 0,
187+
num_unit: "",
188+
};
189+
let label2 = crate::api::Label {
190+
key: "memory_usage",
191+
str: "",
192+
num: 1024,
193+
num_unit: "bytes",
194+
};
195+
196+
// Add a sample with these labels
197+
let sample = crate::api::Sample {
198+
locations: vec![],
199+
values: &[42],
200+
labels: vec![label1, label2],
201+
};
202+
203+
let _ = internal_profile.add_sample(sample, None);
204+
205+
// Convert to OpenTelemetry ProfilesData
206+
let otel_profiles_data = ProfilesData::try_from(internal_profile).unwrap();
207+
208+
// Verify the conversion
209+
assert!(otel_profiles_data.dictionary.is_some());
210+
let dictionary = otel_profiles_data.dictionary.unwrap();
211+
212+
// Should have 2 labels converted to attributes
213+
assert_eq!(dictionary.attribute_table.len(), 2);
214+
215+
// Should have 1 attribute unit (for the numeric label with unit)
216+
assert_eq!(dictionary.attribute_units.len(), 1);
217+
218+
// Verify the first attribute (string label)
219+
let attr1 = &dictionary.attribute_table[0];
220+
assert_eq!(attr1.key, "thread_id");
221+
match &attr1.value {
222+
Some(datadog_profiling_otel::key_value::Value::StringValue(s)) => {
223+
assert_eq!(s, "main");
224+
}
225+
_ => panic!("Expected StringValue"),
226+
}
227+
228+
// Verify the second attribute (numeric label)
229+
let attr2 = &dictionary.attribute_table[1];
230+
assert_eq!(attr2.key, "memory_usage");
231+
match &attr2.value {
232+
Some(datadog_profiling_otel::key_value::Value::IntValue(n)) => {
233+
assert_eq!(*n, 1024);
234+
}
235+
_ => panic!("Expected IntValue"),
236+
}
237+
238+
// Verify the attribute unit mapping
239+
let unit = &dictionary.attribute_units[0];
240+
// The key should map to the memory_usage string index
241+
// and the unit should map to the "bytes" string index
242+
assert!(unit.attribute_key_strindex > 0);
243+
assert!(unit.unit_strindex > 0);
244+
}
149245
}

0 commit comments

Comments
 (0)