diff --git a/Cargo.toml b/Cargo.toml
index 0bef514329..792a217402 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,7 +28,7 @@ futures-util = { version = "0.3", default-features = false }
hyper = { version = "0.14", default-features = false }
http = { version = "0.2", default-features = false }
isahc = { version = "1.4", default-features = false }
-log = "0.4"
+log = "0.4.21"
once_cell = "1.13"
ordered-float = "4.0"
pin-project-lite = "0.2"
diff --git a/opentelemetry-appender-log/CHANGELOG.md b/opentelemetry-appender-log/CHANGELOG.md
index 6ecf96c7e2..dd574e51ca 100644
--- a/opentelemetry-appender-log/CHANGELOG.md
+++ b/opentelemetry-appender-log/CHANGELOG.md
@@ -2,6 +2,8 @@
## vNext
+- Add log key-values as attributes [#1628](https://github.com/open-telemetry/opentelemetry-rust/pull/1628)
+
## v0.3.0
## v0.2.0
diff --git a/opentelemetry-appender-log/Cargo.toml b/opentelemetry-appender-log/Cargo.toml
index 0ed81735f3..9a94411637 100644
--- a/opentelemetry-appender-log/Cargo.toml
+++ b/opentelemetry-appender-log/Cargo.toml
@@ -12,12 +12,17 @@ edition = "2021"
[dependencies]
opentelemetry = { version = "0.22", path = "../opentelemetry", features = ["logs"]}
-log = { workspace = true, features = ["kv_unstable", "std"]}
+log = { workspace = true, features = ["kv", "std"]}
+serde = { workspace = true, optional = true, features = ["std"] }
[features]
logs_level_enabled = ["opentelemetry/logs_level_enabled"]
+with-serde = ["log/kv_serde", "serde"]
default = ["logs_level_enabled"]
[dev-dependencies]
opentelemetry_sdk = { path = "../opentelemetry-sdk", features = [ "testing", "logs_level_enabled" ] }
+opentelemetry-stdout = { path = "../opentelemetry-stdout", features = ["logs"]}
+log = { workspace = true, features = ["kv_serde"] }
tokio = { workspace = true }
+serde = { workspace = true, features = ["std", "derive"] }
diff --git a/opentelemetry-appender-log/examples/logs-basic-in-memory.rs b/opentelemetry-appender-log/examples/logs-basic.rs
similarity index 69%
rename from opentelemetry-appender-log/examples/logs-basic-in-memory.rs
rename to opentelemetry-appender-log/examples/logs-basic.rs
index 3348c6f3cf..adc3602c2a 100644
--- a/opentelemetry-appender-log/examples/logs-basic-in-memory.rs
+++ b/opentelemetry-appender-log/examples/logs-basic.rs
@@ -1,4 +1,4 @@
-//! run with `$ cargo run --example logs-basic-in-memory
+//! run with `$ cargo run --example logs-basic`
/// This example shows how to use in_memory_exporter for logs. This uses opentelemetry-appender-log crate, which is a
/// [logging appender](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#log-appender--bridge) that bridges logs from the [log crate](https://docs.rs/log/latest/log/) to OpenTelemetry.
@@ -8,15 +8,15 @@ use log::{error, info, warn, Level};
use opentelemetry_appender_log::OpenTelemetryLogBridge;
use opentelemetry_sdk::logs::{BatchLogProcessor, LoggerProvider};
use opentelemetry_sdk::runtime;
-use opentelemetry_sdk::testing::logs::InMemoryLogsExporter;
+use opentelemetry_stdout::LogExporter;
#[tokio::main]
async fn main() {
- //Create an InMemoryLogsExporter
- let exporter: InMemoryLogsExporter = InMemoryLogsExporter::default();
+ //Create an exporter that writes to stdout
+ let exporter = LogExporter::default();
//Create a LoggerProvider and register the exporter
let logger_provider = LoggerProvider::builder()
- .with_log_processor(BatchLogProcessor::builder(exporter.clone(), runtime::Tokio).build())
+ .with_log_processor(BatchLogProcessor::builder(exporter, runtime::Tokio).build())
.build();
// Setup Log Appender for the log crate.
@@ -25,14 +25,12 @@ async fn main() {
log::set_max_level(Level::Info.to_level_filter());
// Emit logs using macros from the log crate.
- error!("hello from {}. My price is {}", "apple", 2.99);
+ let fruit = "apple";
+ let price = 2.99;
+
+ error!(fruit, price; "hello from {fruit}. My price is {price}");
warn!("warn!");
info!("test log!");
logger_provider.force_flush();
-
- let emitted_logs = exporter.get_emitted_logs().unwrap();
- for log in emitted_logs {
- println!("{:?}", log);
- }
}
diff --git a/opentelemetry-appender-log/src/lib.rs b/opentelemetry-appender-log/src/lib.rs
index c6692dc29c..137631a0ea 100644
--- a/opentelemetry-appender-log/src/lib.rs
+++ b/opentelemetry-appender-log/src/lib.rs
@@ -1,5 +1,104 @@
+//! Bridge `log` into OpenTelemetry.
+//!
+//! This library implements a log appender for the [`log`] crate using the [Logs Bridge API].
+//!
+//! # Getting Started
+//!
+//! The bridge requires configuration on both the `log` and OpenTelemetry sides.
+//!
+//! For OpenTelemetry, configure a [`LoggerProvider`] with the desired exporter:
+//!
+//! ```
+//! # #[tokio::main] async fn main() {
+//! # use opentelemetry_sdk::logs::{BatchLogProcessor, LoggerProvider};
+//! # use opentelemetry_sdk::runtime;
+//! let exporter = opentelemetry_stdout::LogExporterBuilder::default().build();
+//!
+//! let logger_provider = LoggerProvider::builder()
+//! .with_log_processor(BatchLogProcessor::builder(exporter, runtime::Tokio).build())
+//! .build();
+//! # }
+//! ```
+//!
+//! For `log`, set the global logger to an [`OpenTelemetryLogBridge`] instance using the `LoggerProvider`:
+//!
+//! ```
+//! # #[tokio::main] async fn main() {
+//! # use opentelemetry_sdk::logs::{BatchLogProcessor, LoggerProvider};
+//! # use opentelemetry_sdk::runtime;
+//! # use opentelemetry_appender_log::OpenTelemetryLogBridge;
+//! # let exporter = opentelemetry_stdout::LogExporterBuilder::default().build();
+//! # let logger_provider = LoggerProvider::builder()
+//! # .with_log_processor(BatchLogProcessor::builder(exporter, runtime::Tokio).build())
+//! # .build();
+//! let otel_log_appender = OpenTelemetryLogBridge::new(&logger_provider);
+//!
+//! log::set_boxed_logger(Box::new(otel_log_appender)).unwrap();
+//! # }
+//! ```
+//!
+//! # Mapping Log Records
+//!
+//! This section outlines how log records produced by `log` are mapped into OpenTelemetry log records.
+//! Each subsection deals with a different property on `opentelemetry::logs::LogRecord`.
+//!
+//! ## Body
+//!
+//! The body is the stringified message ([`log::Record::args`]).
+//!
+//! ## Severity
+//!
+//! The severity number and text are mapped from the [`log::Level`] ([`log::Record::level`]):
+//!
+//! | `log::Level` | Severity Text | Severity Number |
+//! | ------------ | ------------- | --------------- |
+//! | `Error` | Error | 17 |
+//! | `Warn` | Warn | 13 |
+//! | `Info` | Info | 9 |
+//! | `Debug` | Debug | 5 |
+//! | `Trace` | Trace | 1 |
+//!
+//! # Attributes
+//!
+//! Any key-values ([`log::Record::key_values`]) are converted into attributes:
+//!
+//! | Type | Result | Notes |
+//! | --------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------- |
+//! | `i8`-`i128` | [`AnyValue::Int`] | If the value is too big then it will be stringified using [`std::fmt::Display`] |
+//! | `u8`-`u128` | [`AnyValue::Int`] | If the value is too big then it will be stringified using [`std::fmt::Display`] |
+//! | `f32`-`f64` | [`AnyValue::Double`] | |
+//! | `bool` | [`AnyValue::Boolean`] | |
+//! | `str` | [`AnyValue::String`] | |
+//! | Bytes | [`AnyValue::Bytes`] | Requires the `with-serde` feature, otherwise it will be stringified using [`std::fmt::Debug`] |
+//! | `()` | - | Unit values are discared |
+//! | `Some` | Any | `Some` variants use their inner value |
+//! | `None` | - | `None` variants are discared |
+//! | Unit struct | [`AnyValue::String`] | Uses the name of the struct |
+//! | Unit variant | [`AnyValue::String`] | Uses the name of the variant |
+//! | Newtype struct | Any | Uses the inner value of the newtype |
+//! | Newtype variant | [`AnyValue::Map`] | An internally-tagged map. Requires the `with-serde` feature, otherwise it will be stringified using [`std::fmt::Debug`] |
+//! | Sequence | [`AnyValue::ListAny`] | Requires the `with-serde` feature, otherwise it will be stringified using [`std::fmt::Debug`] |
+//! | Tuple | [`AnyValue::ListAny`] | Requires the `with-serde` feature, otherwise it will be stringified using [`std::fmt::Debug`] |
+//! | Tuple struct | [`AnyValue::ListAny`] | Requires the `with-serde` feature, otherwise it will be stringified using [`std::fmt::Debug`] |
+//! | Tuple variant | [`AnyValue::Map`] | An internally-tagged map. Requires the `with-serde` feature, otherwise it will be stringified using [`std::fmt::Debug`] |
+//! | Map | [`AnyValue::Map`] | Requires the `with-serde` feature, otherwise it will be stringified using [`std::fmt::Debug`] |
+//! | Struct | [`AnyValue::Map`] | Requires the `with-serde` feature, otherwise it will be stringified using [`std::fmt::Debug`] |
+//! | Struct variant | [`AnyValue::Map`] | An internally-tagged map. Requires the `with-serde` feature, otherwise it will be stringified using [`std::fmt::Debug`] |
+//!
+//! # Feature Flags
+//!
+//! This library provides the following Cargo features:
+//!
+//! - `logs_level_enabled`: Allow users to control the log level.
+//! - `with-serde`: Support complex values as attributes without stringifying them.
+//!
+//! [Logs Bridge API]: https://opentelemetry.io/docs/specs/otel/logs/bridge-api/
+
use log::{Level, Metadata, Record};
-use opentelemetry::logs::{AnyValue, LogRecordBuilder, Logger, LoggerProvider, Severity};
+use opentelemetry::{
+ logs::{AnyValue, LogRecordBuilder, Logger, LoggerProvider, Severity},
+ Key,
+};
use std::borrow::Cow;
pub struct OpenTelemetryLogBridge
@@ -34,6 +133,7 @@ where
// Not populating ObservedTimestamp, instead relying on OpenTelemetry
// API to populate it with current time.
.with_body(AnyValue::from(record.args().to_string()))
+ .with_attributes(log_attributes(record.key_values()))
.build(),
);
}
@@ -68,13 +168,569 @@ const fn severity_of_level(level: Level) -> Severity {
}
}
-#[cfg(all(test, feature = "testing", feature = "logs"))]
+fn log_attributes(kvs: impl log::kv::Source) -> Vec<(Key, AnyValue)> {
+ struct AttributeVisitor(Vec<(Key, AnyValue)>);
+
+ impl<'kvs> log::kv::VisitSource<'kvs> for AttributeVisitor {
+ fn visit_pair(
+ &mut self,
+ key: log::kv::Key<'kvs>,
+ value: log::kv::Value<'kvs>,
+ ) -> Result<(), log::kv::Error> {
+ let key = Key::from(String::from(key.as_str()));
+
+ if let Some(value) = any_value::serialize(value) {
+ self.0.push((key, value));
+ }
+
+ Ok(())
+ }
+ }
+
+ let mut visitor = AttributeVisitor(Vec::new());
+
+ let _ = kvs.visit(&mut visitor);
+
+ visitor.0
+}
+
+#[cfg(not(feature = "with-serde"))]
+mod any_value {
+ use opentelemetry::{logs::AnyValue, StringValue};
+
+ pub(crate) fn serialize(value: log::kv::Value) -> Option {
+ struct ValueVisitor(Option);
+
+ impl<'kvs> log::kv::VisitValue<'kvs> for ValueVisitor {
+ fn visit_any(&mut self, value: log::kv::Value) -> Result<(), log::kv::Error> {
+ self.0 = Some(AnyValue::String(StringValue::from(value.to_string())));
+
+ Ok(())
+ }
+
+ fn visit_bool(&mut self, value: bool) -> Result<(), log::kv::Error> {
+ self.0 = Some(AnyValue::Boolean(value));
+
+ Ok(())
+ }
+
+ fn visit_str(&mut self, value: &str) -> Result<(), log::kv::Error> {
+ self.0 = Some(AnyValue::String(StringValue::from(value.to_owned())));
+
+ Ok(())
+ }
+
+ fn visit_i64(&mut self, value: i64) -> Result<(), log::kv::Error> {
+ self.0 = Some(AnyValue::Int(value));
+
+ Ok(())
+ }
+
+ fn visit_u64(&mut self, value: u64) -> Result<(), log::kv::Error> {
+ if let Ok(value) = value.try_into() {
+ self.visit_i64(value)
+ } else {
+ self.visit_any(log::kv::Value::from(value))
+ }
+ }
+
+ fn visit_i128(&mut self, value: i128) -> Result<(), log::kv::Error> {
+ if let Ok(value) = value.try_into() {
+ self.visit_i64(value)
+ } else {
+ self.visit_any(log::kv::Value::from(value))
+ }
+ }
+
+ fn visit_u128(&mut self, value: u128) -> Result<(), log::kv::Error> {
+ if let Ok(value) = value.try_into() {
+ self.visit_i64(value)
+ } else {
+ self.visit_any(log::kv::Value::from(value))
+ }
+ }
+
+ fn visit_f64(&mut self, value: f64) -> Result<(), log::kv::Error> {
+ self.0 = Some(AnyValue::Double(value));
+
+ Ok(())
+ }
+ }
+
+ let mut visitor = ValueVisitor(None);
+ value.visit(&mut visitor).unwrap();
+ visitor.0
+ }
+}
+
+// This could make a nice addition to the SDK itself for serializing into `AnyValue`s
+#[cfg(feature = "with-serde")]
+mod any_value {
+ use std::{collections::HashMap, fmt};
+
+ use opentelemetry::{logs::AnyValue, Key, StringValue};
+ use serde::ser::{
+ Error, Serialize, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant,
+ SerializeTuple, SerializeTupleStruct, SerializeTupleVariant, Serializer, StdError,
+ };
+
+ /// Serialize an arbitrary `serde::Serialize` into an `AnyValue`.
+ ///
+ /// This method performs the following translations when converting between `serde`'s data model and OpenTelemetry's:
+ ///
+ /// - Integers that don't fit in a `i64` are converted into strings.
+ /// - Unit types and nones are discarded (effectively treated as undefined).
+ /// - Struct and tuple variants are converted into an internally tagged map.
+ /// - Unit variants are converted into strings.
+ pub(crate) fn serialize(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 Error for ValueError {
+ fn custom(msg: T) -> Self
+ where
+ T: fmt::Display,
+ {
+ ValueError(msg.to_string())
+ }
+ }
+
+ impl StdError for ValueError {}
+
+ impl 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(AnyValue::Boolean(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(AnyValue::Int(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_i64(v as i64)
+ }
+
+ fn serialize_u16(self, v: u16) -> Result {
+ self.serialize_i64(v as i64)
+ }
+
+ fn serialize_u32(self, v: u32) -> Result {
+ self.serialize_i64(v as i64)
+ }
+
+ fn serialize_u64(self, v: u64) -> Result {
+ if let Ok(v) = v.try_into() {
+ self.serialize_i64(v)
+ } else {
+ self.collect_str(&v)
+ }
+ }
+
+ fn serialize_u128(self, v: u128) -> Result {
+ if let Ok(v) = v.try_into() {
+ self.serialize_i64(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(AnyValue::Double(v)))
+ }
+
+ fn serialize_char(self, v: char) -> Result {
+ self.collect_str(&v)
+ }
+
+ fn serialize_str(self, v: &str) -> Result {
+ Ok(Some(AnyValue::String(StringValue::from(v.to_owned()))))
+ }
+
+ fn serialize_bytes(self, v: &[u8]) -> Result {
+ Ok(Some(AnyValue::Bytes(v.to_owned())))
+ }
+
+ 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 {
+ name.serialize(self)
+ }
+
+ fn serialize_unit_variant(
+ self,
+ _: &'static str,
+ _: u32,
+ variant: &'static str,
+ ) -> Result {
+ variant.serialize(self)
+ }
+
+ 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))?;
+ map.serialize_entry(variant, value)?;
+ map.end()
+ }
+
+ fn serialize_seq(self, _: Option) -> Result {
+ Ok(ValueSerializeSeq { value: Vec::new() })
+ }
+
+ fn serialize_tuple(self, _: usize) -> Result {
+ Ok(ValueSerializeTuple { value: Vec::new() })
+ }
+
+ fn serialize_tuple_struct(
+ self,
+ _: &'static str,
+ _: usize,
+ ) -> Result {
+ Ok(ValueSerializeTupleStruct { value: Vec::new() })
+ }
+
+ fn serialize_tuple_variant(
+ self,
+ _: &'static str,
+ _: u32,
+ variant: &'static str,
+ _: usize,
+ ) -> Result {
+ Ok(ValueSerializeTupleVariant {
+ variant,
+ value: Vec::new(),
+ })
+ }
+
+ 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 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(AnyValue::ListAny(self.value)))
+ }
+ }
+
+ impl 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(AnyValue::ListAny(self.value)))
+ }
+ }
+
+ impl 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(AnyValue::ListAny(self.value)))
+ }
+ }
+
+ impl 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(AnyValue::Map({
+ let mut variant = HashMap::new();
+ variant.insert(Key::from(self.variant), AnyValue::ListAny(self.value));
+ variant
+ })))
+ }
+ }
+
+ impl 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(AnyValue::String(key)) => Key::from(String::from(key)),
+ key => Key::from(format!("{:?}", key)),
+ };
+
+ self.key = Some(key);
+
+ Ok(())
+ }
+
+ fn serialize_value(
+ &mut self,
+ value: &T,
+ ) -> Result<(), Self::Error> {
+ let key = self
+ .key
+ .take()
+ .ok_or_else(|| Self::Error::custom("missing key"))?;
+
+ if let Some(value) = value.serialize(ValueSerializer)? {
+ self.value.insert(key, value);
+ }
+
+ Ok(())
+ }
+
+ fn end(self) -> Result {
+ Ok(Some(AnyValue::Map(self.value)))
+ }
+ }
+
+ impl SerializeStruct for ValueSerializeStruct {
+ type Ok = Option;
+
+ type Error = ValueError;
+
+ fn serialize_field(
+ &mut self,
+ key: &'static str,
+ value: &T,
+ ) -> Result<(), Self::Error> {
+ let key = Key::from(key);
+
+ if let Some(value) = value.serialize(ValueSerializer)? {
+ self.value.insert(key, value);
+ }
+
+ Ok(())
+ }
+
+ fn end(self) -> Result {
+ Ok(Some(AnyValue::Map(self.value)))
+ }
+ }
+
+ impl SerializeStructVariant for ValueSerializeStructVariant {
+ type Ok = Option;
+
+ type Error = ValueError;
+
+ fn serialize_field(
+ &mut self,
+ key: &'static str,
+ value: &T,
+ ) -> Result<(), Self::Error> {
+ let key = Key::from(key);
+
+ if let Some(value) = value.serialize(ValueSerializer)? {
+ self.value.insert(key, value);
+ }
+
+ Ok(())
+ }
+
+ fn end(self) -> Result {
+ Ok(Some(AnyValue::Map({
+ let mut variant = HashMap::new();
+ variant.insert(Key::from(self.variant), AnyValue::Map(self.value));
+ variant
+ })))
+ }
+ }
+}
+
+#[cfg(test)]
mod tests {
use super::OpenTelemetryLogBridge;
+ use opentelemetry::{logs::AnyValue, StringValue};
use opentelemetry_sdk::{logs::LoggerProvider, testing::logs::InMemoryLogsExporter};
- use log::{Level, Log};
+ use log::Log;
#[test]
fn logbridge_with_default_metadata_is_enabled() {
@@ -90,15 +746,9 @@ mod tests {
// the processor used is a `SimpleLogProcessor` which has an implementation of `event_enabled`
// that always returns true.
#[cfg(feature = "logs_level_enabled")]
- assert_eq!(
- otel_log_appender.enabled(&log::Metadata::builder().build()),
- true
- );
+ assert!(otel_log_appender.enabled(&log::Metadata::builder().build()));
#[cfg(not(feature = "logs_level_enabled"))]
- assert_eq!(
- otel_log_appender.enabled(&log::Metadata::builder().build()),
- true
- );
+ assert!(otel_log_appender.enabled(&log::Metadata::builder().build()));
}
#[test]
@@ -111,14 +761,45 @@ mod tests {
let otel_log_appender = OpenTelemetryLogBridge::new(&logger_provider);
- log::set_boxed_logger(Box::new(otel_log_appender)).unwrap();
- log::set_max_level(Level::Trace.to_level_filter());
+ // log::trace!("TRACE")
+ otel_log_appender.log(
+ &log::RecordBuilder::new()
+ .level(log::Level::Trace)
+ .args(format_args!("TRACE"))
+ .build(),
+ );
- log::trace!("TRACE");
- log::debug!("DEBUG");
- log::info!("INFO");
- log::warn!("WARN");
- log::error!("ERROR");
+ // log::trace!("DEBUG")
+ otel_log_appender.log(
+ &log::RecordBuilder::new()
+ .level(log::Level::Debug)
+ .args(format_args!("DEBUG"))
+ .build(),
+ );
+
+ // log::trace!("INFO")
+ otel_log_appender.log(
+ &log::RecordBuilder::new()
+ .level(log::Level::Info)
+ .args(format_args!("INFO"))
+ .build(),
+ );
+
+ // log::trace!("WARN")
+ otel_log_appender.log(
+ &log::RecordBuilder::new()
+ .level(log::Level::Warn)
+ .args(format_args!("WARN"))
+ .build(),
+ );
+
+ // log::trace!("ERROR")
+ otel_log_appender.log(
+ &log::RecordBuilder::new()
+ .level(log::Level::Error)
+ .args(format_args!("ERROR"))
+ .build(),
+ );
let logs = exporter.get_emitted_logs().unwrap();
@@ -132,6 +813,314 @@ mod tests {
}
}
+ #[test]
+ fn logbridge_attributes() {
+ #[derive(serde::Serialize)]
+ struct Struct {
+ a: i32,
+ b: i32,
+ c: i32,
+ }
+
+ #[derive(serde::Serialize)]
+ struct Newtype(i32);
+
+ #[derive(serde::Serialize)]
+ enum Enum {
+ Unit,
+ Newtype(i32),
+ Struct { a: i32, b: i32, c: i32 },
+ Tuple(i32, i32, i32),
+ }
+
+ struct Bytes(B);
+
+ impl> serde::Serialize for Bytes {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_bytes(self.0.as_ref())
+ }
+ }
+
+ struct Map {
+ a: i32,
+ b: i32,
+ c: i32,
+ }
+
+ impl serde::Serialize for Map {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ use serde::ser::SerializeMap;
+
+ let mut map = serializer.serialize_map(Some(3))?;
+
+ map.serialize_entry(&"a", &self.a)?;
+ map.serialize_entry(&"b", &self.b)?;
+ map.serialize_entry(&"c", &self.c)?;
+
+ map.end()
+ }
+ }
+
+ let exporter = InMemoryLogsExporter::default();
+
+ let logger_provider = LoggerProvider::builder()
+ .with_simple_exporter(exporter.clone())
+ .build();
+
+ let otel_log_appender = OpenTelemetryLogBridge::new(&logger_provider);
+
+ otel_log_appender.log(
+ &log::RecordBuilder::new()
+ .level(log::Level::Info)
+ .args(format_args!("body"))
+ .key_values(&[
+ ("str_value", log::kv::Value::from("a string")),
+ ("u8_value", log::kv::Value::from(1u8)),
+ ("u16_value", log::kv::Value::from(2u16)),
+ ("u32_value", log::kv::Value::from(42u32)),
+ ("u64_value", log::kv::Value::from(2147483660u64)),
+ ("u128_small_value", log::kv::Value::from(2147483660u128)),
+ (
+ "u128_big_value",
+ log::kv::Value::from(9223372036854775820u128),
+ ),
+ ("i8_value", log::kv::Value::from(1i8)),
+ ("i16_value", log::kv::Value::from(2i16)),
+ ("i32_value", log::kv::Value::from(42i32)),
+ ("i64_value", log::kv::Value::from(2147483660i64)),
+ ("i128_small_value", log::kv::Value::from(2147483660i128)),
+ (
+ "i128_big_value",
+ log::kv::Value::from(9223372036854775820i128),
+ ),
+ ("f64_value", log::kv::Value::from(4.2f64)),
+ ("bool_value", log::kv::Value::from(true)),
+ ("bytes_value", log::kv::Value::from_serde(&Bytes([1, 1, 1]))),
+ ("unit_value", log::kv::Value::from_serde(&())),
+ ("some_value", log::kv::Value::from_serde(&Some(42))),
+ ("none_value", log::kv::Value::from_serde(&None::)),
+ (
+ "slice_value",
+ log::kv::Value::from_serde(&(&[1, 1, 1] as &[i32])),
+ ),
+ (
+ "map_value",
+ log::kv::Value::from_serde(&Map { a: 1, b: 1, c: 1 }),
+ ),
+ (
+ "struct_value",
+ log::kv::Value::from_serde(&Struct { a: 1, b: 1, c: 1 }),
+ ),
+ ("tuple_value", log::kv::Value::from_serde(&(1, 1, 1))),
+ ("newtype_value", log::kv::Value::from_serde(&Newtype(42))),
+ (
+ "unit_variant_value",
+ log::kv::Value::from_serde(&Enum::Unit),
+ ),
+ (
+ "newtype_variant_value",
+ log::kv::Value::from_serde(&Enum::Newtype(42)),
+ ),
+ (
+ "struct_variant_value",
+ log::kv::Value::from_serde(&Enum::Struct { a: 1, b: 1, c: 1 }),
+ ),
+ (
+ "tuple_variant_value",
+ log::kv::Value::from_serde(&Enum::Tuple(1, 1, 1)),
+ ),
+ ])
+ .build(),
+ );
+
+ let logs = exporter.get_emitted_logs().unwrap();
+ let attributes = &logs[0].record.attributes.as_ref().unwrap();
+
+ let get = |needle: &str| {
+ attributes.iter().find_map(|(k, v)| {
+ if k.as_str() == needle {
+ Some(v.clone())
+ } else {
+ None
+ }
+ })
+ };
+
+ assert_eq!(
+ AnyValue::String(StringValue::from("a string")),
+ get("str_value").unwrap()
+ );
+
+ assert_eq!(AnyValue::Int(1), get("i8_value").unwrap());
+ assert_eq!(AnyValue::Int(2), get("i16_value").unwrap());
+ assert_eq!(AnyValue::Int(42), get("i32_value").unwrap());
+ assert_eq!(AnyValue::Int(2147483660), get("i64_value").unwrap());
+ assert_eq!(AnyValue::Int(2147483660), get("i128_small_value").unwrap());
+ assert_eq!(
+ AnyValue::String(StringValue::from("9223372036854775820")),
+ get("i128_big_value").unwrap()
+ );
+
+ assert_eq!(AnyValue::Double(4.2), get("f64_value").unwrap());
+
+ assert_eq!(AnyValue::Boolean(true), get("bool_value").unwrap());
+
+ #[cfg(not(feature = "with-serde"))]
+ {
+ assert_eq!(
+ AnyValue::String(StringValue::from("[1, 1, 1]")),
+ get("slice_value").unwrap()
+ );
+
+ assert_eq!(
+ AnyValue::String(StringValue::from("{\"a\": 1, \"b\": 1, \"c\": 1}")),
+ get("map_value").unwrap()
+ );
+
+ assert_eq!(
+ AnyValue::String(StringValue::from("Struct { a: 1, b: 1, c: 1 }")),
+ get("struct_value").unwrap()
+ );
+
+ assert_eq!(
+ AnyValue::String(StringValue::from("(1, 1, 1)")),
+ get("tuple_value").unwrap()
+ );
+
+ assert_eq!(
+ AnyValue::String(StringValue::from("Newtype(42)")),
+ get("newtype_value").unwrap()
+ );
+
+ assert_eq!(
+ AnyValue::String(StringValue::from("Unit")),
+ get("unit_variant_value").unwrap()
+ );
+
+ assert_eq!(
+ AnyValue::String(StringValue::from("Newtype(42)")),
+ get("newtype_variant_value").unwrap()
+ );
+
+ assert_eq!(
+ AnyValue::String(StringValue::from("Struct { a: 1, b: 1, c: 1 }")),
+ get("struct_variant_value").unwrap()
+ );
+
+ assert_eq!(
+ AnyValue::String(StringValue::from("Tuple(1, 1, 1)")),
+ get("tuple_variant_value").unwrap()
+ );
+ }
+ #[cfg(feature = "with-serde")]
+ {
+ use opentelemetry::Key;
+ use std::collections::HashMap;
+
+ assert_eq!(None, get("unit_value"));
+ assert_eq!(None, get("none_value"));
+ assert_eq!(AnyValue::Int(42), get("some_value").unwrap());
+
+ assert_eq!(
+ AnyValue::ListAny(vec![AnyValue::Int(1), AnyValue::Int(1), AnyValue::Int(1)]),
+ get("slice_value").unwrap()
+ );
+
+ assert_eq!(
+ AnyValue::Map({
+ let mut map = HashMap::new();
+
+ map.insert(Key::from("a"), AnyValue::Int(1));
+ map.insert(Key::from("b"), AnyValue::Int(1));
+ map.insert(Key::from("c"), AnyValue::Int(1));
+
+ map
+ }),
+ get("map_value").unwrap()
+ );
+
+ assert_eq!(
+ AnyValue::Map({
+ let mut map = HashMap::new();
+
+ map.insert(Key::from("a"), AnyValue::Int(1));
+ map.insert(Key::from("b"), AnyValue::Int(1));
+ map.insert(Key::from("c"), AnyValue::Int(1));
+
+ map
+ }),
+ get("struct_value").unwrap()
+ );
+
+ assert_eq!(
+ AnyValue::ListAny(vec![AnyValue::Int(1), AnyValue::Int(1), AnyValue::Int(1)]),
+ get("tuple_value").unwrap()
+ );
+
+ assert_eq!(
+ AnyValue::String(StringValue::from("Unit")),
+ get("unit_variant_value").unwrap()
+ );
+
+ assert_eq!(
+ AnyValue::Map({
+ let mut map = HashMap::new();
+
+ map.insert(Key::from("Newtype"), AnyValue::Int(42));
+
+ map
+ }),
+ get("newtype_variant_value").unwrap()
+ );
+
+ assert_eq!(
+ AnyValue::Map({
+ let mut map = HashMap::new();
+
+ map.insert(
+ Key::from("Struct"),
+ AnyValue::Map({
+ let mut map = HashMap::new();
+
+ map.insert(Key::from("a"), AnyValue::Int(1));
+ map.insert(Key::from("b"), AnyValue::Int(1));
+ map.insert(Key::from("c"), AnyValue::Int(1));
+
+ map
+ }),
+ );
+
+ map
+ }),
+ get("struct_variant_value").unwrap()
+ );
+
+ assert_eq!(
+ AnyValue::Map({
+ let mut map = HashMap::new();
+
+ map.insert(
+ Key::from("Tuple"),
+ AnyValue::ListAny(vec![
+ AnyValue::Int(1),
+ AnyValue::Int(1),
+ AnyValue::Int(1),
+ ]),
+ );
+
+ map
+ }),
+ get("tuple_variant_value").unwrap()
+ );
+ }
+ }
+
#[test]
fn test_flush() {
let exporter = InMemoryLogsExporter::default();