1use ahash::HashMap;
33use rama_core::telemetry::tracing::{
34 Event, Level, Metadata, Subscriber,
35 field::{Field, Visit},
36 span::{Attributes, Id, Record},
37};
38use rama_utils::octets::kib;
39use std::{
40 ffi::{CString, NulError, c_char, c_void},
41 fmt::{self, Write as _},
42 ptr::NonNull,
43 sync::{
44 Arc,
45 atomic::{AtomicU64, Ordering},
46 },
47};
48use tracing_subscriber::{
49 layer::{Context, Layer},
50 registry::LookupSpan,
51};
52
53const DEFAULT_MAX_MESSAGE_BYTES: usize = kib(1);
54const MIN_MESSAGE_BYTES: usize = 3;
55const OS_SIGNPOST_ID_NULL: u64 = 0;
56const OS_SIGNPOST_ID_INVALID: u64 = u64::MAX;
57
58static NEXT_LAYER_ID: AtomicU64 = AtomicU64::new(1);
59
60#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
66#[repr(u8)]
67pub enum Privacy {
68 #[default]
70 Private = 0,
71 Public = 1,
73}
74
75#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
77pub enum SpanMode {
78 Disabled,
83 #[default]
92 Signposts,
93}
94
95#[derive(Clone, Copy, Debug, Eq, PartialEq)]
97#[repr(u8)]
98pub enum OsLogType {
99 Default = 0x00,
101 Info = 0x01,
103 Debug = 0x02,
105 Error = 0x10,
107 Fault = 0x11,
109}
110
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113pub struct LevelMap {
114 trace: OsLogType,
115 debug: OsLogType,
116 info: OsLogType,
117 warn: OsLogType,
118 error: OsLogType,
119}
120
121impl LevelMap {
122 pub const fn new(
124 trace: OsLogType,
125 debug: OsLogType,
126 info: OsLogType,
127 warn: OsLogType,
128 error: OsLogType,
129 ) -> Self {
130 Self {
131 trace,
132 debug,
133 info,
134 warn,
135 error,
136 }
137 }
138
139 pub const fn apple() -> Self {
144 Self::new(
145 OsLogType::Debug,
146 OsLogType::Debug,
147 OsLogType::Info,
148 OsLogType::Error,
149 OsLogType::Error,
150 )
151 }
152
153 pub const fn persistent_info() -> Self {
158 Self::new(
159 OsLogType::Debug,
160 OsLogType::Info,
161 OsLogType::Default,
162 OsLogType::Error,
163 OsLogType::Error,
164 )
165 }
166
167 pub const fn tracing_oslog_compatible() -> Self {
172 Self::new(
173 OsLogType::Debug,
174 OsLogType::Info,
175 OsLogType::Default,
176 OsLogType::Error,
177 OsLogType::Fault,
178 )
179 }
180
181 const fn get(self, level: Level) -> OsLogType {
182 match level {
183 Level::TRACE => self.trace,
184 Level::DEBUG => self.debug,
185 Level::INFO => self.info,
186 Level::WARN => self.warn,
187 Level::ERROR => self.error,
188 }
189 }
190}
191
192impl Default for LevelMap {
193 fn default() -> Self {
194 Self::apple()
195 }
196}
197
198#[derive(Debug)]
200pub enum OsLogError {
201 InvalidSubsystem(NulError),
203 InvalidCategory(NulError),
205 CreateFailed,
207}
208
209impl fmt::Display for OsLogError {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 match self {
212 Self::InvalidSubsystem(_) => f.write_str("os_log subsystem contains a NUL byte"),
213 Self::InvalidCategory(_) => f.write_str("os_log category contains a NUL byte"),
214 Self::CreateFailed => f.write_str("Apple os_log_create returned a null handle"),
215 }
216 }
217}
218
219impl std::error::Error for OsLogError {
220 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
221 match self {
222 Self::InvalidSubsystem(err) | Self::InvalidCategory(err) => Some(err),
223 Self::CreateFailed => None,
224 }
225 }
226}
227
228pub struct OsLogLayer {
231 log: Arc<LogHandle>,
232 layer_id: u64,
233 privacy: Privacy,
234 span_mode: SpanMode,
235 level_map: LevelMap,
236 include_target: bool,
237 include_span_context: bool,
238 max_message_bytes: usize,
239}
240
241impl OsLogLayer {
242 pub fn new(subsystem: impl AsRef<str>, category: impl AsRef<str>) -> Result<Self, OsLogError> {
248 let subsystem = CString::new(subsystem.as_ref()).map_err(OsLogError::InvalidSubsystem)?;
249 let category = CString::new(category.as_ref()).map_err(OsLogError::InvalidCategory)?;
250
251 let raw = unsafe { ffi::rama_apple_oslog_create(subsystem.as_ptr(), category.as_ptr()) };
255 let raw = NonNull::new(raw).ok_or(OsLogError::CreateFailed)?;
256
257 Ok(Self {
258 log: Arc::new(LogHandle(raw)),
259 layer_id: next_layer_id(),
260 privacy: Privacy::default(),
261 span_mode: SpanMode::default(),
262 level_map: LevelMap::default(),
263 include_target: true,
264 include_span_context: false,
265 max_message_bytes: DEFAULT_MAX_MESSAGE_BYTES,
266 })
267 }
268
269 rama_utils::macros::generate_set_and_with! {
270 pub fn privacy(mut self, privacy: Privacy) -> Self {
272 self.privacy = privacy;
273 self
274 }
275 }
276
277 rama_utils::macros::generate_set_and_with! {
278 pub fn span_mode(mut self, span_mode: SpanMode) -> Self {
282 self.span_mode = span_mode;
283 self
284 }
285 }
286
287 rama_utils::macros::generate_set_and_with! {
288 pub fn level_map(mut self, level_map: LevelMap) -> Self {
290 self.level_map = level_map;
291 self
292 }
293 }
294
295 rama_utils::macros::generate_set_and_with! {
296 pub fn target(mut self, include_target: bool) -> Self {
298 self.include_target = include_target;
299 self
300 }
301 }
302
303 rama_utils::macros::generate_set_and_with! {
304 pub fn span_context(mut self, include_span_context: bool) -> Self {
306 self.include_span_context = include_span_context;
307 self
308 }
309 }
310
311 rama_utils::macros::generate_set_and_with! {
312 pub fn max_message_bytes(mut self, max_message_bytes: usize) -> Self {
318 self.max_message_bytes = if max_message_bytes < MIN_MESSAGE_BYTES {
319 MIN_MESSAGE_BYTES
320 } else {
321 max_message_bytes
322 };
323 self
324 }
325 }
326
327 fn format_span(&self, state: &SpanState) -> Vec<u8> {
328 let mut output = BoundedString::new(self.max_message_bytes);
329 if self.include_target {
330 _ = write!(output, "[{}] ", state.metadata.target());
331 }
332 output.push_str(state.metadata.name());
333 if !state.fields.is_empty() {
334 output.push_str(" ");
335 output.push_bounded(&state.fields);
336 }
337 output.into_c_message()
338 }
339
340 fn format_event<S>(&self, event: &Event<'_>, ctx: &Context<'_, S>) -> Vec<u8>
341 where
342 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
343 {
344 let mut message = BoundedString::new(self.max_message_bytes);
345 let mut fields = BoundedString::new(self.max_message_bytes);
346 event.record(&mut FieldVisitor::event(&mut message, &mut fields));
347
348 let mut output = BoundedString::new(self.max_message_bytes);
349 if self.include_target {
350 _ = write!(output, "[{}] ", event.metadata().target());
351 }
352
353 if !message.is_empty() {
354 output.push_bounded(&message);
355 }
356 if !fields.is_empty() {
357 if !message.is_empty() {
358 output.push_str(" ");
359 }
360 output.push_bounded(&fields);
361 }
362
363 if self.include_span_context {
364 let mut wrote_span = false;
365 if let Some(scope) = ctx.event_scope(event) {
366 for span in scope.from_root() {
367 let extensions = span.extensions();
368 let Some(states) = extensions.get::<SpanStates>() else {
369 continue;
370 };
371 let Some(state) = states.0.get(&self.layer_id) else {
372 continue;
373 };
374
375 if !wrote_span {
376 if !output.is_empty() {
377 output.push_str(" ");
378 }
379 output.push_str("spans=[");
380 wrote_span = true;
381 } else {
382 output.push_str(" > ");
383 }
384
385 output.push_str(state.metadata.name());
386 if !state.fields.is_empty() {
387 output.push_str("{");
388 output.push_bounded(&state.fields);
389 output.push_str("}");
390 }
391 }
392 }
393 if wrote_span {
394 output.push_str("]");
395 }
396 }
397
398 output.into_c_message()
399 }
400}
401
402impl Clone for OsLogLayer {
403 fn clone(&self) -> Self {
404 Self {
405 log: Arc::clone(&self.log),
406 layer_id: next_layer_id(),
407 privacy: self.privacy,
408 span_mode: self.span_mode,
409 level_map: self.level_map,
410 include_target: self.include_target,
411 include_span_context: self.include_span_context,
412 max_message_bytes: self.max_message_bytes,
413 }
414 }
415}
416
417impl fmt::Debug for OsLogLayer {
418 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
419 f.debug_struct("OsLogLayer")
420 .field("layer_id", &self.layer_id)
421 .field("privacy", &self.privacy)
422 .field("span_mode", &self.span_mode)
423 .field("level_map", &self.level_map)
424 .field("include_target", &self.include_target)
425 .field("include_span_context", &self.include_span_context)
426 .field("max_message_bytes", &self.max_message_bytes)
427 .finish_non_exhaustive()
428 }
429}
430
431impl<S> Layer<S> for OsLogLayer
432where
433 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
434{
435 fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
436 let signpost_enabled = self.span_mode == SpanMode::Signposts && self.log.signpost_enabled();
437 if !self.include_span_context && !signpost_enabled {
438 return;
439 }
440
441 let Some(span) = ctx.span(id) else {
442 return;
443 };
444
445 let mut fields = BoundedString::new(self.max_message_bytes);
446 attrs.record(&mut FieldVisitor::fields(&mut fields));
447
448 let signpost_id = if signpost_enabled {
449 let signpost_id = self.log.signpost_id_generate();
450 if is_valid_signpost_id(signpost_id) {
451 Some(signpost_id)
452 } else {
453 None
454 }
455 } else {
456 None
457 };
458
459 let state = SpanState {
460 metadata: attrs.metadata(),
461 fields,
462 signpost_id,
463 };
464
465 if let Some(signpost_id) = signpost_id {
466 let message = self.format_span(&state);
467 self.log.signpost_begin(signpost_id, &message, self.privacy);
468 }
469
470 let mut extensions = span.extensions_mut();
471 if let Some(states) = extensions.get_mut::<SpanStates>() {
472 states.0.insert(self.layer_id, state);
473 } else {
474 let mut states = HashMap::default();
475 states.insert(self.layer_id, state);
476 extensions.insert(SpanStates(states));
477 }
478 }
479
480 fn on_record(&self, id: &Id, values: &Record<'_>, ctx: Context<'_, S>) {
481 let Some(span) = ctx.span(id) else {
482 return;
483 };
484 let mut extensions = span.extensions_mut();
485 let Some(states) = extensions.get_mut::<SpanStates>() else {
486 return;
487 };
488 let Some(state) = states.0.get_mut(&self.layer_id) else {
489 return;
490 };
491
492 values.record(&mut FieldVisitor::fields(&mut state.fields));
493 }
494
495 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
496 let os_type = self.level_map.get(*event.metadata().level());
497 if !self.log.enabled(os_type) {
498 return;
499 }
500
501 let message = self.format_event(event, &ctx);
502 self.log.emit(os_type, &message, self.privacy);
503 }
504
505 fn on_close(&self, id: Id, ctx: Context<'_, S>) {
506 let Some(span) = ctx.span(&id) else {
507 return;
508 };
509 let mut extensions = span.extensions_mut();
510 let Some(states) = extensions.get_mut::<SpanStates>() else {
511 return;
512 };
513 let Some(state) = states.0.remove(&self.layer_id) else {
514 return;
515 };
516
517 if let Some(signpost_id) = state.signpost_id {
518 let message = self.format_span(&state);
519 self.log.signpost_end(signpost_id, &message, self.privacy);
520 }
521 }
522}
523
524struct LogHandle(NonNull<c_void>);
525
526unsafe impl Send for LogHandle {}
529unsafe impl Sync for LogHandle {}
532
533impl LogHandle {
534 fn enabled(&self, os_type: OsLogType) -> bool {
535 unsafe { ffi::rama_apple_oslog_enabled(self.0.as_ptr(), os_type as u8) != 0 }
538 }
539
540 fn emit(&self, os_type: OsLogType, message: &[u8], privacy: Privacy) {
541 debug_assert_eq!(message.last(), Some(&0));
542 unsafe {
545 ffi::rama_apple_oslog_emit(
546 self.0.as_ptr(),
547 os_type as u8,
548 message.as_ptr().cast::<c_char>(),
549 privacy as u8,
550 );
551 }
552 }
553
554 fn signpost_enabled(&self) -> bool {
555 unsafe { ffi::rama_apple_oslog_signpost_enabled(self.0.as_ptr()) != 0 }
558 }
559
560 fn signpost_id_generate(&self) -> u64 {
561 unsafe { ffi::rama_apple_oslog_signpost_id_generate(self.0.as_ptr()) }
564 }
565
566 fn signpost_begin(&self, signpost_id: u64, message: &[u8], privacy: Privacy) {
567 debug_assert!(is_valid_signpost_id(signpost_id));
568 debug_assert_eq!(message.last(), Some(&0));
569 unsafe {
572 ffi::rama_apple_oslog_signpost_begin(
573 self.0.as_ptr(),
574 signpost_id,
575 message.as_ptr().cast::<c_char>(),
576 privacy as u8,
577 );
578 }
579 }
580
581 fn signpost_end(&self, signpost_id: u64, message: &[u8], privacy: Privacy) {
582 debug_assert!(is_valid_signpost_id(signpost_id));
583 debug_assert_eq!(message.last(), Some(&0));
584 unsafe {
587 ffi::rama_apple_oslog_signpost_end(
588 self.0.as_ptr(),
589 signpost_id,
590 message.as_ptr().cast::<c_char>(),
591 privacy as u8,
592 );
593 }
594 }
595}
596
597impl Drop for LogHandle {
598 fn drop(&mut self) {
599 unsafe { ffi::rama_apple_oslog_release(self.0.as_ptr()) };
602 }
603}
604
605struct SpanStates(HashMap<u64, SpanState>);
606
607struct SpanState {
608 metadata: &'static Metadata<'static>,
609 fields: BoundedString,
610 signpost_id: Option<u64>,
611}
612
613struct FieldVisitor<'a> {
614 message: Option<&'a mut BoundedString>,
615 fields: &'a mut BoundedString,
616}
617
618impl<'a> FieldVisitor<'a> {
619 fn event(message: &'a mut BoundedString, fields: &'a mut BoundedString) -> Self {
620 Self {
621 message: Some(message),
622 fields,
623 }
624 }
625
626 fn fields(fields: &'a mut BoundedString) -> Self {
627 Self {
628 message: None,
629 fields,
630 }
631 }
632
633 fn record_value(&mut self, field: &Field, write_value: impl FnOnce(&mut BoundedString, bool)) {
634 if field.name() == "message"
635 && let Some(message) = self.message.as_deref_mut()
636 {
637 write_value(message, true);
638 return;
639 }
640
641 if !self.fields.is_empty() {
642 self.fields.push_str(" ");
643 }
644 self.fields.push_str(field.name());
645 self.fields.push_str("=");
646 write_value(self.fields, false);
647 }
648}
649
650impl Visit for FieldVisitor<'_> {
651 fn record_f64(&mut self, field: &Field, value: f64) {
652 self.record_value(field, |output, _| _ = write!(output, "{value}"));
653 }
654
655 fn record_i64(&mut self, field: &Field, value: i64) {
656 self.record_value(field, |output, _| _ = write!(output, "{value}"));
657 }
658
659 fn record_u64(&mut self, field: &Field, value: u64) {
660 self.record_value(field, |output, _| _ = write!(output, "{value}"));
661 }
662
663 fn record_i128(&mut self, field: &Field, value: i128) {
664 self.record_value(field, |output, _| _ = write!(output, "{value}"));
665 }
666
667 fn record_u128(&mut self, field: &Field, value: u128) {
668 self.record_value(field, |output, _| _ = write!(output, "{value}"));
669 }
670
671 fn record_bool(&mut self, field: &Field, value: bool) {
672 self.record_value(field, |output, _| _ = write!(output, "{value}"));
673 }
674
675 fn record_str(&mut self, field: &Field, value: &str) {
676 self.record_value(field, |output, is_message| {
677 if is_message {
678 output.push_str(value);
679 } else {
680 _ = write!(output, "{value:?}");
681 }
682 });
683 }
684
685 fn record_bytes(&mut self, field: &Field, value: &[u8]) {
686 self.record_value(field, |output, _| _ = write!(output, "{value:?}"));
687 }
688
689 fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
690 self.record_value(field, |output, _| _ = write!(output, "{value}"));
691 }
692
693 fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
694 self.record_value(field, |output, _| _ = write!(output, "{value:?}"));
695 }
696}
697
698#[derive(Debug)]
699struct BoundedString {
700 value: String,
701 max_bytes: usize,
702 truncated: bool,
703}
704
705impl BoundedString {
706 fn new(max_bytes: usize) -> Self {
707 Self {
708 value: String::with_capacity(max_bytes.min(256)),
709 max_bytes,
710 truncated: false,
711 }
712 }
713
714 fn is_empty(&self) -> bool {
715 self.value.is_empty()
716 }
717
718 fn as_str(&self) -> &str {
719 &self.value
720 }
721
722 fn push_str(&mut self, value: &str) {
723 _ = self.write_str(value);
724 }
725
726 fn push_bounded(&mut self, value: &Self) {
727 self.push_str(value.as_str());
728 self.truncated |= value.truncated;
729 }
730
731 fn into_c_message(mut self) -> Vec<u8> {
732 if self.truncated {
733 let keep = self.max_bytes.saturating_sub(MIN_MESSAGE_BYTES);
734 truncate_utf8(&mut self.value, keep);
735 self.value.push_str("...");
736 }
737
738 if self.value.contains('\0') {
739 let mut escaped = Self::new(self.max_bytes);
740 for part in self.value.split_inclusive('\0') {
741 if let Some(without_nul) = part.strip_suffix('\0') {
742 escaped.push_str(without_nul);
743 escaped.push_str("\\0");
744 } else {
745 escaped.push_str(part);
746 }
747 }
748 return escaped.into_c_message();
749 }
750
751 let mut bytes = self.value.into_bytes();
752 bytes.push(0);
753 bytes
754 }
755}
756
757impl fmt::Write for BoundedString {
758 fn write_str(&mut self, value: &str) -> fmt::Result {
759 let remaining = self.max_bytes.saturating_sub(self.value.len());
760 if value.len() <= remaining {
761 self.value.push_str(value);
762 return Ok(());
763 }
764
765 let mut end = remaining;
766 while end > 0 && !value.is_char_boundary(end) {
767 end -= 1;
768 }
769 self.value.push_str(&value[..end]);
770 self.truncated = true;
771 Ok(())
772 }
773}
774
775fn truncate_utf8(value: &mut String, max_bytes: usize) {
776 if value.len() <= max_bytes {
777 return;
778 }
779
780 let mut end = max_bytes;
781 while end > 0 && !value.is_char_boundary(end) {
782 end -= 1;
783 }
784 value.truncate(end);
785}
786
787fn next_layer_id() -> u64 {
788 NEXT_LAYER_ID.fetch_add(1, Ordering::Relaxed)
789}
790
791const fn is_valid_signpost_id(signpost_id: u64) -> bool {
792 signpost_id != OS_SIGNPOST_ID_NULL && signpost_id != OS_SIGNPOST_ID_INVALID
793}
794
795mod ffi {
796 use std::ffi::{c_char, c_void};
797
798 unsafe extern "C" {
799 pub(super) fn rama_apple_oslog_create(
800 subsystem: *const c_char,
801 category: *const c_char,
802 ) -> *mut c_void;
803 pub(super) fn rama_apple_oslog_release(log: *mut c_void);
804 pub(super) fn rama_apple_oslog_enabled(log: *mut c_void, os_type: u8) -> u8;
805 pub(super) fn rama_apple_oslog_emit(
806 log: *mut c_void,
807 os_type: u8,
808 message: *const c_char,
809 is_public: u8,
810 );
811
812 pub(super) fn rama_apple_oslog_signpost_enabled(log: *mut c_void) -> u8;
813 pub(super) fn rama_apple_oslog_signpost_id_generate(log: *mut c_void) -> u64;
814 pub(super) fn rama_apple_oslog_signpost_begin(
815 log: *mut c_void,
816 signpost_id: u64,
817 message: *const c_char,
818 is_public: u8,
819 );
820 pub(super) fn rama_apple_oslog_signpost_end(
821 log: *mut c_void,
822 signpost_id: u64,
823 message: *const c_char,
824 is_public: u8,
825 );
826 }
827}
828
829#[cfg(test)]
830mod tests {
831 use super::*;
832 use crate::telemetry::tracing::{self, subscriber::layer::SubscriberExt as _};
833 use std::sync::RwLock;
834
835 struct FormattingCapture {
836 formatter: OsLogLayer,
837 events: Arc<RwLock<Vec<String>>>,
838 }
839
840 impl<S> Layer<S> for FormattingCapture
841 where
842 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
843 {
844 fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
845 <OsLogLayer as Layer<S>>::on_new_span(&self.formatter, attrs, id, ctx);
846 }
847
848 fn on_record(&self, id: &Id, values: &Record<'_>, ctx: Context<'_, S>) {
849 <OsLogLayer as Layer<S>>::on_record(&self.formatter, id, values, ctx);
850 }
851
852 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
853 let message = self.formatter.format_event(event, &ctx);
854 let message = String::from_utf8(message[..message.len() - 1].to_vec()).unwrap();
855 self.events.write().unwrap().push(message);
856 }
857
858 fn on_close(&self, id: Id, ctx: Context<'_, S>) {
859 <OsLogLayer as Layer<S>>::on_close(&self.formatter, id, ctx);
860 }
861 }
862
863 #[test]
864 fn signposts_are_enabled_by_default() {
865 assert_eq!(SpanMode::default(), SpanMode::Signposts);
866 }
867
868 #[test]
869 fn level_maps_are_explicit_about_faults() {
870 assert_eq!(LevelMap::apple().get(Level::ERROR), OsLogType::Error);
871 assert_eq!(
872 LevelMap::persistent_info().get(Level::INFO),
873 OsLogType::Default
874 );
875 assert_eq!(
876 LevelMap::tracing_oslog_compatible().get(Level::ERROR),
877 OsLogType::Fault
878 );
879 }
880
881 #[test]
882 fn bounded_message_is_utf8_safe_and_nul_free() {
883 let mut message = BoundedString::new(8);
884 message.push_str("ééééé");
885 let message = message.into_c_message();
886
887 assert_eq!(message.last(), Some(&0));
888 std::str::from_utf8(&message[..message.len() - 1]).unwrap();
889 assert!(message.len() <= 9);
890
891 let mut message = BoundedString::new(16);
892 message.push_str("left\0right");
893 let message = message.into_c_message();
894 assert_eq!(&message[..message.len() - 1], b"left\\0right");
895 }
896
897 #[test]
898 fn invalid_subsystem_and_category_are_errors() {
899 assert!(matches!(
900 OsLogLayer::new("bad\0subsystem", "category"),
901 Err(OsLogError::InvalidSubsystem(_))
902 ));
903 assert!(matches!(
904 OsLogLayer::new("com.example", "bad\0category"),
905 Err(OsLogError::InvalidCategory(_))
906 ));
907 }
908
909 #[test]
910 fn explicit_parents_records_and_multiple_layers_do_not_panic() {
911 let first = OsLogLayer::new("org.plabayo.rama.test", "first")
912 .unwrap()
913 .with_privacy(Privacy::Public)
914 .with_level_map(LevelMap::persistent_info())
915 .with_span_mode(SpanMode::Signposts)
916 .with_span_context(true);
917 let second = first.clone().with_target(false);
918 let subscriber = tracing::subscriber::registry().with(first).with(second);
919 let dispatch = tracing::Dispatch::new(subscriber);
920
921 tracing::dispatcher::with_default(&dispatch, || {
922 let span = tracing::info_span!("request", request.id = tracing::field::Empty);
923 span.record("request.id", 42_u64);
924 tracing::info!(parent: &span, answer = 42, "explicit parent");
925 tracing::info!(parent: None, "explicit root");
926 });
927 }
928
929 #[test]
930 fn event_formatting_uses_explicit_scope_and_late_records() {
931 let events = Arc::new(RwLock::new(Vec::new()));
932 let formatter = OsLogLayer::new("org.plabayo.rama.test", "format")
933 .unwrap()
934 .with_target(false)
935 .with_span_context(true);
936 let capture = FormattingCapture {
937 formatter,
938 events: Arc::clone(&events),
939 };
940 let dispatch = tracing::Dispatch::new(tracing::subscriber::registry().with(capture));
941
942 tracing::dispatcher::with_default(&dispatch, || {
943 let span = tracing::info_span!("request", request.id = tracing::field::Empty);
944 span.record("request.id", 42_u64);
945 tracing::info!(parent: &span, answer = 42, "explicit parent");
946 tracing::info!(parent: None, "explicit root");
947 });
948
949 let events = events.read().unwrap();
950 assert_eq!(events.len(), 2);
951 assert!(events[0].contains("explicit parent answer=42"));
952 assert!(events[0].contains("spans=[request{request.id=42}]"));
953 assert_eq!(events[1], "explicit root");
954 }
955}