Skip to main content

nemo_relay/api/
event.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Event types for Agent Trajectory Observability Format (ATOF) runtime events.
17
18use std::collections::BTreeMap;
19use std::sync::Arc;
20
21use chrono::{DateTime, Utc};
22use serde::{Deserialize, Serialize};
23use typed_builder::TypedBuilder;
24use uuid::Uuid;
25
26use crate::api::llm::LlmAttributes;
27use crate::api::scope::{HandleAttributes, ScopeAttributes, ScopeType};
28use crate::api::tool::ToolAttributes;
29use crate::codec::request::AnnotatedLlmRequest;
30use crate::codec::response::AnnotatedLlmResponse;
31use crate::json::Json;
32
33/// Agent Trajectory Observability Format (ATOF) protocol version emitted by this runtime.
34pub const ATOF_VERSION: &str = "0.1";
35
36/// Identifier for the schema that describes an event's opaque `data` payload.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TypedBuilder)]
38#[builder(field_defaults(setter(into)))]
39pub struct DataSchema {
40    /// Schema name.
41    pub name: String,
42    /// Schema version.
43    pub version: String,
44}
45
46/// Semantic category carried by ATOF `category`.
47///
48/// This is intentionally string-backed so consumers can preserve category
49/// values from newer producers without failing deserialization.
50#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
51#[serde(transparent)]
52pub struct EventCategory(String);
53
54impl EventCategory {
55    /// Top-level agent or workflow scope.
56    ///
57    /// # Returns
58    /// An [`EventCategory`] with the wire value `agent`.
59    pub fn agent() -> Self {
60        Self("agent".into())
61    }
62
63    /// Generic function or application step.
64    ///
65    /// # Returns
66    /// An [`EventCategory`] with the wire value `function`.
67    pub fn function() -> Self {
68        Self("function".into())
69    }
70
71    /// LLM call.
72    ///
73    /// # Returns
74    /// An [`EventCategory`] with the wire value `llm`.
75    pub fn llm() -> Self {
76        Self("llm".into())
77    }
78
79    /// Tool invocation.
80    ///
81    /// # Returns
82    /// An [`EventCategory`] with the wire value `tool`.
83    pub fn tool() -> Self {
84        Self("tool".into())
85    }
86
87    /// Retrieval step.
88    ///
89    /// # Returns
90    /// An [`EventCategory`] with the wire value `retriever`.
91    pub fn retriever() -> Self {
92        Self("retriever".into())
93    }
94
95    /// Embedding-generation step.
96    ///
97    /// # Returns
98    /// An [`EventCategory`] with the wire value `embedder`.
99    pub fn embedder() -> Self {
100        Self("embedder".into())
101    }
102
103    /// Result reranking step.
104    ///
105    /// # Returns
106    /// An [`EventCategory`] with the wire value `reranker`.
107    pub fn reranker() -> Self {
108        Self("reranker".into())
109    }
110
111    /// Guardrail or validation step.
112    ///
113    /// # Returns
114    /// An [`EventCategory`] with the wire value `guardrail`.
115    pub fn guardrail() -> Self {
116        Self("guardrail".into())
117    }
118
119    /// Evaluation or scoring step.
120    ///
121    /// # Returns
122    /// An [`EventCategory`] with the wire value `evaluator`.
123    pub fn evaluator() -> Self {
124        Self("evaluator".into())
125    }
126
127    /// Vendor-defined custom category.
128    ///
129    /// # Returns
130    /// An [`EventCategory`] with the wire value `custom`.
131    pub fn custom() -> Self {
132        Self("custom".into())
133    }
134
135    /// Unknown or unclassified work.
136    ///
137    /// # Returns
138    /// An [`EventCategory`] with the wire value `unknown`.
139    pub fn unknown() -> Self {
140        Self("unknown".into())
141    }
142
143    /// Create a category from an arbitrary producer-provided string.
144    ///
145    /// # Parameters
146    /// - `value`: Wire category value to preserve.
147    ///
148    /// # Returns
149    /// An [`EventCategory`] containing `value`.
150    pub fn new(value: impl Into<String>) -> Self {
151        Self(value.into())
152    }
153
154    /// Return the string form serialized on the wire.
155    ///
156    /// # Returns
157    /// The category value as a string slice.
158    pub fn as_str(&self) -> &str {
159        self.0.as_str()
160    }
161
162    /// Convert this category to the closest legacy scope type for internal
163    /// adapters that still need span-kind classification.
164    ///
165    /// # Returns
166    /// The closest matching [`ScopeType`], or [`ScopeType::Unknown`] when the
167    /// category has no legacy equivalent.
168    pub fn to_scope_type(&self) -> ScopeType {
169        match self.as_str() {
170            "agent" => ScopeType::Agent,
171            "function" => ScopeType::Function,
172            "tool" => ScopeType::Tool,
173            "llm" => ScopeType::Llm,
174            "retriever" => ScopeType::Retriever,
175            "embedder" => ScopeType::Embedder,
176            "reranker" => ScopeType::Reranker,
177            "guardrail" => ScopeType::Guardrail,
178            "evaluator" => ScopeType::Evaluator,
179            "custom" => ScopeType::Custom,
180            _ => ScopeType::Unknown,
181        }
182    }
183}
184
185impl From<ScopeType> for EventCategory {
186    fn from(value: ScopeType) -> Self {
187        match value {
188            ScopeType::Agent => Self::agent(),
189            ScopeType::Function => Self::function(),
190            ScopeType::Tool => Self::tool(),
191            ScopeType::Llm => Self::llm(),
192            ScopeType::Retriever => Self::retriever(),
193            ScopeType::Embedder => Self::embedder(),
194            ScopeType::Reranker => Self::reranker(),
195            ScopeType::Guardrail => Self::guardrail(),
196            ScopeType::Evaluator => Self::evaluator(),
197            ScopeType::Custom => Self::custom(),
198            ScopeType::Unknown => Self::unknown(),
199        }
200    }
201}
202
203impl From<&EventCategory> for ScopeType {
204    fn from(value: &EventCategory) -> Self {
205        value.to_scope_type()
206    }
207}
208
209/// Agent Trajectory Observability Format (ATOF) lifecycle phase for a scope event.
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "lowercase")]
212pub enum ScopeCategory {
213    /// Scope was entered.
214    Start,
215    /// Scope was exited.
216    End,
217}
218
219/// Category-specific profile data.
220///
221/// Unknown wire keys are preserved in `extra`. LLM annotations are serialized
222/// under `category_profile` when a codec captures them.
223#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, TypedBuilder)]
224#[builder(field_defaults(setter(into, strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
225pub struct CategoryProfile {
226    /// Normalized model identifier for LLM events.
227    #[builder(default)]
228    #[serde(default, skip_serializing_if = "Option::is_none")]
229    pub model_name: Option<String>,
230
231    /// LLM-provider correlation ID for Tool events.
232    #[builder(default)]
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub tool_call_id: Option<String>,
235
236    /// Vendor subtype required when `category == "custom"`.
237    #[builder(default)]
238    #[serde(default, skip_serializing_if = "Option::is_none")]
239    pub subtype: Option<String>,
240
241    /// Unknown category-profile keys preserved from newer producers.
242    #[builder(default)]
243    #[serde(flatten)]
244    pub extra: BTreeMap<String, Json>,
245
246    /// Normalized request annotation for LLM start events.
247    #[builder(default)]
248    #[serde(default, skip_serializing_if = "Option::is_none")]
249    pub annotated_request: Option<Arc<AnnotatedLlmRequest>>,
250
251    /// Normalized response annotation for LLM end events.
252    #[builder(default)]
253    #[serde(default, skip_serializing_if = "Option::is_none")]
254    pub annotated_response: Option<Arc<AnnotatedLlmResponse>>,
255}
256
257impl CategoryProfile {
258    /// Return true when the profile has no wire-serialized fields.
259    ///
260    /// # Returns
261    /// `true` when no profile fields would be serialized on the wire.
262    pub fn is_wire_empty(&self) -> bool {
263        self.model_name.is_none()
264            && self.tool_call_id.is_none()
265            && self.subtype.is_none()
266            && self.annotated_request.is_none()
267            && self.annotated_response.is_none()
268            && self.extra.is_empty()
269    }
270}
271
272/// Shared event metadata carried by every ATOF event.
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
274#[builder(field_defaults(setter(into, strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
275pub struct BaseEvent {
276    /// ATOF protocol version.
277    #[builder(default = ATOF_VERSION.to_string())]
278    pub atof_version: String,
279    /// UUID of the parent scope, if any.
280    #[builder(default)]
281    pub parent_uuid: Option<Uuid>,
282    /// Unique identifier for the event or span.
283    #[builder(default = Uuid::now_v7())]
284    pub uuid: Uuid,
285    /// Event timestamp in UTC.
286    #[builder(default = Utc::now())]
287    #[serde(with = "timestamp")]
288    pub timestamp: DateTime<Utc>,
289    /// Human-readable event name.
290    pub name: String,
291    /// Application-defined payload.
292    #[builder(default)]
293    pub data: Option<Json>,
294    /// Optional schema identifier for `data`.
295    #[builder(default)]
296    pub data_schema: Option<DataSchema>,
297    /// Optional tracing/correlation metadata.
298    #[builder(default)]
299    pub metadata: Option<Json>,
300}
301
302/// ATOF scope lifecycle event.
303#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
304#[builder(field_defaults(setter(into, strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
305pub struct ScopeEvent {
306    /// Shared ATOF envelope.
307    #[serde(flatten)]
308    #[builder(setter(skip), default = BaseEvent::builder().name("").build())]
309    pub base: BaseEvent,
310    /// Scope lifecycle phase.
311    pub scope_category: ScopeCategory,
312    /// Canonical lowercase behavioral flags.
313    #[builder(default)]
314    pub attributes: Vec<String>,
315    /// Semantic category of work.
316    pub category: EventCategory,
317    /// Category-specific typed fields.
318    #[builder(default)]
319    pub category_profile: Option<CategoryProfile>,
320}
321
322impl ScopeEvent {
323    /// Construct a scope event from a base envelope and ATOF-specific fields.
324    ///
325    /// # Parameters
326    /// - `base`: Shared ATOF event envelope.
327    /// - `scope_category`: Lifecycle phase for the scope event.
328    /// - `attributes`: Scope attributes to canonicalize and attach.
329    /// - `category`: Semantic event category.
330    /// - `category_profile`: Optional category-specific profile data.
331    ///
332    /// # Returns
333    /// A [`ScopeEvent`] containing the provided fields.
334    pub fn new(
335        base: BaseEvent,
336        scope_category: ScopeCategory,
337        attributes: Vec<String>,
338        category: EventCategory,
339        category_profile: Option<CategoryProfile>,
340    ) -> Self {
341        Self {
342            base,
343            scope_category,
344            attributes: canonicalize_attributes(attributes),
345            category,
346            category_profile,
347        }
348    }
349}
350
351/// ATOF point-in-time mark event.
352#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
353#[builder(field_defaults(setter(into, strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
354pub struct MarkEvent {
355    /// Shared ATOF envelope.
356    #[serde(flatten)]
357    #[builder(setter(skip), default = BaseEvent::builder().name("").build())]
358    pub base: BaseEvent,
359    /// Optional semantic category for the checkpoint.
360    #[builder(default)]
361    pub category: Option<EventCategory>,
362    /// Optional category-specific typed fields.
363    #[builder(default)]
364    pub category_profile: Option<CategoryProfile>,
365}
366
367impl MarkEvent {
368    /// Construct a mark event from a base envelope and optional category data.
369    ///
370    /// # Parameters
371    /// - `base`: Shared ATOF event envelope.
372    /// - `category`: Optional semantic event category.
373    /// - `category_profile`: Optional category-specific profile data.
374    ///
375    /// # Returns
376    /// A [`MarkEvent`] containing the provided fields.
377    pub fn new(
378        base: BaseEvent,
379        category: Option<EventCategory>,
380        category_profile: Option<CategoryProfile>,
381    ) -> Self {
382        Self {
383            base,
384            category,
385            category_profile,
386        }
387    }
388}
389
390/// Tagged union covering the two ATOF event kinds emitted by the runtime.
391#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
392#[serde(tag = "kind", rename_all = "lowercase")]
393pub enum Event {
394    /// Scope lifecycle event.
395    Scope(ScopeEvent),
396    /// Point-in-time checkpoint event.
397    Mark(MarkEvent),
398}
399
400impl Event {
401    /// Return the ATOF event kind.
402    ///
403    /// # Returns
404    /// `"scope"` for [`Event::Scope`] and `"mark"` for [`Event::Mark`].
405    pub fn kind(&self) -> &'static str {
406        match self {
407            Self::Scope(_) => "scope",
408            Self::Mark(_) => "mark",
409        }
410    }
411
412    /// Try to return this event as the canonical JSON object delivered by
413    /// language bindings to subscriber callbacks and ATOF exporters.
414    pub fn try_to_json_value(&self) -> serde_json::Result<Json> {
415        serde_json::to_value(self)
416    }
417
418    /// Return this event as the canonical JSON object delivered by language
419    /// bindings to subscriber callbacks.
420    pub fn to_json_value(&self) -> Json {
421        self.try_to_json_value()
422            .expect("serializing an ATOF event to JSON should not fail")
423    }
424
425    /// Return this event as canonical JSON.
426    pub fn to_json_string(&self) -> serde_json::Result<String> {
427        serde_json::to_string(&self.try_to_json_value()?)
428    }
429
430    /// Return the lifecycle phase for scope events.
431    ///
432    /// # Returns
433    /// `Some` lifecycle phase for scope events, otherwise `None`.
434    pub fn scope_category(&self) -> Option<ScopeCategory> {
435        match self {
436            Self::Scope(event) => Some(event.scope_category),
437            Self::Mark(_) => None,
438        }
439    }
440
441    /// Return the semantic category if present.
442    ///
443    /// # Returns
444    /// `Some` category for scope events and categorized mark events, otherwise
445    /// `None`.
446    pub fn category(&self) -> Option<&EventCategory> {
447        match self {
448            Self::Scope(event) => Some(&event.category),
449            Self::Mark(event) => event.category.as_ref(),
450        }
451    }
452
453    /// Return the category-specific profile if present.
454    ///
455    /// # Returns
456    /// `Some` profile when category-specific fields are present.
457    pub fn category_profile(&self) -> Option<&CategoryProfile> {
458        match self {
459            Self::Scope(event) => event.category_profile.as_ref(),
460            Self::Mark(event) => event.category_profile.as_ref(),
461        }
462    }
463
464    /// Return the mutable category-specific profile if present.
465    ///
466    /// # Returns
467    /// `Some` mutable profile when category-specific fields are present.
468    pub fn category_profile_mut(&mut self) -> Option<&mut CategoryProfile> {
469        match self {
470            Self::Scope(event) => event.category_profile.as_mut(),
471            Self::Mark(event) => event.category_profile.as_mut(),
472        }
473    }
474
475    /// Return the parent scope UUID, if the event is nested under a scope.
476    ///
477    /// # Returns
478    /// `Some` parent UUID when the event has a parent scope, otherwise `None`.
479    pub fn parent_uuid(&self) -> Option<Uuid> {
480        self.base().parent_uuid
481    }
482
483    /// Return the unique event or span UUID.
484    ///
485    /// # Returns
486    /// The event UUID.
487    pub fn uuid(&self) -> Uuid {
488        self.base().uuid
489    }
490
491    /// Return the event timestamp.
492    ///
493    /// # Returns
494    /// The UTC event timestamp.
495    pub fn timestamp(&self) -> &DateTime<Utc> {
496        &self.base().timestamp
497    }
498
499    /// Return the human-readable event name.
500    ///
501    /// # Returns
502    /// The event name.
503    pub fn name(&self) -> &str {
504        self.base().name.as_str()
505    }
506
507    /// Return the optional application payload attached to the event.
508    ///
509    /// # Returns
510    /// `Some` payload when event data is present, otherwise `None`.
511    pub fn data(&self) -> Option<&Json> {
512        self.base().data.as_ref()
513    }
514
515    /// Return the optional data schema.
516    ///
517    /// # Returns
518    /// `Some` schema when the event payload declares one, otherwise `None`.
519    pub fn data_schema(&self) -> Option<&DataSchema> {
520        self.base().data_schema.as_ref()
521    }
522
523    /// Return the optional metadata attached to the event.
524    ///
525    /// # Returns
526    /// `Some` metadata when present, otherwise `None`.
527    pub fn metadata(&self) -> Option<&Json> {
528        self.base().metadata.as_ref()
529    }
530
531    /// Return attributes for scope events.
532    ///
533    /// # Returns
534    /// `Some` attributes for scope events, otherwise `None`.
535    pub fn attributes(&self) -> Option<&[String]> {
536        match self {
537            Self::Scope(event) => Some(event.attributes.as_slice()),
538            Self::Mark(_) => None,
539        }
540    }
541
542    /// Return the semantic scope category for scope events.
543    ///
544    /// # Returns
545    /// `Some` legacy [`ScopeType`] when the event has a category.
546    pub fn scope_type(&self) -> Option<ScopeType> {
547        self.category().map(EventCategory::to_scope_type)
548    }
549
550    /// Return the semantic input payload for start events.
551    ///
552    /// # Returns
553    /// `Some` payload for scope-start events with data, otherwise `None`.
554    pub fn input(&self) -> Option<&Json> {
555        match self {
556            Self::Scope(event) if event.scope_category == ScopeCategory::Start => {
557                event.base.data.as_ref()
558            }
559            _ => None,
560        }
561    }
562
563    /// Return the semantic output payload for end events.
564    ///
565    /// # Returns
566    /// `Some` payload for scope-end events with data, otherwise `None`.
567    pub fn output(&self) -> Option<&Json> {
568        match self {
569            Self::Scope(event) if event.scope_category == ScopeCategory::End => {
570                event.base.data.as_ref()
571            }
572            _ => None,
573        }
574    }
575
576    /// Return the normalized model name for LLM events.
577    ///
578    /// # Returns
579    /// `Some` model name when the event profile includes one.
580    pub fn model_name(&self) -> Option<&str> {
581        self.category_profile()
582            .and_then(|profile| profile.model_name.as_deref())
583    }
584
585    /// Return the provider-specific tool-call correlation identifier.
586    ///
587    /// # Returns
588    /// `Some` tool call identifier when the event profile includes one.
589    pub fn tool_call_id(&self) -> Option<&str> {
590        self.category_profile()
591            .and_then(|profile| profile.tool_call_id.as_deref())
592    }
593
594    /// Return the runtime-only annotated LLM request.
595    ///
596    /// # Returns
597    /// `Some` annotated request when the event profile includes one.
598    pub fn annotated_request(&self) -> Option<&Arc<AnnotatedLlmRequest>> {
599        self.category_profile()
600            .and_then(|profile| profile.annotated_request.as_ref())
601    }
602
603    /// Return the runtime-only annotated LLM response.
604    ///
605    /// # Returns
606    /// `Some` annotated response when the event profile includes one.
607    pub fn annotated_response(&self) -> Option<&Arc<AnnotatedLlmResponse>> {
608        self.category_profile()
609            .and_then(|profile| profile.annotated_response.as_ref())
610    }
611
612    /// Return true for scope-start events.
613    ///
614    /// # Returns
615    /// `true` when the event is a scope-start event.
616    pub fn is_scope_start(&self) -> bool {
617        matches!(
618            self,
619            Self::Scope(ScopeEvent {
620                scope_category: ScopeCategory::Start,
621                ..
622            })
623        )
624    }
625
626    /// Return true for scope-end events.
627    ///
628    /// # Returns
629    /// `true` when the event is a scope-end event.
630    pub fn is_scope_end(&self) -> bool {
631        matches!(
632            self,
633            Self::Scope(ScopeEvent {
634                scope_category: ScopeCategory::End,
635                ..
636            })
637        )
638    }
639
640    fn base(&self) -> &BaseEvent {
641        match self {
642            Self::Scope(event) => &event.base,
643            Self::Mark(event) => &event.base,
644        }
645    }
646}
647
648/// Convert handle bitflags into ATOF attributes.
649///
650/// # Parameters
651/// - `attributes`: Handle-specific attribute bitflags.
652///
653/// # Returns
654/// Canonical lowercase ATOF attribute strings for the provided bitflags.
655pub fn attributes_from_handle(attributes: HandleAttributes) -> Vec<String> {
656    match attributes {
657        HandleAttributes::Scope(attributes) => scope_attributes_to_strings(attributes),
658        HandleAttributes::Tool(attributes) => tool_attributes_to_strings(attributes),
659        HandleAttributes::Llm(attributes) => llm_attributes_to_strings(attributes),
660    }
661}
662
663/// Convert scope bitflags into ATOF attributes.
664///
665/// # Parameters
666/// - `attributes`: Scope attribute bitflags.
667///
668/// # Returns
669/// Canonical lowercase ATOF attribute strings for the provided bitflags.
670pub fn scope_attributes_to_strings(attributes: ScopeAttributes) -> Vec<String> {
671    let mut values = Vec::new();
672    if attributes.contains(ScopeAttributes::PARALLEL) {
673        values.push("parallel".to_string());
674    }
675    if attributes.contains(ScopeAttributes::RELOCATABLE) {
676        values.push("relocatable".to_string());
677    }
678    values
679}
680
681/// Convert tool bitflags into ATOF attributes.
682///
683/// # Parameters
684/// - `attributes`: Tool attribute bitflags.
685///
686/// # Returns
687/// Canonical lowercase ATOF attribute strings for the provided bitflags.
688pub fn tool_attributes_to_strings(attributes: ToolAttributes) -> Vec<String> {
689    let mut values = Vec::new();
690    if attributes.contains(ToolAttributes::REMOTE) {
691        values.push("remote".to_string());
692    }
693    values
694}
695
696/// Convert LLM bitflags into ATOF attributes.
697///
698/// # Parameters
699/// - `attributes`: LLM attribute bitflags.
700///
701/// # Returns
702/// Canonical lowercase ATOF attribute strings for the provided bitflags.
703pub fn llm_attributes_to_strings(attributes: LlmAttributes) -> Vec<String> {
704    let mut values = Vec::new();
705    if attributes.contains(LlmAttributes::STATEFUL) {
706        values.push("stateful".to_string());
707    }
708    if attributes.contains(LlmAttributes::STREAMING) {
709        values.push("streaming".to_string());
710    }
711    values
712}
713
714fn canonicalize_attributes(mut attributes: Vec<String>) -> Vec<String> {
715    attributes.sort();
716    attributes.dedup();
717    attributes
718}
719
720mod timestamp {
721    use chrono::{DateTime, Utc};
722    use serde::{
723        Deserializer, Serializer,
724        de::{self, Visitor},
725    };
726    use std::fmt;
727
728    /// Serialize a UTC timestamp as RFC 3339.
729    ///
730    /// # Parameters
731    /// - `value`: Timestamp to serialize.
732    /// - `serializer`: Serde serializer receiving the string value.
733    ///
734    /// # Returns
735    /// The serializer's success value.
736    ///
737    /// # Errors
738    /// Returns any error produced by the serializer.
739    pub fn serialize<S>(value: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
740    where
741        S: Serializer,
742    {
743        serializer.serialize_str(&value.to_rfc3339())
744    }
745
746    /// Deserialize a UTC timestamp from an RFC 3339 string.
747    ///
748    /// # Parameters
749    /// - `deserializer`: Serde deserializer providing the timestamp value.
750    ///
751    /// # Returns
752    /// Parsed UTC timestamp.
753    ///
754    /// # Errors
755    /// Returns a serde error when the input is not a valid RFC 3339 timestamp.
756    pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
757    where
758        D: Deserializer<'de>,
759    {
760        deserializer.deserialize_any(TimestampVisitor)
761    }
762
763    struct TimestampVisitor;
764
765    impl<'de> Visitor<'de> for TimestampVisitor {
766        type Value = DateTime<Utc>;
767
768        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
769            formatter.write_str("an RFC 3339 timestamp string or epoch microseconds integer")
770        }
771
772        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
773        where
774            E: de::Error,
775        {
776            DateTime::parse_from_rfc3339(value)
777                .map(|timestamp| timestamp.with_timezone(&Utc))
778                .map_err(E::custom)
779        }
780
781        fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
782        where
783            E: de::Error,
784        {
785            DateTime::<Utc>::from_timestamp_micros(value)
786                .ok_or_else(|| E::custom("epoch microseconds value is out of range"))
787        }
788
789        fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
790        where
791            E: de::Error,
792        {
793            let value = i64::try_from(value)
794                .map_err(|_| E::custom("epoch microseconds value is out of range"))?;
795            self.visit_i64(value)
796        }
797    }
798}