#[cfg(feature = "alloc")]
use alloc::string::String;
#[cfg(feature = "alloc")]
use alloc::sync::Arc;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
#[cfg(feature = "std")]
use std::io::{self, Write};
#[cfg(feature = "std")]
use std::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Level {
Info,
Warn,
Error,
}
impl Level {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Info => "info",
Self::Warn => "warn",
Self::Error => "error",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Component {
Dcps,
Discovery,
Rtps,
Security,
Transport,
User,
}
impl Component {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Dcps => "dcps",
Self::Discovery => "discovery",
Self::Rtps => "rtps",
Self::Security => "security",
Self::Transport => "transport",
Self::User => "user",
}
}
}
#[cfg(feature = "alloc")]
#[derive(Debug, Clone)]
pub struct Attribute {
pub key: &'static str,
pub value: String,
}
#[cfg(feature = "alloc")]
#[derive(Debug, Clone)]
pub struct Event {
pub level: Level,
pub component: Component,
pub name: &'static str,
pub attrs: Vec<Attribute>,
}
#[cfg(feature = "alloc")]
impl Event {
#[must_use]
pub fn new(level: Level, component: Component, name: &'static str) -> Self {
Self {
level,
component,
name,
attrs: Vec::new(),
}
}
#[must_use]
pub fn with_attr(mut self, key: &'static str, value: impl Into<String>) -> Self {
self.attrs.push(Attribute {
key,
value: value.into(),
});
self
}
}
#[cfg(feature = "alloc")]
pub trait Sink: Send + Sync {
fn record(&self, event: &Event);
}
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, Copy)]
pub struct NullSink;
#[cfg(feature = "alloc")]
impl Sink for NullSink {
fn record(&self, _event: &Event) {}
}
#[cfg(feature = "std")]
#[derive(Debug)]
pub struct StderrJsonSink {
out: Mutex<io::Stderr>,
}
#[cfg(feature = "std")]
impl Default for StderrJsonSink {
fn default() -> Self {
Self {
out: Mutex::new(io::stderr()),
}
}
}
#[cfg(feature = "std")]
impl StderrJsonSink {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
#[cfg(feature = "std")]
impl Sink for StderrJsonSink {
fn record(&self, event: &Event) {
let line = serialize_json_line(event);
if let Ok(mut out) = self.out.lock() {
let _ = out.write_all(line.as_bytes());
let _ = out.write_all(b"\n");
let _ = out.flush();
}
}
}
#[cfg(feature = "std")]
#[derive(Debug, Default)]
pub struct VecSink {
events: Mutex<Vec<Event>>,
}
#[cfg(feature = "std")]
impl VecSink {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn snapshot(&self) -> Vec<Event> {
self.events.lock().map(|e| e.clone()).unwrap_or_default()
}
#[must_use]
pub fn len(&self) -> usize {
self.events.lock().map(|e| e.len()).unwrap_or(0)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[cfg(feature = "std")]
impl Sink for VecSink {
fn record(&self, event: &Event) {
if let Ok(mut v) = self.events.lock() {
v.push(event.clone());
}
}
}
#[cfg(feature = "alloc")]
pub type SharedSink = Arc<dyn Sink>;
#[cfg(feature = "alloc")]
#[must_use]
pub fn null_sink() -> SharedSink {
Arc::new(NullSink)
}
#[cfg(feature = "alloc")]
#[allow(dead_code)]
fn serialize_json_line(event: &Event) -> String {
let mut s = String::new();
s.push('{');
s.push_str("\"level\":");
push_json_string(&mut s, event.level.as_str());
s.push_str(",\"component\":");
push_json_string(&mut s, event.component.as_str());
s.push_str(",\"name\":");
push_json_string(&mut s, event.name);
if !event.attrs.is_empty() {
s.push_str(",\"attrs\":{");
for (i, a) in event.attrs.iter().enumerate() {
if i > 0 {
s.push(',');
}
push_json_string(&mut s, a.key);
s.push(':');
push_json_string(&mut s, &a.value);
}
s.push('}');
}
s.push('}');
s
}
#[cfg(feature = "alloc")]
#[allow(dead_code)]
fn push_json_string(out: &mut String, value: &str) {
out.push('"');
for ch in value.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
let _ = core::fmt::Write::write_fmt(out, core::format_args!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn level_labels() {
assert_eq!(Level::Info.as_str(), "info");
assert_eq!(Level::Warn.as_str(), "warn");
assert_eq!(Level::Error.as_str(), "error");
}
#[test]
fn component_labels() {
assert_eq!(Component::Dcps.as_str(), "dcps");
assert_eq!(Component::Discovery.as_str(), "discovery");
assert_eq!(Component::Rtps.as_str(), "rtps");
assert_eq!(Component::Security.as_str(), "security");
assert_eq!(Component::Transport.as_str(), "transport");
assert_eq!(Component::User.as_str(), "user");
}
#[test]
fn event_builder_attrs() {
let e = Event::new(Level::Info, Component::Dcps, "user_writer.created")
.with_attr("topic", "Foo")
.with_attr("reliable", "true");
assert_eq!(e.attrs.len(), 2);
assert_eq!(e.attrs[0].key, "topic");
assert_eq!(e.attrs[0].value, "Foo");
}
#[test]
fn null_sink_is_no_op() {
let s = NullSink;
let e = Event::new(Level::Info, Component::Dcps, "x");
s.record(&e); }
#[test]
fn vec_sink_collects() {
let s = VecSink::new();
s.record(&Event::new(Level::Info, Component::Dcps, "a"));
s.record(&Event::new(Level::Warn, Component::Rtps, "b"));
assert_eq!(s.len(), 2);
let snap = s.snapshot();
assert_eq!(snap[0].name, "a");
assert_eq!(snap[1].level, Level::Warn);
}
#[test]
fn serialize_json_line_basic() {
let e = Event::new(Level::Info, Component::Dcps, "user_writer.created");
let s = serialize_json_line(&e);
assert_eq!(
s,
r#"{"level":"info","component":"dcps","name":"user_writer.created"}"#
);
}
#[test]
fn serialize_json_line_with_attrs() {
let e = Event::new(Level::Info, Component::Dcps, "writer.created")
.with_attr("topic", "Foo")
.with_attr("reliable", "true");
let s = serialize_json_line(&e);
assert!(s.contains(r#""attrs":{"topic":"Foo","reliable":"true"}"#));
}
#[test]
fn serialize_escapes_special_chars() {
let e = Event::new(Level::Info, Component::User, "x").with_attr("k", "a\"b\\c\nd\te");
let s = serialize_json_line(&e);
assert!(s.contains(r#""k":"a\"b\\c\nd\te""#));
}
#[test]
fn serialize_escapes_control_chars() {
let e = Event::new(Level::Info, Component::User, "x").with_attr("k", "\x01");
let s = serialize_json_line(&e);
assert!(
s.contains("\\u0001"),
"control-char must be \\uXXXX, got: {s}"
);
}
#[test]
fn null_sink_handle_typed() {
let h: SharedSink = null_sink();
h.record(&Event::new(Level::Info, Component::Dcps, "x"));
}
#[test]
fn vec_sink_threadsafe_smoke() {
use std::sync::Arc as StdArc;
use std::thread;
let s: StdArc<VecSink> = StdArc::new(VecSink::new());
let mut handles = Vec::new();
for i in 0..4 {
let s = StdArc::clone(&s);
handles.push(thread::spawn(move || {
for _ in 0..100 {
s.record(&Event::new(
Level::Info,
Component::User,
if i % 2 == 0 { "even" } else { "odd" },
));
}
}));
}
for h in handles {
h.join().unwrap();
}
assert_eq!(s.len(), 400);
}
#[test]
fn stderr_json_sink_does_not_panic() {
let s = StderrJsonSink::new();
s.record(&Event::new(Level::Info, Component::Dcps, "stderr.smoke"));
}
}