libdd_trace_utils/stats_payload_encoder/
mod.rs1use libdd_trace_protobuf::pb;
12
13pub const MAX_GROUPED_STATS_PER_PAYLOAD: usize = 4000;
16
17pub fn build_stats_payload(
21 payload: pb::ClientStatsPayload,
22 hostname: String,
23 env: String,
24 version: String,
25 split_payload: bool,
26) -> pb::StatsPayload {
27 pb::StatsPayload {
28 agent_hostname: hostname,
29 agent_env: env,
30 stats: vec![payload],
31 agent_version: version,
32 client_computed: true,
35 split_payload,
36 }
37}
38
39pub fn split_stats_buckets(
44 buckets: Vec<pb::ClientStatsBucket>,
45 max_entries: usize,
46) -> Vec<Vec<pb::ClientStatsBucket>> {
47 let max_entries = max_entries.max(1);
48 let mut groups: Vec<Vec<pb::ClientStatsBucket>> = Vec::new();
49 let mut current: Vec<pb::ClientStatsBucket> = Vec::new();
50 let mut current_count = 0usize;
51
52 for bucket in buckets {
53 let pb::ClientStatsBucket {
54 start,
55 duration,
56 agent_time_shift,
57 mut stats,
58 } = bucket;
59 while !stats.is_empty() {
60 if current_count == max_entries {
61 groups.push(std::mem::take(&mut current));
62 current_count = 0;
63 }
64 let take = (max_entries - current_count).min(stats.len());
65 let rest = stats.split_off(take);
66 let chunk = std::mem::replace(&mut stats, rest);
67 current.push(pb::ClientStatsBucket {
68 start,
69 duration,
70 agent_time_shift,
71 stats: chunk,
72 });
73 current_count += take;
74 }
75 }
76
77 if !current.is_empty() {
78 groups.push(current);
79 }
80 groups
81}
82
83pub fn encode_stats_payload_msgpack(
86 payload: &pb::StatsPayload,
87) -> Result<Vec<u8>, rmp_serde::encode::Error> {
88 rmp_serde::to_vec_named(payload)
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use libdd_trace_protobuf::pb;
95
96 fn sample_client_payload() -> pb::ClientStatsPayload {
97 pb::ClientStatsPayload {
98 hostname: "client-host".to_string(),
99 env: "test".to_string(),
100 version: "1.0.0".to_string(),
101 stats: vec![pb::ClientStatsBucket {
102 start: 0,
103 duration: 10_000_000_000,
104 stats: vec![pb::ClientGroupedStats {
105 service: "svc".to_string(),
106 name: "op".to_string(),
107 resource: "res".to_string(),
108 hits: 3,
109 top_level_hits: 3,
110 duration: 42,
111 ..Default::default()
112 }],
113 agent_time_shift: 0,
114 }],
115 lang: "rust".to_string(),
116 tracer_version: "0.0.0".to_string(),
117 runtime_id: "00000000-0000-0000-0000-000000000000".to_string(),
118 sequence: 1,
119 ..Default::default()
120 }
121 }
122
123 #[test]
124 fn build_wraps_single_payload() {
125 let client = sample_client_payload();
126 let payload = build_stats_payload(
127 client.clone(),
128 "host-a".to_string(),
129 "prod".to_string(),
130 "1.2.3-libdatadog".to_string(),
131 false,
132 );
133
134 assert_eq!(payload.agent_hostname, "host-a");
135 assert_eq!(payload.agent_env, "prod");
136 assert_eq!(payload.agent_version, "1.2.3-libdatadog");
137 assert!(payload.client_computed);
138 assert!(!payload.split_payload);
139 assert_eq!(payload.stats.len(), 1);
140 assert_eq!(payload.stats[0], client);
141 }
142
143 #[test]
144 fn encode_roundtrips_through_msgpack() {
145 let payload = build_stats_payload(
146 sample_client_payload(),
147 "host-a".to_string(),
148 "prod".to_string(),
149 "1.2.3-libdatadog".to_string(),
150 false,
151 );
152
153 let encoded = encode_stats_payload_msgpack(&payload).expect("encode should succeed");
154 let decoded: pb::StatsPayload =
155 rmp_serde::from_slice(&encoded).expect("decode should succeed");
156
157 assert_eq!(decoded, payload);
158 }
159
160 fn grouped(resource: &str) -> pb::ClientGroupedStats {
161 pb::ClientGroupedStats {
162 resource: resource.to_string(),
163 ..Default::default()
164 }
165 }
166
167 fn bucket(start: u64, count: usize) -> pb::ClientStatsBucket {
168 pb::ClientStatsBucket {
169 start,
170 duration: 10,
171 agent_time_shift: 0,
172 stats: (0..count).map(|i| grouped(&i.to_string())).collect(),
173 }
174 }
175
176 fn total_stats(groups: &[Vec<pb::ClientStatsBucket>]) -> usize {
177 groups
178 .iter()
179 .flat_map(|g| g.iter())
180 .map(|b| b.stats.len())
181 .sum()
182 }
183
184 #[test]
185 fn split_keeps_single_group_when_under_limit() {
186 let groups = split_stats_buckets(vec![bucket(0, 3), bucket(10, 2)], 4000);
187 assert_eq!(groups.len(), 1);
188 assert_eq!(groups[0].len(), 2);
189 assert_eq!(total_stats(&groups), 5);
190 }
191
192 #[test]
193 fn split_breaks_across_multiple_groups() {
194 let groups = split_stats_buckets(vec![bucket(0, 3), bucket(10, 2)], 2);
196 assert_eq!(groups.len(), 3);
197 for group in &groups {
198 let count: usize = group.iter().map(|b| b.stats.len()).sum();
199 assert!(count <= 2);
200 }
201 assert_eq!(total_stats(&groups), 5);
202 }
203
204 #[test]
205 fn split_splits_an_oversized_bucket_preserving_metadata() {
206 let groups = split_stats_buckets(vec![bucket(42, 5)], 2);
209 assert_eq!(groups.len(), 3);
210 for group in &groups {
211 for b in group {
212 assert_eq!(b.start, 42);
213 assert_eq!(b.duration, 10);
214 }
215 }
216 assert_eq!(total_stats(&groups), 5);
217 }
218
219 #[test]
220 fn split_drops_empty_buckets() {
221 let groups = split_stats_buckets(vec![bucket(0, 0), bucket(10, 0)], 2);
222 assert!(groups.is_empty());
223 }
224
225 #[test]
226 fn split_handles_zero_max_as_one() {
227 let groups = split_stats_buckets(vec![bucket(0, 2)], 0);
228 assert_eq!(groups.len(), 2);
229 assert_eq!(total_stats(&groups), 2);
230 }
231}