use libdd_trace_protobuf::pb;
pub const MAX_GROUPED_STATS_PER_PAYLOAD: usize = 4000;
pub fn build_stats_payload(
payload: pb::ClientStatsPayload,
hostname: String,
env: String,
version: String,
split_payload: bool,
) -> pb::StatsPayload {
pb::StatsPayload {
agent_hostname: hostname,
agent_env: env,
stats: vec![payload],
agent_version: version,
client_computed: true,
split_payload,
}
}
pub fn split_stats_buckets(
buckets: Vec<pb::ClientStatsBucket>,
max_entries: usize,
) -> Vec<Vec<pb::ClientStatsBucket>> {
let max_entries = max_entries.max(1);
let mut groups: Vec<Vec<pb::ClientStatsBucket>> = Vec::new();
let mut current: Vec<pb::ClientStatsBucket> = Vec::new();
let mut current_count = 0usize;
for bucket in buckets {
let pb::ClientStatsBucket {
start,
duration,
agent_time_shift,
mut stats,
} = bucket;
while !stats.is_empty() {
if current_count == max_entries {
groups.push(std::mem::take(&mut current));
current_count = 0;
}
let take = (max_entries - current_count).min(stats.len());
let rest = stats.split_off(take);
let chunk = std::mem::replace(&mut stats, rest);
current.push(pb::ClientStatsBucket {
start,
duration,
agent_time_shift,
stats: chunk,
});
current_count += take;
}
}
if !current.is_empty() {
groups.push(current);
}
groups
}
pub fn encode_stats_payload_msgpack(
payload: &pb::StatsPayload,
) -> Result<Vec<u8>, rmp_serde::encode::Error> {
rmp_serde::to_vec_named(payload)
}
#[cfg(test)]
mod tests {
use super::*;
use libdd_trace_protobuf::pb;
fn sample_client_payload() -> pb::ClientStatsPayload {
pb::ClientStatsPayload {
hostname: "client-host".to_string(),
env: "test".to_string(),
version: "1.0.0".to_string(),
stats: vec![pb::ClientStatsBucket {
start: 0,
duration: 10_000_000_000,
stats: vec![pb::ClientGroupedStats {
service: "svc".to_string(),
name: "op".to_string(),
resource: "res".to_string(),
hits: 3,
top_level_hits: 3,
duration: 42,
..Default::default()
}],
agent_time_shift: 0,
}],
lang: "rust".to_string(),
tracer_version: "0.0.0".to_string(),
runtime_id: "00000000-0000-0000-0000-000000000000".to_string(),
sequence: 1,
..Default::default()
}
}
#[test]
fn build_wraps_single_payload() {
let client = sample_client_payload();
let payload = build_stats_payload(
client.clone(),
"host-a".to_string(),
"prod".to_string(),
"1.2.3-libdatadog".to_string(),
false,
);
assert_eq!(payload.agent_hostname, "host-a");
assert_eq!(payload.agent_env, "prod");
assert_eq!(payload.agent_version, "1.2.3-libdatadog");
assert!(payload.client_computed);
assert!(!payload.split_payload);
assert_eq!(payload.stats.len(), 1);
assert_eq!(payload.stats[0], client);
}
#[test]
fn encode_roundtrips_through_msgpack() {
let payload = build_stats_payload(
sample_client_payload(),
"host-a".to_string(),
"prod".to_string(),
"1.2.3-libdatadog".to_string(),
false,
);
let encoded = encode_stats_payload_msgpack(&payload).expect("encode should succeed");
let decoded: pb::StatsPayload =
rmp_serde::from_slice(&encoded).expect("decode should succeed");
assert_eq!(decoded, payload);
}
fn grouped(resource: &str) -> pb::ClientGroupedStats {
pb::ClientGroupedStats {
resource: resource.to_string(),
..Default::default()
}
}
fn bucket(start: u64, count: usize) -> pb::ClientStatsBucket {
pb::ClientStatsBucket {
start,
duration: 10,
agent_time_shift: 0,
stats: (0..count).map(|i| grouped(&i.to_string())).collect(),
}
}
fn total_stats(groups: &[Vec<pb::ClientStatsBucket>]) -> usize {
groups
.iter()
.flat_map(|g| g.iter())
.map(|b| b.stats.len())
.sum()
}
#[test]
fn split_keeps_single_group_when_under_limit() {
let groups = split_stats_buckets(vec![bucket(0, 3), bucket(10, 2)], 4000);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].len(), 2);
assert_eq!(total_stats(&groups), 5);
}
#[test]
fn split_breaks_across_multiple_groups() {
let groups = split_stats_buckets(vec![bucket(0, 3), bucket(10, 2)], 2);
assert_eq!(groups.len(), 3);
for group in &groups {
let count: usize = group.iter().map(|b| b.stats.len()).sum();
assert!(count <= 2);
}
assert_eq!(total_stats(&groups), 5);
}
#[test]
fn split_splits_an_oversized_bucket_preserving_metadata() {
let groups = split_stats_buckets(vec![bucket(42, 5)], 2);
assert_eq!(groups.len(), 3);
for group in &groups {
for b in group {
assert_eq!(b.start, 42);
assert_eq!(b.duration, 10);
}
}
assert_eq!(total_stats(&groups), 5);
}
#[test]
fn split_drops_empty_buckets() {
let groups = split_stats_buckets(vec![bucket(0, 0), bucket(10, 0)], 2);
assert!(groups.is_empty());
}
#[test]
fn split_handles_zero_max_as_one() {
let groups = split_stats_buckets(vec![bucket(0, 2)], 0);
assert_eq!(groups.len(), 2);
assert_eq!(total_stats(&groups), 2);
}
}