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::{CStr, CString, NulError, 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 PublicMessagePrivateFields = 2,
79}
80
81#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
83pub enum SpanMode {
84 Disabled,
89 #[default]
98 Signposts,
99}
100
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103#[repr(u8)]
104pub enum OsLogType {
105 Default = 0x00,
107 Info = 0x01,
109 Debug = 0x02,
111 Error = 0x10,
113 Fault = 0x11,
115}
116
117#[derive(Clone, Copy, Debug, Eq, PartialEq)]
119pub struct LevelMap {
120 trace: OsLogType,
121 debug: OsLogType,
122 info: OsLogType,
123 warn: OsLogType,
124 error: OsLogType,
125}
126
127impl LevelMap {
128 pub const fn new(
130 trace: OsLogType,
131 debug: OsLogType,
132 info: OsLogType,
133 warn: OsLogType,
134 error: OsLogType,
135 ) -> Self {
136 Self {
137 trace,
138 debug,
139 info,
140 warn,
141 error,
142 }
143 }
144
145 pub const fn apple() -> Self {
150 Self::new(
151 OsLogType::Debug,
152 OsLogType::Debug,
153 OsLogType::Info,
154 OsLogType::Error,
155 OsLogType::Error,
156 )
157 }
158
159 pub const fn persistent_info() -> Self {
164 Self::new(
165 OsLogType::Debug,
166 OsLogType::Info,
167 OsLogType::Default,
168 OsLogType::Error,
169 OsLogType::Error,
170 )
171 }
172
173 pub const fn tracing_oslog_compatible() -> Self {
178 Self::new(
179 OsLogType::Debug,
180 OsLogType::Info,
181 OsLogType::Default,
182 OsLogType::Error,
183 OsLogType::Fault,
184 )
185 }
186
187 const fn get(self, level: Level) -> OsLogType {
188 match level {
189 Level::TRACE => self.trace,
190 Level::DEBUG => self.debug,
191 Level::INFO => self.info,
192 Level::WARN => self.warn,
193 Level::ERROR => self.error,
194 }
195 }
196}
197
198impl Default for LevelMap {
199 fn default() -> Self {
200 Self::apple()
201 }
202}
203
204#[derive(Debug)]
206pub enum OsLogError {
207 InvalidSubsystem(NulError),
209 InvalidCategory(NulError),
211 CreateFailed,
213}
214
215impl fmt::Display for OsLogError {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 match self {
218 Self::InvalidSubsystem(_) => f.write_str("os_log subsystem contains a NUL byte"),
219 Self::InvalidCategory(_) => f.write_str("os_log category contains a NUL byte"),
220 Self::CreateFailed => f.write_str("Apple os_log_create returned a null handle"),
221 }
222 }
223}
224
225impl std::error::Error for OsLogError {
226 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
227 match self {
228 Self::InvalidSubsystem(err) | Self::InvalidCategory(err) => Some(err),
229 Self::CreateFailed => None,
230 }
231 }
232}
233
234pub struct OsLogLayer {
237 log: Arc<LogHandle>,
238 layer_id: u64,
239 privacy: Privacy,
240 span_mode: SpanMode,
241 level_map: LevelMap,
242 include_target: bool,
243 include_span_context: bool,
244 max_message_bytes: usize,
245}
246
247impl OsLogLayer {
248 pub fn new(subsystem: impl AsRef<str>, category: impl AsRef<str>) -> Result<Self, OsLogError> {
254 let subsystem = CString::new(subsystem.as_ref()).map_err(OsLogError::InvalidSubsystem)?;
255 let category = CString::new(category.as_ref()).map_err(OsLogError::InvalidCategory)?;
256
257 let raw = unsafe { ffi::rama_apple_oslog_create(subsystem.as_ptr(), category.as_ptr()) };
260 let raw = NonNull::new(raw).ok_or(OsLogError::CreateFailed)?;
261
262 Ok(Self {
263 log: Arc::new(LogHandle(raw)),
266 layer_id: next_layer_id(),
267 privacy: Privacy::default(),
268 span_mode: SpanMode::default(),
269 level_map: LevelMap::default(),
270 include_target: true,
271 include_span_context: false,
272 max_message_bytes: DEFAULT_MAX_MESSAGE_BYTES,
273 })
274 }
275
276 rama_utils::macros::generate_set_and_with! {
277 pub fn privacy(mut self, privacy: Privacy) -> Self {
279 self.privacy = privacy;
280 self
281 }
282 }
283
284 rama_utils::macros::generate_set_and_with! {
285 pub fn span_mode(mut self, span_mode: SpanMode) -> Self {
289 self.span_mode = span_mode;
290 self
291 }
292 }
293
294 rama_utils::macros::generate_set_and_with! {
295 pub fn level_map(mut self, level_map: LevelMap) -> Self {
297 self.level_map = level_map;
298 self
299 }
300 }
301
302 rama_utils::macros::generate_set_and_with! {
303 pub fn target(mut self, include_target: bool) -> Self {
305 self.include_target = include_target;
306 self
307 }
308 }
309
310 rama_utils::macros::generate_set_and_with! {
311 pub fn span_context(mut self, include_span_context: bool) -> Self {
313 self.include_span_context = include_span_context;
314 self
315 }
316 }
317
318 rama_utils::macros::generate_set_and_with! {
319 pub fn max_message_bytes(mut self, max_message_bytes: usize) -> Self {
325 self.max_message_bytes = if max_message_bytes < MIN_MESSAGE_BYTES {
326 MIN_MESSAGE_BYTES
327 } else {
328 max_message_bytes
329 };
330 self
331 }
332 }
333
334 fn format_span(&self, state: &SpanState) -> CString {
335 let mut output = BoundedString::new(self.max_message_bytes);
336 if self.include_target {
337 _ = write!(output, "[{}] ", state.metadata.target());
338 }
339 output.push_str(state.metadata.name());
340 if !state.fields.is_empty() {
341 output.push_str(" ");
342 output.push_bounded(&state.fields);
343 }
344 output.into_c_message()
345 }
346
347 fn format_span_split(&self, state: &SpanState) -> (CString, CString) {
348 let mut public = BoundedString::new(self.max_message_bytes);
349 if self.include_target {
350 _ = write!(public, "[{}] ", state.metadata.target());
351 }
352 public.push_str(state.metadata.name());
353
354 let mut private = BoundedString::new(self.max_message_bytes);
355 private.push_bounded(&state.fields);
356 (public.into_c_message(), private.into_c_message())
357 }
358
359 fn format_event<S>(&self, event: &Event<'_>, ctx: &Context<'_, S>) -> CString
360 where
361 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
362 {
363 let mut message = BoundedString::new(self.max_message_bytes);
364 let mut fields = BoundedString::new(self.max_message_bytes);
365 event.record(&mut FieldVisitor::event(&mut message, &mut fields));
366
367 let mut output = BoundedString::new(self.max_message_bytes);
368 if self.include_target {
369 _ = write!(output, "[{}] ", event.metadata().target());
370 }
371
372 if !message.is_empty() {
373 output.push_bounded(&message);
374 }
375 if !fields.is_empty() {
376 if !message.is_empty() {
377 output.push_str(" ");
378 }
379 output.push_bounded(&fields);
380 }
381
382 if self.include_span_context {
383 self.append_span_context(event, ctx, &mut output);
384 }
385
386 output.into_c_message()
387 }
388
389 fn format_event_split<S>(&self, event: &Event<'_>, ctx: &Context<'_, S>) -> (CString, CString)
390 where
391 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
392 {
393 let mut message = BoundedString::new(self.max_message_bytes);
394 let mut fields = BoundedString::new(self.max_message_bytes);
395 event.record(&mut FieldVisitor::event(&mut message, &mut fields));
396
397 let mut public = BoundedString::new(self.max_message_bytes);
398 if self.include_target {
399 _ = write!(public, "[{}] ", event.metadata().target());
400 }
401 public.push_bounded(&message);
402
403 let mut private = BoundedString::new(self.max_message_bytes);
404 private.push_bounded(&fields);
405 if self.include_span_context {
406 self.append_span_context(event, ctx, &mut private);
407 }
408
409 (public.into_c_message(), private.into_c_message())
410 }
411
412 fn append_span_context<S>(
413 &self,
414 event: &Event<'_>,
415 ctx: &Context<'_, S>,
416 output: &mut BoundedString,
417 ) where
418 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
419 {
420 let mut wrote_span = false;
421 if let Some(scope) = ctx.event_scope(event) {
422 for span in scope.from_root() {
423 let extensions = span.extensions();
424 let Some(states) = extensions.get::<SpanStates>() else {
425 continue;
426 };
427 let Some(state) = states.0.get(&self.layer_id) else {
428 continue;
429 };
430
431 if !wrote_span {
432 if !output.is_empty() {
433 output.push_str(" ");
434 }
435 output.push_str("spans=[");
436 wrote_span = true;
437 } else {
438 output.push_str(" > ");
439 }
440
441 output.push_str(state.metadata.name());
442 if !state.fields.is_empty() {
443 output.push_str("{");
444 output.push_bounded(&state.fields);
445 output.push_str("}");
446 }
447 }
448 }
449 if wrote_span {
450 output.push_str("]");
451 }
452 }
453}
454
455impl Clone for OsLogLayer {
456 fn clone(&self) -> Self {
457 Self {
458 log: Arc::clone(&self.log),
459 layer_id: next_layer_id(),
460 privacy: self.privacy,
461 span_mode: self.span_mode,
462 level_map: self.level_map,
463 include_target: self.include_target,
464 include_span_context: self.include_span_context,
465 max_message_bytes: self.max_message_bytes,
466 }
467 }
468}
469
470impl fmt::Debug for OsLogLayer {
471 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472 f.debug_struct("OsLogLayer")
473 .field("layer_id", &self.layer_id)
474 .field("privacy", &self.privacy)
475 .field("span_mode", &self.span_mode)
476 .field("level_map", &self.level_map)
477 .field("include_target", &self.include_target)
478 .field("include_span_context", &self.include_span_context)
479 .field("max_message_bytes", &self.max_message_bytes)
480 .finish_non_exhaustive()
481 }
482}
483
484impl<S> Layer<S> for OsLogLayer
485where
486 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
487{
488 fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
489 let signpost_enabled = self.span_mode == SpanMode::Signposts && self.log.signpost_enabled();
490 if !self.include_span_context && !signpost_enabled {
491 return;
492 }
493
494 let Some(span) = ctx.span(id) else {
495 return;
496 };
497
498 let mut fields = BoundedString::new(self.max_message_bytes);
499 attrs.record(&mut FieldVisitor::fields(&mut fields));
500
501 let signpost_id = if signpost_enabled {
502 let signpost_id = self.log.signpost_id_generate();
503 if is_valid_signpost_id(signpost_id) {
504 Some(signpost_id)
505 } else {
506 None
507 }
508 } else {
509 None
510 };
511
512 let state = SpanState {
513 metadata: attrs.metadata(),
514 fields,
515 signpost_id,
516 };
517
518 if let Some(signpost_id) = signpost_id {
519 if self.privacy == Privacy::PublicMessagePrivateFields {
520 let (public, private) = self.format_span_split(&state);
521 self.log
522 .signpost_begin_split(signpost_id, &public, &private);
523 } else {
524 let message = self.format_span(&state);
525 self.log.signpost_begin(signpost_id, &message, self.privacy);
526 }
527 }
528
529 let mut extensions = span.extensions_mut();
530 if let Some(states) = extensions.get_mut::<SpanStates>() {
531 states.0.insert(self.layer_id, state);
532 } else {
533 let mut states = HashMap::default();
534 states.insert(self.layer_id, state);
535 extensions.insert(SpanStates(states));
536 }
537 }
538
539 fn on_record(&self, id: &Id, values: &Record<'_>, ctx: Context<'_, S>) {
540 let Some(span) = ctx.span(id) else {
541 return;
542 };
543 let mut extensions = span.extensions_mut();
544 let Some(states) = extensions.get_mut::<SpanStates>() else {
545 return;
546 };
547 let Some(state) = states.0.get_mut(&self.layer_id) else {
548 return;
549 };
550
551 values.record(&mut FieldVisitor::fields(&mut state.fields));
552 }
553
554 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
555 let os_type = self.level_map.get(*event.metadata().level());
556 if !self.log.enabled(os_type) {
557 return;
558 }
559
560 if self.privacy == Privacy::PublicMessagePrivateFields {
561 let (public, private) = self.format_event_split(event, &ctx);
562 self.log.emit_split(os_type, &public, &private);
563 } else {
564 let message = self.format_event(event, &ctx);
565 self.log.emit(os_type, &message, self.privacy);
566 }
567 }
568
569 fn on_close(&self, id: Id, ctx: Context<'_, S>) {
570 let Some(span) = ctx.span(&id) else {
571 return;
572 };
573 let mut extensions = span.extensions_mut();
574 let Some(states) = extensions.get_mut::<SpanStates>() else {
575 return;
576 };
577 let Some(state) = states.0.remove(&self.layer_id) else {
578 return;
579 };
580
581 if let Some(signpost_id) = state.signpost_id {
582 if self.privacy == Privacy::PublicMessagePrivateFields {
583 let (public, private) = self.format_span_split(&state);
584 self.log.signpost_end_split(signpost_id, &public, &private);
585 } else {
586 let message = self.format_span(&state);
587 self.log.signpost_end(signpost_id, &message, self.privacy);
588 }
589 }
590 }
591}
592
593struct LogHandle(
594 NonNull<c_void>,
597);
598
599unsafe impl Send for LogHandle {}
606unsafe impl Sync for LogHandle {}
609
610impl LogHandle {
611 fn enabled(&self, os_type: OsLogType) -> bool {
612 unsafe { ffi::rama_apple_oslog_enabled(self.0.as_ptr(), os_type as u8) != 0 }
615 }
616
617 fn emit(&self, os_type: OsLogType, message: &CStr, privacy: Privacy) {
618 unsafe {
622 ffi::rama_apple_oslog_emit(
623 self.0.as_ptr(),
624 os_type as u8,
625 message.as_ptr(),
626 privacy as u8,
627 );
628 }
629 }
630
631 fn emit_split(&self, os_type: OsLogType, public: &CStr, private: &CStr) {
632 if private.is_empty() {
633 self.emit(os_type, public, Privacy::Public);
634 return;
635 }
636 unsafe {
639 ffi::rama_apple_oslog_emit_split(
640 self.0.as_ptr(),
641 os_type as u8,
642 public.as_ptr(),
643 private.as_ptr(),
644 );
645 }
646 }
647
648 fn signpost_enabled(&self) -> bool {
649 unsafe { ffi::rama_apple_oslog_signpost_enabled(self.0.as_ptr()) != 0 }
652 }
653
654 fn signpost_id_generate(&self) -> u64 {
655 unsafe { ffi::rama_apple_oslog_signpost_id_generate(self.0.as_ptr()) }
658 }
659
660 fn signpost_begin(&self, signpost_id: u64, message: &CStr, privacy: Privacy) {
661 debug_assert!(is_valid_signpost_id(signpost_id));
662 unsafe {
665 ffi::rama_apple_oslog_signpost_begin(
666 self.0.as_ptr(),
667 signpost_id,
668 message.as_ptr(),
669 privacy as u8,
670 );
671 }
672 }
673
674 fn signpost_end(&self, signpost_id: u64, message: &CStr, privacy: Privacy) {
675 debug_assert!(is_valid_signpost_id(signpost_id));
676 unsafe {
679 ffi::rama_apple_oslog_signpost_end(
680 self.0.as_ptr(),
681 signpost_id,
682 message.as_ptr(),
683 privacy as u8,
684 );
685 }
686 }
687
688 fn signpost_begin_split(&self, signpost_id: u64, public: &CStr, private: &CStr) {
689 debug_assert!(is_valid_signpost_id(signpost_id));
690 if private.is_empty() {
691 self.signpost_begin(signpost_id, public, Privacy::Public);
692 return;
693 }
694 unsafe {
697 ffi::rama_apple_oslog_signpost_begin_split(
698 self.0.as_ptr(),
699 signpost_id,
700 public.as_ptr(),
701 private.as_ptr(),
702 );
703 }
704 }
705
706 fn signpost_end_split(&self, signpost_id: u64, public: &CStr, private: &CStr) {
707 debug_assert!(is_valid_signpost_id(signpost_id));
708 if private.is_empty() {
709 self.signpost_end(signpost_id, public, Privacy::Public);
710 return;
711 }
712 unsafe {
715 ffi::rama_apple_oslog_signpost_end_split(
716 self.0.as_ptr(),
717 signpost_id,
718 public.as_ptr(),
719 private.as_ptr(),
720 );
721 }
722 }
723}
724
725impl Drop for LogHandle {
726 fn drop(&mut self) {
727 unsafe { ffi::rama_apple_oslog_release(self.0.as_ptr()) };
731 }
732}
733
734struct SpanStates(HashMap<u64, SpanState>);
735
736struct SpanState {
737 metadata: &'static Metadata<'static>,
738 fields: BoundedString,
739 signpost_id: Option<u64>,
740}
741
742struct FieldVisitor<'a> {
743 message: Option<&'a mut BoundedString>,
744 fields: &'a mut BoundedString,
745}
746
747impl<'a> FieldVisitor<'a> {
748 fn event(message: &'a mut BoundedString, fields: &'a mut BoundedString) -> Self {
749 Self {
750 message: Some(message),
751 fields,
752 }
753 }
754
755 fn fields(fields: &'a mut BoundedString) -> Self {
756 Self {
757 message: None,
758 fields,
759 }
760 }
761
762 fn record_value(&mut self, field: &Field, write_value: impl FnOnce(&mut BoundedString, bool)) {
763 if field.name() == "message"
764 && let Some(message) = self.message.as_deref_mut()
765 {
766 write_value(message, true);
767 return;
768 }
769
770 if !self.fields.is_empty() {
771 self.fields.push_str(" ");
772 }
773 self.fields.push_str(field.name());
774 self.fields.push_str("=");
775 write_value(self.fields, false);
776 }
777}
778
779impl Visit for FieldVisitor<'_> {
780 fn record_f64(&mut self, field: &Field, value: f64) {
781 self.record_value(field, |output, _| _ = write!(output, "{value}"));
782 }
783
784 fn record_i64(&mut self, field: &Field, value: i64) {
785 self.record_value(field, |output, _| _ = write!(output, "{value}"));
786 }
787
788 fn record_u64(&mut self, field: &Field, value: u64) {
789 self.record_value(field, |output, _| _ = write!(output, "{value}"));
790 }
791
792 fn record_i128(&mut self, field: &Field, value: i128) {
793 self.record_value(field, |output, _| _ = write!(output, "{value}"));
794 }
795
796 fn record_u128(&mut self, field: &Field, value: u128) {
797 self.record_value(field, |output, _| _ = write!(output, "{value}"));
798 }
799
800 fn record_bool(&mut self, field: &Field, value: bool) {
801 self.record_value(field, |output, _| _ = write!(output, "{value}"));
802 }
803
804 fn record_str(&mut self, field: &Field, value: &str) {
805 self.record_value(field, |output, is_message| {
806 if is_message {
807 output.push_str(value);
808 } else {
809 _ = write!(output, "{value:?}");
810 }
811 });
812 }
813
814 fn record_bytes(&mut self, field: &Field, value: &[u8]) {
815 self.record_value(field, |output, _| _ = write!(output, "{value:?}"));
816 }
817
818 fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
819 self.record_value(field, |output, _| _ = write!(output, "{value}"));
820 }
821
822 fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
823 self.record_value(field, |output, _| _ = write!(output, "{value:?}"));
824 }
825}
826
827#[derive(Debug)]
828struct BoundedString {
829 value: String,
830 max_bytes: usize,
831 truncated: bool,
832}
833
834impl BoundedString {
835 fn new(max_bytes: usize) -> Self {
836 Self {
837 value: String::with_capacity(max_bytes.min(256)),
838 max_bytes,
839 truncated: false,
840 }
841 }
842
843 fn is_empty(&self) -> bool {
844 self.value.is_empty()
845 }
846
847 fn as_str(&self) -> &str {
848 &self.value
849 }
850
851 fn push_str(&mut self, value: &str) {
852 _ = self.write_str(value);
853 }
854
855 fn push_bounded(&mut self, value: &Self) {
856 self.push_str(value.as_str());
857 self.truncated |= value.truncated;
858 }
859
860 fn into_c_message(mut self) -> CString {
861 if self.truncated {
862 let keep = self.max_bytes.saturating_sub(MIN_MESSAGE_BYTES);
863 truncate_utf8(&mut self.value, keep);
864 self.value.push_str("...");
865 }
866
867 if self.value.contains('\0') {
868 let mut escaped = Self::new(self.max_bytes);
869 for part in self.value.split_inclusive('\0') {
870 if let Some(without_nul) = part.strip_suffix('\0') {
871 escaped.push_str(without_nul);
872 escaped.push_str("\\0");
873 } else {
874 escaped.push_str(part);
875 }
876 }
877 return escaped.into_c_message();
878 }
879
880 CString::new(self.value).unwrap_or_default()
882 }
883}
884
885impl fmt::Write for BoundedString {
886 fn write_str(&mut self, value: &str) -> fmt::Result {
887 let remaining = self.max_bytes.saturating_sub(self.value.len());
888 if value.len() <= remaining {
889 self.value.push_str(value);
890 return Ok(());
891 }
892
893 let mut end = remaining;
894 while end > 0 && !value.is_char_boundary(end) {
895 end -= 1;
896 }
897 self.value.push_str(&value[..end]);
898 self.truncated = true;
899 Ok(())
900 }
901}
902
903fn truncate_utf8(value: &mut String, max_bytes: usize) {
904 if value.len() <= max_bytes {
905 return;
906 }
907
908 let mut end = max_bytes;
909 while end > 0 && !value.is_char_boundary(end) {
910 end -= 1;
911 }
912 value.truncate(end);
913}
914
915fn next_layer_id() -> u64 {
916 NEXT_LAYER_ID.fetch_add(1, Ordering::Relaxed)
917}
918
919const fn is_valid_signpost_id(signpost_id: u64) -> bool {
920 signpost_id != OS_SIGNPOST_ID_NULL && signpost_id != OS_SIGNPOST_ID_INVALID
921}
922
923mod ffi {
940 use std::ffi::{c_char, c_void};
941
942 unsafe extern "C" {
943 pub(super) fn rama_apple_oslog_create(
944 subsystem: *const c_char,
945 category: *const c_char,
946 ) -> *mut c_void;
947 pub(super) fn rama_apple_oslog_release(log: *mut c_void);
948 pub(super) fn rama_apple_oslog_enabled(log: *mut c_void, os_type: u8) -> u8;
949 pub(super) fn rama_apple_oslog_emit(
950 log: *mut c_void,
951 os_type: u8,
952 message: *const c_char,
953 is_public: u8,
954 );
955 pub(super) fn rama_apple_oslog_emit_split(
956 log: *mut c_void,
957 os_type: u8,
958 public_message: *const c_char,
959 private_fields: *const c_char,
960 );
961
962 pub(super) fn rama_apple_oslog_signpost_enabled(log: *mut c_void) -> u8;
963 pub(super) fn rama_apple_oslog_signpost_id_generate(log: *mut c_void) -> u64;
964 pub(super) fn rama_apple_oslog_signpost_begin(
965 log: *mut c_void,
966 signpost_id: u64,
967 message: *const c_char,
968 is_public: u8,
969 );
970 pub(super) fn rama_apple_oslog_signpost_end(
971 log: *mut c_void,
972 signpost_id: u64,
973 message: *const c_char,
974 is_public: u8,
975 );
976 pub(super) fn rama_apple_oslog_signpost_begin_split(
977 log: *mut c_void,
978 signpost_id: u64,
979 public_message: *const c_char,
980 private_fields: *const c_char,
981 );
982 pub(super) fn rama_apple_oslog_signpost_end_split(
983 log: *mut c_void,
984 signpost_id: u64,
985 public_message: *const c_char,
986 private_fields: *const c_char,
987 );
988 }
989}
990
991#[cfg(test)]
992mod tests {
993 use super::*;
994 use crate::telemetry::tracing::{self, subscriber::layer::SubscriberExt as _};
995 use std::assert_matches;
996 use std::sync::RwLock;
997
998 struct FormattingCapture {
999 formatter: OsLogLayer,
1000 events: Arc<RwLock<Vec<String>>>,
1001 }
1002
1003 impl<S> Layer<S> for FormattingCapture
1004 where
1005 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
1006 {
1007 fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
1008 <OsLogLayer as Layer<S>>::on_new_span(&self.formatter, attrs, id, ctx);
1009 }
1010
1011 fn on_record(&self, id: &Id, values: &Record<'_>, ctx: Context<'_, S>) {
1012 <OsLogLayer as Layer<S>>::on_record(&self.formatter, id, values, ctx);
1013 }
1014
1015 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
1016 let message = if self.formatter.privacy == Privacy::PublicMessagePrivateFields {
1017 let (public, private) = self.formatter.format_event_split(event, &ctx);
1018 format!(
1019 "{} |private| {}",
1020 public.to_str().unwrap(),
1021 private.to_str().unwrap()
1022 )
1023 } else {
1024 let message = self.formatter.format_event(event, &ctx);
1025 message.into_string().unwrap()
1026 };
1027 self.events.write().unwrap().push(message);
1028 }
1029
1030 fn on_close(&self, id: Id, ctx: Context<'_, S>) {
1031 <OsLogLayer as Layer<S>>::on_close(&self.formatter, id, ctx);
1032 }
1033 }
1034
1035 #[test]
1036 fn signposts_are_enabled_by_default() {
1037 assert_eq!(SpanMode::default(), SpanMode::Signposts);
1038 }
1039
1040 #[test]
1041 fn level_maps_are_explicit_about_faults() {
1042 assert_eq!(LevelMap::apple().get(Level::ERROR), OsLogType::Error);
1043 assert_eq!(
1044 LevelMap::persistent_info().get(Level::INFO),
1045 OsLogType::Default
1046 );
1047 assert_eq!(
1048 LevelMap::tracing_oslog_compatible().get(Level::ERROR),
1049 OsLogType::Fault
1050 );
1051 }
1052
1053 #[test]
1054 fn bounded_message_is_utf8_safe_and_nul_free() {
1055 let mut message = BoundedString::new(8);
1056 message.push_str("ééééé");
1057 let message = message.into_c_message();
1058
1059 message.to_str().unwrap();
1060 assert!(message.as_bytes().len() <= 8);
1061
1062 let mut message = BoundedString::new(16);
1063 message.push_str("left\0right");
1064 let message = message.into_c_message();
1065 assert_eq!(message.as_bytes(), b"left\\0right");
1066
1067 let mut message = BoundedString::new(4);
1068 message.push_str("\0\0\0\0");
1069 let message = message.into_c_message();
1070 assert!(!message.as_bytes().is_empty());
1071 assert!(!message.as_bytes().contains(&0));
1072 }
1073
1074 #[test]
1075 fn invalid_subsystem_and_category_are_errors() {
1076 assert_matches!(
1077 OsLogLayer::new("bad\0subsystem", "category"),
1078 Err(OsLogError::InvalidSubsystem(_)),
1079 );
1080 assert_matches!(
1081 OsLogLayer::new("com.example", "bad\0category"),
1082 Err(OsLogError::InvalidCategory(_)),
1083 );
1084 }
1085
1086 #[test]
1087 fn explicit_parents_records_and_multiple_layers_do_not_panic() {
1088 let first = OsLogLayer::new("org.plabayo.rama.test", "first")
1089 .unwrap()
1090 .with_privacy(Privacy::Public)
1091 .with_level_map(LevelMap::persistent_info())
1092 .with_span_mode(SpanMode::Signposts)
1093 .with_span_context(true);
1094 let second = first.clone().with_target(false);
1095 let subscriber = tracing::subscriber::registry().with(first).with(second);
1096 let dispatch = tracing::Dispatch::new(subscriber);
1097
1098 tracing::dispatcher::with_default(&dispatch, || {
1099 let span = tracing::info_span!("request", request.id = tracing::field::Empty);
1100 span.record("request.id", 42_u64);
1101 tracing::info!(parent: &span, answer = 42, "explicit parent");
1102 tracing::info!(parent: None, "explicit root");
1103 });
1104 }
1105
1106 #[test]
1107 fn event_formatting_uses_explicit_scope_and_late_records() {
1108 let events = Arc::new(RwLock::new(Vec::new()));
1109 let formatter = OsLogLayer::new("org.plabayo.rama.test", "format")
1110 .unwrap()
1111 .with_target(false)
1112 .with_span_context(true);
1113 let capture = FormattingCapture {
1114 formatter,
1115 events: Arc::clone(&events),
1116 };
1117 let dispatch = tracing::Dispatch::new(tracing::subscriber::registry().with(capture));
1118
1119 tracing::dispatcher::with_default(&dispatch, || {
1120 let span = tracing::info_span!("request", request.id = tracing::field::Empty);
1121 span.record("request.id", 42_u64);
1122 tracing::info!(parent: &span, answer = 42, "explicit parent");
1123 tracing::info!(parent: None, "explicit root");
1124 });
1125
1126 let events = events.read().unwrap();
1127 assert_eq!(events.len(), 2);
1128 assert!(events[0].contains("explicit parent answer=42"));
1129 assert!(events[0].contains("spans=[request{request.id=42}]"));
1130 assert_eq!(events[1], "explicit root");
1131 }
1132
1133 #[test]
1134 fn split_privacy_keeps_fields_out_of_public_message() {
1135 let events = Arc::new(RwLock::new(Vec::new()));
1136 let formatter = OsLogLayer::new("org.plabayo.rama.test", "format")
1137 .unwrap()
1138 .with_privacy(Privacy::PublicMessagePrivateFields)
1139 .with_target(false)
1140 .with_span_context(true);
1141 let capture = FormattingCapture {
1142 formatter,
1143 events: Arc::clone(&events),
1144 };
1145 let dispatch = tracing::Dispatch::new(tracing::subscriber::registry().with(capture));
1146
1147 tracing::dispatcher::with_default(&dispatch, || {
1148 let span = tracing::info_span!("request", user = "private-user");
1149 tracing::info!(parent: &span, endpoint = "/private", "request finished");
1150 });
1151
1152 let events = events.read().unwrap();
1153 assert!(events[0].starts_with("request finished |private| "));
1154 assert!(
1155 !events[0]
1156 .split(" |private| ")
1157 .next()
1158 .unwrap()
1159 .contains("private")
1160 );
1161 assert!(events[0].contains("endpoint=\"/private\""));
1162 assert!(events[0].contains("user=\"private-user\""));
1163 }
1164}