use std::sync::Arc;
use arrow::array::{ArrayRef, AsArray};
use arrow::compute::take;
use arrow::datatypes::{Int64Type, TimestampMicrosecondType};
use arrow::record_batch::RecordBatch;
use indexmap::IndexMap;
use crate::decode::SkippedMetrics;
#[derive(Debug, Clone)]
pub struct PartitionedBatch {
pub batch: RecordBatch,
pub service_name: Arc<str>,
pub min_timestamp_micros: i64,
pub record_count: usize,
}
#[derive(Debug, Clone, Default)]
pub struct ServiceGroupedBatches {
pub batches: Vec<PartitionedBatch>,
pub total_records: usize,
}
impl ServiceGroupedBatches {
pub fn iter(&self) -> impl Iterator<Item = (&str, &RecordBatch)> {
self.batches
.iter()
.map(|pb| (pb.service_name.as_ref(), &pb.batch))
}
pub fn is_empty(&self) -> bool {
self.batches.is_empty()
}
pub fn len(&self) -> usize {
self.batches.len()
}
}
impl IntoIterator for ServiceGroupedBatches {
type Item = PartitionedBatch;
type IntoIter = std::vec::IntoIter<PartitionedBatch>;
fn into_iter(self) -> Self::IntoIter {
self.batches.into_iter()
}
}
#[derive(Debug, Default)]
pub struct PartitionedMetrics {
pub gauge: ServiceGroupedBatches,
pub sum: ServiceGroupedBatches,
pub histogram: ServiceGroupedBatches,
pub exp_histogram: ServiceGroupedBatches,
pub skipped: SkippedMetrics,
}
pub fn group_batch_by_service(batch: RecordBatch) -> ServiceGroupedBatches {
if batch.num_rows() == 0 {
return ServiceGroupedBatches::default();
}
let service_col = batch
.column_by_name("service_name")
.expect("service_name column required");
let service_array = service_col
.as_string_opt::<i32>()
.expect("service_name must be Utf8 string type");
let mut groups: IndexMap<&str, Vec<u32>> = IndexMap::new();
for (idx, service) in service_array.iter().enumerate() {
let service = service.unwrap_or("unknown");
groups.entry(service).or_default().push(idx as u32);
}
let total_records = batch.num_rows();
let schema = batch.schema();
let batches = groups
.into_iter()
.map(|(service, indices)| {
let indices = arrow::array::UInt32Array::from(indices);
let columns: Vec<ArrayRef> = batch
.columns()
.iter()
.map(|col| take(col.as_ref(), &indices, None).expect("take should succeed"))
.collect();
let grouped =
RecordBatch::try_new(schema.clone(), columns).expect("schema should match");
let min_ts = extract_min_timestamp_micros(&grouped);
PartitionedBatch {
record_count: grouped.num_rows(),
batch: grouped,
service_name: Arc::from(service),
min_timestamp_micros: min_ts,
}
})
.collect();
ServiceGroupedBatches {
batches,
total_records,
}
}
pub fn extract_min_timestamp_micros(batch: &RecordBatch) -> i64 {
if batch.num_rows() == 0 {
return 0;
}
let ts_col = match batch.column_by_name("timestamp") {
Some(col) => col,
None => return 0,
};
if let Some(ts_array) = ts_col.as_primitive_opt::<TimestampMicrosecondType>() {
ts_array.iter().flatten().min().unwrap_or(0)
} else if let Some(ts_array) = ts_col.as_primitive_opt::<Int64Type>() {
let min_ms = ts_array.iter().flatten().min().unwrap_or(0);
min_ms * 1000
} else {
0
}
}
pub fn extract_service_name(batch: &RecordBatch) -> Arc<str> {
if batch.num_rows() == 0 {
return Arc::from("unknown");
}
let service_col = match batch.column_by_name("service_name") {
Some(col) => col,
None => return Arc::from("unknown"),
};
if let Some(service_array) = service_col.as_string_opt::<i32>() {
service_array
.iter()
.flatten()
.next()
.map(Arc::from)
.unwrap_or_else(|| Arc::from("unknown"))
} else {
Arc::from("unknown")
}
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::{Int32Array, StringBuilder, TimestampMicrosecondBuilder};
use arrow::datatypes::{DataType, Field, Schema, TimeUnit};
use std::sync::Arc as StdArc;
fn create_test_batch(services: &[&str], timestamps_micros: &[i64]) -> RecordBatch {
let schema = StdArc::new(Schema::new(vec![
Field::new(
"timestamp",
DataType::Timestamp(TimeUnit::Microsecond, None),
false,
),
Field::new("service_name", DataType::Utf8, false),
Field::new("value", DataType::Int32, false),
]));
let mut ts_builder = TimestampMicrosecondBuilder::new();
let mut service_builder = StringBuilder::new();
let mut value_builder = Int32Array::builder(services.len());
for (i, (service, ts)) in services.iter().zip(timestamps_micros.iter()).enumerate() {
ts_builder.append_value(*ts);
service_builder.append_value(service);
value_builder.append_value(i as i32);
}
RecordBatch::try_new(
schema,
vec![
StdArc::new(ts_builder.finish()),
StdArc::new(service_builder.finish()),
StdArc::new(value_builder.finish()),
],
)
.unwrap()
}
#[test]
fn test_group_batch_by_service_single_service() {
let batch = create_test_batch(&["svc-a", "svc-a", "svc-a"], &[100, 200, 150]);
let grouped = group_batch_by_service(batch);
assert_eq!(grouped.len(), 1);
assert_eq!(grouped.total_records, 3);
assert_eq!(grouped.batches[0].service_name.as_ref(), "svc-a");
assert_eq!(grouped.batches[0].record_count, 3);
assert_eq!(grouped.batches[0].min_timestamp_micros, 100);
}
#[test]
fn test_group_batch_by_service_multiple_services() {
let batch = create_test_batch(
&["svc-a", "svc-b", "svc-a", "svc-c", "svc-b"],
&[100, 200, 150, 300, 250],
);
let grouped = group_batch_by_service(batch);
assert_eq!(grouped.len(), 3);
assert_eq!(grouped.total_records, 5);
assert_eq!(grouped.batches[0].service_name.as_ref(), "svc-a");
assert_eq!(grouped.batches[0].record_count, 2);
assert_eq!(grouped.batches[0].min_timestamp_micros, 100);
assert_eq!(grouped.batches[1].service_name.as_ref(), "svc-b");
assert_eq!(grouped.batches[1].record_count, 2);
assert_eq!(grouped.batches[1].min_timestamp_micros, 200);
assert_eq!(grouped.batches[2].service_name.as_ref(), "svc-c");
assert_eq!(grouped.batches[2].record_count, 1);
assert_eq!(grouped.batches[2].min_timestamp_micros, 300);
}
#[test]
fn test_group_batch_by_service_empty() {
let schema = StdArc::new(Schema::new(vec![
Field::new(
"timestamp",
DataType::Timestamp(TimeUnit::Microsecond, None),
false,
),
Field::new("service_name", DataType::Utf8, false),
]));
let batch = RecordBatch::new_empty(schema);
let grouped = group_batch_by_service(batch);
assert!(grouped.is_empty());
assert_eq!(grouped.total_records, 0);
}
#[test]
fn test_extract_min_timestamp_micros() {
let batch = create_test_batch(&["svc-a", "svc-a"], &[100, 50]);
let min_ts = extract_min_timestamp_micros(&batch);
assert_eq!(min_ts, 50);
}
#[test]
fn test_extract_service_name() {
let batch = create_test_batch(&["my-service", "other"], &[100, 200]);
let service = extract_service_name(&batch);
assert_eq!(service.as_ref(), "my-service");
}
}