1#[cfg(feature = "alloc")]
30use alloc::string::String;
31#[cfg(feature = "alloc")]
32use alloc::sync::Arc;
33#[cfg(feature = "alloc")]
34use alloc::vec::Vec;
35
36#[cfg(feature = "std")]
37use std::io::{self, Write};
38#[cfg(feature = "std")]
39use std::sync::Mutex;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum Level {
44 Info,
46 Warn,
49 Error,
51}
52
53impl Level {
54 #[must_use]
56 pub const fn as_str(self) -> &'static str {
57 match self {
58 Self::Info => "info",
59 Self::Warn => "warn",
60 Self::Error => "error",
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67pub enum Component {
68 Dcps,
70 Discovery,
72 Rtps,
74 Security,
76 Transport,
78 User,
80}
81
82impl Component {
83 #[must_use]
85 pub const fn as_str(self) -> &'static str {
86 match self {
87 Self::Dcps => "dcps",
88 Self::Discovery => "discovery",
89 Self::Rtps => "rtps",
90 Self::Security => "security",
91 Self::Transport => "transport",
92 Self::User => "user",
93 }
94 }
95}
96
97#[cfg(feature = "alloc")]
100#[derive(Debug, Clone)]
101pub struct Attribute {
102 pub key: &'static str,
104 pub value: String,
106}
107
108#[cfg(feature = "alloc")]
110#[derive(Debug, Clone)]
111pub struct Event {
112 pub level: Level,
114 pub component: Component,
116 pub name: &'static str,
119 pub attrs: Vec<Attribute>,
121}
122
123#[cfg(feature = "alloc")]
124impl Event {
125 #[must_use]
127 pub fn new(level: Level, component: Component, name: &'static str) -> Self {
128 Self {
129 level,
130 component,
131 name,
132 attrs: Vec::new(),
133 }
134 }
135
136 #[must_use]
138 pub fn with_attr(mut self, key: &'static str, value: impl Into<String>) -> Self {
139 self.attrs.push(Attribute {
140 key,
141 value: value.into(),
142 });
143 self
144 }
145}
146
147#[cfg(feature = "alloc")]
150pub trait Sink: Send + Sync {
151 fn record(&self, event: &Event);
155}
156
157#[cfg(feature = "alloc")]
160#[derive(Debug, Clone, Copy)]
161pub struct NullSink;
162
163#[cfg(feature = "alloc")]
164impl Sink for NullSink {
165 fn record(&self, _event: &Event) {}
166}
167
168#[cfg(feature = "std")]
181#[derive(Debug)]
182pub struct StderrJsonSink {
183 out: Mutex<io::Stderr>,
184}
185
186#[cfg(feature = "std")]
187impl Default for StderrJsonSink {
188 fn default() -> Self {
189 Self {
190 out: Mutex::new(io::stderr()),
191 }
192 }
193}
194
195#[cfg(feature = "std")]
196impl StderrJsonSink {
197 #[must_use]
199 pub fn new() -> Self {
200 Self::default()
201 }
202}
203
204#[cfg(feature = "std")]
205impl Sink for StderrJsonSink {
206 fn record(&self, event: &Event) {
207 let line = serialize_json_line(event);
208 if let Ok(mut out) = self.out.lock() {
209 let _ = out.write_all(line.as_bytes());
212 let _ = out.write_all(b"\n");
213 let _ = out.flush();
214 }
215 }
216}
217
218#[cfg(feature = "std")]
220#[derive(Debug, Default)]
221pub struct VecSink {
222 events: Mutex<Vec<Event>>,
223}
224
225#[cfg(feature = "std")]
226impl VecSink {
227 #[must_use]
229 pub fn new() -> Self {
230 Self::default()
231 }
232
233 #[must_use]
235 pub fn snapshot(&self) -> Vec<Event> {
236 self.events.lock().map(|e| e.clone()).unwrap_or_default()
237 }
238
239 #[must_use]
241 pub fn len(&self) -> usize {
242 self.events.lock().map(|e| e.len()).unwrap_or(0)
243 }
244
245 #[must_use]
247 pub fn is_empty(&self) -> bool {
248 self.len() == 0
249 }
250}
251
252#[cfg(feature = "std")]
253impl Sink for VecSink {
254 fn record(&self, event: &Event) {
255 if let Ok(mut v) = self.events.lock() {
256 v.push(event.clone());
257 }
258 }
259}
260
261#[cfg(feature = "alloc")]
269pub type SharedSink = Arc<dyn Sink>;
270
271#[cfg(feature = "alloc")]
273#[must_use]
274pub fn null_sink() -> SharedSink {
275 Arc::new(NullSink)
276}
277
278#[cfg(feature = "alloc")]
287#[allow(dead_code)]
288fn serialize_json_line(event: &Event) -> String {
289 let mut s = String::new();
290 s.push('{');
291 s.push_str("\"level\":");
292 push_json_string(&mut s, event.level.as_str());
293 s.push_str(",\"component\":");
294 push_json_string(&mut s, event.component.as_str());
295 s.push_str(",\"name\":");
296 push_json_string(&mut s, event.name);
297 if !event.attrs.is_empty() {
298 s.push_str(",\"attrs\":{");
299 for (i, a) in event.attrs.iter().enumerate() {
300 if i > 0 {
301 s.push(',');
302 }
303 push_json_string(&mut s, a.key);
304 s.push(':');
305 push_json_string(&mut s, &a.value);
306 }
307 s.push('}');
308 }
309 s.push('}');
310 s
311}
312
313#[cfg(feature = "alloc")]
314#[allow(dead_code)]
315fn push_json_string(out: &mut String, value: &str) {
316 out.push('"');
317 for ch in value.chars() {
318 match ch {
319 '"' => out.push_str("\\\""),
320 '\\' => out.push_str("\\\\"),
321 '\n' => out.push_str("\\n"),
322 '\r' => out.push_str("\\r"),
323 '\t' => out.push_str("\\t"),
324 c if (c as u32) < 0x20 => {
325 let _ = core::fmt::Write::write_fmt(out, core::format_args!("\\u{:04x}", c as u32));
327 }
328 c => out.push(c),
329 }
330 }
331 out.push('"');
332}
333
334#[cfg(test)]
335#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn level_labels() {
341 assert_eq!(Level::Info.as_str(), "info");
342 assert_eq!(Level::Warn.as_str(), "warn");
343 assert_eq!(Level::Error.as_str(), "error");
344 }
345
346 #[test]
347 fn component_labels() {
348 assert_eq!(Component::Dcps.as_str(), "dcps");
349 assert_eq!(Component::Discovery.as_str(), "discovery");
350 assert_eq!(Component::Rtps.as_str(), "rtps");
351 assert_eq!(Component::Security.as_str(), "security");
352 assert_eq!(Component::Transport.as_str(), "transport");
353 assert_eq!(Component::User.as_str(), "user");
354 }
355
356 #[test]
357 fn event_builder_attrs() {
358 let e = Event::new(Level::Info, Component::Dcps, "user_writer.created")
359 .with_attr("topic", "Foo")
360 .with_attr("reliable", "true");
361 assert_eq!(e.attrs.len(), 2);
362 assert_eq!(e.attrs[0].key, "topic");
363 assert_eq!(e.attrs[0].value, "Foo");
364 }
365
366 #[test]
367 fn null_sink_is_no_op() {
368 let s = NullSink;
369 let e = Event::new(Level::Info, Component::Dcps, "x");
370 s.record(&e); }
372
373 #[test]
374 fn vec_sink_collects() {
375 let s = VecSink::new();
376 s.record(&Event::new(Level::Info, Component::Dcps, "a"));
377 s.record(&Event::new(Level::Warn, Component::Rtps, "b"));
378 assert_eq!(s.len(), 2);
379 let snap = s.snapshot();
380 assert_eq!(snap[0].name, "a");
381 assert_eq!(snap[1].level, Level::Warn);
382 }
383
384 #[test]
385 fn serialize_json_line_basic() {
386 let e = Event::new(Level::Info, Component::Dcps, "user_writer.created");
387 let s = serialize_json_line(&e);
388 assert_eq!(
389 s,
390 r#"{"level":"info","component":"dcps","name":"user_writer.created"}"#
391 );
392 }
393
394 #[test]
395 fn serialize_json_line_with_attrs() {
396 let e = Event::new(Level::Info, Component::Dcps, "writer.created")
397 .with_attr("topic", "Foo")
398 .with_attr("reliable", "true");
399 let s = serialize_json_line(&e);
400 assert!(s.contains(r#""attrs":{"topic":"Foo","reliable":"true"}"#));
401 }
402
403 #[test]
404 fn serialize_escapes_special_chars() {
405 let e = Event::new(Level::Info, Component::User, "x").with_attr("k", "a\"b\\c\nd\te");
406 let s = serialize_json_line(&e);
407 assert!(s.contains(r#""k":"a\"b\\c\nd\te""#));
408 }
409
410 #[test]
411 fn serialize_escapes_control_chars() {
412 let e = Event::new(Level::Info, Component::User, "x").with_attr("k", "\x01");
413 let s = serialize_json_line(&e);
414 assert!(
415 s.contains("\\u0001"),
416 "control-char must be \\uXXXX, got: {s}"
417 );
418 }
419
420 #[test]
421 fn null_sink_handle_typed() {
422 let h: SharedSink = null_sink();
423 h.record(&Event::new(Level::Info, Component::Dcps, "x"));
424 }
425
426 #[test]
427 fn vec_sink_threadsafe_smoke() {
428 use std::sync::Arc as StdArc;
429 use std::thread;
430 let s: StdArc<VecSink> = StdArc::new(VecSink::new());
431 let mut handles = Vec::new();
432 for i in 0..4 {
433 let s = StdArc::clone(&s);
434 handles.push(thread::spawn(move || {
435 for _ in 0..100 {
436 s.record(&Event::new(
437 Level::Info,
438 Component::User,
439 if i % 2 == 0 { "even" } else { "odd" },
440 ));
441 }
442 }));
443 }
444 for h in handles {
445 h.join().unwrap();
446 }
447 assert_eq!(s.len(), 400);
448 }
449
450 #[test]
451 fn stderr_json_sink_does_not_panic() {
452 let s = StderrJsonSink::new();
454 s.record(&Event::new(Level::Info, Component::Dcps, "stderr.smoke"));
455 }
456}