use arrow::array::{Array, ArrayRef, AsArray as _};
use arrow::datatypes::{DataType, Int32Type, Int64Type, TimestampNanosecondType};
use arrow::record_batch::RecordBatch;
use chrono::{DateTime, Utc};
use serde_json::{Map, Value};
use wyrd_queue::variant::variant_cell_to_json;
use wyrd_spec::vala::ids::{SpanId, TraceId};
use wyrd_spec::vala::trace::{
InstrumentationScope, Resource, SpanEvent, SpanKind, SpanLink, SpanRecord, SpanStatus,
};
pub const SPAN_COLUMNS: &str = "trace_id, span_id, parent_span_id, trace_state, flags, name, kind, \
start_time_unix_nano, end_time_unix_nano, status_code, status_message, \
attributes, dropped_attributes_count, events, dropped_events_count, \
links, dropped_links_count, scope_name, service_name";
pub fn span_record(batch: &RecordBatch, row: usize) -> Result<SpanRecord, String> {
let trace_id = bytes(batch, "trace_id", row)?
.ok_or("trace_id is missing")
.and_then(|id| <[u8; 16]>::try_from(id).map_err(|_| "trace_id is not 16 bytes"))?;
let trace_id = TraceId::from_bytes(trace_id).map_err(|error| error.to_string())?;
let span_id = |name| -> Result<Option<SpanId>, String> {
bytes(batch, name, row)?
.map(|id| {
<[u8; 8]>::try_from(id)
.map_err(|_| format!("{name} is not 8 bytes"))
.and_then(|id| SpanId::from_bytes(id).map_err(|error| error.to_string()))
})
.transpose()
};
let nanos = |name| -> Result<DateTime<Utc>, String> {
instant(batch, name, row)?.ok_or_else(|| format!("{name} is missing"))
};
let (start_time, end_time) = (nanos("start_time_unix_nano")?, nanos("end_time_unix_nano")?);
let status = match int32(batch, "status_code", row)? {
Some(1) => SpanStatus::Ok,
Some(2) => SpanStatus::Error {
description: text(batch, "status_message", row)?,
},
_ => SpanStatus::Unset,
};
let kind = match int32(batch, "kind", row)? {
Some(2) => SpanKind::Server,
Some(3) => SpanKind::Client,
Some(4) => SpanKind::Producer,
Some(5) => SpanKind::Consumer,
_ => SpanKind::Internal,
};
Ok(SpanRecord {
trace_id,
span_id: span_id("span_id")?.ok_or("span_id is missing")?,
parent_span_id: span_id("parent_span_id")?,
flags: u32::try_from(int64(batch, "flags", row)?.unwrap_or_default()).unwrap_or_default(),
trace_state: text(batch, "trace_state", row)?.unwrap_or_default(),
name: text(batch, "name", row)?.unwrap_or_default(),
kind,
start_time,
end_time,
duration_ms: u64::try_from((end_time - start_time).num_milliseconds()).unwrap_or(0),
status,
attributes: attribute_column(batch, "attributes", row)?,
dropped_attributes_count: count(batch, "dropped_attributes_count", row)?,
events: nested(batch, "events", row, event)?,
dropped_events_count: count(batch, "dropped_events_count", row)?,
links: nested(batch, "links", row, link)?,
dropped_links_count: count(batch, "dropped_links_count", row)?,
scope: InstrumentationScope {
name: text(batch, "scope_name", row)?.unwrap_or_default(),
version: None,
attributes: Map::new(),
},
resource: Resource {
service_name: text(batch, "service_name", row)?.unwrap_or_default(),
service_namespace: None,
service_version: None,
service_instance_id: None,
attributes: Map::new(),
},
})
}
fn event(items: &RecordBatch, row: usize) -> Result<SpanEvent, String> {
Ok(SpanEvent {
timestamp: instant(items, "time_unix_nano", row)?
.ok_or("event time_unix_nano is missing")?,
name: text(items, "name", row)?.unwrap_or_default(),
attributes: attribute_column(items, "attributes", row)?,
dropped_attributes_count: count(items, "dropped_attributes_count", row)?,
})
}
fn link(items: &RecordBatch, row: usize) -> Result<SpanLink, String> {
let trace_id = bytes(items, "trace_id", row)?.ok_or("link trace_id is missing")?;
let span_id = bytes(items, "span_id", row)?.ok_or("link span_id is missing")?;
Ok(SpanLink {
trace_id: <[u8; 16]>::try_from(trace_id)
.map_err(|_| "link trace_id is not 16 bytes".to_owned())
.and_then(|id| TraceId::from_bytes(id).map_err(|error| error.to_string()))?,
span_id: <[u8; 8]>::try_from(span_id)
.map_err(|_| "link span_id is not 8 bytes".to_owned())
.and_then(|id| SpanId::from_bytes(id).map_err(|error| error.to_string()))?,
trace_state: text(items, "trace_state", row)?.unwrap_or_default(),
flags: count(items, "flags", row)?,
attributes: attribute_column(items, "attributes", row)?,
dropped_attributes_count: count(items, "dropped_attributes_count", row)?,
})
}
fn nested<T>(
batch: &RecordBatch,
name: &str,
row: usize,
decode: fn(&RecordBatch, usize) -> Result<T, String>,
) -> Result<Vec<T>, String> {
let list = batch
.column_by_name(name)
.ok_or_else(|| format!("column {name} is missing"))?
.as_list_opt::<i32>()
.ok_or_else(|| format!("column {name} is not a list"))?;
if list.is_null(row) {
return Ok(Vec::new());
}
let elements = list.value(row);
let elements = elements
.as_struct_opt()
.filter(|elements| elements.null_count() == 0)
.ok_or_else(|| format!("column {name} does not hold non-null structs"))?;
let elements = RecordBatch::from(elements.clone());
(0..elements.num_rows())
.map(|element| decode(&elements, element))
.collect()
}
fn attribute_column(
batch: &RecordBatch,
name: &str,
row: usize,
) -> Result<Map<String, Value>, String> {
let column = batch
.column_by_name(name)
.ok_or_else(|| format!("column {name} is missing"))?;
let ordinal = u64::try_from(row).unwrap_or(u64::MAX);
match variant_cell_to_json(column.as_ref(), row)
.map_err(|violation| violation.into_error(name, ordinal).to_string())?
{
Value::Null => Ok(Map::new()),
Value::Object(attributes) => Ok(attributes),
_ => Err(format!("column {name} does not hold an attribute object")),
}
}
fn count(batch: &RecordBatch, name: &str, row: usize) -> Result<u32, String> {
u32::try_from(int64(batch, name, row)?.unwrap_or_default())
.map_err(|_| format!("column {name} is not a 32-bit count"))
}
pub fn column<'a>(batch: &'a RecordBatch, name: &str) -> Result<&'a ArrayRef, String> {
batch
.column_by_name(name)
.ok_or_else(|| format!("column {name} is missing"))
}
pub fn text(batch: &RecordBatch, name: &str, row: usize) -> Result<Option<String>, String> {
let column = column(batch, name)?;
if column.is_null(row) {
return Ok(None);
}
let value = match column.data_type() {
DataType::Utf8 => column.as_string::<i32>().value(row),
DataType::LargeUtf8 => column.as_string::<i64>().value(row),
DataType::Utf8View => column.as_string_view().value(row),
_ => return Err(format!("column {name} is not text")),
};
Ok(Some(value.to_owned()))
}
pub fn bytes(batch: &RecordBatch, name: &str, row: usize) -> Result<Option<Vec<u8>>, String> {
let column = column(batch, name)?;
if column.is_null(row) {
return Ok(None);
}
let value = match column.data_type() {
DataType::Binary => column.as_binary::<i32>().value(row),
DataType::LargeBinary => column.as_binary::<i64>().value(row),
DataType::BinaryView => column.as_binary_view().value(row),
DataType::FixedSizeBinary(_) => column.as_fixed_size_binary().value(row),
_ => return Err(format!("column {name} is not binary")),
};
Ok(Some(value.to_vec()))
}
fn int64(batch: &RecordBatch, name: &str, row: usize) -> Result<Option<i64>, String> {
let column = column(batch, name)?
.as_primitive_opt::<Int64Type>()
.ok_or_else(|| format!("column {name} is not a 64-bit integer"))?;
Ok(column.is_valid(row).then(|| column.value(row)))
}
fn instant(batch: &RecordBatch, name: &str, row: usize) -> Result<Option<DateTime<Utc>>, String> {
let column = column(batch, name)?
.as_primitive_opt::<TimestampNanosecondType>()
.ok_or_else(|| format!("column {name} is not a nanosecond timestamp"))?;
Ok(column
.is_valid(row)
.then(|| DateTime::<Utc>::from_timestamp_nanos(column.value(row))))
}
fn int32(batch: &RecordBatch, name: &str, row: usize) -> Result<Option<i32>, String> {
let column = column(batch, name)?
.as_primitive_opt::<Int32Type>()
.ok_or_else(|| format!("column {name} is not a 32-bit integer"))?;
Ok(column.is_valid(row).then(|| column.value(row)))
}