use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;
use uuid::Uuid;
use crate::Json;
use crate::api::llm::LlmAttributes;
use crate::api::scope::{HandleAttributes, ScopeAttributes, ScopeType};
use crate::api::tool::ToolAttributes;
use crate::codec::request::AnnotatedLlmRequest;
use crate::codec::response::AnnotatedLlmResponse;
pub const ATOF_VERSION: &str = "0.1";
pub const LOG_SEVERITY_METADATA_KEY: &str = "nemo_relay.log.severity";
pub const METRIC_DATA_SCHEMA_NAME: &str = "nemo.relay.metric_measurements";
pub const METRIC_DATA_SCHEMA_VERSION: &str = "1";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LogSeverity {
Trace,
Debug,
#[default]
Info,
Warn,
Error,
}
impl LogSeverity {
pub const fn as_str(self) -> &'static str {
match self {
Self::Trace => "trace",
Self::Debug => "debug",
Self::Info => "info",
Self::Warn => "warn",
Self::Error => "error",
}
}
}
impl fmt::Display for LogSeverity {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseLogSeverityError {
value: String,
}
impl fmt::Display for ParseLogSeverityError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"invalid log severity {:?}; expected trace, debug, info, warn, warning, or error",
self.value
)
}
}
impl std::error::Error for ParseLogSeverityError {}
impl FromStr for LogSeverity {
type Err = ParseLogSeverityError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"trace" => Ok(Self::Trace),
"debug" => Ok(Self::Debug),
"info" => Ok(Self::Info),
"warn" | "warning" => Ok(Self::Warn),
"error" => Ok(Self::Error),
_ => Err(ParseLogSeverityError {
value: value.to_string(),
}),
}
}
}
impl Serialize for LogSeverity {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for LogSeverity {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
value.parse().map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MetricKind {
Counter,
UpDownCounter,
Gauge,
Histogram,
}
impl MetricKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::Counter => "counter",
Self::UpDownCounter => "up_down_counter",
Self::Gauge => "gauge",
Self::Histogram => "histogram",
}
}
}
impl fmt::Display for MetricKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MetricValueType {
U64,
I64,
F64,
}
impl MetricValueType {
pub const fn as_str(self) -> &'static str {
match self {
Self::U64 => "u64",
Self::I64 => "i64",
Self::F64 => "f64",
}
}
}
impl fmt::Display for MetricValueType {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[serde(deny_unknown_fields)]
#[builder(field_defaults(setter(into, strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
pub struct MetricMeasurement {
pub name: String,
pub kind: MetricKind,
pub value_type: MetricValueType,
pub value: Json,
#[builder(default)]
pub unit: Option<String>,
#[builder(default)]
pub description: Option<String>,
#[builder(default)]
pub attributes: Option<Json>,
#[builder(default)]
pub boundaries: Option<Vec<f64>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[serde(deny_unknown_fields)]
#[builder(field_defaults(setter(into)))]
pub struct MetricEnvelope {
pub measurements: Vec<MetricMeasurement>,
}
impl MetricEnvelope {
pub fn validate(&self) -> Result<(), MetricValidationError> {
self.validated_measurements().map(|_| ())
}
pub fn validated_measurements(
&self,
) -> Result<Vec<ValidatedMetricMeasurement>, MetricValidationError> {
validate_metric_measurements(&self.measurements)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetricValidationError {
message: String,
}
impl MetricValidationError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
pub fn message(&self) -> &str {
self.message.as_str()
}
}
impl fmt::Display for MetricValidationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}
impl std::error::Error for MetricValidationError {}
const MAX_HISTOGRAM_BOUNDARIES: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FiniteF64(f64);
impl FiniteF64 {
pub const fn get(self) -> f64 {
self.0
}
}
impl TryFrom<f64> for FiniteF64 {
type Error = MetricValidationError;
fn try_from(value: f64) -> Result<Self, Self::Error> {
value
.is_finite()
.then_some(Self(value))
.ok_or_else(|| MetricValidationError::new("must be finite"))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct InstrumentName(String);
impl InstrumentName {
pub fn as_str(&self) -> &str {
&self.0
}
pub fn canonical(&self) -> String {
self.0.to_ascii_lowercase()
}
}
impl FromStr for InstrumentName {
type Err = MetricValidationError;
fn from_str(name: &str) -> Result<Self, Self::Err> {
let bytes = name.as_bytes();
let valid = matches!(bytes.first(), Some(first) if first.is_ascii_alphabetic())
&& bytes.len() <= 255
&& bytes[1..].iter().all(|byte| {
byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-' | b'/')
});
valid.then(|| Self(name.to_owned())).ok_or_else(|| {
MetricValidationError::new(
"name must be 1-255 ASCII bytes, start with a letter, and contain only letters, digits, '_', '.', '-', or '/'",
)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MetricValue {
U64(u64),
I64(i64),
F64(FiniteF64),
}
impl MetricValue {
pub const fn value_type(self) -> MetricValueType {
match self {
Self::U64(_) => MetricValueType::U64,
Self::I64(_) => MetricValueType::I64,
Self::F64(_) => MetricValueType::F64,
}
}
fn parse(value_type: MetricValueType, value: &Json) -> Result<Self, MetricValidationError> {
match value_type {
MetricValueType::U64 => value
.as_u64()
.filter(|value| *value <= i64::MAX as u64)
.map(Self::U64)
.ok_or_else(|| {
MetricValidationError::new(
"value must be an unsigned integer no greater than i64::MAX",
)
}),
MetricValueType::I64 => value
.as_i64()
.map(Self::I64)
.ok_or_else(|| MetricValidationError::new("value must be a signed integer")),
MetricValueType::F64 => value
.as_f64()
.ok_or_else(|| MetricValidationError::new("value must be a number"))
.and_then(FiniteF64::try_from)
.map(Self::F64),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct HistogramBoundaries(Vec<FiniteF64>);
impl HistogramBoundaries {
pub fn values(&self) -> Vec<f64> {
self.0.iter().map(|boundary| boundary.get()).collect()
}
}
impl TryFrom<Vec<f64>> for HistogramBoundaries {
type Error = MetricValidationError;
fn try_from(boundaries: Vec<f64>) -> Result<Self, Self::Error> {
if boundaries.len() > MAX_HISTOGRAM_BOUNDARIES {
return Err(MetricValidationError::new(
"boundaries must contain at most 64 entries",
));
}
let boundaries = boundaries
.into_iter()
.map(FiniteF64::try_from)
.collect::<Result<Vec<_>, _>>()?;
if boundaries
.windows(2)
.any(|pair| pair[0].get() >= pair[1].get())
{
return Err(MetricValidationError::new(
"boundaries must be strictly increasing without duplicates",
));
}
Ok(Self(boundaries))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct InstrumentDescriptor {
pub name: InstrumentName,
pub kind: MetricKind,
pub unit: Option<String>,
pub description: Option<String>,
pub boundaries: Option<HistogramBoundaries>,
}
impl InstrumentDescriptor {
fn new(
name: InstrumentName,
kind: MetricKind,
unit: Option<String>,
description: Option<String>,
boundaries: Option<HistogramBoundaries>,
) -> Result<Self, MetricValidationError> {
if boundaries.is_some() && kind != MetricKind::Histogram {
return Err(MetricValidationError::new(
"boundaries are only valid for histogram measurements",
));
}
if unit
.as_ref()
.is_some_and(|unit| !unit.is_ascii() || unit.len() > 63)
{
return Err(MetricValidationError::new(
"unit must be ASCII and at most 63 bytes",
));
}
Ok(Self {
name,
kind,
unit,
description,
boundaries,
})
}
pub fn descriptor_key(&self) -> String {
self.name.canonical()
}
fn accepts(&self, value: MetricValue) -> Result<(), MetricValidationError> {
if self.kind == MetricKind::Counter
&& matches!(value, MetricValue::F64(value) if value.get() < 0.0)
{
return Err(MetricValidationError::new(
"counter values must be non-negative",
));
}
let accepted = matches!(
(self.kind, value),
(MetricKind::Counter, MetricValue::U64(_))
| (MetricKind::UpDownCounter, MetricValue::I64(_))
| (MetricKind::UpDownCounter, MetricValue::F64(_))
| (MetricKind::Gauge, _)
| (MetricKind::Histogram, MetricValue::U64(_))
| (MetricKind::Histogram, MetricValue::F64(_))
| (MetricKind::Counter, MetricValue::F64(_))
);
accepted.then_some(()).ok_or_else(|| {
MetricValidationError::new(format!(
"kind {} does not support value_type {}",
self.kind,
value.value_type()
))
})
}
fn has_same_identity(&self, other: &Self) -> bool {
self.kind == other.kind && self.unit == other.unit
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AttributeValue {
String(String),
Bool(bool),
I64(i64),
F64(FiniteF64),
StringArray(Vec<String>),
BoolArray(Vec<bool>),
I64Array(Vec<i64>),
F64Array(Vec<FiniteF64>),
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct MetricAttributes(BTreeMap<String, AttributeValue>);
impl MetricAttributes {
pub fn insert(&mut self, key: String, value: AttributeValue) {
self.0.insert(key, value);
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &AttributeValue)> {
self.0.iter()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl TryFrom<Option<&Json>> for MetricAttributes {
type Error = MetricValidationError;
fn try_from(attributes: Option<&Json>) -> Result<Self, Self::Error> {
let Some(attributes) = attributes else {
return Ok(Self::default());
};
let object = attributes
.as_object()
.ok_or_else(|| MetricValidationError::new("attributes must be a JSON object"))?;
object
.iter()
.map(|(key, value)| {
if key.trim().is_empty() {
return Err(MetricValidationError::new(
"attributes contains a blank attribute key",
));
}
parse_attribute_value(value).map(|value| (key.clone(), value))
})
.collect::<Result<BTreeMap<_, _>, _>>()
.map(Self)
}
}
fn parse_attribute_value(value: &Json) -> Result<AttributeValue, MetricValidationError> {
match value {
Json::String(value) => Ok(AttributeValue::String(value.clone())),
Json::Bool(value) => Ok(AttributeValue::Bool(*value)),
Json::Number(number) if number.as_i64().is_some() => Ok(AttributeValue::I64(
number.as_i64().expect("checked signed integer"),
)),
Json::Number(number) if number.as_u64().is_some() => Err(MetricValidationError::new(
"attribute values must not exceed the maximum signed 64-bit integer",
)),
Json::Number(number) => number
.as_f64()
.ok_or_else(|| MetricValidationError::new("attribute values must be finite numbers"))
.and_then(FiniteF64::try_from)
.map(AttributeValue::F64),
Json::Array(values) => parse_attribute_array(values),
Json::Null => Err(MetricValidationError::new(
"attribute values must not be null",
)),
Json::Object(_) => Err(MetricValidationError::new(
"attribute values must be primitive values or homogeneous primitive arrays",
)),
}
}
fn parse_attribute_array(values: &[Json]) -> Result<AttributeValue, MetricValidationError> {
let Some(first) = values.first() else {
return Err(MetricValidationError::new(
"attribute arrays must not be empty and untyped",
));
};
match parse_attribute_value(first)? {
AttributeValue::String(_) => values
.iter()
.map(|value| value.as_str().map(str::to_owned))
.collect::<Option<Vec<_>>>()
.map(AttributeValue::StringArray)
.ok_or_else(|| {
MetricValidationError::new("attribute arrays must contain one primitive type")
}),
AttributeValue::Bool(_) => values
.iter()
.map(Json::as_bool)
.collect::<Option<Vec<_>>>()
.map(AttributeValue::BoolArray)
.ok_or_else(|| {
MetricValidationError::new("attribute arrays must contain one primitive type")
}),
AttributeValue::I64(_) => values
.iter()
.map(Json::as_i64)
.collect::<Option<Vec<_>>>()
.map(AttributeValue::I64Array)
.ok_or_else(|| {
MetricValidationError::new("attribute arrays must contain one primitive type")
}),
AttributeValue::F64(_) => values
.iter()
.map(|value| {
value
.as_f64()
.filter(|_| value.as_i64().is_none())
.and_then(|value| FiniteF64::try_from(value).ok())
})
.collect::<Option<Vec<_>>>()
.map(AttributeValue::F64Array)
.ok_or_else(|| {
MetricValidationError::new("attribute arrays must contain one primitive type")
}),
AttributeValue::StringArray(_)
| AttributeValue::BoolArray(_)
| AttributeValue::I64Array(_)
| AttributeValue::F64Array(_) => Err(MetricValidationError::new(
"attribute arrays must contain primitive values",
)),
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ValidatedMetricMeasurement {
pub descriptor: InstrumentDescriptor,
pub value: MetricValue,
pub attributes: MetricAttributes,
}
impl TryFrom<&MetricMeasurement> for ValidatedMetricMeasurement {
type Error = MetricValidationError;
fn try_from(wire: &MetricMeasurement) -> Result<Self, Self::Error> {
let descriptor = InstrumentDescriptor::new(
wire.name.parse()?,
wire.kind,
wire.unit.clone(),
wire.description.clone(),
wire.boundaries
.clone()
.map(HistogramBoundaries::try_from)
.transpose()?,
)?;
let value = MetricValue::parse(wire.value_type, &wire.value)?;
descriptor.accepts(value)?;
Ok(Self {
descriptor,
value,
attributes: MetricAttributes::try_from(wire.attributes.as_ref())?,
})
}
}
pub fn validate_metric_measurements(
measurements: &[MetricMeasurement],
) -> Result<Vec<ValidatedMetricMeasurement>, MetricValidationError> {
if measurements.is_empty() {
return Err(MetricValidationError::new(
"measurements must contain at least one entry",
));
}
let parsed = measurements
.iter()
.enumerate()
.map(|(index, measurement)| {
ValidatedMetricMeasurement::try_from(measurement).map_err(|error| {
MetricValidationError::new(format!("measurements[{index}] {error}"))
})
})
.collect::<Result<Vec<_>, _>>()?;
let mut descriptors = BTreeMap::<String, usize>::new();
for (index, measurement) in parsed.iter().enumerate() {
let key = measurement.descriptor.descriptor_key();
if let Some(previous_index) = descriptors.insert(key, index) {
let previous = &parsed[previous_index];
if !previous
.descriptor
.has_same_identity(&measurement.descriptor)
|| previous.value.value_type() != measurement.value.value_type()
{
return Err(MetricValidationError::new(format!(
"measurements[{index}] conflicts with the descriptor for measurements[{previous_index}]"
)));
}
}
}
Ok(parsed)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TypedBuilder)]
#[builder(field_defaults(setter(into)))]
pub struct DataSchema {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct EventCategory(String);
impl EventCategory {
pub fn agent() -> Self {
Self("agent".into())
}
pub fn function() -> Self {
Self("function".into())
}
pub fn llm() -> Self {
Self("llm".into())
}
pub fn tool() -> Self {
Self("tool".into())
}
pub fn retriever() -> Self {
Self("retriever".into())
}
pub fn embedder() -> Self {
Self("embedder".into())
}
pub fn reranker() -> Self {
Self("reranker".into())
}
pub fn guardrail() -> Self {
Self("guardrail".into())
}
pub fn evaluator() -> Self {
Self("evaluator".into())
}
pub fn custom() -> Self {
Self("custom".into())
}
pub fn unknown() -> Self {
Self("unknown".into())
}
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn to_scope_type(&self) -> ScopeType {
match self.as_str() {
"agent" => ScopeType::Agent,
"function" => ScopeType::Function,
"tool" => ScopeType::Tool,
"llm" => ScopeType::Llm,
"retriever" => ScopeType::Retriever,
"embedder" => ScopeType::Embedder,
"reranker" => ScopeType::Reranker,
"guardrail" => ScopeType::Guardrail,
"evaluator" => ScopeType::Evaluator,
"custom" => ScopeType::Custom,
_ => ScopeType::Unknown,
}
}
}
impl From<ScopeType> for EventCategory {
fn from(value: ScopeType) -> Self {
match value {
ScopeType::Agent => Self::agent(),
ScopeType::Function => Self::function(),
ScopeType::Tool => Self::tool(),
ScopeType::Llm => Self::llm(),
ScopeType::Retriever => Self::retriever(),
ScopeType::Embedder => Self::embedder(),
ScopeType::Reranker => Self::reranker(),
ScopeType::Guardrail => Self::guardrail(),
ScopeType::Evaluator => Self::evaluator(),
ScopeType::Custom => Self::custom(),
ScopeType::Unknown => Self::unknown(),
}
}
}
impl From<&EventCategory> for ScopeType {
fn from(value: &EventCategory) -> Self {
value.to_scope_type()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ScopeCategory {
Start,
End,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[builder(field_defaults(setter(into, strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
pub struct CategoryProfile {
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_name: Option<String>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subtype: Option<String>,
#[builder(default)]
#[serde(default, skip_serializing_if = "option_json_is_none_or_null")]
pub tool_result_annotation: Option<Json>,
#[builder(default)]
#[serde(flatten)]
pub extra: BTreeMap<String, Json>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotated_request: Option<Arc<AnnotatedLlmRequest>>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotated_response: Option<Arc<AnnotatedLlmResponse>>,
}
impl CategoryProfile {
pub fn is_wire_empty(&self) -> bool {
self.model_name.is_none()
&& self.tool_call_id.is_none()
&& self.subtype.is_none()
&& option_json_is_none_or_null(&self.tool_result_annotation)
&& self.annotated_request.is_none()
&& self.annotated_response.is_none()
&& self.extra.is_empty()
}
}
fn option_json_is_none_or_null(value: &Option<Json>) -> bool {
value.as_ref().is_none_or(Json::is_null)
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[builder(field_defaults(setter(into, strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
pub struct BaseEvent {
#[builder(default = ATOF_VERSION.to_string())]
pub atof_version: String,
#[builder(default)]
pub parent_uuid: Option<Uuid>,
#[builder(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub propagation_root_uuid: Option<Uuid>,
#[builder(default)]
#[serde(skip)]
pub propagation_parent_uuid: Option<Uuid>,
#[builder(default = Uuid::now_v7())]
pub uuid: Uuid,
#[builder(default = Utc::now())]
#[serde(with = "timestamp")]
pub timestamp: DateTime<Utc>,
pub name: String,
#[builder(default)]
pub data: Option<Json>,
#[builder(default)]
pub data_schema: Option<DataSchema>,
#[builder(default)]
pub metadata: Option<Json>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[builder(field_defaults(setter(into, strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
pub struct ScopeEvent {
#[serde(flatten)]
#[builder(setter(skip), default = BaseEvent::builder().name("").build())]
pub base: BaseEvent,
pub scope_category: ScopeCategory,
#[builder(default)]
pub attributes: Vec<String>,
pub category: EventCategory,
#[builder(default)]
pub category_profile: Option<CategoryProfile>,
}
impl ScopeEvent {
pub fn new(
base: BaseEvent,
scope_category: ScopeCategory,
attributes: Vec<String>,
category: EventCategory,
category_profile: Option<CategoryProfile>,
) -> Self {
Self {
base,
scope_category,
attributes: canonicalize_attributes(attributes),
category,
category_profile,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[builder(field_defaults(setter(into, strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
pub struct MarkEvent {
#[serde(flatten)]
#[builder(setter(skip), default = BaseEvent::builder().name("").build())]
pub base: BaseEvent,
#[builder(default)]
pub category: Option<EventCategory>,
#[builder(default)]
pub category_profile: Option<CategoryProfile>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[builder(field_defaults(setter(into, strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
pub struct EventSanitizeFields {
#[builder(default)]
pub data: Option<Json>,
#[builder(default)]
pub category_profile: Option<CategoryProfile>,
#[builder(default)]
pub metadata: Option<Json>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[builder(field_defaults(setter(into, strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
pub struct PendingMarkSpec {
pub name: String,
#[builder(default)]
pub category: Option<EventCategory>,
#[builder(default)]
pub category_profile: Option<CategoryProfile>,
#[builder(default)]
pub data: Option<Json>,
#[builder(default)]
pub data_schema: Option<DataSchema>,
#[builder(default)]
pub metadata: Option<Json>,
#[builder(default)]
pub severity: Option<LogSeverity>,
}
impl MarkEvent {
pub fn new(
base: BaseEvent,
category: Option<EventCategory>,
category_profile: Option<CategoryProfile>,
) -> Self {
Self {
base,
category,
category_profile,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Event {
Scope(ScopeEvent),
Mark(MarkEvent),
}
impl Event {
pub fn kind(&self) -> &'static str {
match self {
Self::Scope(_) => "scope",
Self::Mark(_) => "mark",
}
}
pub fn try_to_json_value(&self) -> serde_json::Result<Json> {
serde_json::to_value(self)
}
pub fn to_json_value(&self) -> Json {
self.try_to_json_value()
.expect("serializing an ATOF event to JSON should not fail")
}
pub fn to_json_string(&self) -> serde_json::Result<String> {
serde_json::to_string(&self.try_to_json_value()?)
}
pub fn scope_category(&self) -> Option<ScopeCategory> {
match self {
Self::Scope(event) => Some(event.scope_category),
Self::Mark(_) => None,
}
}
pub fn category(&self) -> Option<&EventCategory> {
match self {
Self::Scope(event) => Some(&event.category),
Self::Mark(event) => event.category.as_ref(),
}
}
pub fn category_profile(&self) -> Option<&CategoryProfile> {
match self {
Self::Scope(event) => event.category_profile.as_ref(),
Self::Mark(event) => event.category_profile.as_ref(),
}
}
pub fn category_profile_mut(&mut self) -> Option<&mut CategoryProfile> {
match self {
Self::Scope(event) => event.category_profile.as_mut(),
Self::Mark(event) => event.category_profile.as_mut(),
}
}
pub fn tool_result_annotation(&self) -> Option<Json> {
self.category_profile()?
.tool_result_annotation
.as_ref()
.filter(|value| !value.is_null())
.cloned()
}
pub fn parent_uuid(&self) -> Option<Uuid> {
self.base().parent_uuid
}
pub fn propagation_root_uuid(&self) -> Option<Uuid> {
self.base().propagation_root_uuid
}
pub fn set_propagation_root_uuid(&mut self, root_uuid: Option<Uuid>) {
self.base_mut().propagation_root_uuid = root_uuid;
}
pub fn propagation_parent_uuid(&self) -> Option<Uuid> {
self.base().propagation_parent_uuid
}
pub fn set_propagation_parent_uuid(&mut self, parent_uuid: Option<Uuid>) {
self.base_mut().propagation_parent_uuid = parent_uuid;
}
pub fn uuid(&self) -> Uuid {
self.base().uuid
}
pub fn timestamp(&self) -> &DateTime<Utc> {
&self.base().timestamp
}
pub fn name(&self) -> &str {
self.base().name.as_str()
}
pub fn data(&self) -> Option<&Json> {
self.base().data.as_ref()
}
pub fn sanitize_fields(&self) -> EventSanitizeFields {
EventSanitizeFields {
data: self.base().data.clone(),
category_profile: self.category_profile().cloned(),
metadata: self.base().metadata.clone(),
}
}
pub fn apply_sanitize_fields(&mut self, fields: EventSanitizeFields) {
self.base_mut().data = fields.data;
self.base_mut().metadata = fields.metadata;
match self {
Self::Scope(event) => event.category_profile = fields.category_profile,
Self::Mark(event) => event.category_profile = fields.category_profile,
}
}
pub fn data_schema(&self) -> Option<&DataSchema> {
self.base().data_schema.as_ref()
}
pub fn metadata(&self) -> Option<&Json> {
self.base().metadata.as_ref()
}
pub fn attributes(&self) -> Option<&[String]> {
match self {
Self::Scope(event) => Some(event.attributes.as_slice()),
Self::Mark(_) => None,
}
}
pub fn scope_type(&self) -> Option<ScopeType> {
self.category().map(EventCategory::to_scope_type)
}
pub fn input(&self) -> Option<&Json> {
match self {
Self::Scope(event) if event.scope_category == ScopeCategory::Start => {
event.base.data.as_ref()
}
_ => None,
}
}
pub fn output(&self) -> Option<&Json> {
match self {
Self::Scope(event) if event.scope_category == ScopeCategory::End => {
event.base.data.as_ref()
}
_ => None,
}
}
pub fn model_name(&self) -> Option<&str> {
self.category_profile()
.and_then(|profile| profile.model_name.as_deref())
}
pub fn tool_call_id(&self) -> Option<&str> {
self.category_profile()
.and_then(|profile| profile.tool_call_id.as_deref())
}
pub fn annotated_request(&self) -> Option<&Arc<AnnotatedLlmRequest>> {
self.category_profile()
.and_then(|profile| profile.annotated_request.as_ref())
}
pub fn annotated_response(&self) -> Option<&Arc<AnnotatedLlmResponse>> {
self.category_profile()
.and_then(|profile| profile.annotated_response.as_ref())
}
pub fn is_scope_start(&self) -> bool {
matches!(
self,
Self::Scope(ScopeEvent {
scope_category: ScopeCategory::Start,
..
})
)
}
pub fn is_scope_end(&self) -> bool {
matches!(
self,
Self::Scope(ScopeEvent {
scope_category: ScopeCategory::End,
..
})
)
}
fn base(&self) -> &BaseEvent {
match self {
Self::Scope(event) => &event.base,
Self::Mark(event) => &event.base,
}
}
fn base_mut(&mut self) -> &mut BaseEvent {
match self {
Self::Scope(event) => &mut event.base,
Self::Mark(event) => &mut event.base,
}
}
}
pub fn attributes_from_handle(attributes: HandleAttributes) -> Vec<String> {
match attributes {
HandleAttributes::Scope(attributes) => scope_attributes_to_strings(attributes),
HandleAttributes::Tool(attributes) => tool_attributes_to_strings(attributes),
HandleAttributes::Llm(attributes) => llm_attributes_to_strings(attributes),
}
}
pub fn scope_attributes_to_strings(attributes: ScopeAttributes) -> Vec<String> {
let mut values = Vec::new();
if attributes.contains(ScopeAttributes::PARALLEL) {
values.push("parallel".to_string());
}
if attributes.contains(ScopeAttributes::RELOCATABLE) {
values.push("relocatable".to_string());
}
values
}
pub fn tool_attributes_to_strings(attributes: ToolAttributes) -> Vec<String> {
let mut values = Vec::new();
if attributes.contains(ToolAttributes::REMOTE) {
values.push("remote".to_string());
}
values
}
pub fn llm_attributes_to_strings(attributes: LlmAttributes) -> Vec<String> {
let mut values = Vec::new();
if attributes.contains(LlmAttributes::STATEFUL) {
values.push("stateful".to_string());
}
if attributes.contains(LlmAttributes::STREAMING) {
values.push("streaming".to_string());
}
values
}
fn canonicalize_attributes(mut attributes: Vec<String>) -> Vec<String> {
attributes.sort();
attributes.dedup();
attributes
}
mod timestamp {
use chrono::{DateTime, Utc};
use serde::{
Deserializer, Serializer,
de::{self, Visitor},
};
use std::fmt;
pub fn serialize<S>(value: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&value.to_rfc3339())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(TimestampVisitor)
}
struct TimestampVisitor;
impl<'de> Visitor<'de> for TimestampVisitor {
type Value = DateTime<Utc>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("an RFC 3339 timestamp string or epoch microseconds integer")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
DateTime::parse_from_rfc3339(value)
.map(|timestamp| timestamp.with_timezone(&Utc))
.map_err(E::custom)
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
DateTime::<Utc>::from_timestamp_micros(value)
.ok_or_else(|| E::custom("epoch microseconds value is out of range"))
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
let value = i64::try_from(value)
.map_err(|_| E::custom("epoch microseconds value is out of range"))?;
self.visit_i64(value)
}
}
}