|
| 1 | +#![cfg(feature = "monitoring-structs")] |
| 2 | + |
| 3 | +use bytemuck::Zeroable; |
| 4 | +use cortexbrain_common::buffer_type::{ |
| 5 | + CpuFrequency, CpuIdle, MemAlloc, PacketLossMetrics, SchedStatRuntime, SchedStatWait, SslEvent, |
| 6 | + TimeStampMetrics, |
| 7 | +}; |
| 8 | +use cortexbrain_common::metadata::Metadata; |
| 9 | +use cortexbrain_common::otel_metrics::Metrics; |
| 10 | +use opentelemetry::KeyValue; |
| 11 | +use opentelemetry::metrics::MeterProvider; |
| 12 | +use opentelemetry_sdk::metrics::data::{AggregatedMetrics, Metric, MetricData, ResourceMetrics}; |
| 13 | +use opentelemetry_sdk::metrics::{InMemoryMetricExporter, SdkMeterProvider}; |
| 14 | + |
| 15 | +fn collect(record: impl FnOnce(&Metrics)) -> ResourceMetrics { |
| 16 | + let exporter = InMemoryMetricExporter::default(); |
| 17 | + let provider = SdkMeterProvider::builder() |
| 18 | + .with_periodic_exporter(exporter.clone()) |
| 19 | + .build(); |
| 20 | + let metrics = Metrics::new(&provider.meter("cortexbrain-common-test")); |
| 21 | + |
| 22 | + record(&metrics); |
| 23 | + provider |
| 24 | + .force_flush() |
| 25 | + .expect("metric export should succeed"); |
| 26 | + let snapshot = exporter |
| 27 | + .get_finished_metrics() |
| 28 | + .expect("in-memory metrics should be readable") |
| 29 | + .pop() |
| 30 | + .expect("metric export should contain a snapshot"); |
| 31 | + provider |
| 32 | + .shutdown() |
| 33 | + .expect("meter provider should shut down"); |
| 34 | + snapshot |
| 35 | +} |
| 36 | + |
| 37 | +fn metric<'a>(snapshot: &'a ResourceMetrics, name: &str) -> &'a Metric { |
| 38 | + snapshot |
| 39 | + .scope_metrics() |
| 40 | + .flat_map(|scope| scope.metrics()) |
| 41 | + .find(|metric| metric.name() == name) |
| 42 | + .unwrap_or_else(|| panic!("missing metric {name}")) |
| 43 | +} |
| 44 | + |
| 45 | +fn sum(snapshot: &ResourceMetrics, name: &str, expected: u64) -> Vec<KeyValue> { |
| 46 | + let AggregatedMetrics::U64(MetricData::Sum(sum)) = metric(snapshot, name).data() else { |
| 47 | + panic!("{name} should be a u64 counter"); |
| 48 | + }; |
| 49 | + let points: Vec<_> = sum.data_points().collect(); |
| 50 | + assert_eq!(points.len(), 1, "{name} should have one attribute set"); |
| 51 | + assert_eq!(points[0].value(), expected, "wrong {name} count"); |
| 52 | + points[0].attributes().cloned().collect() |
| 53 | +} |
| 54 | + |
| 55 | +fn gauge(snapshot: &ResourceMetrics, name: &str, expected: i64) -> Vec<KeyValue> { |
| 56 | + let AggregatedMetrics::I64(MetricData::Gauge(gauge)) = metric(snapshot, name).data() else { |
| 57 | + panic!("{name} should be an i64 gauge"); |
| 58 | + }; |
| 59 | + let points: Vec<_> = gauge.data_points().collect(); |
| 60 | + assert_eq!(points.len(), 1, "{name} should have one attribute set"); |
| 61 | + assert_eq!(points[0].value(), expected, "wrong {name} value"); |
| 62 | + points[0].attributes().cloned().collect() |
| 63 | +} |
| 64 | + |
| 65 | +fn histogram(snapshot: &ResourceMetrics, name: &str, count: u64, total: u64) { |
| 66 | + let AggregatedMetrics::U64(MetricData::Histogram(histogram)) = metric(snapshot, name).data() |
| 67 | + else { |
| 68 | + panic!("{name} should be a u64 histogram"); |
| 69 | + }; |
| 70 | + let points: Vec<_> = histogram.data_points().collect(); |
| 71 | + assert_eq!(points.len(), 1, "{name} should have one attribute set"); |
| 72 | + assert_eq!(points[0].count(), count, "wrong {name} observation count"); |
| 73 | + assert_eq!(points[0].sum(), total, "wrong {name} sum"); |
| 74 | +} |
| 75 | + |
| 76 | +#[test] |
| 77 | +fn packet_loss_counts_events_and_exports_process_and_container_attributes() { |
| 78 | + let mut metadata = Metadata::from_ebpf(Some(42), Some(123), b"worker\0"); |
| 79 | + metadata.container_name = Some("container-a".into()); |
| 80 | + metadata.container_id = Some("container-id".into()); |
| 81 | + metadata.pod_name = Some("pod-a".into()); |
| 82 | + metadata.namespace = Some("team-a".into()); |
| 83 | + |
| 84 | + let snapshot = collect(|metrics| { |
| 85 | + let mut event = PacketLossMetrics::zeroed(); |
| 86 | + event.sk_drops = 3; |
| 87 | + event.sk_err = -2; |
| 88 | + metrics.record_packet_loss_metrics(&event, &metadata); |
| 89 | + event.sk_drops = 7; |
| 90 | + event.sk_err = -4; |
| 91 | + metrics.record_packet_loss_metrics(&event, &metadata); |
| 92 | + }); |
| 93 | + |
| 94 | + let attrs = sum(&snapshot, "events_total", 2); |
| 95 | + sum(&snapshot, "socket_events_total", 2); |
| 96 | + gauge(&snapshot, "sk_drops", 7); |
| 97 | + gauge(&snapshot, "sk_err", -4); |
| 98 | + assert_eq!(attrs.len(), 6); |
| 99 | + for expected in [ |
| 100 | + KeyValue::new("tgid", 42_i64), |
| 101 | + KeyValue::new("command", "worker"), |
| 102 | + KeyValue::new("container.name", "container-a"), |
| 103 | + KeyValue::new("container.id", "container-id"), |
| 104 | + KeyValue::new("k8s.pod.name", "pod-a"), |
| 105 | + KeyValue::new("k8s.namespace.name", "team-a"), |
| 106 | + ] { |
| 107 | + assert!(attrs.contains(&expected), "missing {expected:?}"); |
| 108 | + } |
| 109 | +} |
| 110 | + |
| 111 | +#[test] |
| 112 | +fn latency_histogram_aggregates_observations_without_optional_attributes() { |
| 113 | + let metadata = Metadata::from_ebpf(None, None, b"host-process\0"); |
| 114 | + let snapshot = collect(|metrics| { |
| 115 | + let mut event = TimeStampMetrics::zeroed(); |
| 116 | + event.delta_us = 7; |
| 117 | + metrics.record_timestamp_metrics(&event, &metadata); |
| 118 | + event.delta_us = 11; |
| 119 | + metrics.record_timestamp_metrics(&event, &metadata); |
| 120 | + }); |
| 121 | + |
| 122 | + let attrs = sum(&snapshot, "events_total", 2); |
| 123 | + histogram(&snapshot, "latency_us", 2, 18); |
| 124 | + assert_eq!(attrs.len(), 2); |
| 125 | + assert!(attrs.contains(&KeyValue::new("tgid", -1_i64))); |
| 126 | + assert!(attrs.contains(&KeyValue::new("command", "host-process"))); |
| 127 | +} |
| 128 | + |
| 129 | +#[test] |
| 130 | +fn memory_allocation_records_its_dedicated_counter_and_gauge() { |
| 131 | + let metadata = Metadata::from_ebpf(Some(7), None, b"allocator"); |
| 132 | + let snapshot = collect(|metrics| { |
| 133 | + let mut event = MemAlloc::zeroed(); |
| 134 | + event.length = 4096; |
| 135 | + metrics.record_enter_mem_alloc(&event, &metadata); |
| 136 | + }); |
| 137 | + |
| 138 | + sum(&snapshot, "mem_alloc_events_total", 1); |
| 139 | + gauge(&snapshot, "enter_mem_alloc", 4096); |
| 140 | + // Do not freeze events_total here: its documentation and implementation disagree. |
| 141 | +} |
| 142 | + |
| 143 | +#[test] |
| 144 | +fn scheduler_events_record_latest_values_and_distributions() { |
| 145 | + let metadata = Metadata::from_ebpf(Some(8), None, b"scheduler"); |
| 146 | + let snapshot = collect(|metrics| { |
| 147 | + let mut wait = SchedStatWait::zeroed(); |
| 148 | + wait.delay = 10; |
| 149 | + metrics.record_sched_stat_wait(&wait, &metadata); |
| 150 | + wait.delay = 20; |
| 151 | + metrics.record_sched_stat_wait(&wait, &metadata); |
| 152 | + |
| 153 | + let mut runtime = SchedStatRuntime::zeroed(); |
| 154 | + runtime.runtime = 15; |
| 155 | + metrics.record_sched_stat_runtime(&runtime, &metadata); |
| 156 | + }); |
| 157 | + |
| 158 | + sum(&snapshot, "events_total", 3); |
| 159 | + gauge(&snapshot, "sched_stat_wait", 20); |
| 160 | + histogram(&snapshot, "sched_stat_wait_distribution", 2, 30); |
| 161 | + gauge(&snapshot, "sched_stat_runtime", 15); |
| 162 | + histogram(&snapshot, "sched_stat_runtime_distribution", 1, 15); |
| 163 | +} |
| 164 | + |
| 165 | +#[test] |
| 166 | +fn cpu_idle_and_ssl_events_keep_their_dedicated_series() { |
| 167 | + let metadata = Metadata::from_ebpf(Some(9), None, b"monitor"); |
| 168 | + let snapshot = collect(|metrics| { |
| 169 | + let mut cpu = CpuFrequency::zeroed(); |
| 170 | + cpu.bytes_alloc = 256; |
| 171 | + metrics.record_cpu_bytes_alloc(&cpu, &metadata); |
| 172 | + |
| 173 | + let mut idle = CpuIdle::zeroed(); |
| 174 | + idle.cpu_id = 2; |
| 175 | + idle.state = 3; |
| 176 | + metrics.record_cpu_idle(&idle, &metadata); |
| 177 | + |
| 178 | + let mut ssl = SslEvent::zeroed(); |
| 179 | + ssl.size = 64; |
| 180 | + metrics.record_ssl_read_bytes(&ssl, &metadata); |
| 181 | + ssl.size = 128; |
| 182 | + metrics.record_ssl_write_bytes(&ssl, &metadata); |
| 183 | + }); |
| 184 | + |
| 185 | + sum(&snapshot, "bytes_alloc_events_total", 1); |
| 186 | + gauge(&snapshot, "cpu_bytes_alloc", 256); |
| 187 | + let idle_attrs = gauge(&snapshot, "cpu_idle_state", 3); |
| 188 | + assert!(idle_attrs.contains(&KeyValue::new("cpu_id", 2_i64))); |
| 189 | + gauge(&snapshot, "ssl_read_bytes", 64); |
| 190 | + gauge(&snapshot, "ssl_write_bytes", 128); |
| 191 | + let AggregatedMetrics::U64(MetricData::Sum(total)) = metric(&snapshot, "events_total").data() |
| 192 | + else { |
| 193 | + panic!("events_total should be a u64 counter"); |
| 194 | + }; |
| 195 | + let points: Vec<_> = total.data_points().collect(); |
| 196 | + assert_eq!(points.len(), 2); |
| 197 | + assert!(points.iter().any(|point| { |
| 198 | + point.value() == 1 |
| 199 | + && point |
| 200 | + .attributes() |
| 201 | + .any(|attr| attr == &KeyValue::new("cpu_id", 2_i64)) |
| 202 | + })); |
| 203 | + assert!(points.iter().any(|point| { |
| 204 | + point.value() == 2 && point.attributes().all(|attr| attr.key.as_str() != "cpu_id") |
| 205 | + })); |
| 206 | +} |
0 commit comments