From 27411f3079fec353373302b9f27edbadafb94607 Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 30 May 2026 23:14:24 +0800 Subject: [PATCH 01/19] refactor!: make our own key-value type Signed-off-by: tison --- Cargo.toml | 2 +- bridges/log/src/lib.rs | 1 + core/Cargo.toml | 4 +-- core/src/kv.rs | 79 +++++++++++++++++++++++++++++++++++++----- 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f961adf..40eddd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ fasyslog = { version = "1.0.0" } insta = { version = "1.43.2" } jiff = { version = "0.2" } libc = { version = "0.2.162" } -log = { version = "0.4.27", features = ["kv_std", "kv_sval"] } +log = { version = "0.4.27", default-features = false } oneshot = { version = "0.2.1", default-features = false, features = ["std"] } opentelemetry = { version = "0.32.0", default-features = false } opentelemetry-otlp = { version = "0.32.0", default-features = false } diff --git a/bridges/log/src/lib.rs b/bridges/log/src/lib.rs index ed2c2cc..c9df6d6 100644 --- a/bridges/log/src/lib.rs +++ b/bridges/log/src/lib.rs @@ -132,6 +132,7 @@ fn forward_log(logger: &Logger, record: &Record) { let mut new_kvs = Vec::with_capacity(kvs.len()); for (k, v) in kvs.iter() { + v. new_kvs.push((Key::new_ref(k.as_str()), Value::from_sval2(v))); } builder = builder.key_values(new_kvs.as_slice()); diff --git a/core/Cargo.toml b/core/Cargo.toml index e555b7f..4473ca2 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -33,11 +33,11 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] -serde = ["value-bag/serde"] +serde = ["dep:serde"] [dependencies] anyhow = { workspace = true } -value-bag = { workspace = true } +serde = { workspace = true, optional = true } [dev-dependencies] insta = { workspace = true } diff --git a/core/src/kv.rs b/core/src/kv.rs index 83879bb..5735c72 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -14,15 +14,10 @@ //! Key-value pairs in a log record or a diagnostic context. -pub extern crate value_bag; - use std::borrow::Cow; use std::fmt; use std::slice; -use value_bag::OwnedValueBag; -use value_bag::ValueBag; - use crate::Error; use crate::str::RefStr; @@ -32,9 +27,6 @@ pub trait Visitor { fn visit(&mut self, key: Key, value: Value) -> Result<(), Error>; } -/// A value in a key-value pair. -pub type Value<'a> = ValueBag<'a>; - /// A key in a key-value pair. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Key<'a>(RefStr<'a>); @@ -76,6 +68,77 @@ impl<'a> Key<'a> { } } + +/// A value in a key-value pair. +#[non_exhaustive] +pub enum Value<'a> { + None, + Bool(bool), + I64(i64), + U64(u64), + F64(f64), + I128(i128), + U128(u128), + Char(char), + Str(&'a str), + Debug(&'a dyn fmt::Debug), + Display(&'a dyn fmt::Display), +} + +impl Clone for Value<'_> { + fn clone(&self) -> Self { + match self { + Value::None => Value::None, + Value::Bool(b) => Value::Bool(*b), + Value::I64(i) => Value::I64(*i), + Value::U64(u) => Value::U64(*u), + Value::F64(f) => Value::F64(*f), + Value::I128(i) => Value::I128(*i), + Value::U128(u) => Value::U128(*u), + Value::Char(c) => Value::Char(*c), + Value::Str(s) => Value::Str(s), + Value::Debug(d) => Value::Debug(*d), + Value::Display(d) => Value::Display(*d), + } + } +} + +impl fmt::Debug for Value<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Value::None => f.write_str("None"), + Value::Bool(v) => v.fmt(f), + Value::I64(v) => v.fmt(f), + Value::U64(v) => v.fmt(f), + Value::F64(v) => v.fmt(f), + Value::I128(v) => v.fmt(f), + Value::U128(v) => v.fmt(f), + Value::Char(v) => v.fmt(f), + Value::Str(v) => v.fmt(f), + Value::Debug(v) => v.fmt(f), + Value::Display(v) => v.fmt(f), + } + } +} + +impl fmt::Display for Value<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Value::None => f.write_str("None"), + Value::Bool(v) => v.fmt(f), + Value::I64(v) => v.fmt(f), + Value::U64(v) => v.fmt(f), + Value::F64(v) => v.fmt(f), + Value::I128(v) => v.fmt(f), + Value::U128(v) => v.fmt(f), + Value::Char(v) => v.fmt(f), + Value::Str(v) => v.fmt(f), + Value::Debug(v) => v.fmt(f), + Value::Display(v) => v.fmt(f), + } + } +} + /// An owned value in a key-value pair. pub type ValueOwned = OwnedValueBag; From e512057e687fb6429dafb53c0bd2879742e0a684 Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 1 Jun 2026 11:04:58 +0800 Subject: [PATCH 02/19] round 2 Signed-off-by: tison --- Cargo.toml | 1 - core/src/kv.rs | 75 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 40eddd4..fd8e3c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,7 +73,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } tempfile = { version = "3.16" } tokio = { version = "1.47.1" } -value-bag = { version = "1.11.1", features = ["inline-i128", "owned", "sval"] } which = { version = "8.0.0" } [workspace.lints.rust] diff --git a/core/src/kv.rs b/core/src/kv.rs index 5735c72..1412124 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -15,6 +15,7 @@ //! Key-value pairs in a log record or a diagnostic context. use std::borrow::Cow; +use std::collections::HashMap; use std::fmt; use std::slice; @@ -68,7 +69,6 @@ impl<'a> Key<'a> { } } - /// A value in a key-value pair. #[non_exhaustive] pub enum Value<'a> { @@ -81,8 +81,8 @@ pub enum Value<'a> { U128(u128), Char(char), Str(&'a str), - Debug(&'a dyn fmt::Debug), - Display(&'a dyn fmt::Display), + List(&'a [Value<'a>]), + Map(&'a [(Key<'a>, Value<'a>)]), } impl Clone for Value<'_> { @@ -97,8 +97,8 @@ impl Clone for Value<'_> { Value::U128(u) => Value::U128(*u), Value::Char(c) => Value::Char(*c), Value::Str(s) => Value::Str(s), - Value::Debug(d) => Value::Debug(*d), - Value::Display(d) => Value::Display(*d), + Value::List(l) => Value::List(l), + Value::Map(m) => Value::Map(m), } } } @@ -106,7 +106,7 @@ impl Clone for Value<'_> { impl fmt::Debug for Value<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Value::None => f.write_str("None"), + // passthrough Value::Bool(v) => v.fmt(f), Value::I64(v) => v.fmt(f), Value::U64(v) => v.fmt(f), @@ -115,8 +115,14 @@ impl fmt::Debug for Value<'_> { Value::U128(v) => v.fmt(f), Value::Char(v) => v.fmt(f), Value::Str(v) => v.fmt(f), - Value::Debug(v) => v.fmt(f), - Value::Display(v) => v.fmt(f), + + // implement + Value::None => f.debug_tuple("None").finish(), + Value::List(v) => f.debug_list().entries(v.iter()).finish(), + Value::Map(m) => f + .debug_map() + .entries(m.iter().map(|(k, v)| (k, v))) + .finish(), } } } @@ -124,7 +130,6 @@ impl fmt::Debug for Value<'_> { impl fmt::Display for Value<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Value::None => f.write_str("None"), Value::Bool(v) => v.fmt(f), Value::I64(v) => v.fmt(f), Value::U64(v) => v.fmt(f), @@ -133,17 +138,55 @@ impl fmt::Display for Value<'_> { Value::U128(v) => v.fmt(f), Value::Char(v) => v.fmt(f), Value::Str(v) => v.fmt(f), - Value::Debug(v) => v.fmt(f), - Value::Display(v) => v.fmt(f), + v => fmt::Debug::fmt(v, f), } } } /// An owned value in a key-value pair. -pub type ValueOwned = OwnedValueBag; +#[non_exhaustive] +pub enum ValueOwned { + None, + Bool(bool), + I64(i64), + U64(u64), + F64(f64), + I128(i128), + U128(u128), + Char(char), + Str(String), + List(Box>), + Map(Box>), +} + +impl ValueOwned { + pub fn by_ref(&self) -> Value<'_> { + match self { + ValueOwned::None => Value::None, + ValueOwned::Bool(b) => Value::Bool(*b), + ValueOwned::I64(i) => Value::I64(*i), + ValueOwned::U64(u) => Value::U64(*u), + ValueOwned::F64(f) => Value::F64(*f), + ValueOwned::I128(i) => Value::I128(*i), + ValueOwned::U128(u) => Value::U128(*u), + ValueOwned::Char(c) => Value::Char(*c), + ValueOwned::Str(s) => Value::Str(s.as_str()), + ValueOwned::List(l) => { + let l = l.iter().map(|v| v.by_ref()).collect::>(); + Value::List(l.as_slice()) + } + ValueOwned::Map(m) => { + let m = m + .iter() + .map(|(k, v)| (k.by_ref(), v.by_ref())) + .collect::>(); + Value::Map(m.as_slice()) + } + } + } +} /// An owned key in a key-value pair. -#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct KeyOwned(Cow<'static, str>); impl KeyOwned { @@ -156,12 +199,6 @@ impl KeyOwned { } } -impl fmt::Display for KeyOwned { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} - /// A collection of key-value pairs. pub struct KeyValues<'a>(KeyValuesState<'a>); From b4487f6abfa52af8b85c9830783950359ab0b459 Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 1 Jun 2026 13:07:34 +0800 Subject: [PATCH 03/19] take 2 Signed-off-by: tison --- core/src/kv.rs | 267 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 246 insertions(+), 21 deletions(-) diff --git a/core/src/kv.rs b/core/src/kv.rs index 1412124..6b63eba 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -15,7 +15,6 @@ //! Key-value pairs in a log record or a diagnostic context. use std::borrow::Cow; -use std::collections::HashMap; use std::fmt; use std::slice; @@ -72,17 +71,32 @@ impl<'a> Key<'a> { /// A value in a key-value pair. #[non_exhaustive] pub enum Value<'a> { + /// The absence of a value. None, + /// A boolean value. Bool(bool), + /// A signed 64-bit integer value. I64(i64), + /// An unsigned 64-bit integer value. U64(u64), + /// A 64-bit floating point value. F64(f64), + /// A signed 128-bit integer value. I128(i128), + /// An unsigned 128-bit integer value. U128(u128), + /// A Unicode character value. Char(char), + /// A string value. Str(&'a str), - List(&'a [Value<'a>]), - Map(&'a [(Key<'a>, Value<'a>)]), + /// A list value. + List(ValueList<'a>), + /// A map value. + Map(ValueMap<'a>), + /// A display-formatted value. + Display(&'a dyn fmt::Display), + /// A debug-formatted value. + Debug(&'a dyn fmt::Debug), } impl Clone for Value<'_> { @@ -97,12 +111,121 @@ impl Clone for Value<'_> { Value::U128(u) => Value::U128(*u), Value::Char(c) => Value::Char(*c), Value::Str(s) => Value::Str(s), - Value::List(l) => Value::List(l), - Value::Map(m) => Value::Map(m), + Value::List(l) => Value::List(l.clone()), + Value::Map(m) => Value::Map(m.clone()), + Value::Display(v) => Value::Display(*v), + Value::Debug(v) => Value::Debug(*v), } } } +impl<'a> Value<'a> { + /// Create a list value from borrowed values. + pub fn list(values: &'a [Value<'a>]) -> Self { + Self::List(ValueList(ValueListState::Borrowed(values))) + } + + /// Create a map value from borrowed key-values. + pub fn map(values: &'a [(Key<'a>, Value<'a>)]) -> Self { + Self::Map(ValueMap(ValueMapState::Borrowed(values))) + } + + /// Create a value from a `str`. + pub fn from_str(value: &'a str) -> Self { + Self::Str(value) + } + + /// Create a value from a `bool`. + pub fn from_bool(value: bool) -> Self { + Self::Bool(value) + } + + /// Create a value from a type implementing [`fmt::Display`]. + pub fn from_display(value: &'a T) -> Self + where + T: fmt::Display, + { + Self::Display(value) + } + + /// Create a value from a type implementing [`fmt::Debug`]. + pub fn from_debug(value: &'a T) -> Self + where + T: fmt::Debug, + { + Self::Debug(value) + } + + /// Convert to an owned value. + pub fn to_owned(&self) -> ValueOwned { + match self { + Value::None => ValueOwned::None, + Value::Bool(v) => ValueOwned::Bool(*v), + Value::I64(v) => ValueOwned::I64(*v), + Value::U64(v) => ValueOwned::U64(*v), + Value::F64(v) => ValueOwned::F64(*v), + Value::I128(v) => ValueOwned::I128(*v), + Value::U128(v) => ValueOwned::U128(*v), + Value::Char(v) => ValueOwned::Char(*v), + Value::Str(v) => ValueOwned::Str((*v).to_owned()), + Value::List(v) => ValueOwned::List(Box::new(v.iter().map(|v| v.to_owned()).collect())), + Value::Map(v) => ValueOwned::Map(Box::new( + v.iter().map(|(k, v)| (k.to_owned(), v.to_owned())).collect(), + )), + Value::Display(v) => ValueOwned::Str(v.to_string()), + Value::Debug(v) => ValueOwned::Str(format!("{v:?}")), + } + } + + /// Try to convert this value into a `bool`. + pub fn to_bool(&self) -> Option { + match self { + Value::Bool(value) => Some(*value), + _ => None, + } + } +} + +impl<'a> From<&'a str> for Value<'a> { + fn from(value: &'a str) -> Self { + Value::Str(value) + } +} + +impl<'a> From<&'a String> for Value<'a> { + fn from(value: &'a String) -> Self { + Value::Str(value) + } +} + +macro_rules! impl_value_from { + ($($ty:ty => $variant:ident,)*) => { + $( + impl From<$ty> for Value<'_> { + fn from(value: $ty) -> Self { + Value::$variant(value) + } + } + + impl From<&$ty> for Value<'_> { + fn from(value: &$ty) -> Self { + Value::$variant(*value) + } + } + )* + }; +} + +impl_value_from! { + bool => Bool, + i64 => I64, + u64 => U64, + f64 => F64, + i128 => I128, + u128 => U128, + char => Char, +} + impl fmt::Debug for Value<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -115,14 +238,13 @@ impl fmt::Debug for Value<'_> { Value::U128(v) => v.fmt(f), Value::Char(v) => v.fmt(f), Value::Str(v) => v.fmt(f), + Value::Display(v) => fmt::Display::fmt(v, f), // implement Value::None => f.debug_tuple("None").finish(), Value::List(v) => f.debug_list().entries(v.iter()).finish(), - Value::Map(m) => f - .debug_map() - .entries(m.iter().map(|(k, v)| (k, v))) - .finish(), + Value::Map(m) => f.debug_map().entries(m.iter()).finish(), + Value::Debug(v) => fmt::Debug::fmt(v, f), } } } @@ -138,28 +260,139 @@ impl fmt::Display for Value<'_> { Value::U128(v) => v.fmt(f), Value::Char(v) => v.fmt(f), Value::Str(v) => v.fmt(f), + Value::Display(v) => fmt::Display::fmt(v, f), + Value::Debug(v) => fmt::Debug::fmt(v, f), v => fmt::Debug::fmt(v, f), } } } +/// A list value. +#[derive(Clone)] +pub struct ValueList<'a>(ValueListState<'a>); + +#[derive(Clone)] +enum ValueListState<'a> { + Borrowed(&'a [Value<'a>]), + Owned(&'a [ValueOwned]), +} + +impl<'a> ValueList<'a> { + /// Get an iterator over the list values. + pub fn iter(&self) -> ValueListIter<'a> { + match self.0 { + ValueListState::Borrowed(values) => { + ValueListIter(ValueListIterState::Borrowed(values.iter())) + } + ValueListState::Owned(values) => ValueListIter(ValueListIterState::Owned(values.iter())), + } + } +} + +/// An iterator over list values. +pub struct ValueListIter<'a>(ValueListIterState<'a>); + +enum ValueListIterState<'a> { + Borrowed(slice::Iter<'a, Value<'a>>), + Owned(slice::Iter<'a, ValueOwned>), +} + +impl<'a> Iterator for ValueListIter<'a> { + type Item = Value<'a>; + + fn next(&mut self) -> Option { + match &mut self.0 { + ValueListIterState::Borrowed(iter) => iter.next().cloned(), + ValueListIterState::Owned(iter) => iter.next().map(ValueOwned::by_ref), + } + } + + fn size_hint(&self) -> (usize, Option) { + match &self.0 { + ValueListIterState::Borrowed(iter) => iter.size_hint(), + ValueListIterState::Owned(iter) => iter.size_hint(), + } + } +} + +/// A map value. +#[derive(Clone)] +pub struct ValueMap<'a>(ValueMapState<'a>); + +#[derive(Clone)] +enum ValueMapState<'a> { + Borrowed(&'a [(Key<'a>, Value<'a>)]), + Owned(&'a [(KeyOwned, ValueOwned)]), +} + +impl<'a> ValueMap<'a> { + /// Get an iterator over the map key-values. + pub fn iter(&self) -> ValueMapIter<'a> { + match self.0 { + ValueMapState::Borrowed(values) => { + ValueMapIter(ValueMapIterState::Borrowed(values.iter())) + } + ValueMapState::Owned(values) => ValueMapIter(ValueMapIterState::Owned(values.iter())), + } + } +} + +/// An iterator over map key-values. +pub struct ValueMapIter<'a>(ValueMapIterState<'a>); + +enum ValueMapIterState<'a> { + Borrowed(slice::Iter<'a, (Key<'a>, Value<'a>)>), + Owned(slice::Iter<'a, (KeyOwned, ValueOwned)>), +} + +impl<'a> Iterator for ValueMapIter<'a> { + type Item = (Key<'a>, Value<'a>); + + fn next(&mut self) -> Option { + match &mut self.0 { + ValueMapIterState::Borrowed(iter) => iter.next().map(|(k, v)| (k.clone(), v.clone())), + ValueMapIterState::Owned(iter) => iter.next().map(|(k, v)| (k.by_ref(), v.by_ref())), + } + } + + fn size_hint(&self) -> (usize, Option) { + match &self.0 { + ValueMapIterState::Borrowed(iter) => iter.size_hint(), + ValueMapIterState::Owned(iter) => iter.size_hint(), + } + } +} + /// An owned value in a key-value pair. +#[derive(Clone, Debug)] #[non_exhaustive] pub enum ValueOwned { + /// The absence of a value. None, + /// A boolean value. Bool(bool), + /// A signed 64-bit integer value. I64(i64), + /// An unsigned 64-bit integer value. U64(u64), + /// A 64-bit floating point value. F64(f64), + /// A signed 128-bit integer value. I128(i128), + /// An unsigned 128-bit integer value. U128(u128), + /// A Unicode character value. Char(char), + /// A string value. Str(String), + /// A list value. List(Box>), - Map(Box>), + /// A map value. + Map(Box>), } impl ValueOwned { + /// Create a borrowed view of this owned value. pub fn by_ref(&self) -> Value<'_> { match self { ValueOwned::None => Value::None, @@ -171,22 +404,14 @@ impl ValueOwned { ValueOwned::U128(u) => Value::U128(*u), ValueOwned::Char(c) => Value::Char(*c), ValueOwned::Str(s) => Value::Str(s.as_str()), - ValueOwned::List(l) => { - let l = l.iter().map(|v| v.by_ref()).collect::>(); - Value::List(l.as_slice()) - } - ValueOwned::Map(m) => { - let m = m - .iter() - .map(|(k, v)| (k.by_ref(), v.by_ref())) - .collect::>(); - Value::Map(m.as_slice()) - } + ValueOwned::List(l) => Value::List(ValueList(ValueListState::Owned(l.as_slice()))), + ValueOwned::Map(m) => Value::Map(ValueMap(ValueMapState::Owned(m.as_slice()))), } } } /// An owned key in a key-value pair. +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct KeyOwned(Cow<'static, str>); impl KeyOwned { From b0da4d48c9e1700be2317636ec86635c283bf50c Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 1 Jun 2026 13:07:41 +0800 Subject: [PATCH 04/19] Revert "take 2" This reverts commit b4487f6abfa52af8b85c9830783950359ab0b459. --- core/src/kv.rs | 267 ++++--------------------------------------------- 1 file changed, 21 insertions(+), 246 deletions(-) diff --git a/core/src/kv.rs b/core/src/kv.rs index 6b63eba..1412124 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -15,6 +15,7 @@ //! Key-value pairs in a log record or a diagnostic context. use std::borrow::Cow; +use std::collections::HashMap; use std::fmt; use std::slice; @@ -71,32 +72,17 @@ impl<'a> Key<'a> { /// A value in a key-value pair. #[non_exhaustive] pub enum Value<'a> { - /// The absence of a value. None, - /// A boolean value. Bool(bool), - /// A signed 64-bit integer value. I64(i64), - /// An unsigned 64-bit integer value. U64(u64), - /// A 64-bit floating point value. F64(f64), - /// A signed 128-bit integer value. I128(i128), - /// An unsigned 128-bit integer value. U128(u128), - /// A Unicode character value. Char(char), - /// A string value. Str(&'a str), - /// A list value. - List(ValueList<'a>), - /// A map value. - Map(ValueMap<'a>), - /// A display-formatted value. - Display(&'a dyn fmt::Display), - /// A debug-formatted value. - Debug(&'a dyn fmt::Debug), + List(&'a [Value<'a>]), + Map(&'a [(Key<'a>, Value<'a>)]), } impl Clone for Value<'_> { @@ -111,121 +97,12 @@ impl Clone for Value<'_> { Value::U128(u) => Value::U128(*u), Value::Char(c) => Value::Char(*c), Value::Str(s) => Value::Str(s), - Value::List(l) => Value::List(l.clone()), - Value::Map(m) => Value::Map(m.clone()), - Value::Display(v) => Value::Display(*v), - Value::Debug(v) => Value::Debug(*v), + Value::List(l) => Value::List(l), + Value::Map(m) => Value::Map(m), } } } -impl<'a> Value<'a> { - /// Create a list value from borrowed values. - pub fn list(values: &'a [Value<'a>]) -> Self { - Self::List(ValueList(ValueListState::Borrowed(values))) - } - - /// Create a map value from borrowed key-values. - pub fn map(values: &'a [(Key<'a>, Value<'a>)]) -> Self { - Self::Map(ValueMap(ValueMapState::Borrowed(values))) - } - - /// Create a value from a `str`. - pub fn from_str(value: &'a str) -> Self { - Self::Str(value) - } - - /// Create a value from a `bool`. - pub fn from_bool(value: bool) -> Self { - Self::Bool(value) - } - - /// Create a value from a type implementing [`fmt::Display`]. - pub fn from_display(value: &'a T) -> Self - where - T: fmt::Display, - { - Self::Display(value) - } - - /// Create a value from a type implementing [`fmt::Debug`]. - pub fn from_debug(value: &'a T) -> Self - where - T: fmt::Debug, - { - Self::Debug(value) - } - - /// Convert to an owned value. - pub fn to_owned(&self) -> ValueOwned { - match self { - Value::None => ValueOwned::None, - Value::Bool(v) => ValueOwned::Bool(*v), - Value::I64(v) => ValueOwned::I64(*v), - Value::U64(v) => ValueOwned::U64(*v), - Value::F64(v) => ValueOwned::F64(*v), - Value::I128(v) => ValueOwned::I128(*v), - Value::U128(v) => ValueOwned::U128(*v), - Value::Char(v) => ValueOwned::Char(*v), - Value::Str(v) => ValueOwned::Str((*v).to_owned()), - Value::List(v) => ValueOwned::List(Box::new(v.iter().map(|v| v.to_owned()).collect())), - Value::Map(v) => ValueOwned::Map(Box::new( - v.iter().map(|(k, v)| (k.to_owned(), v.to_owned())).collect(), - )), - Value::Display(v) => ValueOwned::Str(v.to_string()), - Value::Debug(v) => ValueOwned::Str(format!("{v:?}")), - } - } - - /// Try to convert this value into a `bool`. - pub fn to_bool(&self) -> Option { - match self { - Value::Bool(value) => Some(*value), - _ => None, - } - } -} - -impl<'a> From<&'a str> for Value<'a> { - fn from(value: &'a str) -> Self { - Value::Str(value) - } -} - -impl<'a> From<&'a String> for Value<'a> { - fn from(value: &'a String) -> Self { - Value::Str(value) - } -} - -macro_rules! impl_value_from { - ($($ty:ty => $variant:ident,)*) => { - $( - impl From<$ty> for Value<'_> { - fn from(value: $ty) -> Self { - Value::$variant(value) - } - } - - impl From<&$ty> for Value<'_> { - fn from(value: &$ty) -> Self { - Value::$variant(*value) - } - } - )* - }; -} - -impl_value_from! { - bool => Bool, - i64 => I64, - u64 => U64, - f64 => F64, - i128 => I128, - u128 => U128, - char => Char, -} - impl fmt::Debug for Value<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -238,13 +115,14 @@ impl fmt::Debug for Value<'_> { Value::U128(v) => v.fmt(f), Value::Char(v) => v.fmt(f), Value::Str(v) => v.fmt(f), - Value::Display(v) => fmt::Display::fmt(v, f), // implement Value::None => f.debug_tuple("None").finish(), Value::List(v) => f.debug_list().entries(v.iter()).finish(), - Value::Map(m) => f.debug_map().entries(m.iter()).finish(), - Value::Debug(v) => fmt::Debug::fmt(v, f), + Value::Map(m) => f + .debug_map() + .entries(m.iter().map(|(k, v)| (k, v))) + .finish(), } } } @@ -260,139 +138,28 @@ impl fmt::Display for Value<'_> { Value::U128(v) => v.fmt(f), Value::Char(v) => v.fmt(f), Value::Str(v) => v.fmt(f), - Value::Display(v) => fmt::Display::fmt(v, f), - Value::Debug(v) => fmt::Debug::fmt(v, f), v => fmt::Debug::fmt(v, f), } } } -/// A list value. -#[derive(Clone)] -pub struct ValueList<'a>(ValueListState<'a>); - -#[derive(Clone)] -enum ValueListState<'a> { - Borrowed(&'a [Value<'a>]), - Owned(&'a [ValueOwned]), -} - -impl<'a> ValueList<'a> { - /// Get an iterator over the list values. - pub fn iter(&self) -> ValueListIter<'a> { - match self.0 { - ValueListState::Borrowed(values) => { - ValueListIter(ValueListIterState::Borrowed(values.iter())) - } - ValueListState::Owned(values) => ValueListIter(ValueListIterState::Owned(values.iter())), - } - } -} - -/// An iterator over list values. -pub struct ValueListIter<'a>(ValueListIterState<'a>); - -enum ValueListIterState<'a> { - Borrowed(slice::Iter<'a, Value<'a>>), - Owned(slice::Iter<'a, ValueOwned>), -} - -impl<'a> Iterator for ValueListIter<'a> { - type Item = Value<'a>; - - fn next(&mut self) -> Option { - match &mut self.0 { - ValueListIterState::Borrowed(iter) => iter.next().cloned(), - ValueListIterState::Owned(iter) => iter.next().map(ValueOwned::by_ref), - } - } - - fn size_hint(&self) -> (usize, Option) { - match &self.0 { - ValueListIterState::Borrowed(iter) => iter.size_hint(), - ValueListIterState::Owned(iter) => iter.size_hint(), - } - } -} - -/// A map value. -#[derive(Clone)] -pub struct ValueMap<'a>(ValueMapState<'a>); - -#[derive(Clone)] -enum ValueMapState<'a> { - Borrowed(&'a [(Key<'a>, Value<'a>)]), - Owned(&'a [(KeyOwned, ValueOwned)]), -} - -impl<'a> ValueMap<'a> { - /// Get an iterator over the map key-values. - pub fn iter(&self) -> ValueMapIter<'a> { - match self.0 { - ValueMapState::Borrowed(values) => { - ValueMapIter(ValueMapIterState::Borrowed(values.iter())) - } - ValueMapState::Owned(values) => ValueMapIter(ValueMapIterState::Owned(values.iter())), - } - } -} - -/// An iterator over map key-values. -pub struct ValueMapIter<'a>(ValueMapIterState<'a>); - -enum ValueMapIterState<'a> { - Borrowed(slice::Iter<'a, (Key<'a>, Value<'a>)>), - Owned(slice::Iter<'a, (KeyOwned, ValueOwned)>), -} - -impl<'a> Iterator for ValueMapIter<'a> { - type Item = (Key<'a>, Value<'a>); - - fn next(&mut self) -> Option { - match &mut self.0 { - ValueMapIterState::Borrowed(iter) => iter.next().map(|(k, v)| (k.clone(), v.clone())), - ValueMapIterState::Owned(iter) => iter.next().map(|(k, v)| (k.by_ref(), v.by_ref())), - } - } - - fn size_hint(&self) -> (usize, Option) { - match &self.0 { - ValueMapIterState::Borrowed(iter) => iter.size_hint(), - ValueMapIterState::Owned(iter) => iter.size_hint(), - } - } -} - /// An owned value in a key-value pair. -#[derive(Clone, Debug)] #[non_exhaustive] pub enum ValueOwned { - /// The absence of a value. None, - /// A boolean value. Bool(bool), - /// A signed 64-bit integer value. I64(i64), - /// An unsigned 64-bit integer value. U64(u64), - /// A 64-bit floating point value. F64(f64), - /// A signed 128-bit integer value. I128(i128), - /// An unsigned 128-bit integer value. U128(u128), - /// A Unicode character value. Char(char), - /// A string value. Str(String), - /// A list value. List(Box>), - /// A map value. - Map(Box>), + Map(Box>), } impl ValueOwned { - /// Create a borrowed view of this owned value. pub fn by_ref(&self) -> Value<'_> { match self { ValueOwned::None => Value::None, @@ -404,14 +171,22 @@ impl ValueOwned { ValueOwned::U128(u) => Value::U128(*u), ValueOwned::Char(c) => Value::Char(*c), ValueOwned::Str(s) => Value::Str(s.as_str()), - ValueOwned::List(l) => Value::List(ValueList(ValueListState::Owned(l.as_slice()))), - ValueOwned::Map(m) => Value::Map(ValueMap(ValueMapState::Owned(m.as_slice()))), + ValueOwned::List(l) => { + let l = l.iter().map(|v| v.by_ref()).collect::>(); + Value::List(l.as_slice()) + } + ValueOwned::Map(m) => { + let m = m + .iter() + .map(|(k, v)| (k.by_ref(), v.by_ref())) + .collect::>(); + Value::Map(m.as_slice()) + } } } } /// An owned key in a key-value pair. -#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct KeyOwned(Cow<'static, str>); impl KeyOwned { From c2b929e9e1c5f8b4fd7b8d569dd199a81e5b7ae8 Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 1 Jun 2026 17:43:48 +0800 Subject: [PATCH 05/19] round 3 Signed-off-by: tison --- appenders/async/src/worker.rs | 8 +- bridges/log/src/lib.rs | 2 +- core/src/diagnostic/static_global.rs | 6 +- core/src/diagnostic/thread_local.rs | 6 +- core/src/kv.rs | 688 +++++++++++++++++++++------ core/src/layout/plain_text.rs | 5 +- core/src/record.rs | 2 +- core/src/str.rs | 9 + diagnostics/fastrace/src/lib.rs | 21 +- diagnostics/task-local/src/lib.rs | 6 +- layouts/logfmt/src/lib.rs | 10 +- 11 files changed, 590 insertions(+), 173 deletions(-) diff --git a/appenders/async/src/worker.rs b/appenders/async/src/worker.rs index c9a02bc..ec9c35c 100644 --- a/appenders/async/src/worker.rs +++ b/appenders/async/src/worker.rs @@ -54,7 +54,7 @@ impl Worker { let diags: &[Box] = if diags.is_empty() { &[] } else { - &[Box::new(OwnedDiagnostic(diags))] + &[Box::new(AsyncDiagnostic(diags))] }; record.with(|record| { @@ -80,12 +80,12 @@ impl Worker { } #[derive(Debug)] -struct OwnedDiagnostic(Vec<(kv::KeyOwned, kv::ValueOwned)>); +struct AsyncDiagnostic(Vec<(kv::KeyOwned, kv::ValueOwned)>); -impl Diagnostic for OwnedDiagnostic { +impl Diagnostic for AsyncDiagnostic { fn visit(&self, visitor: &mut dyn Visitor) -> Result<(), Error> { for (key, value) in &self.0 { - visitor.visit(key.by_ref(), value.by_ref())?; + visitor.visit(key.view(), value.view())?; } Ok(()) } diff --git a/bridges/log/src/lib.rs b/bridges/log/src/lib.rs index c9df6d6..0bd13e6 100644 --- a/bridges/log/src/lib.rs +++ b/bridges/log/src/lib.rs @@ -133,7 +133,7 @@ fn forward_log(logger: &Logger, record: &Record) { let mut new_kvs = Vec::with_capacity(kvs.len()); for (k, v) in kvs.iter() { v. - new_kvs.push((Key::new_ref(k.as_str()), Value::from_sval2(v))); + new_kvs.push((Key::borrowed(k.as_str()), Value::from_sval2(v))); } builder = builder.key_values(new_kvs.as_slice()); diff --git a/core/src/diagnostic/static_global.rs b/core/src/diagnostic/static_global.rs index 75cad92..25b49c7 100644 --- a/core/src/diagnostic/static_global.rs +++ b/core/src/diagnostic/static_global.rs @@ -59,9 +59,9 @@ impl StaticDiagnostic { fn do_visit(d: &StaticDiagnostic, visitor: &mut dyn Visitor) -> Result<(), Error> { for (key, value) in d.kvs.iter() { - let key = Key::new_ref(key.as_str()); - let value = Value::from(value); - visitor.visit(key, value)?; + let key = Key::borrowed(key.as_str()); + let value = Value::str(value); + visitor.visit(key.view(), value.view())?; } Ok(()) } diff --git a/core/src/diagnostic/thread_local.rs b/core/src/diagnostic/thread_local.rs index 6f1b1b8..835d17a 100644 --- a/core/src/diagnostic/thread_local.rs +++ b/core/src/diagnostic/thread_local.rs @@ -63,9 +63,9 @@ impl Diagnostic for ThreadLocalDiagnostic { THREAD_LOCAL_MAP.with(|map| { let map = map.borrow(); for (key, value) in map.iter() { - let key = Key::new_ref(key.as_str()); - let value = Value::from(value); - visitor.visit(key, value)?; + let key = Key::borrowed(key.as_str()); + let value = Value::str(value); + visitor.visit(key.view(), value.view())?; } Ok(()) }) diff --git a/core/src/kv.rs b/core/src/kv.rs index 1412124..bc92349 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -14,24 +14,27 @@ //! Key-value pairs in a log record or a diagnostic context. -use std::borrow::Cow; -use std::collections::HashMap; -use std::fmt; -use std::slice; - use crate::Error; use crate::str::RefStr; +use std::borrow::Cow; +use std::collections::{HashMap, hash_map}; +use std::{fmt, slice}; -/// A visitor to walk through key-value pairs. pub trait Visitor { /// Visit a key-value pair. - fn visit(&mut self, key: Key, value: Value) -> Result<(), Error>; + fn visit(&mut self, key: KeyView, value: ValueView) -> Result<(), Error>; } /// A key in a key-value pair. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Key<'a>(RefStr<'a>); +impl fmt::Display for Key<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + impl Key<'static> { /// Create a new key from a static `&str`. pub const fn new(k: &'static str) -> Key<'static> { @@ -39,20 +42,56 @@ impl Key<'static> { } } -impl fmt::Display for Key<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} - impl<'a> Key<'a> { /// Create a new key from a `&str`. /// /// The [`Key::new`] method should be preferred where possible. - pub const fn new_ref(k: &'a str) -> Key<'a> { + pub const fn borrowed(k: &'a str) -> Key<'a> { Key(RefStr::Borrowed(k)) } +} + +impl Key<'_> { + pub fn view(&self) -> KeyView<'_> { + KeyView(self.0) + } +} + +/// An owned key in a key-value pair. +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct KeyOwned(Cow<'static, str>); + +impl fmt::Display for KeyOwned { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl KeyOwned { + pub fn new(k: impl Into>) -> KeyOwned { + KeyOwned(k.into()) + } +} + +impl KeyOwned { + pub fn view(&self) -> KeyView<'_> { + KeyView(match &self.0 { + Cow::Borrowed(s) => RefStr::Static(s), + Cow::Owned(s) => RefStr::Borrowed(s), + }) + } +} +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct KeyView<'a>(RefStr<'a>); + +impl fmt::Display for KeyView<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl KeyView<'_> { /// Convert to an owned key. pub fn to_owned(&self) -> KeyOwned { KeyOwned(self.0.into_cow_static()) @@ -69,9 +108,131 @@ impl<'a> Key<'a> { } } -/// A value in a key-value pair. -#[non_exhaustive] -pub enum Value<'a> { +#[derive(Clone, Copy)] +pub struct DebugValue<'a>(&'a dyn fmt::Debug); + +impl fmt::Debug for DebugValue<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + +impl fmt::Display for DebugValue<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + +#[derive(Clone, Copy)] +pub struct DisplayValue<'a>(&'a dyn fmt::Display); + +impl fmt::Debug for DisplayValue<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl fmt::Display for DisplayValue<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ListValue<'a>(ListValueState<'a>); + +#[derive(Debug, Clone, Copy)] +enum ListValueState<'a> { + Borrowed(&'a [Value<'a>]), + Owned(&'a [ValueOwned]), +} + +impl<'a> ListValue<'a> { + pub fn iter(&self) -> ListValueIter<'a> { + match self.0 { + ListValueState::Borrowed(v) => ListValueIter(ListValueIterState::Borrowed(v.iter())), + ListValueState::Owned(v) => ListValueIter(ListValueIterState::Owned(v.iter())), + } + } +} + +#[derive(Debug, Clone)] +pub struct ListValueIter<'a>(ListValueIterState<'a>); + +#[derive(Debug, Clone)] +enum ListValueIterState<'a> { + Borrowed(slice::Iter<'a, Value<'a>>), + Owned(slice::Iter<'a, ValueOwned>), +} + +impl<'a> Iterator for ListValueIter<'a> { + type Item = ValueView<'a>; + + fn next(&mut self) -> Option { + match &mut self.0 { + ListValueIterState::Borrowed(v) => v.next().map(|v| v.view()), + ListValueIterState::Owned(v) => v.next().map(|v| v.view()), + } + } + + fn size_hint(&self) -> (usize, Option) { + match &self.0 { + ListValueIterState::Borrowed(v) => v.size_hint(), + ListValueIterState::Owned(v) => v.size_hint(), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct MapValue<'a>(MapValueState<'a>); + +#[derive(Debug, Clone, Copy)] +enum MapValueState<'a> { + Borrowed(&'a [(Key<'a>, Value<'a>)]), + Owned(&'a HashMap), +} + +impl<'a> MapValue<'a> { + pub fn iter(&self) -> MapValueIter<'a> { + match self.0 { + MapValueState::Borrowed(v) => MapValueIter(MapValueIterState::Borrowed(v.iter())), + MapValueState::Owned(v) => MapValueIter(MapValueIterState::Owned(v.iter())), + } + } +} + +#[derive(Debug, Clone)] +pub struct MapValueIter<'a>(MapValueIterState<'a>); + +#[derive(Debug, Clone)] +enum MapValueIterState<'a> { + Borrowed(slice::Iter<'a, (Key<'a>, Value<'a>)>), + Owned(hash_map::Iter<'a, KeyOwned, ValueOwned>), +} + +impl<'a> Iterator for MapValueIter<'a> { + type Item = (KeyView<'a>, ValueView<'a>); + + fn next(&mut self) -> Option { + match &mut self.0 { + MapValueIterState::Borrowed(v) => v.next().map(|(k, v)| (k.view(), v.view())), + MapValueIterState::Owned(v) => v.next().map(|(k, v)| (k.view(), v.view())), + } + } + + fn size_hint(&self) -> (usize, Option) { + match &self.0 { + MapValueIterState::Borrowed(v) => v.size_hint(), + MapValueIterState::Owned(v) => v.size_hint(), + } + } +} + +#[derive(Debug, Clone)] +pub struct Value<'a>(ValueState<'a>); + +#[derive(Clone)] +enum ValueState<'a> { None, Bool(bool), I64(i64), @@ -80,72 +241,117 @@ pub enum Value<'a> { I128(i128), U128(u128), Char(char), - Str(&'a str), + Str(RefStr<'a>), List(&'a [Value<'a>]), Map(&'a [(Key<'a>, Value<'a>)]), + Debug(&'a dyn fmt::Debug), + Display(&'a dyn fmt::Display), } -impl Clone for Value<'_> { - fn clone(&self) -> Self { +impl fmt::Debug for ValueState<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Value::None => Value::None, - Value::Bool(b) => Value::Bool(*b), - Value::I64(i) => Value::I64(*i), - Value::U64(u) => Value::U64(*u), - Value::F64(f) => Value::F64(*f), - Value::I128(i) => Value::I128(*i), - Value::U128(u) => Value::U128(*u), - Value::Char(c) => Value::Char(*c), - Value::Str(s) => Value::Str(s), - Value::List(l) => Value::List(l), - Value::Map(m) => Value::Map(m), + ValueState::None => write!(f, "None"), + ValueState::Bool(v) => v.fmt(f), + ValueState::I64(v) => v.fmt(f), + ValueState::U64(v) => v.fmt(f), + ValueState::F64(v) => v.fmt(f), + ValueState::I128(v) => v.fmt(f), + ValueState::U128(v) => v.fmt(f), + ValueState::Char(v) => v.fmt(f), + ValueState::Str(v) => v.fmt(f), + ValueState::List(v) => v.fmt(f), + ValueState::Map(v) => v.fmt(f), + ValueState::Debug(v) => fmt::Debug::fmt(v, f), + ValueState::Display(v) => fmt::Display::fmt(v, f), } } } -impl fmt::Debug for Value<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - // passthrough - Value::Bool(v) => v.fmt(f), - Value::I64(v) => v.fmt(f), - Value::U64(v) => v.fmt(f), - Value::F64(v) => v.fmt(f), - Value::I128(v) => v.fmt(f), - Value::U128(v) => v.fmt(f), - Value::Char(v) => v.fmt(f), - Value::Str(v) => v.fmt(f), - - // implement - Value::None => f.debug_tuple("None").finish(), - Value::List(v) => f.debug_list().entries(v.iter()).finish(), - Value::Map(m) => f - .debug_map() - .entries(m.iter().map(|(k, v)| (k, v))) - .finish(), +impl Value<'_> { + pub fn view(&self) -> ValueView<'_> { + match self.0 { + ValueState::None => ValueView::None, + ValueState::Bool(b) => ValueView::Bool(b), + ValueState::I64(i) => ValueView::I64(i), + ValueState::U64(u) => ValueView::U64(u), + ValueState::F64(f) => ValueView::F64(f), + ValueState::I128(i) => ValueView::I128(i), + ValueState::U128(u) => ValueView::U128(u), + ValueState::Char(c) => ValueView::Char(c), + ValueState::Str(RefStr::Static(s)) => ValueView::StaticStr(s), + ValueState::Str(RefStr::Borrowed(s)) => ValueView::BorrowedStr(s), + ValueState::List(l) => ValueView::List(ListValue(ListValueState::Borrowed(l))), + ValueState::Map(m) => ValueView::Map(MapValue(MapValueState::Borrowed(m))), + ValueState::Debug(d) => ValueView::Debug(DebugValue(d)), + ValueState::Display(d) => ValueView::Display(DisplayValue(d)), } } } -impl fmt::Display for Value<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Value::Bool(v) => v.fmt(f), - Value::I64(v) => v.fmt(f), - Value::U64(v) => v.fmt(f), - Value::F64(v) => v.fmt(f), - Value::I128(v) => v.fmt(f), - Value::U128(v) => v.fmt(f), - Value::Char(v) => v.fmt(f), - Value::Str(v) => v.fmt(f), - v => fmt::Debug::fmt(v, f), - } +impl<'a> Value<'a> { + pub fn none() -> Value<'a> { + Value(ValueState::None) + } + + pub fn str(s: &'a str) -> Self { + Value(ValueState::Str(RefStr::Borrowed(s))) + } + + pub fn static_str(s: &'static str) -> Self { + Value(ValueState::Str(RefStr::Static(s))) + } + + pub fn bool(b: bool) -> Self { + Value(ValueState::Bool(b)) + } + + pub fn i64(i: i64) -> Self { + Value(ValueState::I64(i)) + } + + pub fn u64(u: u64) -> Self { + Value(ValueState::U64(u)) + } + + pub fn f64(f: f64) -> Self { + Value(ValueState::F64(f)) + } + + pub fn i128(i: i128) -> Self { + Value(ValueState::I128(i)) + } + + pub fn u128(u: u128) -> Self { + Value(ValueState::U128(u)) + } + + pub fn char(c: char) -> Self { + Value(ValueState::Char(c)) + } + + pub fn list(l: &'a [Value<'a>]) -> Self { + Value(ValueState::List(l)) + } + + pub fn map(m: &'a [(Key<'a>, Value<'a>)]) -> Self { + Value(ValueState::Map(m)) + } + + pub fn debug(d: &'a dyn fmt::Debug) -> Self { + Value(ValueState::Debug(d)) + } + + pub fn display(d: &'a dyn fmt::Display) -> Self { + Value(ValueState::Display(d)) } } -/// An owned value in a key-value pair. -#[non_exhaustive] -pub enum ValueOwned { +#[derive(Debug, Clone)] +pub struct ValueOwned(ValueOwnedState); + +#[derive(Debug, Clone)] +enum ValueOwnedState { None, Bool(bool), I64(i64), @@ -154,59 +360,291 @@ pub enum ValueOwned { I128(i128), U128(u128), Char(char), - Str(String), + Str(Cow<'static, str>), List(Box>), Map(Box>), } impl ValueOwned { - pub fn by_ref(&self) -> Value<'_> { + pub fn view(&self) -> ValueView<'_> { + match &self.0 { + ValueOwnedState::None => ValueView::None, + ValueOwnedState::Bool(b) => ValueView::Bool(*b), + ValueOwnedState::I64(i) => ValueView::I64(*i), + ValueOwnedState::U64(u) => ValueView::U64(*u), + ValueOwnedState::F64(f) => ValueView::F64(*f), + ValueOwnedState::I128(i) => ValueView::I128(*i), + ValueOwnedState::U128(u) => ValueView::U128(*u), + ValueOwnedState::Char(c) => ValueView::Char(*c), + ValueOwnedState::Str(Cow::Borrowed(s)) => ValueView::StaticStr(s), + ValueOwnedState::Str(Cow::Owned(s)) => ValueView::BorrowedStr(s.as_str()), + ValueOwnedState::List(l) => ValueView::List(ListValue(ListValueState::Owned(l))), + ValueOwnedState::Map(m) => ValueView::Map(MapValue(MapValueState::Owned(m))), + } + } +} + +impl ValueOwned { + pub fn none() -> ValueOwned { + ValueOwned(ValueOwnedState::None) + } + + pub fn bool(b: bool) -> ValueOwned { + ValueOwned(ValueOwnedState::Bool(b)) + } + + pub fn i64(i: i64) -> ValueOwned { + ValueOwned(ValueOwnedState::I64(i)) + } + + pub fn u64(u: u64) -> ValueOwned { + ValueOwned(ValueOwnedState::U64(u)) + } + + pub fn f64(f: f64) -> ValueOwned { + ValueOwned(ValueOwnedState::F64(f)) + } + + pub fn i128(i: i128) -> ValueOwned { + ValueOwned(ValueOwnedState::I128(i)) + } + + pub fn u128(u: u128) -> ValueOwned { + ValueOwned(ValueOwnedState::U128(u)) + } + + pub fn char(c: char) -> ValueOwned { + ValueOwned(ValueOwnedState::Char(c)) + } + + pub fn str(s: impl Into>) -> ValueOwned { + ValueOwned(ValueOwnedState::Str(s.into())) + } + + pub fn list(l: impl IntoIterator) -> ValueOwned { + ValueOwned(ValueOwnedState::List(Box::new(l.into_iter().collect()))) + } + + pub fn map(m: impl IntoIterator) -> ValueOwned { + ValueOwned(ValueOwnedState::Map(Box::new(m.into_iter().collect()))) + } + + pub fn from_vec(v: Vec) -> ValueOwned { + ValueOwned(ValueOwnedState::List(Box::new(v))) + } + + pub fn from_hash_map(m: HashMap) -> ValueOwned { + ValueOwned(ValueOwnedState::Map(Box::new(m))) + } +} + +#[non_exhaustive] +#[derive(Debug, Clone)] +pub enum ValueView<'a> { + None, + BorrowedStr(&'a str), + StaticStr(&'static str), + Bool(bool), + I64(i64), + U64(u64), + F64(f64), + I128(i128), + U128(u128), + Char(char), + List(ListValue<'a>), + Map(MapValue<'a>), + Debug(DebugValue<'a>), + Display(DisplayValue<'a>), +} + +impl fmt::Display for ValueView<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ValueOwned::None => Value::None, - ValueOwned::Bool(b) => Value::Bool(*b), - ValueOwned::I64(i) => Value::I64(*i), - ValueOwned::U64(u) => Value::U64(*u), - ValueOwned::F64(f) => Value::F64(*f), - ValueOwned::I128(i) => Value::I128(*i), - ValueOwned::U128(u) => Value::U128(*u), - ValueOwned::Char(c) => Value::Char(*c), - ValueOwned::Str(s) => Value::Str(s.as_str()), - ValueOwned::List(l) => { - let l = l.iter().map(|v| v.by_ref()).collect::>(); - Value::List(l.as_slice()) + ValueView::None => write!(f, "None"), + ValueView::BorrowedStr(v) => v.fmt(f), + ValueView::StaticStr(v) => v.fmt(f), + ValueView::Bool(v) => v.fmt(f), + ValueView::I64(v) => v.fmt(f), + ValueView::U64(v) => v.fmt(f), + ValueView::F64(v) => v.fmt(f), + ValueView::I128(v) => v.fmt(f), + ValueView::U128(v) => v.fmt(f), + ValueView::Char(v) => v.fmt(f), + ValueView::List(v) => { + let mut dbg = f.debug_list(); + for item in v.iter() { + dbg.entry(&item); + } + dbg.finish() } - ValueOwned::Map(m) => { - let m = m - .iter() - .map(|(k, v)| (k.by_ref(), v.by_ref())) - .collect::>(); - Value::Map(m.as_slice()) + ValueView::Map(v) => { + let mut dbg = f.debug_map(); + for (k, v) in v.iter() { + dbg.entry(&k, &v); + } + dbg.finish() } + ValueView::Debug(v) => fmt::Debug::fmt(v, f), + ValueView::Display(v) => fmt::Display::fmt(v, f), } } } -/// An owned key in a key-value pair. -pub struct KeyOwned(Cow<'static, str>); +impl ValueView<'_> { + pub fn to_owned(&self) -> ValueOwned { + match &self { + ValueView::None => ValueOwned(ValueOwnedState::None), + ValueView::BorrowedStr(s) => { + ValueOwned(ValueOwnedState::Str(Cow::Owned(s.to_string()))) + } + ValueView::StaticStr(s) => { + ValueOwned(ValueOwnedState::Str(Cow::Owned((*s).to_string()))) + } + ValueView::Bool(b) => ValueOwned(ValueOwnedState::Bool(*b)), + ValueView::I64(i) => ValueOwned(ValueOwnedState::I64(*i)), + ValueView::U64(u) => ValueOwned(ValueOwnedState::U64(*u)), + ValueView::F64(f) => ValueOwned(ValueOwnedState::F64(*f)), + ValueView::I128(i) => ValueOwned(ValueOwnedState::I128(*i)), + ValueView::U128(u) => ValueOwned(ValueOwnedState::U128(*u)), + ValueView::Char(c) => ValueOwned(ValueOwnedState::Char(*c)), + ValueView::List(l) => ValueOwned(ValueOwnedState::List(Box::new( + l.iter().map(|v| v.to_owned()).collect(), + ))), + ValueView::Map(m) => ValueOwned(ValueOwnedState::Map(Box::new( + m.iter() + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect(), + ))), + ValueView::Debug(d) => ValueOwned(ValueOwnedState::Str(Cow::Owned(format!("{d:?}")))), + ValueView::Display(d) => ValueOwned(ValueOwnedState::Str(Cow::Owned(format!("{d}")))), + } + } +} -impl KeyOwned { - /// Create a `Key` ref. - pub fn by_ref(&self) -> Key<'_> { - Key(match &self.0 { - Cow::Borrowed(s) => RefStr::Static(s), - Cow::Owned(s) => RefStr::Borrowed(s), - }) +impl<'a> ValueView<'a> { + pub fn to_bool(&self) -> Option { + if let ValueView::Bool(b) = self { + Some(*b) + } else { + None + } + } + + pub fn to_i64(&self) -> Option { + if let ValueView::I64(i) = self { + Some(*i) + } else { + None + } + } + + pub fn to_u64(&self) -> Option { + if let ValueView::U64(u) = self { + Some(*u) + } else { + None + } + } + + pub fn to_f64(&self) -> Option { + if let ValueView::F64(f) = self { + Some(*f) + } else { + None + } + } + + pub fn to_i128(&self) -> Option { + if let ValueView::I128(i) = self { + Some(*i) + } else { + None + } + } + + pub fn to_u128(&self) -> Option { + if let ValueView::U128(u) = self { + Some(*u) + } else { + None + } + } + + pub fn to_char(&self) -> Option { + if let ValueView::Char(c) = self { + Some(*c) + } else { + None + } + } + + pub fn to_str(&self) -> Option<&'a str> { + if let ValueView::BorrowedStr(s) = self { + Some(*s) + } else if let ValueView::StaticStr(s) = self { + Some(*s) + } else { + None + } + } + + pub fn to_static_str(&self) -> Option<&'static str> { + if let ValueView::StaticStr(s) = self { + Some(*s) + } else { + None + } + } + + pub fn to_display(&self) -> Option> { + if let ValueView::Display(d) = self { + Some(*d) + } else { + None + } + } + + pub fn to_list(&self) -> Option> { + if let ValueView::List(l) = self { + Some(*l) + } else { + None + } + } + + pub fn to_map(&self) -> Option> { + if let ValueView::Map(m) = self { + Some(*m) + } else { + None + } + } + + pub fn to_debug(&self) -> Option> { + if let ValueView::Debug(d) = self { + Some(*d) + } else { + None + } } } /// A collection of key-value pairs. +#[derive(Debug, Clone, Copy)] pub struct KeyValues<'a>(KeyValuesState<'a>); +#[derive(Debug, Clone, Copy)] enum KeyValuesState<'a> { Borrowed(&'a [(Key<'a>, Value<'a>)]), Owned(&'a [(KeyOwned, ValueOwned)]), } +impl KeyValues<'_> { + pub fn empty() -> Self { + KeyValues(KeyValuesState::Borrowed(&[])) + } +} + impl<'a> KeyValues<'a> { /// Get the number of key-value pairs. pub fn len(&self) -> usize { @@ -235,22 +673,14 @@ impl<'a> KeyValues<'a> { /// Get the value for a given key. /// /// If the key appears multiple times in the source then which key is returned is undetermined. - pub fn get(&self, key: &str) -> Option> { + pub fn get(&self, key: &str) -> Option> { match &self.0 { - KeyValuesState::Borrowed(p) => p.iter().find_map(|(k, v)| { - if k.0.get() != key { - None - } else { - Some(v.clone()) - } - }), - KeyValuesState::Owned(p) => p.iter().find_map(|(k, v)| { - if k.0.as_ref() != key { - None - } else { - Some(v.by_ref()) - } - }), + KeyValuesState::Borrowed(p) => p + .iter() + .find_map(|(k, v)| if (&*k.0) != key { None } else { Some(v.view()) }), + KeyValuesState::Owned(p) => p + .iter() + .find_map(|(k, v)| if (&*k.0) != key { None } else { Some(v.view()) }), } } @@ -263,36 +693,6 @@ impl<'a> KeyValues<'a> { } } -impl fmt::Debug for KeyValues<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_list().entries(self.iter()).finish() - } -} - -impl Clone for KeyValues<'_> { - fn clone(&self) -> Self { - match &self.0 { - KeyValuesState::Borrowed(p) => KeyValues(KeyValuesState::Borrowed(p)), - KeyValuesState::Owned(p) => KeyValues(KeyValuesState::Owned(p)), - } - } -} - -impl Default for KeyValues<'_> { - fn default() -> Self { - KeyValues(KeyValuesState::Borrowed(&[])) - } -} - -impl<'a> IntoIterator for KeyValues<'a> { - type Item = (Key<'a>, Value<'a>); - type IntoIter = KeyValuesIter<'a>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - impl<'a> From<&'a [(Key<'a>, Value<'a>)]> for KeyValues<'a> { fn from(kvs: &'a [(Key<'a>, Value<'a>)]) -> Self { Self(KeyValuesState::Borrowed(kvs)) @@ -314,12 +714,12 @@ enum KeyValuesIterState<'a> { } impl<'a> Iterator for KeyValuesIter<'a> { - type Item = (Key<'a>, Value<'a>); + type Item = (KeyView<'a>, ValueView<'a>); fn next(&mut self) -> Option { match &mut self.0 { - KeyValuesIterState::Borrowed(iter) => iter.next().map(|(k, v)| (k.clone(), v.clone())), - KeyValuesIterState::Owned(iter) => iter.next().map(|(k, v)| (k.by_ref(), v.by_ref())), + KeyValuesIterState::Borrowed(iter) => iter.next().map(|(k, v)| (k.view(), v.view())), + KeyValuesIterState::Owned(iter) => iter.next().map(|(k, v)| (k.view(), v.view())), } } diff --git a/core/src/layout/plain_text.rs b/core/src/layout/plain_text.rs index 06bd96b..10f2885 100644 --- a/core/src/layout/plain_text.rs +++ b/core/src/layout/plain_text.rs @@ -18,9 +18,8 @@ use std::time::SystemTime; use crate::Diagnostic; use crate::Error; use crate::Layout; -use crate::kv::Key; -use crate::kv::Value; use crate::kv::Visitor; +use crate::kv::{KeyView, ValueView}; use crate::record::Record; /// A layout that formats log record as plain text. @@ -52,7 +51,7 @@ struct KvWriter { } impl Visitor for KvWriter { - fn visit(&mut self, key: Key, value: Value) -> Result<(), Error> { + fn visit(&mut self, key: KeyView, value: ValueView) -> Result<(), Error> { // SAFETY: write to a string always succeeds write!(&mut self.text, " {key}={value}").unwrap(); Ok(()) diff --git a/core/src/record.rs b/core/src/record.rs index 6aabce5..2d37e12 100644 --- a/core/src/record.rs +++ b/core/src/record.rs @@ -197,7 +197,7 @@ impl Default for RecordBuilder<'_> { line: None, column: None, payload: format_args!(""), - kvs: Default::default(), + kvs: KeyValues::empty(), }, } } diff --git a/core/src/str.rs b/core/src/str.rs index 7174fda..cee7f63 100644 --- a/core/src/str.rs +++ b/core/src/str.rs @@ -16,6 +16,7 @@ use std::borrow::Cow; use std::cmp::Ordering; use std::fmt; use std::hash::Hash; +use std::ops::Deref; #[derive(Clone, Copy)] pub enum RefStr<'a> { @@ -58,6 +59,14 @@ impl<'a> RefStr<'a> { } } +impl Deref for RefStr<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.get() + } +} + impl PartialEq for RefStr<'_> { fn eq(&self, other: &Self) -> bool { PartialEq::eq(self.get(), other.get()) diff --git a/diagnostics/fastrace/src/lib.rs b/diagnostics/fastrace/src/lib.rs index c5265b6..a6024d1 100644 --- a/diagnostics/fastrace/src/lib.rs +++ b/diagnostics/fastrace/src/lib.rs @@ -44,9 +44,18 @@ impl Diagnostic for FastraceDiagnostic { let trace_id = span.trace_id.to_string(); let span_id = span.span_id.to_string(); - visitor.visit(Key::new("trace_id"), Value::from_str(&trace_id))?; - visitor.visit(Key::new("span_id"), Value::from_str(&span_id))?; - visitor.visit(Key::new("sampled"), Value::from_bool(span.sampled))?; + visitor.visit( + Key::new("trace_id").view(), + Value::str(&trace_id).view(), + )?; + visitor.visit( + Key::new("span_id").view(), + Value::str(&span_id).view(), + )?; + visitor.visit( + Key::new("sampled").view(), + Value::bool(span.sampled).view(), + )?; } Ok(()) @@ -58,7 +67,7 @@ mod tests { use std::collections::BTreeMap; use fastrace::Span; - use logforth_core::kv::ValueOwned; + use logforth_core::kv::{KeyView, ValueOwned, ValueView}; use super::*; @@ -67,7 +76,7 @@ mod tests { struct Collector(BTreeMap); impl Visitor for Collector { - fn visit(&mut self, key: Key<'_>, value: Value<'_>) -> Result<(), Error> { + fn visit(&mut self, key: KeyView<'_>, value: ValueView<'_>) -> Result<(), Error> { self.0.insert(key.to_string(), value.to_owned()); Ok(()) } @@ -89,7 +98,7 @@ mod tests { let span_id = map.remove("span_id").unwrap(); assert_eq!(16, span_id.to_string().len()); let sampled = map.remove("sampled").unwrap(); - assert!(sampled.by_ref().to_bool().unwrap()); + assert!(sampled.view().to_bool().unwrap()); assert!(map.is_empty()); } diff --git a/diagnostics/task-local/src/lib.rs b/diagnostics/task-local/src/lib.rs index 5fa1319..313e573 100644 --- a/diagnostics/task-local/src/lib.rs +++ b/diagnostics/task-local/src/lib.rs @@ -52,9 +52,9 @@ impl Diagnostic for TaskLocalDiagnostic { TASK_LOCAL_MAP.with(|map| { let map = map.borrow(); for (key, value) in map.iter() { - let key = Key::new_ref(key.as_str()); - let value = Value::from(value.as_str()); - visitor.visit(key, value)?; + let key = Key::borrowed(key.as_str()); + let value = Value::str(value.as_str()); + visitor.visit(key.view(), value.view())?; } Ok(()) }) diff --git a/layouts/logfmt/src/lib.rs b/layouts/logfmt/src/lib.rs index eda4239..604591b 100644 --- a/layouts/logfmt/src/lib.rs +++ b/layouts/logfmt/src/lib.rs @@ -124,13 +124,13 @@ impl Layout for LogfmtLayout { text: format!("timestamp={time:.6}"), }; - visitor.visit(Key::new("level"), level.name().into())?; - visitor.visit(Key::new("module"), target.into())?; + visitor.visit(Key::new("level").view(), level.name().into())?; + visitor.visit(Key::new("module").view(), target.into())?; visitor.visit( - Key::new("position"), - Value::from_display(&format_args!("{file}:{line}")), + Key::new("position").view(), + Value::display(&format_args!("{file}:{line}")).view(), )?; - visitor.visit(Key::new("message"), Value::from_str(message.as_ref()))?; + visitor.visit(Key::new("message"), Value::str(message.as_ref()))?; record.key_values().visit(&mut visitor)?; for d in diags { From ee101b31bf1d48359599af05449fd6808f158a03 Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 1 Jun 2026 17:49:58 +0800 Subject: [PATCH 06/19] add docs Signed-off-by: tison --- core/src/kv.rs | 120 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 2 deletions(-) diff --git a/core/src/kv.rs b/core/src/kv.rs index bc92349..488adcb 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -20,6 +20,7 @@ use std::borrow::Cow; use std::collections::{HashMap, hash_map}; use std::{fmt, slice}; +/// A visitor to walk through key-value pairs. pub trait Visitor { /// Visit a key-value pair. fn visit(&mut self, key: KeyView, value: ValueView) -> Result<(), Error>; @@ -52,6 +53,7 @@ impl<'a> Key<'a> { } impl Key<'_> { + /// Create a borrowed view of this key. pub fn view(&self) -> KeyView<'_> { KeyView(self.0) } @@ -68,12 +70,14 @@ impl fmt::Display for KeyOwned { } impl KeyOwned { + /// Create an owned key. pub fn new(k: impl Into>) -> KeyOwned { KeyOwned(k.into()) } } impl KeyOwned { + /// Create a borrowed view of this owned key. pub fn view(&self) -> KeyView<'_> { KeyView(match &self.0 { Cow::Borrowed(s) => RefStr::Static(s), @@ -82,6 +86,7 @@ impl KeyOwned { } } +/// A borrowed view of a key in a key-value pair. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct KeyView<'a>(RefStr<'a>); @@ -108,6 +113,7 @@ impl KeyView<'_> { } } +/// A value captured through its [`fmt::Debug`] representation. #[derive(Clone, Copy)] pub struct DebugValue<'a>(&'a dyn fmt::Debug); @@ -123,6 +129,7 @@ impl fmt::Display for DebugValue<'_> { } } +/// A value captured through its [`fmt::Display`] representation. #[derive(Clone, Copy)] pub struct DisplayValue<'a>(&'a dyn fmt::Display); @@ -138,6 +145,7 @@ impl fmt::Display for DisplayValue<'_> { } } +/// A borrowed view over a list value. #[derive(Debug, Clone, Copy)] pub struct ListValue<'a>(ListValueState<'a>); @@ -148,6 +156,23 @@ enum ListValueState<'a> { } impl<'a> ListValue<'a> { + /// Get the number of elements. + pub fn len(&self) -> usize { + match self.0 { + ListValueState::Borrowed(p) => p.len(), + ListValueState::Owned(p) => p.len(), + } + } + + /// Check if this is an empty list. + pub fn is_empty(&self) -> bool { + match self.0 { + ListValueState::Borrowed(p) => p.is_empty(), + ListValueState::Owned(p) => p.is_empty(), + } + } + + /// Get an iterator over the list values. pub fn iter(&self) -> ListValueIter<'a> { match self.0 { ListValueState::Borrowed(v) => ListValueIter(ListValueIterState::Borrowed(v.iter())), @@ -156,6 +181,7 @@ impl<'a> ListValue<'a> { } } +/// An iterator over list values. #[derive(Debug, Clone)] pub struct ListValueIter<'a>(ListValueIterState<'a>); @@ -183,6 +209,7 @@ impl<'a> Iterator for ListValueIter<'a> { } } +/// A borrowed view over a map value. #[derive(Debug, Clone, Copy)] pub struct MapValue<'a>(MapValueState<'a>); @@ -193,14 +220,44 @@ enum MapValueState<'a> { } impl<'a> MapValue<'a> { + /// Get the number of key-values. + pub fn len(&self) -> usize { + match self.0 { + MapValueState::Borrowed(p) => p.len(), + MapValueState::Owned(p) => p.len(), + } + } + + /// Check if there are no key-value pairs. + pub fn is_empty(&self) -> bool { + match self.0 { + MapValueState::Borrowed(p) => p.is_empty(), + MapValueState::Owned(p) => p.is_empty(), + } + } + + /// Get an iterator over the map key-value pairs. pub fn iter(&self) -> MapValueIter<'a> { match self.0 { MapValueState::Borrowed(v) => MapValueIter(MapValueIterState::Borrowed(v.iter())), MapValueState::Owned(v) => MapValueIter(MapValueIterState::Owned(v.iter())), } } + + /// Get the value for a given key. + pub fn get(&self, key: &str) -> Option> { + match &self.0 { + MapValueState::Borrowed(p) => p + .iter() + .find_map(|(k, v)| if (&*k.0) != key { None } else { Some(v.view()) }), + MapValueState::Owned(p) => p + .iter() + .find_map(|(k, v)| if (&*k.0) != key { None } else { Some(v.view()) }), + } + } } +/// An iterator over map key-value pairs. #[derive(Debug, Clone)] pub struct MapValueIter<'a>(MapValueIterState<'a>); @@ -228,6 +285,7 @@ impl<'a> Iterator for MapValueIter<'a> { } } +/// A borrowed value in a key-value pair. #[derive(Debug, Clone)] pub struct Value<'a>(ValueState<'a>); @@ -269,6 +327,7 @@ impl fmt::Debug for ValueState<'_> { } impl Value<'_> { + /// Create a borrowed view of this value. pub fn view(&self) -> ValueView<'_> { match self.0 { ValueState::None => ValueView::None, @@ -290,63 +349,78 @@ impl Value<'_> { } impl<'a> Value<'a> { + /// Create a value representing the absence of data. pub fn none() -> Value<'a> { Value(ValueState::None) } + /// Create a value from a borrowed string. pub fn str(s: &'a str) -> Self { Value(ValueState::Str(RefStr::Borrowed(s))) } + /// Create a value from a static string. pub fn static_str(s: &'static str) -> Self { Value(ValueState::Str(RefStr::Static(s))) } + /// Create a value from a boolean. pub fn bool(b: bool) -> Self { Value(ValueState::Bool(b)) } + /// Create a value from a signed 64-bit integer. pub fn i64(i: i64) -> Self { Value(ValueState::I64(i)) } + /// Create a value from an unsigned 64-bit integer. pub fn u64(u: u64) -> Self { Value(ValueState::U64(u)) } + /// Create a value from a 64-bit floating point number. pub fn f64(f: f64) -> Self { Value(ValueState::F64(f)) } + /// Create a value from a signed 128-bit integer. pub fn i128(i: i128) -> Self { Value(ValueState::I128(i)) } + /// Create a value from an unsigned 128-bit integer. pub fn u128(u: u128) -> Self { Value(ValueState::U128(u)) } + /// Create a value from a Unicode scalar value. pub fn char(c: char) -> Self { Value(ValueState::Char(c)) } + /// Create a value from a borrowed list of values. pub fn list(l: &'a [Value<'a>]) -> Self { Value(ValueState::List(l)) } + /// Create a value from a borrowed map of key-value pairs. pub fn map(m: &'a [(Key<'a>, Value<'a>)]) -> Self { Value(ValueState::Map(m)) } + /// Create a value that is formatted lazily with [`fmt::Debug`]. pub fn debug(d: &'a dyn fmt::Debug) -> Self { Value(ValueState::Debug(d)) } + /// Create a value that is formatted lazily with [`fmt::Display`]. pub fn display(d: &'a dyn fmt::Display) -> Self { Value(ValueState::Display(d)) } } +/// An owned value in a key-value pair. #[derive(Debug, Clone)] pub struct ValueOwned(ValueOwnedState); @@ -366,6 +440,7 @@ enum ValueOwnedState { } impl ValueOwned { + /// Create a borrowed view of this owned value. pub fn view(&self) -> ValueView<'_> { match &self.0 { ValueOwnedState::None => ValueView::None, @@ -385,75 +460,103 @@ impl ValueOwned { } impl ValueOwned { + /// Create an owned value representing the absence of data. pub fn none() -> ValueOwned { ValueOwned(ValueOwnedState::None) } + /// Create an owned value from a boolean. pub fn bool(b: bool) -> ValueOwned { ValueOwned(ValueOwnedState::Bool(b)) } + /// Create an owned value from a signed 64-bit integer. pub fn i64(i: i64) -> ValueOwned { ValueOwned(ValueOwnedState::I64(i)) } + /// Create an owned value from an unsigned 64-bit integer. pub fn u64(u: u64) -> ValueOwned { ValueOwned(ValueOwnedState::U64(u)) } + /// Create an owned value from a 64-bit floating point number. pub fn f64(f: f64) -> ValueOwned { ValueOwned(ValueOwnedState::F64(f)) } + /// Create an owned value from a signed 128-bit integer. pub fn i128(i: i128) -> ValueOwned { ValueOwned(ValueOwnedState::I128(i)) } + /// Create an owned value from an unsigned 128-bit integer. pub fn u128(u: u128) -> ValueOwned { ValueOwned(ValueOwnedState::U128(u)) } + /// Create an owned value from a Unicode scalar value. pub fn char(c: char) -> ValueOwned { ValueOwned(ValueOwnedState::Char(c)) } + /// Create an owned value from a string. pub fn str(s: impl Into>) -> ValueOwned { ValueOwned(ValueOwnedState::Str(s.into())) } + /// Create an owned value from a list of owned values. pub fn list(l: impl IntoIterator) -> ValueOwned { ValueOwned(ValueOwnedState::List(Box::new(l.into_iter().collect()))) } + /// Create an owned value from a map of owned key-value pairs. pub fn map(m: impl IntoIterator) -> ValueOwned { ValueOwned(ValueOwnedState::Map(Box::new(m.into_iter().collect()))) } + /// Create an owned list value from a vector. pub fn from_vec(v: Vec) -> ValueOwned { ValueOwned(ValueOwnedState::List(Box::new(v))) } + /// Create an owned map value from a hash map. pub fn from_hash_map(m: HashMap) -> ValueOwned { ValueOwned(ValueOwnedState::Map(Box::new(m))) } } #[non_exhaustive] +/// A borrowed view of a value. #[derive(Debug, Clone)] pub enum ValueView<'a> { + /// The absence of a value. None, + /// A borrowed string value. BorrowedStr(&'a str), + /// A static string value. StaticStr(&'static str), + /// A boolean value. Bool(bool), + /// A signed 64-bit integer value. I64(i64), + /// An unsigned 64-bit integer value. U64(u64), + /// A 64-bit floating point value. F64(f64), + /// A signed 128-bit integer value. I128(i128), + /// An unsigned 128-bit integer value. U128(u128), + /// A Unicode scalar value. Char(char), + /// A list value. List(ListValue<'a>), + /// A map value. Map(MapValue<'a>), + /// A lazily debug-formatted value. Debug(DebugValue<'a>), + /// A lazily display-formatted value. Display(DisplayValue<'a>), } @@ -491,6 +594,7 @@ impl fmt::Display for ValueView<'_> { } impl ValueView<'_> { + /// Convert this view into an owned value. pub fn to_owned(&self) -> ValueOwned { match &self { ValueView::None => ValueOwned(ValueOwnedState::None), @@ -522,6 +626,7 @@ impl ValueView<'_> { } impl<'a> ValueView<'a> { + /// Try to convert this view into a boolean. pub fn to_bool(&self) -> Option { if let ValueView::Bool(b) = self { Some(*b) @@ -530,6 +635,7 @@ impl<'a> ValueView<'a> { } } + /// Try to convert this view into a signed 64-bit integer. pub fn to_i64(&self) -> Option { if let ValueView::I64(i) = self { Some(*i) @@ -538,6 +644,7 @@ impl<'a> ValueView<'a> { } } + /// Try to convert this view into an unsigned 64-bit integer. pub fn to_u64(&self) -> Option { if let ValueView::U64(u) = self { Some(*u) @@ -546,6 +653,7 @@ impl<'a> ValueView<'a> { } } + /// Try to convert this view into a 64-bit floating point number. pub fn to_f64(&self) -> Option { if let ValueView::F64(f) = self { Some(*f) @@ -554,6 +662,7 @@ impl<'a> ValueView<'a> { } } + /// Try to convert this view into a signed 128-bit integer. pub fn to_i128(&self) -> Option { if let ValueView::I128(i) = self { Some(*i) @@ -562,6 +671,7 @@ impl<'a> ValueView<'a> { } } + /// Try to convert this view into an unsigned 128-bit integer. pub fn to_u128(&self) -> Option { if let ValueView::U128(u) = self { Some(*u) @@ -570,6 +680,7 @@ impl<'a> ValueView<'a> { } } + /// Try to convert this view into a Unicode scalar value. pub fn to_char(&self) -> Option { if let ValueView::Char(c) = self { Some(*c) @@ -578,6 +689,7 @@ impl<'a> ValueView<'a> { } } + /// Try to convert this view into a string slice. pub fn to_str(&self) -> Option<&'a str> { if let ValueView::BorrowedStr(s) = self { Some(*s) @@ -588,6 +700,7 @@ impl<'a> ValueView<'a> { } } + /// Try to convert this view into a static string slice. pub fn to_static_str(&self) -> Option<&'static str> { if let ValueView::StaticStr(s) = self { Some(*s) @@ -596,6 +709,7 @@ impl<'a> ValueView<'a> { } } + /// Try to convert this view into a display-formatted value. pub fn to_display(&self) -> Option> { if let ValueView::Display(d) = self { Some(*d) @@ -604,6 +718,7 @@ impl<'a> ValueView<'a> { } } + /// Try to convert this view into a list value. pub fn to_list(&self) -> Option> { if let ValueView::List(l) = self { Some(*l) @@ -612,6 +727,7 @@ impl<'a> ValueView<'a> { } } + /// Try to convert this view into a map value. pub fn to_map(&self) -> Option> { if let ValueView::Map(m) = self { Some(*m) @@ -620,6 +736,7 @@ impl<'a> ValueView<'a> { } } + /// Try to convert this view into a debug-formatted value. pub fn to_debug(&self) -> Option> { if let ValueView::Debug(d) = self { Some(*d) @@ -640,6 +757,7 @@ enum KeyValuesState<'a> { } impl KeyValues<'_> { + /// Create an empty key-value collection. pub fn empty() -> Self { KeyValues(KeyValuesState::Borrowed(&[])) } @@ -671,8 +789,6 @@ impl<'a> KeyValues<'a> { } /// Get the value for a given key. - /// - /// If the key appears multiple times in the source then which key is returned is undetermined. pub fn get(&self, key: &str) -> Option> { match &self.0 { KeyValuesState::Borrowed(p) => p From 457e4f823d4f4bc948e28884be365d6f327f662d Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 1 Jun 2026 18:12:58 +0800 Subject: [PATCH 07/19] fixup Signed-off-by: tison --- appenders/async/src/append.rs | 2 +- appenders/fastrace/src/lib.rs | 15 +++-- appenders/journald/src/field.rs | 4 +- appenders/journald/src/lib.rs | 4 +- bridges/log/Cargo.toml | 2 +- bridges/log/src/lib.rs | 1 - core/src/kv.rs | 73 ++++++++++++++++++++++++- diagnostics/fastrace/src/lib.rs | 19 ++----- layouts/google-cloud-logging/src/lib.rs | 4 +- layouts/json/src/lib.rs | 4 +- layouts/text/src/lib.rs | 4 +- 11 files changed, 99 insertions(+), 33 deletions(-) diff --git a/appenders/async/src/append.rs b/appenders/async/src/append.rs index a800a7c..29f4e54 100644 --- a/appenders/async/src/append.rs +++ b/appenders/async/src/append.rs @@ -159,7 +159,7 @@ impl AsyncBuilder { struct DiagnosticCollector<'a>(&'a mut Vec<(kv::KeyOwned, kv::ValueOwned)>); impl<'a> Visitor for DiagnosticCollector<'a> { - fn visit(&mut self, key: kv::Key, value: kv::Value) -> Result<(), Error> { + fn visit(&mut self, key: kv::KeyView, value: kv::ValueView) -> Result<(), Error> { self.0.push((key.to_owned(), value.to_owned())); Ok(()) } diff --git a/appenders/fastrace/src/lib.rs b/appenders/fastrace/src/lib.rs index b02bf27..270a0ee 100644 --- a/appenders/fastrace/src/lib.rs +++ b/appenders/fastrace/src/lib.rs @@ -23,9 +23,8 @@ use jiff::Zoned; use logforth_core::Diagnostic; use logforth_core::Error; use logforth_core::append::Append; -use logforth_core::kv::Key; -use logforth_core::kv::Value; use logforth_core::kv::Visitor; +use logforth_core::kv::{KeyView, ValueView}; use logforth_core::record::Record; /// An appender that adds log records to fastrace as an event associated to the current span. @@ -87,12 +86,18 @@ impl Append for FastraceEvent { } struct KvCollector { - kv: Vec<(String, String)>, + kv: Vec<(Cow<'static, str>, Cow<'static, str>)>, } impl Visitor for KvCollector { - fn visit(&mut self, key: Key, value: Value) -> Result<(), Error> { - self.kv.push((key.to_string(), value.to_string())); + fn visit(&mut self, key: KeyView, value: ValueView) -> Result<(), Error> { + let k = key.to_cow(); + let v = if let Some(s) = value.to_static_str() { + Cow::Borrowed(s) + } else { + Cow::Owned(value.to_string()) + }; + self.kv.push((k, v)); Ok(()) } } diff --git a/appenders/journald/src/field.rs b/appenders/journald/src/field.rs index b786fd9..9bc3718 100644 --- a/appenders/journald/src/field.rs +++ b/appenders/journald/src/field.rs @@ -18,7 +18,7 @@ use std::io::Write; -use logforth_core::kv::Value; +use logforth_core::kv::ValueView; pub(super) enum FieldName<'a> { WellFormed(&'a str), @@ -90,7 +90,7 @@ impl PutAsFieldValue for std::fmt::Arguments<'_> { } } -impl PutAsFieldValue for Value<'_> { +impl PutAsFieldValue for ValueView<'_> { fn put_field_value(self, buffer: &mut Vec) { // SAFETY: no more than an allocate-less version // buffer.extend_from_slice(format!("{}", self)) diff --git a/appenders/journald/src/lib.rs b/appenders/journald/src/lib.rs index ffd3af5..48de0dc 100644 --- a/appenders/journald/src/lib.rs +++ b/appenders/journald/src/lib.rs @@ -24,7 +24,7 @@ use std::os::unix::net::UnixDatagram; use logforth_core::Append; use logforth_core::Diagnostic; use logforth_core::Error; -use logforth_core::kv::Key; +use logforth_core::kv::{Key, KeyView, ValueView}; use logforth_core::kv::Value; use logforth_core::kv::Visitor; use logforth_core::record::Level; @@ -237,7 +237,7 @@ impl Journald { struct WriteKeyValues<'a>(&'a mut Vec); impl Visitor for WriteKeyValues<'_> { - fn visit(&mut self, key: Key, value: Value) -> Result<(), Error> { + fn visit(&mut self, key: KeyView, value: ValueView) -> Result<(), Error> { let key = key.as_str(); field::put_field_length_encoded(self.0, field::FieldName::WriteEscaped(key), value); Ok(()) diff --git a/bridges/log/Cargo.toml b/bridges/log/Cargo.toml index a21408e..c6a8df5 100644 --- a/bridges/log/Cargo.toml +++ b/bridges/log/Cargo.toml @@ -32,7 +32,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -log = { workspace = true } +log = { workspace = true, features = ["kv"] } logforth-core = { workspace = true } [lints] diff --git a/bridges/log/src/lib.rs b/bridges/log/src/lib.rs index 0bd13e6..d0283cd 100644 --- a/bridges/log/src/lib.rs +++ b/bridges/log/src/lib.rs @@ -132,7 +132,6 @@ fn forward_log(logger: &Logger, record: &Record) { let mut new_kvs = Vec::with_capacity(kvs.len()); for (k, v) in kvs.iter() { - v. new_kvs.push((Key::borrowed(k.as_str()), Value::from_sval2(v))); } builder = builder.key_values(new_kvs.as_slice()); diff --git a/core/src/kv.rs b/core/src/kv.rs index 488adcb..e78d4cc 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -36,6 +36,13 @@ impl fmt::Display for Key<'_> { } } +#[cfg(feature = "serde")] +impl serde::Serialize for Key<'_> { + fn serialize(&self, serializer: S) -> Result { + serializer.collect_str(self) + } +} + impl Key<'static> { /// Create a new key from a static `&str`. pub const fn new(k: &'static str) -> Key<'static> { @@ -69,6 +76,13 @@ impl fmt::Display for KeyOwned { } } +#[cfg(feature = "serde")] +impl serde::Serialize for KeyOwned { + fn serialize(&self, serializer: S) -> Result { + serializer.collect_str(self) + } +} + impl KeyOwned { /// Create an owned key. pub fn new(k: impl Into>) -> KeyOwned { @@ -96,6 +110,13 @@ impl fmt::Display for KeyView<'_> { } } +#[cfg(feature = "serde")] +impl serde::Serialize for KeyView<'_> { + fn serialize(&self, serializer: S) -> Result { + serializer.collect_str(self) + } +} + impl KeyView<'_> { /// Convert to an owned key. pub fn to_owned(&self) -> KeyOwned { @@ -155,6 +176,13 @@ enum ListValueState<'a> { Owned(&'a [ValueOwned]), } +#[cfg(feature = "serde")] +impl serde::Serialize for ListValue<'_> { + fn serialize(&self, serializer: S) -> Result { + serializer.collect_seq(self.iter()) + } +} + impl<'a> ListValue<'a> { /// Get the number of elements. pub fn len(&self) -> usize { @@ -219,6 +247,13 @@ enum MapValueState<'a> { Owned(&'a HashMap), } +#[cfg(feature = "serde")] +impl serde::Serialize for MapValue<'_> { + fn serialize(&self, serializer: S) -> Result { + serializer.collect_map(self.iter()) + } +} + impl<'a> MapValue<'a> { /// Get the number of key-values. pub fn len(&self) -> usize { @@ -326,6 +361,13 @@ impl fmt::Debug for ValueState<'_> { } } +#[cfg(feature = "serde")] +impl serde::Serialize for Value<'_> { + fn serialize(&self, serializer: S) -> Result { + self.view().serialize(serializer) + } +} + impl Value<'_> { /// Create a borrowed view of this value. pub fn view(&self) -> ValueView<'_> { @@ -439,6 +481,13 @@ enum ValueOwnedState { Map(Box>), } +#[cfg(feature = "serde")] +impl serde::Serialize for ValueOwned { + fn serialize(&self, serializer: S) -> Result { + self.view().serialize(serializer) + } +} + impl ValueOwned { /// Create a borrowed view of this owned value. pub fn view(&self) -> ValueView<'_> { @@ -526,9 +575,9 @@ impl ValueOwned { } } -#[non_exhaustive] /// A borrowed view of a value. #[derive(Debug, Clone)] +#[non_exhaustive] pub enum ValueView<'a> { /// The absence of a value. None, @@ -593,6 +642,28 @@ impl fmt::Display for ValueView<'_> { } } +#[cfg(feature = "serde")] +impl serde::Serialize for ValueView<'_> { + fn serialize(&self, serializer: S) -> Result { + match &self { + ValueView::None => serializer.serialize_none(), + ValueView::BorrowedStr(v) => serializer.serialize_str(v), + ValueView::StaticStr(v) => serializer.serialize_str(v), + ValueView::Bool(v) => serializer.serialize_bool(*v), + ValueView::I64(v) => serializer.serialize_i64(*v), + ValueView::U64(v) => serializer.serialize_u64(*v), + ValueView::F64(v) => serializer.serialize_f64(*v), + ValueView::I128(v) => serializer.serialize_i128(*v), + ValueView::U128(v) => serializer.serialize_u128(*v), + ValueView::Char(v) => serializer.serialize_char(*v), + ValueView::List(v) => v.serialize(serializer), + ValueView::Map(v) => v.serialize(serializer), + ValueView::Debug(v) => serializer.collect_str(v), + ValueView::Display(v) => serializer.collect_str(v), + } + } +} + impl ValueView<'_> { /// Convert this view into an owned value. pub fn to_owned(&self) -> ValueOwned { diff --git a/diagnostics/fastrace/src/lib.rs b/diagnostics/fastrace/src/lib.rs index a6024d1..de7774f 100644 --- a/diagnostics/fastrace/src/lib.rs +++ b/diagnostics/fastrace/src/lib.rs @@ -44,18 +44,9 @@ impl Diagnostic for FastraceDiagnostic { let trace_id = span.trace_id.to_string(); let span_id = span.span_id.to_string(); - visitor.visit( - Key::new("trace_id").view(), - Value::str(&trace_id).view(), - )?; - visitor.visit( - Key::new("span_id").view(), - Value::str(&span_id).view(), - )?; - visitor.visit( - Key::new("sampled").view(), - Value::bool(span.sampled).view(), - )?; + visitor.visit(Key::new("trace_id").view(), Value::str(&trace_id).view())?; + visitor.visit(Key::new("span_id").view(), Value::str(&span_id).view())?; + visitor.visit(Key::new("sampled").view(), Value::bool(span.sampled).view())?; } Ok(()) @@ -94,9 +85,9 @@ mod tests { }; let trace_id = map.remove("trace_id").unwrap(); - assert_eq!(32, trace_id.to_string().len()); + assert_eq!(32, trace_id.view().to_string().len()); let span_id = map.remove("span_id").unwrap(); - assert_eq!(16, span_id.to_string().len()); + assert_eq!(16, span_id.view().to_string().len()); let sampled = map.remove("sampled").unwrap(); assert!(sampled.view().to_bool().unwrap()); diff --git a/layouts/google-cloud-logging/src/lib.rs b/layouts/google-cloud-logging/src/lib.rs index 65983ed..7aca9db 100644 --- a/layouts/google-cloud-logging/src/lib.rs +++ b/layouts/google-cloud-logging/src/lib.rs @@ -22,7 +22,7 @@ use std::collections::BTreeSet; use logforth_core::Diagnostic; use logforth_core::Error; -use logforth_core::kv::Key; +use logforth_core::kv::{Key, KeyView, ValueView}; use logforth_core::kv::Value; use logforth_core::kv::Visitor; use logforth_core::layout::Layout; @@ -132,7 +132,7 @@ struct KvCollector<'a> { } impl Visitor for KvCollector<'_> { - fn visit(&mut self, key: Key, value: Value) -> Result<(), Error> { + fn visit(&mut self, key: KeyView, value: ValueView) -> Result<(), Error> { let key = key.as_str(); if let Some(trace_project_id) = self.layout.trace_project_id.as_ref() { diff --git a/layouts/json/src/lib.rs b/layouts/json/src/lib.rs index 2346aac..476e347 100644 --- a/layouts/json/src/lib.rs +++ b/layouts/json/src/lib.rs @@ -23,7 +23,7 @@ use jiff::Timestamp; use jiff::tz::TimeZone; use logforth_core::Diagnostic; use logforth_core::Error; -use logforth_core::kv::Key; +use logforth_core::kv::{Key, KeyView, ValueView}; use logforth_core::kv::Value; use logforth_core::kv::Visitor; use logforth_core::layout::Layout; @@ -111,7 +111,7 @@ struct KvCollector<'a> { } impl Visitor for KvCollector<'_> { - fn visit(&mut self, key: Key, value: Value) -> Result<(), Error> { + fn visit(&mut self, key: KeyView, value: ValueView) -> Result<(), Error> { let key = key.to_string(); match serde_json::to_value(&value) { Ok(value) => self.kvs.insert(key, value), diff --git a/layouts/text/src/lib.rs b/layouts/text/src/lib.rs index f9ec77f..92886a8 100644 --- a/layouts/text/src/lib.rs +++ b/layouts/text/src/lib.rs @@ -29,9 +29,9 @@ use jiff::Timestamp; use jiff::tz::TimeZone; use logforth_core::Diagnostic; use logforth_core::Error; -use logforth_core::kv::Key; use logforth_core::kv::Value; use logforth_core::kv::Visitor; +use logforth_core::kv::{Key, KeyView, ValueView}; use logforth_core::layout::Layout; use logforth_core::record::Level; use logforth_core::record::Record; @@ -187,7 +187,7 @@ struct KvWriter { } impl Visitor for KvWriter { - fn visit(&mut self, key: Key, value: Value) -> Result<(), Error> { + fn visit(&mut self, key: KeyView, value: ValueView) -> Result<(), Error> { use std::fmt::Write; // SAFETY: write to a string always succeeds From 695a067125796480e4c0f840786fb0ccc6638bfd Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 1 Jun 2026 21:30:13 +0800 Subject: [PATCH 08/19] value_to_any_value Signed-off-by: tison --- appenders/opentelemetry/src/lib.rs | 70 +++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/appenders/opentelemetry/src/lib.rs b/appenders/opentelemetry/src/lib.rs index 25d2c45..d186191 100644 --- a/appenders/opentelemetry/src/lib.rs +++ b/appenders/opentelemetry/src/lib.rs @@ -18,6 +18,7 @@ #![deny(missing_docs)] use std::borrow::Cow; +use std::collections::HashMap; use std::fmt; use std::time::SystemTime; @@ -25,16 +26,14 @@ use logforth_core::Diagnostic; use logforth_core::Error; use logforth_core::Layout; use logforth_core::append::Append; -use logforth_core::kv::Key; -use logforth_core::kv::Value; -use logforth_core::kv::Visitor; +use logforth_core::kv::{KeyView, ValueView, Visitor}; use logforth_core::record::Level; use logforth_core::record::Record; -use opentelemetry::InstrumentationScope; use opentelemetry::logs::AnyValue; use opentelemetry::logs::LogRecord; use opentelemetry::logs::Logger; use opentelemetry::logs::LoggerProvider; +use opentelemetry::{InstrumentationScope, Key}; use opentelemetry_otlp::LogExporter; use opentelemetry_sdk::logs::SdkLogRecord; use opentelemetry_sdk::logs::SdkLoggerProvider; @@ -361,10 +360,67 @@ struct KvExtractor<'a> { } impl Visitor for KvExtractor<'_> { - fn visit(&mut self, key: Key, value: Value) -> Result<(), Error> { - let key = key.to_cow(); - let value = value.to_string(); + fn visit(&mut self, key: KeyView, value: ValueView) -> Result<(), Error> { + let key = key_to_key(key); + let value = value_to_any_value(value); self.record.add_attribute(key, value); Ok(()) } } + +fn key_to_key(key: KeyView) -> Key { + Key::from(key.to_cow()) +} + +fn value_to_any_value(value: ValueView) -> AnyValue { + match value { + // wait for https://github.com/open-telemetry/opentelemetry-rust/issues/3528 + ValueView::None => AnyValue::String("null".into()), + ValueView::BorrowedStr(v) => AnyValue::String(v.to_string().into()), + ValueView::StaticStr(v) => AnyValue::String(v.into()), + ValueView::Char(v) => AnyValue::String(v.to_string().into()), + ValueView::Debug(v) => AnyValue::String(v.to_string().into()), + ValueView::Display(v) => AnyValue::String(v.to_string().into()), + ValueView::Bool(v) => AnyValue::Boolean(v), + ValueView::I64(v) => AnyValue::Int(v), + ValueView::F64(v) => AnyValue::Double(v), + // the following three integer transforms follow what opentelemetry-appender-log does + // https://github.com/open-telemetry/opentelemetry-rust/blob/f7b0dd99/opentelemetry-appender-log/src/lib.rs#L259-L287 + ValueView::U64(v) => { + if let Ok(i) = i64::try_from(v) { + AnyValue::Int(i) + } else { + AnyValue::String(v.to_string().into()) + } + } + ValueView::I128(v) => { + if let Ok(i) = i64::try_from(v) { + AnyValue::Int(i) + } else { + AnyValue::String(v.to_string().into()) + } + } + ValueView::U128(v) => { + if let Ok(i) = i64::try_from(v) { + AnyValue::Int(i) + } else { + AnyValue::String(v.to_string().into()) + } + } + ValueView::List(v) => { + let mut l = Vec::new(); + for item in v.iter() { + l.push(value_to_any_value(item)); + } + AnyValue::ListAny(Box::new(l)) + } + ValueView::Map(v) => { + let mut m = HashMap::new(); + for (k, v) in v.iter() { + m.insert(key_to_key(k), value_to_any_value(v)); + } + AnyValue::Map(Box::new(m)) + } + v => AnyValue::String(v.to_string().into()), + } +} From cd4d6ca6c5c667bfef67c06fced4eefe27a9a5d5 Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 1 Jun 2026 21:58:16 +0800 Subject: [PATCH 09/19] bridge key and value Signed-off-by: tison --- appenders/fastrace/src/lib.rs | 2 +- appenders/journald/src/lib.rs | 3 +- bridges/log/src/lib.rs | 112 +++++++++++++++++++++--- core/src/record.rs | 2 +- layouts/google-cloud-logging/src/lib.rs | 3 +- layouts/json/src/lib.rs | 3 +- layouts/logfmt/src/lib.rs | 16 ++-- layouts/text/src/lib.rs | 3 +- 8 files changed, 115 insertions(+), 29 deletions(-) diff --git a/appenders/fastrace/src/lib.rs b/appenders/fastrace/src/lib.rs index 270a0ee..7d3b17a 100644 --- a/appenders/fastrace/src/lib.rs +++ b/appenders/fastrace/src/lib.rs @@ -71,7 +71,7 @@ impl Append for FastraceEvent { collector .kv .into_iter() - .map(|(k, v)| (Cow::from(k), Cow::from(v))), + .map(|(k, v)| (k, v)), ) }, )); diff --git a/appenders/journald/src/lib.rs b/appenders/journald/src/lib.rs index 48de0dc..0d6486c 100644 --- a/appenders/journald/src/lib.rs +++ b/appenders/journald/src/lib.rs @@ -24,8 +24,7 @@ use std::os::unix::net::UnixDatagram; use logforth_core::Append; use logforth_core::Diagnostic; use logforth_core::Error; -use logforth_core::kv::{Key, KeyView, ValueView}; -use logforth_core::kv::Value; +use logforth_core::kv::{KeyView, ValueView}; use logforth_core::kv::Visitor; use logforth_core::record::Level; use logforth_core::record::Record; diff --git a/bridges/log/src/lib.rs b/bridges/log/src/lib.rs index d0283cd..4501356 100644 --- a/bridges/log/src/lib.rs +++ b/bridges/log/src/lib.rs @@ -23,8 +23,6 @@ use std::sync::Arc; use log::Metadata; use log::Record; use logforth_core::Logger; -use logforth_core::kv::Key; -use logforth_core::kv::Value; use logforth_core::record::FilterCriteria; /// Adapter to use a `logforth` logger instance as a `log` crate logger. @@ -64,16 +62,6 @@ impl log::Log for LogAdapter { } } -fn level_to_level(level: log::Level) -> logforth_core::record::Level { - match level { - log::Level::Error => logforth_core::record::Level::Error, - log::Level::Warn => logforth_core::record::Level::Warn, - log::Level::Info => logforth_core::record::Level::Info, - log::Level::Debug => logforth_core::record::Level::Debug, - log::Level::Trace => logforth_core::record::Level::Trace, - } -} - fn forward_enabled(logger: &Logger, metadata: &Metadata) -> bool { let criteria = FilterCriteria::builder() .target(metadata.target()) @@ -113,7 +101,7 @@ fn forward_log(logger: &Logger, record: &Record) { let mut kvs = Vec::new(); struct KeyValueVisitor<'a, 'b> { - kvs: &'b mut Vec<(log::kv::Key<'a>, log::kv::Value<'a>)>, + kvs: &'b mut Vec<(log::kv::Key<'a>, kv::MaybeOwnedValue<'a>)>, } impl<'a, 'b> log::kv::VisitSource<'a> for KeyValueVisitor<'a, 'b> { @@ -122,6 +110,7 @@ fn forward_log(logger: &Logger, record: &Record) { key: log::kv::Key<'a>, value: log::kv::Value<'a>, ) -> Result<(), log::kv::Error> { + let value = kv::value_to_value(value); self.kvs.push((key, value)); Ok(()) } @@ -132,9 +121,104 @@ fn forward_log(logger: &Logger, record: &Record) { let mut new_kvs = Vec::with_capacity(kvs.len()); for (k, v) in kvs.iter() { - new_kvs.push((Key::borrowed(k.as_str()), Value::from_sval2(v))); + new_kvs.push((key_to_key(k), v.to_value())); } builder = builder.key_values(new_kvs.as_slice()); Logger::log(logger, &builder.build()); } + +fn level_to_level(level: log::Level) -> logforth_core::record::Level { + match level { + log::Level::Error => logforth_core::record::Level::Error, + log::Level::Warn => logforth_core::record::Level::Warn, + log::Level::Info => logforth_core::record::Level::Info, + log::Level::Debug => logforth_core::record::Level::Debug, + log::Level::Trace => logforth_core::record::Level::Trace, + } +} + +fn key_to_key<'a>(key: &'a log::kv::Key<'a>) -> logforth_core::kv::Key<'a> { + logforth_core::kv::Key::borrowed(key.as_str()) +} + +mod kv { + pub(super) enum MaybeOwnedValue<'a> { + Borrowed(logforth_core::kv::Value<'a>), + Owned(String), + } + + impl MaybeOwnedValue<'_> { + pub(super) fn to_value(&self) -> logforth_core::kv::Value<'_> { + match self { + MaybeOwnedValue::Borrowed(v) => v.clone(), + MaybeOwnedValue::Owned(s) => logforth_core::kv::Value::str(s.as_str()), + } + } + } + + pub(super) fn value_to_value(value: log::kv::Value) -> MaybeOwnedValue { + struct ValueVisitor<'a>(MaybeOwnedValue<'a>); + + impl<'a> log::kv::VisitValue<'a> for ValueVisitor<'a> { + fn visit_any(&mut self, value: log::kv::Value) -> Result<(), log::kv::Error> { + self.0 = MaybeOwnedValue::Owned(value.to_string()); + Ok(()) + } + + fn visit_null(&mut self) -> Result<(), log::kv::Error> { + self.0 = MaybeOwnedValue::Borrowed(logforth_core::kv::Value::none()); + Ok(()) + } + + fn visit_u64(&mut self, value: u64) -> Result<(), log::kv::Error> { + self.0 = MaybeOwnedValue::Borrowed(logforth_core::kv::Value::u64(value)); + Ok(()) + } + + fn visit_i64(&mut self, value: i64) -> Result<(), log::kv::Error> { + self.0 = MaybeOwnedValue::Borrowed(logforth_core::kv::Value::i64(value)); + Ok(()) + } + + fn visit_u128(&mut self, value: u128) -> Result<(), log::kv::Error> { + self.0 = MaybeOwnedValue::Borrowed(logforth_core::kv::Value::u128(value)); + Ok(()) + } + + fn visit_i128(&mut self, value: i128) -> Result<(), log::kv::Error> { + self.0 = MaybeOwnedValue::Borrowed(logforth_core::kv::Value::i128(value)); + Ok(()) + } + + fn visit_f64(&mut self, value: f64) -> Result<(), log::kv::Error> { + self.0 = MaybeOwnedValue::Borrowed(logforth_core::kv::Value::f64(value)); + Ok(()) + } + + fn visit_bool(&mut self, value: bool) -> Result<(), log::kv::Error> { + self.0 = MaybeOwnedValue::Borrowed(logforth_core::kv::Value::bool(value)); + Ok(()) + } + + fn visit_str(&mut self, value: &str) -> Result<(), log::kv::Error> { + self.0 = MaybeOwnedValue::Owned(value.to_string()); + Ok(()) + } + + fn visit_borrowed_str(&mut self, value: &'a str) -> Result<(), log::kv::Error> { + self.0 = MaybeOwnedValue::Borrowed(logforth_core::kv::Value::str(value)); + Ok(()) + } + + fn visit_char(&mut self, value: char) -> Result<(), log::kv::Error> { + self.0 = MaybeOwnedValue::Borrowed(logforth_core::kv::Value::char(value)); + Ok(()) + } + } + + let mut visitor = ValueVisitor(MaybeOwnedValue::Borrowed(logforth_core::kv::Value::none())); + value.visit(&mut visitor).unwrap(); + visitor.0 + } +} diff --git a/core/src/record.rs b/core/src/record.rs index 2d37e12..162956b 100644 --- a/core/src/record.rs +++ b/core/src/record.rs @@ -145,7 +145,7 @@ impl<'a> Record<'a> { line: self.line, column: self.column, payload: self.payload, - kvs: self.kvs.clone(), + kvs: self.kvs, }, } } diff --git a/layouts/google-cloud-logging/src/lib.rs b/layouts/google-cloud-logging/src/lib.rs index 7aca9db..d6b177b 100644 --- a/layouts/google-cloud-logging/src/lib.rs +++ b/layouts/google-cloud-logging/src/lib.rs @@ -22,8 +22,7 @@ use std::collections::BTreeSet; use logforth_core::Diagnostic; use logforth_core::Error; -use logforth_core::kv::{Key, KeyView, ValueView}; -use logforth_core::kv::Value; +use logforth_core::kv::{KeyView, ValueView}; use logforth_core::kv::Visitor; use logforth_core::layout::Layout; use logforth_core::record::Record; diff --git a/layouts/json/src/lib.rs b/layouts/json/src/lib.rs index 476e347..7b05ef6 100644 --- a/layouts/json/src/lib.rs +++ b/layouts/json/src/lib.rs @@ -23,8 +23,7 @@ use jiff::Timestamp; use jiff::tz::TimeZone; use logforth_core::Diagnostic; use logforth_core::Error; -use logforth_core::kv::{Key, KeyView, ValueView}; -use logforth_core::kv::Value; +use logforth_core::kv::{KeyView, ValueView}; use logforth_core::kv::Visitor; use logforth_core::layout::Layout; use logforth_core::record::Record; diff --git a/layouts/logfmt/src/lib.rs b/layouts/logfmt/src/lib.rs index 604591b..a249004 100644 --- a/layouts/logfmt/src/lib.rs +++ b/layouts/logfmt/src/lib.rs @@ -25,9 +25,9 @@ use jiff::Timestamp; use jiff::tz::TimeZone; use logforth_core::Diagnostic; use logforth_core::Error; -use logforth_core::kv::Key; use logforth_core::kv::Value; use logforth_core::kv::Visitor; +use logforth_core::kv::{Key, KeyView, ValueView}; use logforth_core::layout::Layout; use logforth_core::record::Record; @@ -78,7 +78,7 @@ struct KvFormatter { impl Visitor for KvFormatter { // The encode logic is copied from https://github.com/go-logfmt/logfmt/blob/76262ea7/encode.go. - fn visit(&mut self, key: Key, value: Value) -> Result<(), Error> { + fn visit(&mut self, key: KeyView, value: ValueView) -> Result<(), Error> { use std::fmt::Write; let key = key.as_str(); @@ -124,13 +124,19 @@ impl Layout for LogfmtLayout { text: format!("timestamp={time:.6}"), }; - visitor.visit(Key::new("level").view(), level.name().into())?; - visitor.visit(Key::new("module").view(), target.into())?; + visitor.visit( + Key::new("level").view(), + Value::static_str(level.name()).view(), + )?; + visitor.visit(Key::new("module").view(), Value::str(target).view())?; visitor.visit( Key::new("position").view(), Value::display(&format_args!("{file}:{line}")).view(), )?; - visitor.visit(Key::new("message"), Value::str(message.as_ref()))?; + visitor.visit( + Key::new("message").view(), + Value::str(message.as_ref()).view(), + )?; record.key_values().visit(&mut visitor)?; for d in diags { diff --git a/layouts/text/src/lib.rs b/layouts/text/src/lib.rs index 92886a8..f930b56 100644 --- a/layouts/text/src/lib.rs +++ b/layouts/text/src/lib.rs @@ -29,9 +29,8 @@ use jiff::Timestamp; use jiff::tz::TimeZone; use logforth_core::Diagnostic; use logforth_core::Error; -use logforth_core::kv::Value; use logforth_core::kv::Visitor; -use logforth_core::kv::{Key, KeyView, ValueView}; +use logforth_core::kv::{KeyView, ValueView}; use logforth_core::layout::Layout; use logforth_core::record::Level; use logforth_core::record::Record; From 895919a0d2616a2b163ff3bae25d999e9cc5bd92 Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 1 Jun 2026 22:00:04 +0800 Subject: [PATCH 10/19] clippy Signed-off-by: tison --- appenders/fastrace/src/lib.rs | 10 +++------- appenders/journald/src/lib.rs | 3 ++- appenders/opentelemetry/src/lib.rs | 7 +++++-- core/src/kv.rs | 11 ++++++++--- core/src/layout/plain_text.rs | 3 ++- diagnostics/fastrace/src/lib.rs | 4 +++- layouts/google-cloud-logging/src/lib.rs | 3 ++- layouts/json/src/lib.rs | 3 ++- layouts/logfmt/src/lib.rs | 4 +++- layouts/text/src/lib.rs | 3 ++- 10 files changed, 32 insertions(+), 19 deletions(-) diff --git a/appenders/fastrace/src/lib.rs b/appenders/fastrace/src/lib.rs index 7d3b17a..b4d729c 100644 --- a/appenders/fastrace/src/lib.rs +++ b/appenders/fastrace/src/lib.rs @@ -23,8 +23,9 @@ use jiff::Zoned; use logforth_core::Diagnostic; use logforth_core::Error; use logforth_core::append::Append; +use logforth_core::kv::KeyView; +use logforth_core::kv::ValueView; use logforth_core::kv::Visitor; -use logforth_core::kv::{KeyView, ValueView}; use logforth_core::record::Record; /// An appender that adds log records to fastrace as an event associated to the current span. @@ -67,12 +68,7 @@ impl Append for FastraceEvent { (Cow::from("timestamp"), Cow::from(Zoned::now().to_string())), ] .into_iter() - .chain( - collector - .kv - .into_iter() - .map(|(k, v)| (k, v)), - ) + .chain(collector.kv) }, )); diff --git a/appenders/journald/src/lib.rs b/appenders/journald/src/lib.rs index 0d6486c..3e349e4 100644 --- a/appenders/journald/src/lib.rs +++ b/appenders/journald/src/lib.rs @@ -24,7 +24,8 @@ use std::os::unix::net::UnixDatagram; use logforth_core::Append; use logforth_core::Diagnostic; use logforth_core::Error; -use logforth_core::kv::{KeyView, ValueView}; +use logforth_core::kv::KeyView; +use logforth_core::kv::ValueView; use logforth_core::kv::Visitor; use logforth_core::record::Level; use logforth_core::record::Record; diff --git a/appenders/opentelemetry/src/lib.rs b/appenders/opentelemetry/src/lib.rs index d186191..7785fbd 100644 --- a/appenders/opentelemetry/src/lib.rs +++ b/appenders/opentelemetry/src/lib.rs @@ -26,14 +26,17 @@ use logforth_core::Diagnostic; use logforth_core::Error; use logforth_core::Layout; use logforth_core::append::Append; -use logforth_core::kv::{KeyView, ValueView, Visitor}; +use logforth_core::kv::KeyView; +use logforth_core::kv::ValueView; +use logforth_core::kv::Visitor; use logforth_core::record::Level; use logforth_core::record::Record; +use opentelemetry::InstrumentationScope; +use opentelemetry::Key; use opentelemetry::logs::AnyValue; use opentelemetry::logs::LogRecord; use opentelemetry::logs::Logger; use opentelemetry::logs::LoggerProvider; -use opentelemetry::{InstrumentationScope, Key}; use opentelemetry_otlp::LogExporter; use opentelemetry_sdk::logs::SdkLogRecord; use opentelemetry_sdk::logs::SdkLoggerProvider; diff --git a/core/src/kv.rs b/core/src/kv.rs index e78d4cc..b39eada 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -14,11 +14,14 @@ //! Key-value pairs in a log record or a diagnostic context. +use std::borrow::Cow; +use std::collections::HashMap; +use std::collections::hash_map; +use std::fmt; +use std::slice; + use crate::Error; use crate::str::RefStr; -use std::borrow::Cow; -use std::collections::{HashMap, hash_map}; -use std::{fmt, slice}; /// A visitor to walk through key-value pairs. pub trait Visitor { @@ -477,7 +480,9 @@ enum ValueOwnedState { U128(u128), Char(char), Str(Cow<'static, str>), + #[expect(clippy::box_collection)] List(Box>), + #[expect(clippy::box_collection)] Map(Box>), } diff --git a/core/src/layout/plain_text.rs b/core/src/layout/plain_text.rs index 10f2885..7a134c4 100644 --- a/core/src/layout/plain_text.rs +++ b/core/src/layout/plain_text.rs @@ -18,8 +18,9 @@ use std::time::SystemTime; use crate::Diagnostic; use crate::Error; use crate::Layout; +use crate::kv::KeyView; +use crate::kv::ValueView; use crate::kv::Visitor; -use crate::kv::{KeyView, ValueView}; use crate::record::Record; /// A layout that formats log record as plain text. diff --git a/diagnostics/fastrace/src/lib.rs b/diagnostics/fastrace/src/lib.rs index de7774f..03ab700 100644 --- a/diagnostics/fastrace/src/lib.rs +++ b/diagnostics/fastrace/src/lib.rs @@ -58,7 +58,9 @@ mod tests { use std::collections::BTreeMap; use fastrace::Span; - use logforth_core::kv::{KeyView, ValueOwned, ValueView}; + use logforth_core::kv::KeyView; + use logforth_core::kv::ValueOwned; + use logforth_core::kv::ValueView; use super::*; diff --git a/layouts/google-cloud-logging/src/lib.rs b/layouts/google-cloud-logging/src/lib.rs index d6b177b..9a93a50 100644 --- a/layouts/google-cloud-logging/src/lib.rs +++ b/layouts/google-cloud-logging/src/lib.rs @@ -22,7 +22,8 @@ use std::collections::BTreeSet; use logforth_core::Diagnostic; use logforth_core::Error; -use logforth_core::kv::{KeyView, ValueView}; +use logforth_core::kv::KeyView; +use logforth_core::kv::ValueView; use logforth_core::kv::Visitor; use logforth_core::layout::Layout; use logforth_core::record::Record; diff --git a/layouts/json/src/lib.rs b/layouts/json/src/lib.rs index 7b05ef6..6afa8a5 100644 --- a/layouts/json/src/lib.rs +++ b/layouts/json/src/lib.rs @@ -23,7 +23,8 @@ use jiff::Timestamp; use jiff::tz::TimeZone; use logforth_core::Diagnostic; use logforth_core::Error; -use logforth_core::kv::{KeyView, ValueView}; +use logforth_core::kv::KeyView; +use logforth_core::kv::ValueView; use logforth_core::kv::Visitor; use logforth_core::layout::Layout; use logforth_core::record::Record; diff --git a/layouts/logfmt/src/lib.rs b/layouts/logfmt/src/lib.rs index a249004..957ee2e 100644 --- a/layouts/logfmt/src/lib.rs +++ b/layouts/logfmt/src/lib.rs @@ -25,9 +25,11 @@ use jiff::Timestamp; use jiff::tz::TimeZone; use logforth_core::Diagnostic; use logforth_core::Error; +use logforth_core::kv::Key; +use logforth_core::kv::KeyView; use logforth_core::kv::Value; +use logforth_core::kv::ValueView; use logforth_core::kv::Visitor; -use logforth_core::kv::{Key, KeyView, ValueView}; use logforth_core::layout::Layout; use logforth_core::record::Record; diff --git a/layouts/text/src/lib.rs b/layouts/text/src/lib.rs index f930b56..a101973 100644 --- a/layouts/text/src/lib.rs +++ b/layouts/text/src/lib.rs @@ -29,8 +29,9 @@ use jiff::Timestamp; use jiff::tz::TimeZone; use logforth_core::Diagnostic; use logforth_core::Error; +use logforth_core::kv::KeyView; +use logforth_core::kv::ValueView; use logforth_core::kv::Visitor; -use logforth_core::kv::{KeyView, ValueView}; use logforth_core::layout::Layout; use logforth_core::record::Level; use logforth_core::record::Record; From ab1045217193abc6a8fd059e4c8ebae13f6f5ba3 Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 1 Jun 2026 22:10:58 +0800 Subject: [PATCH 11/19] bytes Signed-off-by: tison --- appenders/opentelemetry/src/lib.rs | 2 +- core/src/kv.rs | 41 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/appenders/opentelemetry/src/lib.rs b/appenders/opentelemetry/src/lib.rs index 7785fbd..1ff724f 100644 --- a/appenders/opentelemetry/src/lib.rs +++ b/appenders/opentelemetry/src/lib.rs @@ -387,7 +387,7 @@ fn value_to_any_value(value: ValueView) -> AnyValue { ValueView::Bool(v) => AnyValue::Boolean(v), ValueView::I64(v) => AnyValue::Int(v), ValueView::F64(v) => AnyValue::Double(v), - // the following three integer transforms follow what opentelemetry-appender-log does + // the following three integer transforms follow what `opentelemetry-appender-log` does: // https://github.com/open-telemetry/opentelemetry-rust/blob/f7b0dd99/opentelemetry-appender-log/src/lib.rs#L259-L287 ValueView::U64(v) => { if let Ok(i) = i64::try_from(v) { diff --git a/core/src/kv.rs b/core/src/kv.rs index b39eada..df7152b 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -338,6 +338,7 @@ enum ValueState<'a> { U128(u128), Char(char), Str(RefStr<'a>), + Bytes(&'a [u8]), List(&'a [Value<'a>]), Map(&'a [(Key<'a>, Value<'a>)]), Debug(&'a dyn fmt::Debug), @@ -356,6 +357,7 @@ impl fmt::Debug for ValueState<'_> { ValueState::U128(v) => v.fmt(f), ValueState::Char(v) => v.fmt(f), ValueState::Str(v) => v.fmt(f), + ValueState::Bytes(v) => v.fmt(f), ValueState::List(v) => v.fmt(f), ValueState::Map(v) => v.fmt(f), ValueState::Debug(v) => fmt::Debug::fmt(v, f), @@ -385,6 +387,7 @@ impl Value<'_> { ValueState::Char(c) => ValueView::Char(c), ValueState::Str(RefStr::Static(s)) => ValueView::StaticStr(s), ValueState::Str(RefStr::Borrowed(s)) => ValueView::BorrowedStr(s), + ValueState::Bytes(b) => ValueView::Bytes(b), ValueState::List(l) => ValueView::List(ListValue(ListValueState::Borrowed(l))), ValueState::Map(m) => ValueView::Map(MapValue(MapValueState::Borrowed(m))), ValueState::Debug(d) => ValueView::Debug(DebugValue(d)), @@ -409,6 +412,11 @@ impl<'a> Value<'a> { Value(ValueState::Str(RefStr::Static(s))) } + /// Create a value from a byte array. + pub fn bytes(b: &'a [u8]) -> Self { + Value(ValueState::Bytes(b)) + } + /// Create a value from a boolean. pub fn bool(b: bool) -> Self { Value(ValueState::Bool(b)) @@ -481,6 +489,8 @@ enum ValueOwnedState { Char(char), Str(Cow<'static, str>), #[expect(clippy::box_collection)] + Bytes(Box>), + #[expect(clippy::box_collection)] List(Box>), #[expect(clippy::box_collection)] Map(Box>), @@ -507,6 +517,7 @@ impl ValueOwned { ValueOwnedState::Char(c) => ValueView::Char(*c), ValueOwnedState::Str(Cow::Borrowed(s)) => ValueView::StaticStr(s), ValueOwnedState::Str(Cow::Owned(s)) => ValueView::BorrowedStr(s.as_str()), + ValueOwnedState::Bytes(b) => ValueView::Bytes(b.as_slice()), ValueOwnedState::List(l) => ValueView::List(ListValue(ListValueState::Owned(l))), ValueOwnedState::Map(m) => ValueView::Map(MapValue(MapValueState::Owned(m))), } @@ -590,6 +601,8 @@ pub enum ValueView<'a> { BorrowedStr(&'a str), /// A static string value. StaticStr(&'static str), + /// A byte array value. + Bytes(&'a [u8]), /// A boolean value. Bool(bool), /// A signed 64-bit integer value. @@ -620,6 +633,32 @@ impl fmt::Display for ValueView<'_> { ValueView::None => write!(f, "None"), ValueView::BorrowedStr(v) => v.fmt(f), ValueView::StaticStr(v) => v.fmt(f), + ValueView::Bytes(v) => { + // this follows what `bytes` does: + // https://github.com/tokio-rs/bytes/blob/2256e6dc/src/fmt/debug.rs + write!(f, "b\"")?; + for &b in v.iter() { + // https://doc.rust-lang.org/reference/tokens.html#byte-escapes + if b == b'\n' { + write!(f, "\\n")?; + } else if b == b'\r' { + write!(f, "\\r")?; + } else if b == b'\t' { + write!(f, "\\t")?; + } else if b == b'\\' || b == b'"' { + write!(f, "\\{}", b as char)?; + } else if b == b'\0' { + write!(f, "\\0")?; + // ASCII printable + } else if (0x20..0x7f).contains(&b) { + write!(f, "{}", b as char)?; + } else { + write!(f, "\\x{:02x}", b)?; + } + } + write!(f, "\"")?; + Ok(()) + } ValueView::Bool(v) => v.fmt(f), ValueView::I64(v) => v.fmt(f), ValueView::U64(v) => v.fmt(f), @@ -654,6 +693,7 @@ impl serde::Serialize for ValueView<'_> { ValueView::None => serializer.serialize_none(), ValueView::BorrowedStr(v) => serializer.serialize_str(v), ValueView::StaticStr(v) => serializer.serialize_str(v), + ValueView::Bytes(v) => serializer.serialize_bytes(v), ValueView::Bool(v) => serializer.serialize_bool(*v), ValueView::I64(v) => serializer.serialize_i64(*v), ValueView::U64(v) => serializer.serialize_u64(*v), @@ -680,6 +720,7 @@ impl ValueView<'_> { ValueView::StaticStr(s) => { ValueOwned(ValueOwnedState::Str(Cow::Owned((*s).to_string()))) } + ValueView::Bytes(b) => ValueOwned(ValueOwnedState::Bytes(Box::new(b.to_vec()))), ValueView::Bool(b) => ValueOwned(ValueOwnedState::Bool(*b)), ValueView::I64(i) => ValueOwned(ValueOwnedState::I64(*i)), ValueView::U64(u) => ValueOwned(ValueOwnedState::U64(*u)), From e792617c631118063de5715f1104c7233db3ff3c Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 1 Jun 2026 23:13:42 +0800 Subject: [PATCH 12/19] work out kvs Signed-off-by: tison --- bridges/log/Cargo.toml | 4 ++ bridges/log/src/lib.rs | 104 ++++++++++++++++++++++++++--------------- 2 files changed, 71 insertions(+), 37 deletions(-) diff --git a/bridges/log/Cargo.toml b/bridges/log/Cargo.toml index c6a8df5..8c461f0 100644 --- a/bridges/log/Cargo.toml +++ b/bridges/log/Cargo.toml @@ -31,6 +31,10 @@ rust-version.workspace = true all-features = true rustdoc-args = ["--cfg", "docsrs"] +[features] +default = [] +serde = ["log/kv_serde", "logforth-core/serde"] + [dependencies] log = { workspace = true, features = ["kv"] } logforth-core = { workspace = true } diff --git a/bridges/log/src/lib.rs b/bridges/log/src/lib.rs index 4501356..f3819fc 100644 --- a/bridges/log/src/lib.rs +++ b/bridges/log/src/lib.rs @@ -98,32 +98,9 @@ fn forward_log(logger: &Logger, record: &Record) { builder = builder.payload(*record.args()); // key-values - let mut kvs = Vec::new(); - - struct KeyValueVisitor<'a, 'b> { - kvs: &'b mut Vec<(log::kv::Key<'a>, kv::MaybeOwnedValue<'a>)>, - } - - impl<'a, 'b> log::kv::VisitSource<'a> for KeyValueVisitor<'a, 'b> { - fn visit_pair( - &mut self, - key: log::kv::Key<'a>, - value: log::kv::Value<'a>, - ) -> Result<(), log::kv::Error> { - let value = kv::value_to_value(value); - self.kvs.push((key, value)); - Ok(()) - } - } - - let mut visitor = KeyValueVisitor { kvs: &mut kvs }; - record.key_values().visit(&mut visitor).unwrap(); - - let mut new_kvs = Vec::with_capacity(kvs.len()); - for (k, v) in kvs.iter() { - new_kvs.push((key_to_key(k), v.to_value())); - } - builder = builder.key_values(new_kvs.as_slice()); + let kvs = kv::key_values_stage_one(record.key_values()); + let new_kvs = kv::key_value_stage_two(&kvs); + builder = builder.key_values(new_kvs.to_key_values()); Logger::log(logger, &builder.build()); } @@ -138,26 +115,49 @@ fn level_to_level(level: log::Level) -> logforth_core::record::Level { } } -fn key_to_key<'a>(key: &'a log::kv::Key<'a>) -> logforth_core::kv::Key<'a> { - logforth_core::kv::Key::borrowed(key.as_str()) -} - +#[cfg(not(feature = "serde"))] mod kv { - pub(super) enum MaybeOwnedValue<'a> { + pub(super) struct KeyValuesStageOne<'a> { + kvs: Vec<(KeyStageOne<'a>, ValueStageOne<'a>)>, + } + + struct KeyStageOne<'a>(log::kv::Key<'a>); + + struct ValueStageOne<'a>(MaybeOwnedValue<'a>); + + enum MaybeOwnedValue<'a> { Borrowed(logforth_core::kv::Value<'a>), Owned(String), } - impl MaybeOwnedValue<'_> { - pub(super) fn to_value(&self) -> logforth_core::kv::Value<'_> { - match self { - MaybeOwnedValue::Borrowed(v) => v.clone(), - MaybeOwnedValue::Owned(s) => logforth_core::kv::Value::str(s.as_str()), + pub(super) fn key_values_stage_one<'a>( + source: &'a dyn log::kv::Source, + ) -> KeyValuesStageOne<'a> { + let mut kvs = Vec::new(); + + struct KeyValueVisitor<'a, 'b> { + kvs: &'b mut Vec<(KeyStageOne<'a>, ValueStageOne<'a>)>, + } + + impl<'a, 'b> log::kv::VisitSource<'a> for KeyValueVisitor<'a, 'b> { + fn visit_pair( + &mut self, + key: log::kv::Key<'a>, + value: log::kv::Value<'a>, + ) -> Result<(), log::kv::Error> { + let key = KeyStageOne(key); + let value = ValueStageOne(value_to_value(value)); + self.kvs.push((key, value)); + Ok(()) } } + + let mut visitor = KeyValueVisitor { kvs: &mut kvs }; + log::kv::Source::visit(source, &mut visitor).unwrap(); + KeyValuesStageOne { kvs } } - pub(super) fn value_to_value(value: log::kv::Value) -> MaybeOwnedValue { + fn value_to_value(value: log::kv::Value) -> MaybeOwnedValue { struct ValueVisitor<'a>(MaybeOwnedValue<'a>); impl<'a> log::kv::VisitValue<'a> for ValueVisitor<'a> { @@ -221,4 +221,34 @@ mod kv { value.visit(&mut visitor).unwrap(); visitor.0 } + + pub(super) struct KeyValuesStageTwo<'a> { + kvs: Vec<(KeyStageTwo<'a>, ValueStageTwo<'a>)>, + } + + impl<'a> KeyValuesStageTwo<'a> { + pub(super) fn to_key_values(&self) -> logforth_core::kv::KeyValues<'_> { + logforth_core::kv::KeyValues::from(self.kvs.as_slice()) + } + } + + type KeyStageTwo<'a> = logforth_core::kv::Key<'a>; + + type ValueStageTwo<'a> = logforth_core::kv::Value<'a>; + + pub(super) fn key_value_stage_two<'a>(kvs: &'a KeyValuesStageOne<'a>) -> KeyValuesStageTwo<'a> { + let mut new_kvs = Vec::with_capacity(kvs.kvs.len()); + for (k, v) in &kvs.kvs { + let k = logforth_core::kv::Key::borrowed(k.0.as_str()); + let v = match &v.0 { + MaybeOwnedValue::Borrowed(v) => v.clone(), + MaybeOwnedValue::Owned(s) => logforth_core::kv::Value::str(s.as_str()), + }; + new_kvs.push((k, v)); + } + KeyValuesStageTwo { kvs: new_kvs } + } } + +#[cfg(feature = "serde")] +mod kv {} From 2883ad0386d531a1faa2f6f42ad3881545751b14 Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 1 Jun 2026 23:49:47 +0800 Subject: [PATCH 13/19] serde log kvs Signed-off-by: tison --- appenders/opentelemetry/src/lib.rs | 2 +- bridges/log/Cargo.toml | 5 +- bridges/log/src/lib.rs | 502 ++++++++++++++++++++++++++++- core/Cargo.toml | 2 + core/src/kv.rs | 5 + 5 files changed, 508 insertions(+), 8 deletions(-) diff --git a/appenders/opentelemetry/src/lib.rs b/appenders/opentelemetry/src/lib.rs index 1ff724f..6fda716 100644 --- a/appenders/opentelemetry/src/lib.rs +++ b/appenders/opentelemetry/src/lib.rs @@ -377,7 +377,7 @@ fn key_to_key(key: KeyView) -> Key { fn value_to_any_value(value: ValueView) -> AnyValue { match value { - // wait for https://github.com/open-telemetry/opentelemetry-rust/issues/3528 + // TODO(@tisonkun): see https://github.com/open-telemetry/opentelemetry-rust/issues/3528 ValueView::None => AnyValue::String("null".into()), ValueView::BorrowedStr(v) => AnyValue::String(v.to_string().into()), ValueView::StaticStr(v) => AnyValue::String(v.into()), diff --git a/bridges/log/Cargo.toml b/bridges/log/Cargo.toml index 8c461f0..407b31b 100644 --- a/bridges/log/Cargo.toml +++ b/bridges/log/Cargo.toml @@ -33,11 +33,14 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] -serde = ["log/kv_serde", "logforth-core/serde"] +serde = ["dep:serde", "log/kv_serde", "logforth-core/serde"] [dependencies] log = { workspace = true, features = ["kv"] } logforth-core = { workspace = true } +# Optional dependencies +serde = { workspace = true, optional = true } + [lints] workspace = true diff --git a/bridges/log/src/lib.rs b/bridges/log/src/lib.rs index f3819fc..e10e7a3 100644 --- a/bridges/log/src/lib.rs +++ b/bridges/log/src/lib.rs @@ -98,11 +98,20 @@ fn forward_log(logger: &Logger, record: &Record) { builder = builder.payload(*record.args()); // key-values - let kvs = kv::key_values_stage_one(record.key_values()); - let new_kvs = kv::key_value_stage_two(&kvs); - builder = builder.key_values(new_kvs.to_key_values()); + #[cfg(not(feature = "serde"))] + { + let kvs = kv::key_values_stage_one(record.key_values()); + let new_kvs = kv::key_value_stage_two(&kvs); + builder = builder.key_values(new_kvs.to_key_values()); + Logger::log(logger, &builder.build()); + } - Logger::log(logger, &builder.build()); + #[cfg(feature = "serde")] + { + let kvs = kv::key_values(record.key_values()); + builder = builder.key_values(kvs.to_key_values()); + Logger::log(logger, &builder.build()); + } } fn level_to_level(level: log::Level) -> logforth_core::record::Level { @@ -133,7 +142,7 @@ mod kv { pub(super) fn key_values_stage_one<'a>( source: &'a dyn log::kv::Source, ) -> KeyValuesStageOne<'a> { - let mut kvs = Vec::new(); + let mut kvs = Vec::with_capacity(log::kv::Source::key_values(source)); struct KeyValueVisitor<'a, 'b> { kvs: &'b mut Vec<(KeyStageOne<'a>, ValueStageOne<'a>)>, @@ -251,4 +260,485 @@ mod kv { } #[cfg(feature = "serde")] -mod kv {} +mod kv { + use logforth_core::kv::{KeyOwned, ValueOwned, ValueView}; + use std::collections::HashMap; + use std::fmt; + use std::marker::PhantomData; + + pub(super) struct KeyValues<'a> { + kvs: Vec<(KeyOwned, ValueOwned)>, + p: PhantomData<&'a ()>, + } + + impl<'a> KeyValues<'a> { + pub(super) fn to_key_values(&self) -> logforth_core::kv::KeyValues<'_> { + logforth_core::kv::KeyValues::from(self.kvs.as_slice()) + } + } + + pub(super) fn key_values<'a>(source: &'a dyn log::kv::Source) -> KeyValues<'a> { + struct KeyValueVisitor(Vec<(KeyOwned, ValueOwned)>); + + impl<'a> log::kv::VisitSource<'a> for KeyValueVisitor { + fn visit_pair( + &mut self, + key: log::kv::Key<'a>, + value: log::kv::Value<'a>, + ) -> Result<(), log::kv::Error> { + // TODO(@tisonkun): see https://github.com/rust-lang/log/pull/727 + let key = KeyOwned::new(key.to_string()); + if let Some(value) = value_to_value(value) { + self.0.push((key, value)); + } + Ok(()) + } + } + + let mut visitor = KeyValueVisitor(Vec::with_capacity(log::kv::Source::count(source))); + log::kv::Source::visit(source, &mut visitor).unwrap(); + + KeyValues { + kvs: visitor.0, + p: PhantomData, + } + } + + // this is derived from `opentelemetry-appender-log`'s serde impl: + // https://github.com/open-telemetry/opentelemetry-rust/blob/f7b0dd99/opentelemetry-appender-log/src/lib.rs#L304-L763 + fn value_to_value(value: impl serde::Serialize) -> Option { + value.serialize(ValueSerializer).ok()? + } + + struct ValueSerializer; + + struct ValueSerializeSeq { + value: Vec, + } + + struct ValueSerializeTuple { + value: Vec, + } + + struct ValueSerializeTupleStruct { + value: Vec, + } + + struct ValueSerializeMap { + key: Option, + value: HashMap, + } + + struct ValueSerializeStruct { + value: HashMap, + } + + struct ValueSerializeTupleVariant { + variant: &'static str, + value: Vec, + } + + struct ValueSerializeStructVariant { + variant: &'static str, + value: HashMap, + } + + #[derive(Debug)] + struct ValueError(String); + + impl fmt::Display for ValueError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } + } + + impl serde::ser::Error for ValueError { + fn custom(msg: T) -> Self + where + T: fmt::Display, + { + ValueError(msg.to_string()) + } + } + + impl std::error::Error for ValueError {} + + impl serde::Serializer for ValueSerializer { + type Ok = Option; + + type Error = ValueError; + + type SerializeSeq = ValueSerializeSeq; + + type SerializeTuple = ValueSerializeTuple; + + type SerializeTupleStruct = ValueSerializeTupleStruct; + + type SerializeTupleVariant = ValueSerializeTupleVariant; + + type SerializeMap = ValueSerializeMap; + + type SerializeStruct = ValueSerializeStruct; + + type SerializeStructVariant = ValueSerializeStructVariant; + + fn serialize_bool(self, v: bool) -> Result { + Ok(Some(ValueOwned::bool(v))) + } + + fn serialize_i8(self, v: i8) -> Result { + self.serialize_i64(v as i64) + } + + fn serialize_i16(self, v: i16) -> Result { + self.serialize_i64(v as i64) + } + + fn serialize_i32(self, v: i32) -> Result { + self.serialize_i64(v as i64) + } + + fn serialize_i64(self, v: i64) -> Result { + Ok(Some(ValueOwned::i64(v))) + } + + fn serialize_i128(self, v: i128) -> Result { + if let Ok(v) = v.try_into() { + self.serialize_i64(v) + } else { + self.collect_str(&v) + } + } + + fn serialize_u8(self, v: u8) -> Result { + self.serialize_u64(v as u64) + } + + fn serialize_u16(self, v: u16) -> Result { + self.serialize_u64(v as u64) + } + + fn serialize_u32(self, v: u32) -> Result { + self.serialize_u64(v as u64) + } + + fn serialize_u64(self, v: u64) -> Result { + Ok(Some(ValueOwned::u64(v))) + } + + fn serialize_u128(self, v: u128) -> Result { + if let Ok(v) = v.try_into() { + self.serialize_u64(v) + } else { + self.collect_str(&v) + } + } + + fn serialize_f32(self, v: f32) -> Result { + self.serialize_f64(v as f64) + } + + fn serialize_f64(self, v: f64) -> Result { + Ok(Some(ValueOwned::f64(v))) + } + + fn serialize_char(self, v: char) -> Result { + Ok(Some(ValueOwned::char(v))) + } + + fn serialize_str(self, v: &str) -> Result { + Ok(Some(ValueOwned::str(v.to_string()))) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + Ok(Some(ValueOwned::bytes(v.to_vec()))) + } + + fn serialize_none(self) -> Result { + Ok(None) + } + + fn serialize_some( + self, + value: &T, + ) -> Result { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + Ok(None) + } + + fn serialize_unit_struct(self, name: &'static str) -> Result { + Ok(Some(ValueOwned::str(name))) + } + + fn serialize_unit_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + ) -> Result { + Ok(Some(ValueOwned::str(variant))) + } + + fn serialize_newtype_struct( + self, + _: &'static str, + value: &T, + ) -> Result { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + value: &T, + ) -> Result { + let mut map = self.serialize_map(Some(1))?; + serde::ser::SerializeMap::serialize_entry(&mut map, variant, value)?; + serde::ser::SerializeMap::end(map) + } + + fn serialize_seq(self, _: Option) -> Result { + Ok(ValueSerializeSeq { value: vec![] }) + } + + fn serialize_tuple(self, _: usize) -> Result { + Ok(ValueSerializeTuple { value: vec![] }) + } + + fn serialize_tuple_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + Ok(ValueSerializeTupleStruct { value: vec![] }) + } + + fn serialize_tuple_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + _: usize, + ) -> Result { + Ok(ValueSerializeTupleVariant { + variant, + value: vec![], + }) + } + + fn serialize_map(self, _: Option) -> Result { + Ok(ValueSerializeMap { + key: None, + value: HashMap::new(), + }) + } + + fn serialize_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + Ok(ValueSerializeStruct { + value: HashMap::new(), + }) + } + + fn serialize_struct_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + _: usize, + ) -> Result { + Ok(ValueSerializeStructVariant { + variant, + value: HashMap::new(), + }) + } + } + + impl serde::ser::SerializeSeq for ValueSerializeSeq { + type Ok = Option; + + type Error = ValueError; + + fn serialize_element( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + if let Some(value) = value.serialize(ValueSerializer)? { + self.value.push(value); + } + + Ok(()) + } + + fn end(self) -> Result { + Ok(Some(ValueOwned::from_vec(self.value))) + } + } + + impl serde::ser::SerializeTuple for ValueSerializeTuple { + type Ok = Option; + + type Error = ValueError; + + fn serialize_element( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + if let Some(value) = value.serialize(ValueSerializer)? { + self.value.push(value); + } + + Ok(()) + } + + fn end(self) -> Result { + Ok(Some(ValueOwned::from_vec(self.value))) + } + } + + impl serde::ser::SerializeTupleStruct for ValueSerializeTupleStruct { + type Ok = Option; + + type Error = ValueError; + + fn serialize_field( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + if let Some(value) = value.serialize(ValueSerializer)? { + self.value.push(value); + } + + Ok(()) + } + + fn end(self) -> Result { + Ok(Some(ValueOwned::from_vec(self.value))) + } + } + + impl serde::ser::SerializeTupleVariant for ValueSerializeTupleVariant { + type Ok = Option; + + type Error = ValueError; + + fn serialize_field( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + if let Some(value) = value.serialize(ValueSerializer)? { + self.value.push(value); + } + + Ok(()) + } + + fn end(self) -> Result { + Ok(Some(ValueOwned::from_hash_map({ + let mut variant = HashMap::::new(); + variant.insert(KeyOwned::new(self.variant), ValueOwned::list(self.value)); + variant + }))) + } + } + + impl serde::ser::SerializeMap for ValueSerializeMap { + type Ok = Option; + + type Error = ValueError; + + fn serialize_key( + &mut self, + key: &T, + ) -> Result<(), Self::Error> { + let key = match key.serialize(ValueSerializer)? { + Some(v) => match v.view() { + ValueView::StaticStr(s) => KeyOwned::new(s), + value => KeyOwned::new(value.to_string()), + }, + None => KeyOwned::new("None"), + }; + + self.key = Some(key); + + Ok(()) + } + + fn serialize_value( + &mut self, + value: &T, + ) -> Result<(), Self::Error> { + let key = self + .key + .take() + .ok_or_else(|| serde::ser::Error::custom("missing key"))?; + if let Some(value) = value.serialize(ValueSerializer)? { + self.value.insert(key, value); + } + Ok(()) + } + + fn end(self) -> Result { + Ok(Some(ValueOwned::from_hash_map(self.value))) + } + } + + impl serde::ser::SerializeStruct for ValueSerializeStruct { + type Ok = Option; + + type Error = ValueError; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> { + let key = KeyOwned::new(key); + if let Some(value) = value.serialize(ValueSerializer)? { + self.value.insert(key, value); + } + Ok(()) + } + + fn end(self) -> Result { + Ok(Some(ValueOwned::from_hash_map(self.value))) + } + } + + impl serde::ser::SerializeStructVariant for ValueSerializeStructVariant { + type Ok = Option; + + type Error = ValueError; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> { + let key = KeyOwned::new(key); + if let Some(value) = value.serialize(ValueSerializer)? { + self.value.insert(key, value); + } + Ok(()) + } + + fn end(self) -> Result { + Ok(Some(ValueOwned::from_hash_map({ + let mut variant = HashMap::::new(); + variant.insert( + KeyOwned::new(self.variant), + ValueOwned::from_hash_map(self.value), + ); + variant + }))) + } + } +} diff --git a/core/Cargo.toml b/core/Cargo.toml index 4473ca2..4516093 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -37,6 +37,8 @@ serde = ["dep:serde"] [dependencies] anyhow = { workspace = true } + +# Optional dependencies serde = { workspace = true, optional = true } [dev-dependencies] diff --git a/core/src/kv.rs b/core/src/kv.rs index df7152b..0acce55 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -570,6 +570,11 @@ impl ValueOwned { ValueOwned(ValueOwnedState::Str(s.into())) } + /// Create an owned value from a byte array. + pub fn bytes(b: impl Into>) -> ValueOwned { + ValueOwned(ValueOwnedState::Bytes(Box::new(b.into()))) + } + /// Create an owned value from a list of owned values. pub fn list(l: impl IntoIterator) -> ValueOwned { ValueOwned(ValueOwnedState::List(Box::new(l.into_iter().collect()))) From 3feb201dd78f33713546ee3abc068f22890809b2 Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 1 Jun 2026 23:55:08 +0800 Subject: [PATCH 14/19] clippy Signed-off-by: tison --- bridges/log/Cargo.toml | 2 +- bridges/log/src/lib.rs | 7 +++++-- examples/Cargo.toml | 4 +++- logforth/Cargo.toml | 1 + 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/bridges/log/Cargo.toml b/bridges/log/Cargo.toml index 407b31b..6e306da 100644 --- a/bridges/log/Cargo.toml +++ b/bridges/log/Cargo.toml @@ -36,7 +36,7 @@ default = [] serde = ["dep:serde", "log/kv_serde", "logforth-core/serde"] [dependencies] -log = { workspace = true, features = ["kv"] } +log = { workspace = true, features = ["kv", "std"] } logforth-core = { workspace = true } # Optional dependencies diff --git a/bridges/log/src/lib.rs b/bridges/log/src/lib.rs index e10e7a3..e46eb45 100644 --- a/bridges/log/src/lib.rs +++ b/bridges/log/src/lib.rs @@ -142,7 +142,7 @@ mod kv { pub(super) fn key_values_stage_one<'a>( source: &'a dyn log::kv::Source, ) -> KeyValuesStageOne<'a> { - let mut kvs = Vec::with_capacity(log::kv::Source::key_values(source)); + let mut kvs = Vec::with_capacity(log::kv::Source::count(source)); struct KeyValueVisitor<'a, 'b> { kvs: &'b mut Vec<(KeyStageOne<'a>, ValueStageOne<'a>)>, @@ -261,11 +261,14 @@ mod kv { #[cfg(feature = "serde")] mod kv { - use logforth_core::kv::{KeyOwned, ValueOwned, ValueView}; use std::collections::HashMap; use std::fmt; use std::marker::PhantomData; + use logforth_core::kv::KeyOwned; + use logforth_core::kv::ValueOwned; + use logforth_core::kv::ValueView; + pub(super) struct KeyValues<'a> { kvs: Vec<(KeyOwned, ValueOwned)>, p: PhantomData<&'a ()>, diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 0e595d4..98a9b87 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -28,6 +28,7 @@ starter-log = ["logforth/starter-log"] # Bridges bridge-log = ["logforth/bridge-log"] +bridge-log-serde = ["logforth/bridge-log-serde"] # Appenders append-async = ["logforth/append-async"] @@ -53,7 +54,7 @@ logforth = { workspace = true } [dev-dependencies] ctor = { workspace = true } fastrace = { workspace = true, features = ["enable"] } -log = { workspace = true, features = ["kv_serde"] } +log = { workspace = true } serde = { workspace = true } tokio = { workspace = true, features = ["full"] } @@ -140,6 +141,7 @@ name = "google_cloud_logging" path = "src/google_cloud_logging.rs" required-features = [ "starter-log", + "bridge-log-serde", "diagnostic-fastrace", "layout-google-cloud-logging", ] diff --git a/logforth/Cargo.toml b/logforth/Cargo.toml index 536e504..735e81e 100644 --- a/logforth/Cargo.toml +++ b/logforth/Cargo.toml @@ -45,6 +45,7 @@ starter-log = [ # Bridges bridge-log = ["dep:logforth-bridge-log"] +bridge-log-serde = ["bridge-log", "logforth-bridge-log/serde"] # Appenders append-async = ["dep:logforth-append-async"] From 80061a38c3c5559a05e5d2e6f1b47fefa9c460f0 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 2 Jun 2026 00:01:45 +0800 Subject: [PATCH 15/19] fixup ci Signed-off-by: tison --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 286eb00..a7627a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,7 +99,7 @@ jobs: cargo run --features="starter-log" --example single_file cargo run --features="starter-log,diagnostic-task-local" --example task_local cargo run --features="starter-log,append-async" --example asynchronous - cargo run --features="starter-log,diagnostic-fastrace,layout-google-cloud-logging" --example google_cloud_logging + cargo run --features="starter-log,bridge-log-serde,diagnostic-fastrace,layout-google-cloud-logging" --example google_cloud_logging cargo run --features="starter-log,append-fastrace,diagnostic-fastrace" --example fastrace cargo test --features="starter-log" --example testing -- --show-output From 6fbaf88cd383a52651ccda43c17a4ab40ff19646 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 2 Jun 2026 00:26:15 +0800 Subject: [PATCH 16/19] address comments Signed-off-by: tison --- bridges/log/src/lib.rs | 4 ++-- core/src/kv.rs | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/bridges/log/src/lib.rs b/bridges/log/src/lib.rs index e46eb45..b100c6d 100644 --- a/bridges/log/src/lib.rs +++ b/bridges/log/src/lib.rs @@ -458,7 +458,7 @@ mod kv { } fn serialize_none(self) -> Result { - Ok(None) + Ok(Some(ValueOwned::none())) } fn serialize_some( @@ -469,7 +469,7 @@ mod kv { } fn serialize_unit(self) -> Result { - Ok(None) + Ok(Some(ValueOwned::none())) } fn serialize_unit_struct(self, name: &'static str) -> Result { diff --git a/core/src/kv.rs b/core/src/kv.rs index 0acce55..138b520 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -722,9 +722,7 @@ impl ValueView<'_> { ValueView::BorrowedStr(s) => { ValueOwned(ValueOwnedState::Str(Cow::Owned(s.to_string()))) } - ValueView::StaticStr(s) => { - ValueOwned(ValueOwnedState::Str(Cow::Owned((*s).to_string()))) - } + ValueView::StaticStr(s) => ValueOwned(ValueOwnedState::Str(Cow::Borrowed(s))), ValueView::Bytes(b) => ValueOwned(ValueOwnedState::Bytes(Box::new(b.to_vec()))), ValueView::Bool(b) => ValueOwned(ValueOwnedState::Bool(*b)), ValueView::I64(i) => ValueOwned(ValueOwnedState::I64(*i)), From 5cd21f8a45f12ea14fba786d4e5444ac18da3715 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 2 Jun 2026 00:35:01 +0800 Subject: [PATCH 17/19] try address more Signed-off-by: tison --- bridges/log/src/lib.rs | 104 +++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 62 deletions(-) diff --git a/bridges/log/src/lib.rs b/bridges/log/src/lib.rs index b100c6d..e521db9 100644 --- a/bridges/log/src/lib.rs +++ b/bridges/log/src/lib.rs @@ -310,7 +310,7 @@ mod kv { // this is derived from `opentelemetry-appender-log`'s serde impl: // https://github.com/open-telemetry/opentelemetry-rust/blob/f7b0dd99/opentelemetry-appender-log/src/lib.rs#L304-L763 fn value_to_value(value: impl serde::Serialize) -> Option { - value.serialize(ValueSerializer).ok()? + value.serialize(ValueSerializer).ok() } struct ValueSerializer; @@ -367,7 +367,7 @@ mod kv { impl std::error::Error for ValueError {} impl serde::Serializer for ValueSerializer { - type Ok = Option; + type Ok = ValueOwned; type Error = ValueError; @@ -386,7 +386,7 @@ mod kv { type SerializeStructVariant = ValueSerializeStructVariant; fn serialize_bool(self, v: bool) -> Result { - Ok(Some(ValueOwned::bool(v))) + Ok(ValueOwned::bool(v)) } fn serialize_i8(self, v: i8) -> Result { @@ -402,7 +402,7 @@ mod kv { } fn serialize_i64(self, v: i64) -> Result { - Ok(Some(ValueOwned::i64(v))) + Ok(ValueOwned::i64(v)) } fn serialize_i128(self, v: i128) -> Result { @@ -426,7 +426,7 @@ mod kv { } fn serialize_u64(self, v: u64) -> Result { - Ok(Some(ValueOwned::u64(v))) + Ok(ValueOwned::u64(v)) } fn serialize_u128(self, v: u128) -> Result { @@ -442,23 +442,23 @@ mod kv { } fn serialize_f64(self, v: f64) -> Result { - Ok(Some(ValueOwned::f64(v))) + Ok(ValueOwned::f64(v)) } fn serialize_char(self, v: char) -> Result { - Ok(Some(ValueOwned::char(v))) + Ok(ValueOwned::char(v)) } fn serialize_str(self, v: &str) -> Result { - Ok(Some(ValueOwned::str(v.to_string()))) + Ok(ValueOwned::str(v.to_string())) } fn serialize_bytes(self, v: &[u8]) -> Result { - Ok(Some(ValueOwned::bytes(v.to_vec()))) + Ok(ValueOwned::bytes(v.to_vec())) } fn serialize_none(self) -> Result { - Ok(Some(ValueOwned::none())) + Ok(ValueOwned::none()) } fn serialize_some( @@ -469,11 +469,11 @@ mod kv { } fn serialize_unit(self) -> Result { - Ok(Some(ValueOwned::none())) + Ok(ValueOwned::none()) } fn serialize_unit_struct(self, name: &'static str) -> Result { - Ok(Some(ValueOwned::str(name))) + Ok(ValueOwned::str(name)) } fn serialize_unit_variant( @@ -482,7 +482,7 @@ mod kv { _: u32, variant: &'static str, ) -> Result { - Ok(Some(ValueOwned::str(variant))) + Ok(ValueOwned::str(variant)) } fn serialize_newtype_struct( @@ -566,7 +566,7 @@ mod kv { } impl serde::ser::SerializeSeq for ValueSerializeSeq { - type Ok = Option; + type Ok = ValueOwned; type Error = ValueError; @@ -574,20 +574,17 @@ mod kv { &mut self, value: &T, ) -> Result<(), Self::Error> { - if let Some(value) = value.serialize(ValueSerializer)? { - self.value.push(value); - } - + self.value.push(value.serialize(ValueSerializer)?); Ok(()) } fn end(self) -> Result { - Ok(Some(ValueOwned::from_vec(self.value))) + Ok(ValueOwned::from_vec(self.value)) } } impl serde::ser::SerializeTuple for ValueSerializeTuple { - type Ok = Option; + type Ok = ValueOwned; type Error = ValueError; @@ -595,20 +592,17 @@ mod kv { &mut self, value: &T, ) -> Result<(), Self::Error> { - if let Some(value) = value.serialize(ValueSerializer)? { - self.value.push(value); - } - + self.value.push(value.serialize(ValueSerializer)?); Ok(()) } fn end(self) -> Result { - Ok(Some(ValueOwned::from_vec(self.value))) + Ok(ValueOwned::from_vec(self.value)) } } impl serde::ser::SerializeTupleStruct for ValueSerializeTupleStruct { - type Ok = Option; + type Ok = ValueOwned; type Error = ValueError; @@ -616,20 +610,17 @@ mod kv { &mut self, value: &T, ) -> Result<(), Self::Error> { - if let Some(value) = value.serialize(ValueSerializer)? { - self.value.push(value); - } - + self.value.push(value.serialize(ValueSerializer)?); Ok(()) } fn end(self) -> Result { - Ok(Some(ValueOwned::from_vec(self.value))) + Ok(ValueOwned::from_vec(self.value)) } } impl serde::ser::SerializeTupleVariant for ValueSerializeTupleVariant { - type Ok = Option; + type Ok = ValueOwned; type Error = ValueError; @@ -637,24 +628,21 @@ mod kv { &mut self, value: &T, ) -> Result<(), Self::Error> { - if let Some(value) = value.serialize(ValueSerializer)? { - self.value.push(value); - } - + self.value.push(value.serialize(ValueSerializer)?); Ok(()) } fn end(self) -> Result { - Ok(Some(ValueOwned::from_hash_map({ + Ok(ValueOwned::from_hash_map({ let mut variant = HashMap::::new(); variant.insert(KeyOwned::new(self.variant), ValueOwned::list(self.value)); variant - }))) + })) } } impl serde::ser::SerializeMap for ValueSerializeMap { - type Ok = Option; + type Ok = ValueOwned; type Error = ValueError; @@ -662,16 +650,11 @@ mod kv { &mut self, key: &T, ) -> Result<(), Self::Error> { - let key = match key.serialize(ValueSerializer)? { - Some(v) => match v.view() { - ValueView::StaticStr(s) => KeyOwned::new(s), - value => KeyOwned::new(value.to_string()), - }, - None => KeyOwned::new("None"), + let key = match key.serialize(ValueSerializer)?.view() { + ValueView::StaticStr(s) => KeyOwned::new(s), + value => KeyOwned::new(value.to_string()), }; - self.key = Some(key); - Ok(()) } @@ -683,19 +666,18 @@ mod kv { .key .take() .ok_or_else(|| serde::ser::Error::custom("missing key"))?; - if let Some(value) = value.serialize(ValueSerializer)? { - self.value.insert(key, value); - } + let value = value.serialize(ValueSerializer)?; + self.value.insert(key, value); Ok(()) } fn end(self) -> Result { - Ok(Some(ValueOwned::from_hash_map(self.value))) + Ok(ValueOwned::from_hash_map(self.value)) } } impl serde::ser::SerializeStruct for ValueSerializeStruct { - type Ok = Option; + type Ok = ValueOwned; type Error = ValueError; @@ -705,19 +687,18 @@ mod kv { value: &T, ) -> Result<(), Self::Error> { let key = KeyOwned::new(key); - if let Some(value) = value.serialize(ValueSerializer)? { - self.value.insert(key, value); - } + let value = value.serialize(ValueSerializer)?; + self.value.insert(key, value); Ok(()) } fn end(self) -> Result { - Ok(Some(ValueOwned::from_hash_map(self.value))) + Ok(ValueOwned::from_hash_map(self.value)) } } impl serde::ser::SerializeStructVariant for ValueSerializeStructVariant { - type Ok = Option; + type Ok = ValueOwned; type Error = ValueError; @@ -727,21 +708,20 @@ mod kv { value: &T, ) -> Result<(), Self::Error> { let key = KeyOwned::new(key); - if let Some(value) = value.serialize(ValueSerializer)? { - self.value.insert(key, value); - } + let value = value.serialize(ValueSerializer)?; + self.value.insert(key, value); Ok(()) } fn end(self) -> Result { - Ok(Some(ValueOwned::from_hash_map({ + Ok(ValueOwned::from_hash_map({ let mut variant = HashMap::::new(); variant.insert( KeyOwned::new(self.variant), ValueOwned::from_hash_map(self.value), ); variant - }))) + })) } } } From d4ee3480acd22a1695f968bfd5d2db74a3856d0a Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 2 Jun 2026 00:39:02 +0800 Subject: [PATCH 18/19] test result Signed-off-by: tison --- core/src/kv.rs | 4 ++-- examples/src/google_cloud_logging.rs | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/src/kv.rs b/core/src/kv.rs index 138b520..87ee3eb 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -348,7 +348,7 @@ enum ValueState<'a> { impl fmt::Debug for ValueState<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ValueState::None => write!(f, "None"), + ValueState::None => write!(f, ""), ValueState::Bool(v) => v.fmt(f), ValueState::I64(v) => v.fmt(f), ValueState::U64(v) => v.fmt(f), @@ -635,7 +635,7 @@ pub enum ValueView<'a> { impl fmt::Display for ValueView<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ValueView::None => write!(f, "None"), + ValueView::None => write!(f, ""), ValueView::BorrowedStr(v) => v.fmt(f), ValueView::StaticStr(v) => v.fmt(f), ValueView::Bytes(v) => { diff --git a/examples/src/google_cloud_logging.rs b/examples/src/google_cloud_logging.rs index 75786a1..dd3d863 100644 --- a/examples/src/google_cloud_logging.rs +++ b/examples/src/google_cloud_logging.rs @@ -35,6 +35,12 @@ struct MyStructuredValue { b: NestedStructuredValue, } +#[derive(Serialize)] +struct MyEmptyStructuredValue { + #[serde(skip_serializing_if = "Vec::is_empty")] + vec: Vec<()>, +} + fn main() { logforth::starter_log::builder() .dispatch(|d| { @@ -68,5 +74,10 @@ fn main() { notLabel:serde = structured_value; "Hello label value!", ); + + let empty_value = MyEmptyStructuredValue { vec: vec![] }; + log::info!(empty_value:serde; "Hello empty value!"); + let non_empty_value = MyEmptyStructuredValue { vec: vec![()] }; + log::info!(non_empty_value:serde; "Hello non-empty value!"); } } From 68a251aa3e713b0c6e8db41f096a34c908558cf6 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 2 Jun 2026 00:50:15 +0800 Subject: [PATCH 19/19] key can be used as map key Signed-off-by: tison --- core/src/kv.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/core/src/kv.rs b/core/src/kv.rs index 87ee3eb..eb1ff5b 100644 --- a/core/src/kv.rs +++ b/core/src/kv.rs @@ -14,6 +14,7 @@ //! Key-value pairs in a log record or a diagnostic context. +use std::borrow::Borrow; use std::borrow::Cow; use std::collections::HashMap; use std::collections::hash_map; @@ -46,6 +47,12 @@ impl serde::Serialize for Key<'_> { } } +impl Borrow for Key<'_> { + fn borrow(&self) -> &str { + &self.0 + } +} + impl Key<'static> { /// Create a new key from a static `&str`. pub const fn new(k: &'static str) -> Key<'static> { @@ -73,6 +80,12 @@ impl Key<'_> { #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct KeyOwned(Cow<'static, str>); +impl Borrow for KeyOwned { + fn borrow(&self) -> &str { + &self.0 + } +} + impl fmt::Display for KeyOwned { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) @@ -107,6 +120,12 @@ impl KeyOwned { #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct KeyView<'a>(RefStr<'a>); +impl Borrow for KeyView<'_> { + fn borrow(&self) -> &str { + &self.0 + } +} + impl fmt::Display for KeyView<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) @@ -288,9 +307,7 @@ impl<'a> MapValue<'a> { MapValueState::Borrowed(p) => p .iter() .find_map(|(k, v)| if (&*k.0) != key { None } else { Some(v.view()) }), - MapValueState::Owned(p) => p - .iter() - .find_map(|(k, v)| if (&*k.0) != key { None } else { Some(v.view()) }), + MapValueState::Owned(p) => p.get(key).map(|v| v.view()), } } }