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