crate::endpoints! {
self: Stream<Event, Out>;
}
#[derive(
phoxal_macros::DescribeWire, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize,
)]
pub struct Timestamp {
pub unix_seconds: i64,
pub nanos: u32,
}
#[derive(
phoxal_macros::DescribeWire,
Clone,
Copy,
Debug,
Eq,
PartialEq,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum Level {
Error,
Warn,
Info,
Debug,
Trace,
}
#[derive(
phoxal_macros::DescribeWire, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize,
)]
#[serde(untagged)]
pub enum LogValue {
Bool(bool),
I64(i64),
U64(u64),
F64(#[serde(deserialize_with = "crate::runtime::api::logs::deserialize_finite_f64")] f64),
String(String),
}
#[derive(
phoxal_macros::DescribeWire, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize,
)]
pub struct Event {
pub seq: u64,
pub time: Timestamp,
pub level: Level,
pub target: String,
pub message: String,
pub fields: ::std::collections::BTreeMap<String, LogValue>,
pub dropped: u32,
#[serde(default)]
pub truncated: u32,
}
fn deserialize_finite_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
if value.is_finite() {
Ok(value)
} else {
Err(serde::de::Error::custom(
"expected a finite floating-point value",
))
}
}
#[cfg(test)]
mod tests {
use crate::__compat::wire::{DescribeWire, EnumRepresentation, WireSchema};
use super::{Event, Level, LogValue, Timestamp};
#[test]
fn the_field_value_is_declared_as_an_untagged_sum_in_declaration_order() {
let schema = LogValue::wire_schema();
let WireSchema::Enum {
representation,
variants,
} = &schema
else {
panic!("a log value is a sum type: {schema:?}");
};
assert_eq!(*representation, EnumRepresentation::Untagged);
assert_eq!(
variants
.iter()
.map(|variant| variant.name.as_str())
.collect::<Vec<_>>(),
["Bool", "I64", "U64", "F64", "String"]
);
for value in [
LogValue::Bool(true),
LogValue::I64(-4),
LogValue::U64(4),
LogValue::F64(1.5),
LogValue::String(String::from("text")),
] {
let json = serde_json::to_value(&value).expect("a field value serializes");
assert_eq!(schema.conforms(&json), Ok(()), "{json}");
}
}
#[test]
fn the_declared_event_shape_is_the_shape_serde_writes() {
let event = Event {
seq: 7,
time: Timestamp {
unix_seconds: 17,
nanos: 250,
},
level: Level::Warn,
target: String::from("phoxal::drive"),
message: String::from("target stale"),
fields: [(String::from("age_ms"), LogValue::U64(120))]
.into_iter()
.collect(),
dropped: 0,
truncated: 0,
};
let json = serde_json::to_value(&event).expect("an event serializes");
assert_eq!(Event::wire_schema().conforms(&json), Ok(()));
assert_eq!(json["level"], "warn");
let mut without_counter = json.clone();
let object = without_counter.as_object_mut().expect("an event is a map");
object.remove("truncated");
assert_eq!(Event::wire_schema().conforms(&without_counter), Ok(()));
assert!(serde_json::from_value::<Event>(without_counter).is_ok());
}
}