use std::collections::BTreeMap;
use std::fmt;
use serde::{Deserialize, Serialize};
use crate::canonical::{CanonicalError, record_canonical_bytes};
use crate::limits::TRACE_V1;
use crate::trace_fields::{fixed, span};
use crate::{Document, DocumentError};
pub const TRACE: &str = "rustev.trace/1";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
pub enum TraceValue {
Str(String),
Int(i64),
Double(f64),
Bool(bool),
StrArray(Vec<String>),
IntArray(Vec<i64>),
DoubleArray(Vec<f64>),
BoolArray(Vec<bool>),
}
impl TraceValue {
pub fn as_str(&self) -> Option<&str> {
match self {
TraceValue::Str(s) => Some(s),
_ => None,
}
}
pub fn is_well_formed(&self) -> bool {
match self {
TraceValue::Double(d) => d.is_finite(),
TraceValue::DoubleArray(a) => !a.is_empty() && a.iter().all(|d| d.is_finite()),
TraceValue::IntArray(a) => !a.is_empty(),
TraceValue::BoolArray(a) => !a.is_empty(),
_ => true,
}
}
}
impl From<&str> for TraceValue {
fn from(s: &str) -> Self {
TraceValue::Str(s.to_string())
}
}
impl From<String> for TraceValue {
fn from(s: String) -> Self {
TraceValue::Str(s)
}
}
impl From<i64> for TraceValue {
fn from(i: i64) -> Self {
TraceValue::Int(i)
}
}
impl From<bool> for TraceValue {
fn from(b: bool) -> Self {
TraceValue::Bool(b)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Mark {
Allow,
Digest,
Truncated,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct TraceAttr {
pub v: TraceValue,
pub r: Mark,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SpanKind {
Internal,
Client,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SpanStatus {
Unset,
Error,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct TraceEvent {
pub name: String,
pub time_unix_nanos: u64,
pub attributes: BTreeMap<String, TraceAttr>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct TraceScope {
pub name: String,
pub version: String,
pub schema_url: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct TraceResource {
pub attributes: BTreeMap<String, TraceAttr>,
pub schema_url: String,
pub scope: TraceScope,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct TraceLine {
pub schema: String,
pub schema_version: u32,
pub trace_id: String,
pub span_id: String,
pub parent_span_id: Option<String>,
pub name: String,
pub kind: SpanKind,
pub start_unix_nanos: u64,
pub end_unix_nanos: u64,
pub status: SpanStatus,
pub attributes: BTreeMap<String, TraceAttr>,
pub events: Vec<TraceEvent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resource: Option<TraceResource>,
}
crate::document::document!(TraceLine, TRACE, TRACE_V1);
#[derive(Debug, Clone, PartialEq)]
pub enum TraceLineError {
Parse(DocumentError),
Invalid { what: String },
Canonical(CanonicalError),
TooLong { bytes: usize },
}
impl fmt::Display for TraceLineError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TraceLineError::Parse(e) => write!(f, "{e}"),
TraceLineError::Invalid { what } => write!(f, "invalid trace line: {what}"),
TraceLineError::Canonical(e) => write!(f, "{e}"),
TraceLineError::TooLong { bytes } => write!(
f,
"trace line is {bytes} bytes, at most {}",
TRACE_V1.max_bytes
),
}
}
}
impl std::error::Error for TraceLineError {}
fn is_hex(s: &str, len: usize) -> bool {
s.len() == len
&& s.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
&& s.bytes().any(|b| b != b'0')
}
fn invalid(what: impl Into<String>) -> TraceLineError {
TraceLineError::Invalid { what: what.into() }
}
impl TraceLine {
pub fn parse_line(bytes: &[u8]) -> Result<Self, TraceLineError> {
let line = Self::parse(bytes).map_err(TraceLineError::Parse)?;
line.check()?;
Ok(line)
}
pub fn check(&self) -> Result<(), TraceLineError> {
if self.schema_version != crate::trace_fields::SPAN_SCHEMA_VERSION {
return Err(invalid("schema_version"));
}
if !is_hex(&self.trace_id, 32) {
return Err(invalid("trace_id"));
}
if !is_hex(&self.span_id, 16) {
return Err(invalid("span_id"));
}
if let Some(p) = &self.parent_span_id
&& !is_hex(p, 16)
{
return Err(invalid("parent_span_id"));
}
if !span::ALL.contains(&self.name.as_str()) {
return Err(invalid("name"));
}
if self.end_unix_nanos < self.start_unix_nanos {
return Err(invalid("end_unix_nanos"));
}
if self.resource.is_some() != (self.name == span::DECISION) {
return Err(invalid("resource"));
}
let finite = self.attributes.values().all(|a| a.v.is_well_formed())
&& self
.events
.iter()
.all(|e| e.attributes.values().all(|a| a.v.is_well_formed()));
if !finite {
return Err(invalid("a non-finite number or an empty non-string array"));
}
match self.attributes.get(fixed::WITHHELD) {
Some(TraceAttr {
v: TraceValue::StrArray(_),
r: Mark::Allow,
}) => {}
_ => return Err(invalid(fixed::WITHHELD)),
}
Ok(())
}
pub fn to_line(&self) -> Result<Vec<u8>, TraceLineError> {
self.check()?;
let bytes = record_canonical_bytes(self).map_err(TraceLineError::Canonical)?;
if bytes.len() > TRACE_V1.max_bytes {
return Err(TraceLineError::TooLong { bytes: bytes.len() });
}
Ok(bytes)
}
}