Skip to main content

rama/telemetry/tracing/apple/oslog/
mod.rs

1//! A [`tracing_subscriber`] layer for Apple unified logging.
2//!
3//! [`OsLogLayer`] writes tracing events to an Apple `os_log_t`. By default,
4//! spans are represented as signpost intervals for inspection in Instruments.
5//! Each interval measures the span's lifetime from creation until final close,
6//! rather than only the time during which the span is entered. Span context in
7//! event messages remains optional and is disabled by default.
8//!
9//! The layer checks whether Apple logging is enabled for an event's mapped
10//! level before it visits or formats that event. This check is deliberately
11//! performed inside [`Layer::on_event`]: returning `false` from
12//! [`Layer::enabled`] would disable the event for every other layer in the
13//! subscriber stack as well.
14//!
15//! # Example
16//!
17//! ```no_run
18//! use rama::telemetry::tracing::{
19//!     self,
20//!     apple::oslog::{OsLogLayer, Privacy},
21//!     subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _},
22//! };
23//!
24//! let oslog = OsLogLayer::new("com.example.proxy", "network")?
25//!     .with_privacy(Privacy::Public)
26//!     .with_span_context(true);
27//!
28//! tracing::subscriber::registry().with(oslog).try_init()?;
29//! # Ok::<(), Box<dyn std::error::Error>>(())
30//! ```
31
32use 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/// Privacy applied to the event or signpost's dynamic text.
61///
62/// Apple normally treats dynamic strings as private. Use [`Self::Public`] only
63/// when the formatted tracing message and all of its fields are known not to
64/// contain user or secret data.
65#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
66#[repr(u8)]
67pub enum Privacy {
68    /// Redact the dynamic text in persisted logs.
69    #[default]
70    Private = 0,
71    /// Store the dynamic text without redaction.
72    Public = 1,
73    /// Store the target and event message publicly while keeping structured
74    /// fields and span fields private.
75    ///
76    /// Callers must ensure the tracing `message` itself contains no user or
77    /// secret data. Prefer structured fields for values that need redaction.
78    PublicMessagePrivateFields = 2,
79}
80
81/// Controls whether tracing spans are exported as Apple signposts.
82#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
83pub enum SpanMode {
84    /// Do not emit signpost intervals.
85    ///
86    /// This avoids the per-span runtime-enabled check and is useful for
87    /// applications that create spans at particularly high volume.
88    Disabled,
89    /// Emit a signpost interval from span creation until span close.
90    ///
91    /// This represents the full lifetime of a tracing span, which can include
92    /// idle time and multiple enter/exit cycles, rather than only active time.
93    ///
94    /// Apple signposts require a static interval name, so Rama uses the fixed
95    /// name `tracing-span` and writes the tracing target, span name, and fields
96    /// into the signpost's dynamic message.
97    #[default]
98    Signposts,
99}
100
101/// Native Apple unified-log types.
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103#[repr(u8)]
104pub enum OsLogType {
105    /// Persisted notice/default message.
106    Default = 0x00,
107    /// Informational message, normally held in memory only.
108    Info = 0x01,
109    /// Debug message, captured only when debug logging is enabled.
110    Debug = 0x02,
111    /// Process-level error.
112    Error = 0x10,
113    /// System-level or multi-process fault.
114    Fault = 0x11,
115}
116
117/// Maps tracing levels to Apple unified-log types.
118#[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    /// Create a custom level map.
129    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    /// Match the semantics of Apple's Swift `Logger` convenience methods.
146    ///
147    /// Trace and debug share Apple's debug type, warning and error share its
148    /// error type, and fault is never selected implicitly.
149    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    /// Persist tracing `INFO` events while keeping ordinary errors below fault.
160    ///
161    /// This is useful for rare lifecycle events that must survive for later
162    /// `log show` inspection.
163    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    /// Preserve the level mapping used by `tracing-oslog` 0.3.
174    ///
175    /// In particular, every tracing error becomes an Apple fault. Prefer
176    /// [`Self::apple`] or [`Self::persistent_info`] for new integrations.
177    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/// Failure to create an Apple unified-log layer.
205#[derive(Debug)]
206pub enum OsLogError {
207    /// The subsystem contained an interior NUL byte.
208    InvalidSubsystem(NulError),
209    /// The category contained an interior NUL byte.
210    InvalidCategory(NulError),
211    /// Apple unexpectedly returned a null log handle.
212    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
234/// A composable [`tracing_subscriber::Layer`] that writes to Apple unified
235/// logging.
236pub 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    /// Create a layer for one fixed Apple subsystem/category pair.
249    ///
250    /// Apple caches these pairs for the lifetime of the process, so callers
251    /// should create a small, fixed set rather than derive categories from
252    /// request or span data.
253    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        // SAFETY: both pointers are valid NUL-terminated strings for the
258        // duration of this call. The shim returns the retained os_log_t as an
259        // opaque pointer.
260        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        /// Set the privacy applied to all dynamic text emitted by this layer.
277        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        /// Configure native signpost export for spans.
285        ///
286        /// Defaults to [`SpanMode::Signposts`].
287        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        /// Set the tracing-to-Apple level mapping.
295        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        /// Include or omit the tracing target in event and signpost messages.
303        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        /// Include or omit the event's explicit/contextual span path and fields.
311        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        /// Bound the formatted dynamic payload.
319        ///
320        /// Apple caps persisted dynamic content at roughly 1 KiB. Values
321        /// below three bytes are raised to three so truncation can be represented
322        /// by `...`.
323        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
594// SAFETY: Apple documents os_log_t values as process-wide logging handles;
595// os_log calls are safe to make concurrently from different threads.
596unsafe impl Send for LogHandle {}
597// SAFETY: see the Send implementation above. The handle is immutable and all
598// operations are delegated to Apple's thread-safe unified logging runtime.
599unsafe impl Sync for LogHandle {}
600
601impl LogHandle {
602    fn enabled(&self, os_type: OsLogType) -> bool {
603        // SAFETY: self.0 is a retained os_log_t and os_type has one of Apple's
604        // documented os_log_type_t values.
605        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        // SAFETY: message is NUL-terminated and lives for the synchronous shim
611        // call; self.0 is a valid os_log_t.
612        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        // SAFETY: self.0 is a valid os_log_t. The shim also performs the OS
641        // availability check before touching signpost APIs.
642        unsafe { ffi::rama_apple_oslog_signpost_enabled(self.0.as_ptr()) != 0 }
643    }
644
645    fn signpost_id_generate(&self) -> u64 {
646        // SAFETY: self.0 is a valid os_log_t and the shim availability-checks
647        // the signpost API.
648        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        // SAFETY: the ID came from Apple for this handle, message is
655        // NUL-terminated, and the shim performs the availability check.
656        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        // SAFETY: this matches a begin emitted by this handle, message is
670        // NUL-terminated, and the shim availability-checks the API.
671        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        // SAFETY: os_log_create returned this retained handle, and Arc ensures
721        // it is released exactly once after the last layer clone is dropped.
722        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}