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