use chrono::{DateTime, Utc};
use pallas_codec::minicbor::{Decode, Encode};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[repr(u8)]
pub enum Severity {
Debug = 0,
Info = 1,
Notice = 2,
Warning = 3,
Error = 4,
Critical = 5,
Alert = 6,
Emergency = 7,
}
impl Encode<()> for Severity {
fn encode<W: pallas_codec::minicbor::encode::Write>(
&self,
e: &mut pallas_codec::minicbor::Encoder<W>,
_ctx: &mut (),
) -> Result<(), pallas_codec::minicbor::encode::Error<W::Error>> {
e.array(1)?.u8(*self as u8)?;
Ok(())
}
}
impl<'b> Decode<'b, ()> for Severity {
fn decode(
d: &mut pallas_codec::minicbor::Decoder<'b>,
_ctx: &mut (),
) -> Result<Self, pallas_codec::minicbor::decode::Error> {
d.array()?;
let val = d.u8()?;
match val {
0 => Ok(Severity::Debug),
1 => Ok(Severity::Info),
2 => Ok(Severity::Notice),
3 => Ok(Severity::Warning),
4 => Ok(Severity::Error),
5 => Ok(Severity::Critical),
6 => Ok(Severity::Alert),
7 => Ok(Severity::Emergency),
_ => Err(pallas_codec::minicbor::decode::Error::message(
"invalid severity value",
)),
}
}
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Severity::Debug => write!(f, "Debug"),
Severity::Info => write!(f, "Info"),
Severity::Notice => write!(f, "Notice"),
Severity::Warning => write!(f, "Warning"),
Severity::Error => write!(f, "Error"),
Severity::Critical => write!(f, "Critical"),
Severity::Alert => write!(f, "Alert"),
Severity::Emergency => write!(f, "Emergency"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[repr(u8)]
pub enum DetailLevel {
DMinimal = 0,
DNormal = 1,
DDetailed = 2,
DMaximum = 3,
}
impl Encode<()> for DetailLevel {
fn encode<W: pallas_codec::minicbor::encode::Write>(
&self,
e: &mut pallas_codec::minicbor::Encoder<W>,
_ctx: &mut (),
) -> Result<(), pallas_codec::minicbor::encode::Error<W::Error>> {
e.array(1)?.u8(*self as u8)?;
Ok(())
}
}
impl<'b> Decode<'b, ()> for DetailLevel {
fn decode(
d: &mut pallas_codec::minicbor::Decoder<'b>,
_ctx: &mut (),
) -> Result<Self, pallas_codec::minicbor::decode::Error> {
d.array()?;
let val = d.u8()?;
match val {
0 => Ok(DetailLevel::DMinimal),
1 => Ok(DetailLevel::DNormal),
2 => Ok(DetailLevel::DDetailed),
3 => Ok(DetailLevel::DMaximum),
_ => Err(pallas_codec::minicbor::decode::Error::message(
"invalid detail level",
)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceObject {
pub to_human: Option<String>,
pub to_machine: String,
pub to_namespace: Vec<String>,
pub to_severity: Severity,
pub to_details: DetailLevel,
pub to_timestamp: DateTime<Utc>,
pub to_hostname: String,
pub to_thread_id: String,
}
impl Encode<()> for TraceObject {
fn encode<W: pallas_codec::minicbor::encode::Write>(
&self,
e: &mut pallas_codec::minicbor::Encoder<W>,
ctx: &mut (),
) -> Result<(), pallas_codec::minicbor::encode::Error<W::Error>> {
e.array(9)?;
e.u8(0)?;
match &self.to_human {
Some(h) => {
e.array(1)?;
e.str(h)?;
}
None => {
e.array(0)?;
}
}
e.str(&self.to_machine)?;
e.array(self.to_namespace.len() as u64)?;
for ns in &self.to_namespace {
e.str(ns)?;
}
self.to_severity.encode(e, ctx)?;
self.to_details.encode(e, ctx)?;
let secs = self.to_timestamp.timestamp();
let psecs = self.to_timestamp.timestamp_subsec_nanos() as u64 * 1_000;
e.tag(pallas_codec::minicbor::data::Tag::new(1000))?;
e.map(2)?;
e.u8(1)?;
e.i64(secs)?;
e.i64(-12)?;
e.u64(psecs)?;
e.str(&self.to_hostname)?;
e.str(&self.to_thread_id)?;
Ok(())
}
}
impl<'b> Decode<'b, ()> for TraceObject {
fn decode(
d: &mut pallas_codec::minicbor::Decoder<'b>,
ctx: &mut (),
) -> Result<Self, pallas_codec::minicbor::decode::Error> {
let len = d.array()?;
if len != Some(9) {
return Err(pallas_codec::minicbor::decode::Error::message(
"TraceObject must have 9 elements (constructor index + 8 fields)",
));
}
let _constructor_idx = d.u8()?;
let to_human = {
let opt_len = d.array()?;
match opt_len {
Some(0) => None,
Some(1) => Some(d.str()?.to_string()),
_ => {
return Err(pallas_codec::minicbor::decode::Error::message(
"invalid Maybe encoding",
));
}
}
};
let to_machine = d.str()?.to_string();
let mut to_namespace = Vec::new();
for s in d.array_iter::<String>()? {
to_namespace.push(s?);
}
let to_severity = Severity::decode(d, ctx)?;
let to_details = DetailLevel::decode(d, ctx)?;
let tag = d.tag()?;
let to_timestamp = if tag == pallas_codec::minicbor::data::Tag::new(1000) {
let map_len = d.map()?;
if map_len != Some(2) {
return Err(pallas_codec::minicbor::decode::Error::message(
"expected map of length 2 for UTCTime (tag 1000)",
));
}
let k0 = d.i64()?;
if k0 != 1 {
return Err(pallas_codec::minicbor::decode::Error::message(
"expected key 1 (secs) in tag-1000 UTCTime",
));
}
let secs = d.i64()?;
let k1 = d.i64()?;
if k1 != -12 {
return Err(pallas_codec::minicbor::decode::Error::message(
"expected key -12 (psecs) in tag-1000 UTCTime",
));
}
let psecs = d.u64()?;
let nanos = (psecs / 1_000) as u32;
DateTime::from_timestamp(secs, nanos).ok_or_else(|| {
pallas_codec::minicbor::decode::Error::message("invalid timestamp")
})?
} else if tag == pallas_codec::minicbor::data::Tag::new(1) {
let timestamp_f64 = d.f64()?;
let secs = timestamp_f64.floor() as i64;
let nanos = ((timestamp_f64 - secs as f64) * 1_000_000_000.0) as u32;
DateTime::from_timestamp(secs, nanos).ok_or_else(|| {
pallas_codec::minicbor::decode::Error::message("invalid timestamp")
})?
} else {
return Err(pallas_codec::minicbor::decode::Error::message(
"expected UTCTime tag (1000 or 1)",
));
};
let to_hostname = d.str()?.to_string();
let to_thread_id = d.str()?.to_string();
Ok(TraceObject {
to_human,
to_machine,
to_namespace,
to_severity,
to_details,
to_timestamp,
to_hostname,
to_thread_id,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use pallas_codec::minicbor;
fn encode<T: minicbor::Encode<()>>(value: &T) -> Vec<u8> {
let mut buf = Vec::new();
minicbor::encode_with(value, &mut buf, &mut ()).unwrap();
buf
}
fn decode<T: for<'b> minicbor::Decode<'b, ()>>(buf: &[u8]) -> T {
minicbor::decode_with(buf, &mut ()).unwrap()
}
#[test]
fn test_severity_encoding() {
assert_eq!(decode::<Severity>(&encode(&Severity::Info)), Severity::Info);
}
#[test]
fn all_severity_variants_round_trip() {
for sev in [
Severity::Debug,
Severity::Info,
Severity::Notice,
Severity::Warning,
Severity::Error,
Severity::Critical,
Severity::Alert,
Severity::Emergency,
] {
let decoded = decode::<Severity>(&encode(&sev));
assert_eq!(decoded, sev, "Severity::{:?} round-trip failed", sev);
}
}
#[test]
fn severity_encoded_as_array1_constructor_index() {
assert_eq!(encode(&Severity::Debug), &[0x81, 0x00]);
assert_eq!(encode(&Severity::Emergency), &[0x81, 0x07]);
}
#[test]
fn test_detail_level_encoding() {
assert_eq!(
decode::<DetailLevel>(&encode(&DetailLevel::DNormal)),
DetailLevel::DNormal
);
}
#[test]
fn all_detail_level_variants_round_trip() {
for dl in [
DetailLevel::DMinimal,
DetailLevel::DNormal,
DetailLevel::DDetailed,
DetailLevel::DMaximum,
] {
let decoded = decode::<DetailLevel>(&encode(&dl));
assert_eq!(decoded, dl, "DetailLevel::{:?} round-trip failed", dl);
}
}
fn make_trace_object(to_human: Option<&str>) -> TraceObject {
TraceObject {
to_human: to_human.map(str::to_string),
to_machine: r#"{"k":1}"#.to_string(),
to_namespace: vec!["Cardano".to_string(), "Node".to_string()],
to_severity: Severity::Warning,
to_details: DetailLevel::DDetailed,
to_timestamp: chrono::DateTime::from_timestamp(1_700_000_000, 500_000_000).unwrap(),
to_hostname: "node-1".to_string(),
to_thread_id: "99".to_string(),
}
}
#[test]
fn trace_object_with_human_round_trip() {
let original = make_trace_object(Some("human readable"));
let decoded = decode::<TraceObject>(&encode(&original));
assert_eq!(decoded.to_human, Some("human readable".to_string()));
assert_eq!(decoded.to_machine, original.to_machine);
assert_eq!(decoded.to_namespace, original.to_namespace);
assert_eq!(decoded.to_severity, original.to_severity);
assert_eq!(decoded.to_details, original.to_details);
assert_eq!(decoded.to_hostname, original.to_hostname);
assert_eq!(decoded.to_thread_id, original.to_thread_id);
assert_eq!(
decoded.to_timestamp.timestamp(),
original.to_timestamp.timestamp()
);
assert_eq!(
decoded.to_timestamp.timestamp_subsec_nanos(),
original.to_timestamp.timestamp_subsec_nanos()
);
}
#[test]
fn trace_object_without_human_round_trip() {
let original = make_trace_object(None);
let decoded = decode::<TraceObject>(&encode(&original));
assert_eq!(decoded.to_human, None);
}
#[test]
fn trace_object_empty_namespace_round_trip() {
let mut original = make_trace_object(None);
original.to_namespace = vec![];
let decoded = decode::<TraceObject>(&encode(&original));
assert!(decoded.to_namespace.is_empty());
}
#[test]
fn timestamp_tag1_compat_decode() {
let original = make_trace_object(None);
let normal_buf = encode(&original);
let secs = original.to_timestamp.timestamp() as f64;
let mut buf: Vec<u8> = Vec::new();
let mut enc = minicbor::Encoder::new(&mut buf);
enc.array(9).unwrap().u8(0).unwrap();
enc.array(0).unwrap();
enc.str(r#"{"k":1}"#).unwrap();
enc.array(0).unwrap();
enc.array(1).unwrap().u8(3).unwrap();
enc.array(1).unwrap().u8(2).unwrap();
enc.tag(minicbor::data::Tag::new(1))
.unwrap()
.f64(secs)
.unwrap();
enc.str("node-1").unwrap();
enc.str("99").unwrap();
let decoded = decode::<TraceObject>(&buf);
assert_eq!(
decoded.to_timestamp.timestamp(),
original.to_timestamp.timestamp()
);
let _ = normal_buf;
}
}