11use std:: {
22 borrow:: Cow ,
33 collections:: BTreeMap ,
4- fmt:: { Display , Formatter , Result as FmtResult }
4+ fmt:: { Display , Formatter , Result as FmtResult , Write } ,
5+ net:: IpAddr ,
6+ time:: Duration
57} ;
68
79/// Redaction policy associated with a metadata [`Field`].
@@ -18,6 +20,8 @@ pub enum FieldRedaction {
1820 Last4
1921}
2022
23+ #[ cfg( feature = "serde_json" ) ]
24+ use serde_json:: Value as JsonValue ;
2125use uuid:: Uuid ;
2226
2327/// Value stored inside [`Metadata`].
@@ -34,10 +38,19 @@ pub enum FieldValue {
3438 I64 ( i64 ) ,
3539 /// Unsigned 64-bit integer.
3640 U64 ( u64 ) ,
41+ /// Floating-point value.
42+ F64 ( f64 ) ,
3743 /// Boolean flag.
3844 Bool ( bool ) ,
3945 /// UUID represented with the canonical binary type.
40- Uuid ( Uuid )
46+ Uuid ( Uuid ) ,
47+ /// Elapsed duration captured with nanosecond precision.
48+ Duration ( Duration ) ,
49+ /// IP address (v4 or v6).
50+ Ip ( IpAddr ) ,
51+ /// Structured JSON payload (requires the `serde_json` feature).
52+ #[ cfg( feature = "serde_json" ) ]
53+ Json ( JsonValue )
4154}
4255
4356impl Display for FieldValue {
@@ -46,12 +59,82 @@ impl Display for FieldValue {
4659 Self :: Str ( value) => Display :: fmt ( value, f) ,
4760 Self :: I64 ( value) => Display :: fmt ( value, f) ,
4861 Self :: U64 ( value) => Display :: fmt ( value, f) ,
62+ Self :: F64 ( value) => Display :: fmt ( value, f) ,
4963 Self :: Bool ( value) => Display :: fmt ( value, f) ,
50- Self :: Uuid ( value) => Display :: fmt ( value, f)
64+ Self :: Uuid ( value) => Display :: fmt ( value, f) ,
65+ Self :: Duration ( value) => format_duration ( * value, f) ,
66+ Self :: Ip ( value) => Display :: fmt ( value, f) ,
67+ #[ cfg( feature = "serde_json" ) ]
68+ Self :: Json ( value) => Display :: fmt ( value, f)
5169 }
5270 }
5371}
5472
73+ #[ derive( Clone , Copy ) ]
74+ struct TrimmedFraction {
75+ value : u32 ,
76+ width : u8
77+ }
78+
79+ fn duration_parts ( duration : Duration ) -> ( u64 , Option < TrimmedFraction > ) {
80+ let secs = duration. as_secs ( ) ;
81+ let nanos = duration. subsec_nanos ( ) ;
82+ if nanos == 0 {
83+ return ( secs, None ) ;
84+ }
85+
86+ let mut fraction = nanos;
87+ let mut width = 9u8 ;
88+ loop {
89+ let divided = fraction / 10 ;
90+ if divided * 10 != fraction {
91+ break ;
92+ }
93+ fraction = divided;
94+ width -= 1 ;
95+ }
96+
97+ (
98+ secs,
99+ Some ( TrimmedFraction {
100+ value : fraction,
101+ width
102+ } )
103+ )
104+ }
105+
106+ fn format_duration ( duration : Duration , f : & mut Formatter < ' _ > ) -> FmtResult {
107+ let ( secs, fraction) = duration_parts ( duration) ;
108+ if let Some ( fraction) = fraction {
109+ write ! (
110+ f,
111+ "{}.{:0width$}s" ,
112+ secs,
113+ fraction. value,
114+ width = fraction. width as usize
115+ )
116+ } else {
117+ write ! ( f, "{}s" , secs)
118+ }
119+ }
120+
121+ pub ( crate ) fn duration_to_string ( duration : Duration ) -> String {
122+ let ( secs, fraction) = duration_parts ( duration) ;
123+ let mut output = String :: new ( ) ;
124+ if let Some ( fraction) = fraction {
125+ let _ = write ! (
126+ & mut output,
127+ "{}.{:0width$}s" ,
128+ secs,
129+ fraction. value,
130+ width = fraction. width as usize
131+ ) ;
132+ } else {
133+ let _ = write ! ( & mut output, "{}s" , secs) ;
134+ }
135+ output
136+ }
137+
55138/// Single metadata field – name plus value.
56139#[ derive( Clone , Debug , PartialEq ) ]
57140pub struct Field {
@@ -288,8 +371,10 @@ impl IntoIterator for Metadata {
288371
289372/// Factories for [`Field`] values.
290373pub mod field {
291- use std:: borrow:: Cow ;
374+ use std:: { borrow:: Cow , net :: IpAddr , time :: Duration } ;
292375
376+ #[ cfg( feature = "serde_json" ) ]
377+ use serde_json:: Value as JsonValue ;
293378 use uuid:: Uuid ;
294379
295380 use super :: { Field , FieldValue } ;
@@ -312,6 +397,19 @@ pub mod field {
312397 Field :: new ( name, FieldValue :: U64 ( value) )
313398 }
314399
400+ /// Build an `f64` metadata field.
401+ ///
402+ /// ```
403+ /// use masterror::{field, FieldValue};
404+ ///
405+ /// let (_, value, _) = field::f64("ratio", 0.5).into_parts();
406+ /// assert!(matches!(value, FieldValue::F64(ratio) if ratio.to_bits() == 0.5f64.to_bits()));
407+ /// ```
408+ #[ must_use]
409+ pub fn f64 ( name : & ' static str , value : f64 ) -> Field {
410+ Field :: new ( name, FieldValue :: F64 ( value) )
411+ }
412+
315413 /// Build a boolean metadata field.
316414 #[ must_use]
317415 pub fn bool ( name : & ' static str , value : bool ) -> Field {
@@ -323,15 +421,66 @@ pub mod field {
323421 pub fn uuid ( name : & ' static str , value : Uuid ) -> Field {
324422 Field :: new ( name, FieldValue :: Uuid ( value) )
325423 }
424+
425+ /// Build a duration metadata field.
426+ ///
427+ /// ```
428+ /// use std::time::Duration;
429+ /// use masterror::{field, FieldValue};
430+ ///
431+ /// let (_, value, _) = field::duration("elapsed", Duration::from_millis(1500)).into_parts();
432+ /// assert!(matches!(value, FieldValue::Duration(duration) if duration == Duration::from_millis(1500)));
433+ /// ```
434+ #[ must_use]
435+ pub fn duration ( name : & ' static str , value : Duration ) -> Field {
436+ Field :: new ( name, FieldValue :: Duration ( value) )
437+ }
438+
439+ /// Build an IP address metadata field.
440+ ///
441+ /// ```
442+ /// use std::net::{IpAddr, Ipv4Addr};
443+ /// use masterror::{field, FieldValue};
444+ ///
445+ /// let (_, value, _) = field::ip("peer", IpAddr::from(Ipv4Addr::LOCALHOST)).into_parts();
446+ /// assert!(matches!(value, FieldValue::Ip(addr) if addr.is_ipv4()));
447+ /// ```
448+ #[ must_use]
449+ pub fn ip ( name : & ' static str , value : IpAddr ) -> Field {
450+ Field :: new ( name, FieldValue :: Ip ( value) )
451+ }
452+
453+ /// Build a JSON metadata field (requires the `serde_json` feature).
454+ ///
455+ /// ```
456+ /// # #[cfg(feature = "serde_json")]
457+ /// # {
458+ /// use masterror::{field, FieldValue};
459+ ///
460+ /// let (_, value, _) = field::json("payload", serde_json::json!({"ok": true})).into_parts();
461+ /// assert!(matches!(value, FieldValue::Json(payload) if payload["ok"].as_bool() == Some(true)));
462+ /// # }
463+ /// ```
464+ #[ cfg( feature = "serde_json" ) ]
465+ #[ must_use]
466+ pub fn json ( name : & ' static str , value : JsonValue ) -> Field {
467+ Field :: new ( name, FieldValue :: Json ( value) )
468+ }
326469}
327470
328471#[ cfg( test) ]
329472mod tests {
330- use std:: borrow:: Cow ;
331-
473+ use std:: {
474+ borrow:: Cow ,
475+ net:: { IpAddr , Ipv4Addr } ,
476+ time:: Duration
477+ } ;
478+
479+ #[ cfg( feature = "serde_json" ) ]
480+ use serde_json:: json;
332481 use uuid:: Uuid ;
333482
334- use super :: { FieldRedaction , FieldValue , Metadata , field} ;
483+ use super :: { FieldRedaction , FieldValue , Metadata , duration_to_string , field} ;
335484
336485 #[ test]
337486 fn metadata_roundtrip ( ) {
@@ -358,6 +507,37 @@ mod tests {
358507 assert_eq ! ( collected[ 1 ] . 0 , "trace_id" ) ;
359508 }
360509
510+ #[ test]
511+ fn metadata_supports_extended_field_types ( ) {
512+ let meta = Metadata :: from_fields ( [
513+ field:: f64 ( "ratio" , 0.25 ) ,
514+ field:: duration ( "elapsed" , Duration :: from_millis ( 1500 ) ) ,
515+ field:: ip ( "peer" , IpAddr :: from ( Ipv4Addr :: new ( 192 , 168 , 0 , 1 ) ) )
516+ ] ) ;
517+
518+ assert ! ( meta. get( "ratio" ) . is_some_and(
519+ |value| matches!( value, FieldValue :: F64 ( ratio) if ratio. to_bits( ) == 0.25f64 . to_bits( ) )
520+ ) ) ;
521+ assert_eq ! (
522+ meta. get( "elapsed" ) ,
523+ Some ( & FieldValue :: Duration ( Duration :: from_millis( 1500 ) ) )
524+ ) ;
525+ assert_eq ! (
526+ meta. get( "peer" ) ,
527+ Some ( & FieldValue :: Ip ( IpAddr :: from( Ipv4Addr :: new( 192 , 168 , 0 , 1 ) ) ) )
528+ ) ;
529+ }
530+
531+ #[ cfg( feature = "serde_json" ) ]
532+ #[ test]
533+ fn metadata_supports_json_fields ( ) {
534+ let meta = Metadata :: from_fields ( [ field:: json ( "payload" , json ! ( { "status" : "ok" } ) ) ] ) ;
535+ assert ! ( meta. get( "payload" ) . is_some_and( |value| matches!(
536+ value,
537+ FieldValue :: Json ( payload) if payload[ "status" ] == "ok"
538+ ) ) ) ;
539+ }
540+
361541 #[ test]
362542 fn inserting_field_replaces_previous_value ( ) {
363543 let mut meta = Metadata :: from_fields ( [ field:: i64 ( "count" , 1 ) ] ) ;
@@ -389,4 +569,10 @@ mod tests {
389569 assert_eq ! ( owned_value, field. value( ) . clone( ) ) ;
390570 assert_eq ! ( redaction, field. redaction( ) ) ;
391571 }
572+
573+ #[ test]
574+ fn duration_to_string_trims_trailing_zeroes ( ) {
575+ let text = duration_to_string ( Duration :: from_micros ( 1500 ) ) ;
576+ assert_eq ! ( text, "0.0015s" ) ;
577+ }
392578}
0 commit comments