1use schemars::JsonSchema;
22use serde::{Deserialize, Serialize};
23use serde_json::Value;
24use std::collections::BTreeMap;
25use std::sync::Mutex;
26use std::sync::atomic::{AtomicU64, Ordering};
27
28pub fn estimate_json_bytes(v: &Value) -> u64 {
34 match v {
35 Value::Null => 4,
36 Value::Bool(true) => 4,
37 Value::Bool(false) => 5,
38 Value::Number(n) => {
39 if let Some(i) = n.as_i64() {
40 digits_i64(i)
41 } else if let Some(u) = n.as_u64() {
42 digits_u64(u)
43 } else {
44 17
47 }
48 }
49 Value::String(s) => s.len() as u64 + 2,
50 Value::Array(items) => {
51 let inner: u64 = items.iter().map(estimate_json_bytes).sum();
52 inner + 2 + items.len().saturating_sub(1) as u64
53 }
54 Value::Object(map) => {
55 let inner: u64 = map
56 .iter()
57 .map(|(k, val)| k.len() as u64 + 3 + estimate_json_bytes(val))
58 .sum();
59 inner + 2 + map.len().saturating_sub(1) as u64
60 }
61 }
62}
63
64pub fn estimate_page_bytes(records: &[Value]) -> u64 {
66 records.iter().map(estimate_json_bytes).sum()
67}
68
69fn digits_u64(mut u: u64) -> u64 {
70 let mut n = 1;
71 while u >= 10 {
72 u /= 10;
73 n += 1;
74 }
75 n
76}
77
78fn digits_i64(i: i64) -> u64 {
79 if i < 0 {
80 1 + digits_u64(i.unsigned_abs())
81 } else {
82 digits_u64(i as u64)
83 }
84}
85
86#[derive(
88 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
89)]
90#[serde(rename_all = "snake_case")]
91pub enum UsageSide {
92 Source,
93 Sink,
94}
95
96impl UsageSide {
97 pub fn as_str(self) -> &'static str {
98 match self {
99 Self::Source => "source",
100 Self::Sink => "sink",
101 }
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
109pub struct CostSignal {
110 pub kind: String,
113 pub unit: String,
115 pub quantity: f64,
116 pub side: UsageSide,
117 pub connector: String,
119}
120
121fn fold_signal(signals: &mut Vec<CostSignal>, signal: CostSignal) {
122 match signals.iter_mut().find(|s| {
123 s.kind == signal.kind
124 && s.unit == signal.unit
125 && s.side == signal.side
126 && s.connector == signal.connector
127 }) {
128 Some(s) => s.quantity += signal.quantity,
129 None => signals.push(signal),
130 }
131}
132
133#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
135pub struct UsageSnapshot {
136 pub records_read: u64,
138 pub records_written: u64,
140 pub bytes_read: u64,
142 pub bytes_written: u64,
144 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
146 pub source_roundtrips: BTreeMap<String, u64>,
147 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
149 pub sink_roundtrips: BTreeMap<String, u64>,
150 #[serde(default, skip_serializing_if = "Vec::is_empty")]
152 pub signals: Vec<CostSignal>,
153 #[serde(default, skip_serializing_if = "is_zero_u64")]
155 pub throttled: u64,
156 #[serde(default, skip_serializing_if = "is_zero_f64")]
158 pub throttle_wait_secs: f64,
159 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
162 pub source_retries: BTreeMap<String, u64>,
163 #[serde(default, skip_serializing_if = "is_zero_u64")]
165 pub dlq_records_written: u64,
166 #[serde(default, skip_serializing_if = "is_zero_u64")]
168 pub dlq_bytes_written: u64,
169}
170
171fn is_zero_u64(v: &u64) -> bool {
172 *v == 0
173}
174
175fn is_zero_f64(v: &f64) -> bool {
176 *v == 0.0
177}
178
179impl UsageSnapshot {
180 pub fn roundtrips(&self, side: UsageSide) -> u64 {
182 match side {
183 UsageSide::Source => self.source_roundtrips.values().sum(),
184 UsageSide::Sink => self.sink_roundtrips.values().sum(),
185 }
186 }
187
188 pub fn merge(&mut self, other: &UsageSnapshot) {
191 self.records_read += other.records_read;
192 self.records_written += other.records_written;
193 self.bytes_read += other.bytes_read;
194 self.bytes_written += other.bytes_written;
195 for (k, v) in &other.source_roundtrips {
196 *self.source_roundtrips.entry(k.clone()).or_default() += v;
197 }
198 for (k, v) in &other.sink_roundtrips {
199 *self.sink_roundtrips.entry(k.clone()).or_default() += v;
200 }
201 for sig in &other.signals {
202 fold_signal(&mut self.signals, sig.clone());
203 }
204 self.throttled += other.throttled;
205 self.throttle_wait_secs += other.throttle_wait_secs;
206 for (k, v) in &other.source_retries {
207 *self.source_retries.entry(k.clone()).or_default() += v;
208 }
209 self.dlq_records_written += other.dlq_records_written;
210 self.dlq_bytes_written += other.dlq_bytes_written;
211 }
212}
213
214#[derive(Debug, Default)]
216pub struct UsageMeter {
217 records_read: AtomicU64,
218 records_written: AtomicU64,
219 bytes_read: AtomicU64,
220 bytes_written: AtomicU64,
221 roundtrips: Mutex<BTreeMap<(UsageSide, &'static str), u64>>,
222 signals: Mutex<Vec<CostSignal>>,
223 throttled: AtomicU64,
224 throttle_wait_nanos: AtomicU64,
225 source_retries: Mutex<BTreeMap<&'static str, u64>>,
226 dlq_records_written: AtomicU64,
227 dlq_bytes_written: AtomicU64,
228}
229
230impl UsageMeter {
231 pub fn new() -> Self {
232 Self::default()
233 }
234
235 pub fn add_read(&self, records: u64, bytes: u64) {
237 self.records_read.fetch_add(records, Ordering::Relaxed);
238 self.bytes_read.fetch_add(bytes, Ordering::Relaxed);
239 }
240
241 pub fn add_written(&self, records: u64, bytes: u64) {
243 self.records_written.fetch_add(records, Ordering::Relaxed);
244 self.bytes_written.fetch_add(bytes, Ordering::Relaxed);
245 }
246
247 pub fn add_dlq_written(&self, records: u64, bytes: u64) {
249 self.dlq_records_written
250 .fetch_add(records, Ordering::Relaxed);
251 self.dlq_bytes_written.fetch_add(bytes, Ordering::Relaxed);
252 }
253
254 pub fn add_roundtrip(&self, side: UsageSide, op: &'static str) {
256 let mut map = self.roundtrips.lock().unwrap_or_else(|e| e.into_inner());
257 *map.entry((side, op)).or_default() += 1;
258 }
259
260 pub fn add_signal(&self, signal: CostSignal) {
263 fold_signal(
264 &mut self.signals.lock().unwrap_or_else(|e| e.into_inner()),
265 signal,
266 );
267 }
268
269 pub fn add_throttled(&self) {
271 self.throttled.fetch_add(1, Ordering::Relaxed);
272 }
273
274 pub fn add_throttle_wait(&self, slept: std::time::Duration) {
276 self.throttle_wait_nanos.fetch_add(
277 u64::try_from(slept.as_nanos()).unwrap_or(u64::MAX),
278 Ordering::Relaxed,
279 );
280 }
281
282 pub fn add_source_retry(&self, class: &'static str) {
284 let mut map = self
285 .source_retries
286 .lock()
287 .unwrap_or_else(|e| e.into_inner());
288 *map.entry(class).or_default() += 1;
289 }
290
291 pub fn records_written(&self) -> u64 {
293 self.records_written.load(Ordering::Relaxed)
294 }
295
296 pub fn bytes_written(&self) -> u64 {
299 self.bytes_written.load(Ordering::Relaxed)
300 }
301
302 pub fn snapshot(&self) -> UsageSnapshot {
304 let mut source_roundtrips = BTreeMap::new();
305 let mut sink_roundtrips = BTreeMap::new();
306 for ((side, op), n) in self
307 .roundtrips
308 .lock()
309 .unwrap_or_else(|e| e.into_inner())
310 .iter()
311 {
312 match side {
313 UsageSide::Source => *source_roundtrips.entry((*op).to_string()).or_default() += n,
314 UsageSide::Sink => *sink_roundtrips.entry((*op).to_string()).or_default() += n,
315 }
316 }
317 UsageSnapshot {
318 records_read: self.records_read.load(Ordering::Relaxed),
319 records_written: self.records_written.load(Ordering::Relaxed),
320 bytes_read: self.bytes_read.load(Ordering::Relaxed),
321 bytes_written: self.bytes_written.load(Ordering::Relaxed),
322 source_roundtrips,
323 sink_roundtrips,
324 signals: self
325 .signals
326 .lock()
327 .unwrap_or_else(|e| e.into_inner())
328 .clone(),
329 throttled: self.throttled.load(Ordering::Relaxed),
330 throttle_wait_secs: std::time::Duration::from_nanos(
331 self.throttle_wait_nanos.load(Ordering::Relaxed),
332 )
333 .as_secs_f64(),
334 source_retries: self
335 .source_retries
336 .lock()
337 .unwrap_or_else(|e| e.into_inner())
338 .iter()
339 .map(|(k, v)| ((*k).to_string(), *v))
340 .collect(),
341 dlq_records_written: self.dlq_records_written.load(Ordering::Relaxed),
342 dlq_bytes_written: self.dlq_bytes_written.load(Ordering::Relaxed),
343 }
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use serde_json::json;
351
352 #[test]
353 fn byte_estimate_tracks_serialized_size() {
354 for v in [
355 json!(null),
356 json!(true),
357 json!(false),
358 json!(0),
359 json!(-42),
360 json!(1234567890123u64),
361 json!("héllo"),
362 json!([]),
363 json!({}),
364 json!([1, 2, 3]),
365 json!({"a": 1, "bb": [true, null], "c": {"d": "x"}}),
366 ] {
367 let exact = serde_json::to_vec(&v).unwrap().len() as u64;
368 assert_eq!(estimate_json_bytes(&v), exact, "{v}");
369 }
370 assert!(estimate_json_bytes(&json!(1.5)) >= 3);
372 assert_eq!(
373 estimate_page_bytes(&[json!({"a": 1}), json!({"a": 22})]),
374 7 + 8
375 );
376 }
377
378 #[test]
379 fn meter_counts_and_snapshots() {
380 let m = UsageMeter::new();
381 m.add_read(3, 30);
382 m.add_written(2, 20);
383 m.add_roundtrip(UsageSide::Source, "page");
384 m.add_roundtrip(UsageSide::Source, "page");
385 m.add_roundtrip(UsageSide::Sink, "insert");
386 m.add_signal(CostSignal {
387 kind: "bytes_billed".into(),
388 unit: "bytes".into(),
389 quantity: 1024.0,
390 side: UsageSide::Sink,
391 connector: "bigquery".into(),
392 });
393 assert_eq!(m.records_written(), 2);
394 assert_eq!(m.bytes_written(), 20);
395 let s = m.snapshot();
396 assert_eq!(s.records_read, 3);
397 assert_eq!(s.bytes_read, 30);
398 assert_eq!(s.source_roundtrips["page"], 2);
399 assert_eq!(s.sink_roundtrips["insert"], 1);
400 assert_eq!(s.roundtrips(UsageSide::Source), 2);
401 assert_eq!(s.roundtrips(UsageSide::Sink), 1);
402 assert_eq!(s.signals.len(), 1);
403 assert_eq!(UsageSide::Sink.as_str(), "sink");
404
405 let mut total = UsageSnapshot::default();
406 total.merge(&s);
407 total.merge(&s);
408 assert_eq!(total.records_written, 4);
409 assert_eq!(total.source_roundtrips["page"], 4);
410 assert_eq!(total.signals.len(), 1);
411 assert_eq!(total.signals[0].quantity, 2048.0);
412 let round: UsageSnapshot =
413 serde_json::from_value(serde_json::to_value(&s).unwrap()).unwrap();
414 assert_eq!(round, s);
415 }
416
417 #[test]
418 fn meter_tallies_throttling_and_retries() {
419 let m = UsageMeter::new();
420 let quiet = m.snapshot();
421 let v = serde_json::to_value(&quiet).unwrap();
422 assert!(v.get("throttled").is_none());
423 assert!(v.get("throttle_wait_secs").is_none());
424 assert!(v.get("source_retries").is_none());
425
426 m.add_throttled();
427 m.add_throttled();
428 m.add_throttle_wait(std::time::Duration::from_millis(1500));
429 m.add_throttle_wait(std::time::Duration::from_millis(500));
430 m.add_source_retry("rate_limited");
431 m.add_source_retry("rate_limited");
432 m.add_source_retry("http_5xx");
433 let s = m.snapshot();
434 assert_eq!(s.throttled, 2);
435 assert!((s.throttle_wait_secs - 2.0).abs() < 1e-9);
436 assert_eq!(s.source_retries["rate_limited"], 2);
437 assert_eq!(s.source_retries["http_5xx"], 1);
438
439 let mut total = UsageSnapshot::default();
440 total.merge(&s);
441 total.merge(&s);
442 assert_eq!(total.throttled, 4);
443 assert!((total.throttle_wait_secs - 4.0).abs() < 1e-9);
444 assert_eq!(total.source_retries["rate_limited"], 4);
445 let round: UsageSnapshot =
446 serde_json::from_value(serde_json::to_value(&s).unwrap()).unwrap();
447 assert_eq!(round, s);
448 }
449
450 #[test]
451 fn signals_are_summed_per_kind_unit_side_and_connector() {
452 let sig = |kind: &str, q: f64| CostSignal {
453 kind: kind.into(),
454 unit: "bytes".into(),
455 quantity: q,
456 side: UsageSide::Sink,
457 connector: "bigquery".into(),
458 };
459 let m = UsageMeter::new();
460 for _ in 0..1000 {
461 m.add_signal(sig("bytes_streamed", 2.0));
462 }
463 m.add_signal(sig("bytes_billed", 5.0));
464 let mut snap = m.snapshot();
465 assert_eq!(snap.signals.len(), 2);
466 assert_eq!(snap.signals[0].quantity, 2000.0);
467 let other = snap.clone();
468 snap.merge(&other);
469 assert_eq!(snap.signals.len(), 2);
470 assert_eq!(snap.signals[0].quantity, 4000.0);
471 assert_eq!(snap.signals[1].quantity, 10.0);
472 }
473}