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 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()) };
261 let raw = NonNull::new(raw).ok_or(OsLogError::CreateFailed)?;
262
263 Ok(Self {
264 log: Arc::new(LogHandle(raw)),
265 layer_id: next_layer_id(),
266 privacy: Privacy::default(),
267 span_mode: SpanMode::default(),
268 level_map: LevelMap::default(),
269 include_target: true,
270 include_span_context: false,
271 max_message_bytes: DEFAULT_MAX_MESSAGE_BYTES,
272 })
273 }
274
275 rama_utils::macros::generate_set_and_with! {
276 pub fn privacy(mut self, privacy: Privacy) -> Self {
278 self.privacy = privacy;
279 self
280 }
281 }
282
283 rama_utils::macros::generate_set_and_with! {
284 pub fn span_mode(mut self, span_mode: SpanMode) -> Self {
288 self.span_mode = span_mode;
289 self
290 }
291 }
292
293 rama_utils::macros::generate_set_and_with! {
294 pub fn level_map(mut self, level_map: LevelMap) -> Self {
296 self.level_map = level_map;
297 self
298 }
299 }
300
301 rama_utils::macros::generate_set_and_with! {
302 pub fn target(mut self, include_target: bool) -> Self {
304 self.include_target = include_target;
305 self
306 }
307 }
308
309 rama_utils::macros::generate_set_and_with! {
310 pub fn span_context(mut self, include_span_context: bool) -> Self {
312 self.include_span_context = include_span_context;
313 self
314 }
315 }
316
317 rama_utils::macros::generate_set_and_with! {
318 pub fn max_message_bytes(mut self, max_message_bytes: usize) -> Self {
324 self.max_message_bytes = if max_message_bytes < MIN_MESSAGE_BYTES {
325 MIN_MESSAGE_BYTES
326 } else {
327 max_message_bytes
328 };
329 self
330 }
331 }
332
333 fn format_span(&self, state: &SpanState) -> Vec<u8> {
334 let mut output = BoundedString::new(self.max_message_bytes);
335 if self.include_target {
336 _ = write!(output, "[{}] ", state.metadata.target());
337 }
338 output.push_str(state.metadata.name());
339 if !state.fields.is_empty() {
340 output.push_str(" ");
341 output.push_bounded(&state.fields);
342 }
343 output.into_c_message()
344 }
345
346 fn format_span_split(&self, state: &SpanState) -> (Vec<u8>, Vec<u8>) {
347 let mut public = BoundedString::new(self.max_message_bytes);
348 if self.include_target {
349 _ = write!(public, "[{}] ", state.metadata.target());
350 }
351 public.push_str(state.metadata.name());
352
353 let mut private = BoundedString::new(self.max_message_bytes);
354 private.push_bounded(&state.fields);
355 (public.into_c_message(), private.into_c_message())
356 }
357
358 fn format_event<S>(&self, event: &Event<'_>, ctx: &Context<'_, S>) -> Vec<u8>
359 where
360 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
361 {
362 let mut message = BoundedString::new(self.max_message_bytes);
363 let mut fields = BoundedString::new(self.max_message_bytes);
364 event.record(&mut FieldVisitor::event(&mut message, &mut fields));
365
366 let mut output = BoundedString::new(self.max_message_bytes);
367 if self.include_target {
368 _ = write!(output, "[{}] ", event.metadata().target());
369 }
370
371 if !message.is_empty() {
372 output.push_bounded(&message);
373 }
374 if !fields.is_empty() {
375 if !message.is_empty() {
376 output.push_str(" ");
377 }
378 output.push_bounded(&fields);
379 }
380
381 if self.include_span_context {
382 self.append_span_context(event, ctx, &mut output);
383 }
384
385 output.into_c_message()
386 }
387
388 fn format_event_split<S>(&self, event: &Event<'_>, ctx: &Context<'_, S>) -> (Vec<u8>, Vec<u8>)
389 where
390 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
391 {
392 let mut message = BoundedString::new(self.max_message_bytes);
393 let mut fields = BoundedString::new(self.max_message_bytes);
394 event.record(&mut FieldVisitor::event(&mut message, &mut fields));
395
396 let mut public = BoundedString::new(self.max_message_bytes);
397 if self.include_target {
398 _ = write!(public, "[{}] ", event.metadata().target());
399 }
400 public.push_bounded(&message);
401
402 let mut private = BoundedString::new(self.max_message_bytes);
403 private.push_bounded(&fields);
404 if self.include_span_context {
405 self.append_span_context(event, ctx, &mut private);
406 }
407
408 (public.into_c_message(), private.into_c_message())
409 }
410
411 fn append_span_context<S>(
412 &self,
413 event: &Event<'_>,
414 ctx: &Context<'_, S>,
415 output: &mut BoundedString,
416 ) where
417 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
418 {
419 let mut wrote_span = false;
420 if let Some(scope) = ctx.event_scope(event) {
421 for span in scope.from_root() {
422 let extensions = span.extensions();
423 let Some(states) = extensions.get::<SpanStates>() else {
424 continue;
425 };
426 let Some(state) = states.0.get(&self.layer_id) else {
427 continue;
428 };
429
430 if !wrote_span {
431 if !output.is_empty() {
432 output.push_str(" ");
433 }
434 output.push_str("spans=[");
435 wrote_span = true;
436 } else {
437 output.push_str(" > ");
438 }
439
440 output.push_str(state.metadata.name());
441 if !state.fields.is_empty() {
442 output.push_str("{");
443 output.push_bounded(&state.fields);
444 output.push_str("}");
445 }
446 }
447 }
448 if wrote_span {
449 output.push_str("]");
450 }
451 }
452}
453
454impl Clone for OsLogLayer {
455 fn clone(&self) -> Self {
456 Self {
457 log: Arc::clone(&self.log),
458 layer_id: next_layer_id(),
459 privacy: self.privacy,
460 span_mode: self.span_mode,
461 level_map: self.level_map,
462 include_target: self.include_target,
463 include_span_context: self.include_span_context,
464 max_message_bytes: self.max_message_bytes,
465 }
466 }
467}
468
469impl fmt::Debug for OsLogLayer {
470 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
471 f.debug_struct("OsLogLayer")
472 .field("layer_id", &self.layer_id)
473 .field("privacy", &self.privacy)
474 .field("span_mode", &self.span_mode)
475 .field("level_map", &self.level_map)
476 .field("include_target", &self.include_target)
477 .field("include_span_context", &self.include_span_context)
478 .field("max_message_bytes", &self.max_message_bytes)
479 .finish_non_exhaustive()
480 }
481}
482
483impl<S> Layer<S> for OsLogLayer
484where
485 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
486{
487 fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
488 let signpost_enabled = self.span_mode == SpanMode::Signposts && self.log.signpost_enabled();
489 if !self.include_span_context && !signpost_enabled {
490 return;
491 }
492
493 let Some(span) = ctx.span(id) else {
494 return;
495 };
496
497 let mut fields = BoundedString::new(self.max_message_bytes);
498 attrs.record(&mut FieldVisitor::fields(&mut fields));
499
500 let signpost_id = if signpost_enabled {
501 let signpost_id = self.log.signpost_id_generate();
502 if is_valid_signpost_id(signpost_id) {
503 Some(signpost_id)
504 } else {
505 None
506 }
507 } else {
508 None
509 };
510
511 let state = SpanState {
512 metadata: attrs.metadata(),
513 fields,
514 signpost_id,
515 };
516
517 if let Some(signpost_id) = signpost_id {
518 if self.privacy == Privacy::PublicMessagePrivateFields {
519 let (public, private) = self.format_span_split(&state);
520 self.log
521 .signpost_begin_split(signpost_id, &public, &private);
522 } else {
523 let message = self.format_span(&state);
524 self.log.signpost_begin(signpost_id, &message, self.privacy);
525 }
526 }
527
528 let mut extensions = span.extensions_mut();
529 if let Some(states) = extensions.get_mut::<SpanStates>() {
530 states.0.insert(self.layer_id, state);
531 } else {
532 let mut states = HashMap::default();
533 states.insert(self.layer_id, state);
534 extensions.insert(SpanStates(states));
535 }
536 }
537
538 fn on_record(&self, id: &Id, values: &Record<'_>, ctx: Context<'_, S>) {
539 let Some(span) = ctx.span(id) else {
540 return;
541 };
542 let mut extensions = span.extensions_mut();
543 let Some(states) = extensions.get_mut::<SpanStates>() else {
544 return;
545 };
546 let Some(state) = states.0.get_mut(&self.layer_id) else {
547 return;
548 };
549
550 values.record(&mut FieldVisitor::fields(&mut state.fields));
551 }
552
553 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
554 let os_type = self.level_map.get(*event.metadata().level());
555 if !self.log.enabled(os_type) {
556 return;
557 }
558
559 if self.privacy == Privacy::PublicMessagePrivateFields {
560 let (public, private) = self.format_event_split(event, &ctx);
561 self.log.emit_split(os_type, &public, &private);
562 } else {
563 let message = self.format_event(event, &ctx);
564 self.log.emit(os_type, &message, self.privacy);
565 }
566 }
567
568 fn on_close(&self, id: Id, ctx: Context<'_, S>) {
569 let Some(span) = ctx.span(&id) else {
570 return;
571 };
572 let mut extensions = span.extensions_mut();
573 let Some(states) = extensions.get_mut::<SpanStates>() else {
574 return;
575 };
576 let Some(state) = states.0.remove(&self.layer_id) else {
577 return;
578 };
579
580 if let Some(signpost_id) = state.signpost_id {
581 if self.privacy == Privacy::PublicMessagePrivateFields {
582 let (public, private) = self.format_span_split(&state);
583 self.log.signpost_end_split(signpost_id, &public, &private);
584 } else {
585 let message = self.format_span(&state);
586 self.log.signpost_end(signpost_id, &message, self.privacy);
587 }
588 }
589 }
590}
591
592struct LogHandle(NonNull<c_void>);
593
594unsafe impl Send for LogHandle {}
597unsafe impl Sync for LogHandle {}
600
601impl LogHandle {
602 fn enabled(&self, os_type: OsLogType) -> bool {
603 unsafe { ffi::rama_apple_oslog_enabled(self.0.as_ptr(), os_type as u8) != 0 }
606 }
607
608 fn emit(&self, os_type: OsLogType, message: &[u8], privacy: Privacy) {
609 debug_assert_eq!(message.last(), Some(&0));
610 unsafe {
613 ffi::rama_apple_oslog_emit(
614 self.0.as_ptr(),
615 os_type as u8,
616 message.as_ptr().cast::<c_char>(),
617 privacy as u8,
618 );
619 }
620 }
621
622 fn emit_split(&self, os_type: OsLogType, public: &[u8], private: &[u8]) {
623 debug_assert_eq!(public.last(), Some(&0));
624 debug_assert_eq!(private.last(), Some(&0));
625 if private == [0] {
626 self.emit(os_type, public, Privacy::Public);
627 return;
628 }
629 unsafe {
630 ffi::rama_apple_oslog_emit_split(
631 self.0.as_ptr(),
632 os_type as u8,
633 public.as_ptr().cast::<c_char>(),
634 private.as_ptr().cast::<c_char>(),
635 );
636 }
637 }
638
639 fn signpost_enabled(&self) -> bool {
640 unsafe { ffi::rama_apple_oslog_signpost_enabled(self.0.as_ptr()) != 0 }
643 }
644
645 fn signpost_id_generate(&self) -> u64 {
646 unsafe { ffi::rama_apple_oslog_signpost_id_generate(self.0.as_ptr()) }
649 }
650
651 fn signpost_begin(&self, signpost_id: u64, message: &[u8], privacy: Privacy) {
652 debug_assert!(is_valid_signpost_id(signpost_id));
653 debug_assert_eq!(message.last(), Some(&0));
654 unsafe {
657 ffi::rama_apple_oslog_signpost_begin(
658 self.0.as_ptr(),
659 signpost_id,
660 message.as_ptr().cast::<c_char>(),
661 privacy as u8,
662 );
663 }
664 }
665
666 fn signpost_end(&self, signpost_id: u64, message: &[u8], privacy: Privacy) {
667 debug_assert!(is_valid_signpost_id(signpost_id));
668 debug_assert_eq!(message.last(), Some(&0));
669 unsafe {
672 ffi::rama_apple_oslog_signpost_end(
673 self.0.as_ptr(),
674 signpost_id,
675 message.as_ptr().cast::<c_char>(),
676 privacy as u8,
677 );
678 }
679 }
680
681 fn signpost_begin_split(&self, signpost_id: u64, public: &[u8], private: &[u8]) {
682 debug_assert!(is_valid_signpost_id(signpost_id));
683 debug_assert_eq!(public.last(), Some(&0));
684 debug_assert_eq!(private.last(), Some(&0));
685 if private == [0] {
686 self.signpost_begin(signpost_id, public, Privacy::Public);
687 return;
688 }
689 unsafe {
690 ffi::rama_apple_oslog_signpost_begin_split(
691 self.0.as_ptr(),
692 signpost_id,
693 public.as_ptr().cast::<c_char>(),
694 private.as_ptr().cast::<c_char>(),
695 );
696 }
697 }
698
699 fn signpost_end_split(&self, signpost_id: u64, public: &[u8], private: &[u8]) {
700 debug_assert!(is_valid_signpost_id(signpost_id));
701 debug_assert_eq!(public.last(), Some(&0));
702 debug_assert_eq!(private.last(), Some(&0));
703 if private == [0] {
704 self.signpost_end(signpost_id, public, Privacy::Public);
705 return;
706 }
707 unsafe {
708 ffi::rama_apple_oslog_signpost_end_split(
709 self.0.as_ptr(),
710 signpost_id,
711 public.as_ptr().cast::<c_char>(),
712 private.as_ptr().cast::<c_char>(),
713 );
714 }
715 }
716}
717
718impl Drop for LogHandle {
719 fn drop(&mut self) {
720 unsafe { ffi::rama_apple_oslog_release(self.0.as_ptr()) };
723 }
724}
725
726struct SpanStates(HashMap<u64, SpanState>);
727
728struct SpanState {
729 metadata: &'static Metadata<'static>,
730 fields: BoundedString,
731 signpost_id: Option<u64>,
732}
733
734struct FieldVisitor<'a> {
735 message: Option<&'a mut BoundedString>,
736 fields: &'a mut BoundedString,
737}
738
739impl<'a> FieldVisitor<'a> {
740 fn event(message: &'a mut BoundedString, fields: &'a mut BoundedString) -> Self {
741 Self {
742 message: Some(message),
743 fields,
744 }
745 }
746
747 fn fields(fields: &'a mut BoundedString) -> Self {
748 Self {
749 message: None,
750 fields,
751 }
752 }
753
754 fn record_value(&mut self, field: &Field, write_value: impl FnOnce(&mut BoundedString, bool)) {
755 if field.name() == "message"
756 && let Some(message) = self.message.as_deref_mut()
757 {
758 write_value(message, true);
759 return;
760 }
761
762 if !self.fields.is_empty() {
763 self.fields.push_str(" ");
764 }
765 self.fields.push_str(field.name());
766 self.fields.push_str("=");
767 write_value(self.fields, false);
768 }
769}
770
771impl Visit for FieldVisitor<'_> {
772 fn record_f64(&mut self, field: &Field, value: f64) {
773 self.record_value(field, |output, _| _ = write!(output, "{value}"));
774 }
775
776 fn record_i64(&mut self, field: &Field, value: i64) {
777 self.record_value(field, |output, _| _ = write!(output, "{value}"));
778 }
779
780 fn record_u64(&mut self, field: &Field, value: u64) {
781 self.record_value(field, |output, _| _ = write!(output, "{value}"));
782 }
783
784 fn record_i128(&mut self, field: &Field, value: i128) {
785 self.record_value(field, |output, _| _ = write!(output, "{value}"));
786 }
787
788 fn record_u128(&mut self, field: &Field, value: u128) {
789 self.record_value(field, |output, _| _ = write!(output, "{value}"));
790 }
791
792 fn record_bool(&mut self, field: &Field, value: bool) {
793 self.record_value(field, |output, _| _ = write!(output, "{value}"));
794 }
795
796 fn record_str(&mut self, field: &Field, value: &str) {
797 self.record_value(field, |output, is_message| {
798 if is_message {
799 output.push_str(value);
800 } else {
801 _ = write!(output, "{value:?}");
802 }
803 });
804 }
805
806 fn record_bytes(&mut self, field: &Field, value: &[u8]) {
807 self.record_value(field, |output, _| _ = write!(output, "{value:?}"));
808 }
809
810 fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
811 self.record_value(field, |output, _| _ = write!(output, "{value}"));
812 }
813
814 fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
815 self.record_value(field, |output, _| _ = write!(output, "{value:?}"));
816 }
817}
818
819#[derive(Debug)]
820struct BoundedString {
821 value: String,
822 max_bytes: usize,
823 truncated: bool,
824}
825
826impl BoundedString {
827 fn new(max_bytes: usize) -> Self {
828 Self {
829 value: String::with_capacity(max_bytes.min(256)),
830 max_bytes,
831 truncated: false,
832 }
833 }
834
835 fn is_empty(&self) -> bool {
836 self.value.is_empty()
837 }
838
839 fn as_str(&self) -> &str {
840 &self.value
841 }
842
843 fn push_str(&mut self, value: &str) {
844 _ = self.write_str(value);
845 }
846
847 fn push_bounded(&mut self, value: &Self) {
848 self.push_str(value.as_str());
849 self.truncated |= value.truncated;
850 }
851
852 fn into_c_message(mut self) -> Vec<u8> {
853 if self.truncated {
854 let keep = self.max_bytes.saturating_sub(MIN_MESSAGE_BYTES);
855 truncate_utf8(&mut self.value, keep);
856 self.value.push_str("...");
857 }
858
859 if self.value.contains('\0') {
860 let mut escaped = Self::new(self.max_bytes);
861 for part in self.value.split_inclusive('\0') {
862 if let Some(without_nul) = part.strip_suffix('\0') {
863 escaped.push_str(without_nul);
864 escaped.push_str("\\0");
865 } else {
866 escaped.push_str(part);
867 }
868 }
869 return escaped.into_c_message();
870 }
871
872 let mut bytes = self.value.into_bytes();
873 bytes.push(0);
874 bytes
875 }
876}
877
878impl fmt::Write for BoundedString {
879 fn write_str(&mut self, value: &str) -> fmt::Result {
880 let remaining = self.max_bytes.saturating_sub(self.value.len());
881 if value.len() <= remaining {
882 self.value.push_str(value);
883 return Ok(());
884 }
885
886 let mut end = remaining;
887 while end > 0 && !value.is_char_boundary(end) {
888 end -= 1;
889 }
890 self.value.push_str(&value[..end]);
891 self.truncated = true;
892 Ok(())
893 }
894}
895
896fn truncate_utf8(value: &mut String, max_bytes: usize) {
897 if value.len() <= max_bytes {
898 return;
899 }
900
901 let mut end = max_bytes;
902 while end > 0 && !value.is_char_boundary(end) {
903 end -= 1;
904 }
905 value.truncate(end);
906}
907
908fn next_layer_id() -> u64 {
909 NEXT_LAYER_ID.fetch_add(1, Ordering::Relaxed)
910}
911
912const fn is_valid_signpost_id(signpost_id: u64) -> bool {
913 signpost_id != OS_SIGNPOST_ID_NULL && signpost_id != OS_SIGNPOST_ID_INVALID
914}
915
916mod ffi {
917 use std::ffi::{c_char, c_void};
918
919 unsafe extern "C" {
920 pub(super) fn rama_apple_oslog_create(
921 subsystem: *const c_char,
922 category: *const c_char,
923 ) -> *mut c_void;
924 pub(super) fn rama_apple_oslog_release(log: *mut c_void);
925 pub(super) fn rama_apple_oslog_enabled(log: *mut c_void, os_type: u8) -> u8;
926 pub(super) fn rama_apple_oslog_emit(
927 log: *mut c_void,
928 os_type: u8,
929 message: *const c_char,
930 is_public: u8,
931 );
932 pub(super) fn rama_apple_oslog_emit_split(
933 log: *mut c_void,
934 os_type: u8,
935 public_message: *const c_char,
936 private_fields: *const c_char,
937 );
938
939 pub(super) fn rama_apple_oslog_signpost_enabled(log: *mut c_void) -> u8;
940 pub(super) fn rama_apple_oslog_signpost_id_generate(log: *mut c_void) -> u64;
941 pub(super) fn rama_apple_oslog_signpost_begin(
942 log: *mut c_void,
943 signpost_id: u64,
944 message: *const c_char,
945 is_public: u8,
946 );
947 pub(super) fn rama_apple_oslog_signpost_end(
948 log: *mut c_void,
949 signpost_id: u64,
950 message: *const c_char,
951 is_public: u8,
952 );
953 pub(super) fn rama_apple_oslog_signpost_begin_split(
954 log: *mut c_void,
955 signpost_id: u64,
956 public_message: *const c_char,
957 private_fields: *const c_char,
958 );
959 pub(super) fn rama_apple_oslog_signpost_end_split(
960 log: *mut c_void,
961 signpost_id: u64,
962 public_message: *const c_char,
963 private_fields: *const c_char,
964 );
965 }
966}
967
968#[cfg(test)]
969mod tests {
970 use super::*;
971 use crate::telemetry::tracing::{self, subscriber::layer::SubscriberExt as _};
972 use std::sync::RwLock;
973
974 struct FormattingCapture {
975 formatter: OsLogLayer,
976 events: Arc<RwLock<Vec<String>>>,
977 }
978
979 impl<S> Layer<S> for FormattingCapture
980 where
981 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
982 {
983 fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
984 <OsLogLayer as Layer<S>>::on_new_span(&self.formatter, attrs, id, ctx);
985 }
986
987 fn on_record(&self, id: &Id, values: &Record<'_>, ctx: Context<'_, S>) {
988 <OsLogLayer as Layer<S>>::on_record(&self.formatter, id, values, ctx);
989 }
990
991 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
992 let message = if self.formatter.privacy == Privacy::PublicMessagePrivateFields {
993 let (public, private) = self.formatter.format_event_split(event, &ctx);
994 format!(
995 "{} |private| {}",
996 String::from_utf8(public[..public.len() - 1].to_vec()).unwrap(),
997 String::from_utf8(private[..private.len() - 1].to_vec()).unwrap()
998 )
999 } else {
1000 let message = self.formatter.format_event(event, &ctx);
1001 String::from_utf8(message[..message.len() - 1].to_vec()).unwrap()
1002 };
1003 self.events.write().unwrap().push(message);
1004 }
1005
1006 fn on_close(&self, id: Id, ctx: Context<'_, S>) {
1007 <OsLogLayer as Layer<S>>::on_close(&self.formatter, id, ctx);
1008 }
1009 }
1010
1011 #[test]
1012 fn signposts_are_enabled_by_default() {
1013 assert_eq!(SpanMode::default(), SpanMode::Signposts);
1014 }
1015
1016 #[test]
1017 fn level_maps_are_explicit_about_faults() {
1018 assert_eq!(LevelMap::apple().get(Level::ERROR), OsLogType::Error);
1019 assert_eq!(
1020 LevelMap::persistent_info().get(Level::INFO),
1021 OsLogType::Default
1022 );
1023 assert_eq!(
1024 LevelMap::tracing_oslog_compatible().get(Level::ERROR),
1025 OsLogType::Fault
1026 );
1027 }
1028
1029 #[test]
1030 fn bounded_message_is_utf8_safe_and_nul_free() {
1031 let mut message = BoundedString::new(8);
1032 message.push_str("ééééé");
1033 let message = message.into_c_message();
1034
1035 assert_eq!(message.last(), Some(&0));
1036 std::str::from_utf8(&message[..message.len() - 1]).unwrap();
1037 assert!(message.len() <= 9);
1038
1039 let mut message = BoundedString::new(16);
1040 message.push_str("left\0right");
1041 let message = message.into_c_message();
1042 assert_eq!(&message[..message.len() - 1], b"left\\0right");
1043 }
1044
1045 #[test]
1046 fn invalid_subsystem_and_category_are_errors() {
1047 assert!(matches!(
1048 OsLogLayer::new("bad\0subsystem", "category"),
1049 Err(OsLogError::InvalidSubsystem(_))
1050 ));
1051 assert!(matches!(
1052 OsLogLayer::new("com.example", "bad\0category"),
1053 Err(OsLogError::InvalidCategory(_))
1054 ));
1055 }
1056
1057 #[test]
1058 fn explicit_parents_records_and_multiple_layers_do_not_panic() {
1059 let first = OsLogLayer::new("org.plabayo.rama.test", "first")
1060 .unwrap()
1061 .with_privacy(Privacy::Public)
1062 .with_level_map(LevelMap::persistent_info())
1063 .with_span_mode(SpanMode::Signposts)
1064 .with_span_context(true);
1065 let second = first.clone().with_target(false);
1066 let subscriber = tracing::subscriber::registry().with(first).with(second);
1067 let dispatch = tracing::Dispatch::new(subscriber);
1068
1069 tracing::dispatcher::with_default(&dispatch, || {
1070 let span = tracing::info_span!("request", request.id = tracing::field::Empty);
1071 span.record("request.id", 42_u64);
1072 tracing::info!(parent: &span, answer = 42, "explicit parent");
1073 tracing::info!(parent: None, "explicit root");
1074 });
1075 }
1076
1077 #[test]
1078 fn event_formatting_uses_explicit_scope_and_late_records() {
1079 let events = Arc::new(RwLock::new(Vec::new()));
1080 let formatter = OsLogLayer::new("org.plabayo.rama.test", "format")
1081 .unwrap()
1082 .with_target(false)
1083 .with_span_context(true);
1084 let capture = FormattingCapture {
1085 formatter,
1086 events: Arc::clone(&events),
1087 };
1088 let dispatch = tracing::Dispatch::new(tracing::subscriber::registry().with(capture));
1089
1090 tracing::dispatcher::with_default(&dispatch, || {
1091 let span = tracing::info_span!("request", request.id = tracing::field::Empty);
1092 span.record("request.id", 42_u64);
1093 tracing::info!(parent: &span, answer = 42, "explicit parent");
1094 tracing::info!(parent: None, "explicit root");
1095 });
1096
1097 let events = events.read().unwrap();
1098 assert_eq!(events.len(), 2);
1099 assert!(events[0].contains("explicit parent answer=42"));
1100 assert!(events[0].contains("spans=[request{request.id=42}]"));
1101 assert_eq!(events[1], "explicit root");
1102 }
1103
1104 #[test]
1105 fn split_privacy_keeps_fields_out_of_public_message() {
1106 let events = Arc::new(RwLock::new(Vec::new()));
1107 let formatter = OsLogLayer::new("org.plabayo.rama.test", "format")
1108 .unwrap()
1109 .with_privacy(Privacy::PublicMessagePrivateFields)
1110 .with_target(false)
1111 .with_span_context(true);
1112 let capture = FormattingCapture {
1113 formatter,
1114 events: Arc::clone(&events),
1115 };
1116 let dispatch = tracing::Dispatch::new(tracing::subscriber::registry().with(capture));
1117
1118 tracing::dispatcher::with_default(&dispatch, || {
1119 let span = tracing::info_span!("request", user = "private-user");
1120 tracing::info!(parent: &span, endpoint = "/private", "request finished");
1121 });
1122
1123 let events = events.read().unwrap();
1124 assert!(events[0].starts_with("request finished |private| "));
1125 assert!(
1126 !events[0]
1127 .split(" |private| ")
1128 .next()
1129 .unwrap()
1130 .contains("private")
1131 );
1132 assert!(events[0].contains("endpoint=\"/private\""));
1133 assert!(events[0].contains("user=\"private-user\""));
1134 }
1135}