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}
74
75/// Controls whether tracing spans are exported as Apple signposts.
76#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
77pub enum SpanMode {
78    /// Do not emit signpost intervals.
79    ///
80    /// This avoids the per-span runtime-enabled check and is useful for
81    /// applications that create spans at particularly high volume.
82    Disabled,
83    /// Emit a signpost interval from span creation until span close.
84    ///
85    /// This represents the full lifetime of a tracing span, which can include
86    /// idle time and multiple enter/exit cycles, rather than only active time.
87    ///
88    /// Apple signposts require a static interval name, so Rama uses the fixed
89    /// name `tracing-span` and writes the tracing target, span name, and fields
90    /// into the signpost's dynamic message.
91    #[default]
92    Signposts,
93}
94
95/// Native Apple unified-log types.
96#[derive(Clone, Copy, Debug, Eq, PartialEq)]
97#[repr(u8)]
98pub enum OsLogType {
99    /// Persisted notice/default message.
100    Default = 0x00,
101    /// Informational message, normally held in memory only.
102    Info = 0x01,
103    /// Debug message, captured only when debug logging is enabled.
104    Debug = 0x02,
105    /// Process-level error.
106    Error = 0x10,
107    /// System-level or multi-process fault.
108    Fault = 0x11,
109}
110
111/// Maps tracing levels to Apple unified-log types.
112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113pub struct LevelMap {
114    trace: OsLogType,
115    debug: OsLogType,
116    info: OsLogType,
117    warn: OsLogType,
118    error: OsLogType,
119}
120
121impl LevelMap {
122    /// Create a custom level map.
123    pub const fn new(
124        trace: OsLogType,
125        debug: OsLogType,
126        info: OsLogType,
127        warn: OsLogType,
128        error: OsLogType,
129    ) -> Self {
130        Self {
131            trace,
132            debug,
133            info,
134            warn,
135            error,
136        }
137    }
138
139    /// Match the semantics of Apple's Swift `Logger` convenience methods.
140    ///
141    /// Trace and debug share Apple's debug type, warning and error share its
142    /// error type, and fault is never selected implicitly.
143    pub const fn apple() -> Self {
144        Self::new(
145            OsLogType::Debug,
146            OsLogType::Debug,
147            OsLogType::Info,
148            OsLogType::Error,
149            OsLogType::Error,
150        )
151    }
152
153    /// Persist tracing `INFO` events while keeping ordinary errors below fault.
154    ///
155    /// This is useful for rare lifecycle events that must survive for later
156    /// `log show` inspection.
157    pub const fn persistent_info() -> Self {
158        Self::new(
159            OsLogType::Debug,
160            OsLogType::Info,
161            OsLogType::Default,
162            OsLogType::Error,
163            OsLogType::Error,
164        )
165    }
166
167    /// Preserve the level mapping used by `tracing-oslog` 0.3.
168    ///
169    /// In particular, every tracing error becomes an Apple fault. Prefer
170    /// [`Self::apple`] or [`Self::persistent_info`] for new integrations.
171    pub const fn tracing_oslog_compatible() -> Self {
172        Self::new(
173            OsLogType::Debug,
174            OsLogType::Info,
175            OsLogType::Default,
176            OsLogType::Error,
177            OsLogType::Fault,
178        )
179    }
180
181    const fn get(self, level: Level) -> OsLogType {
182        match level {
183            Level::TRACE => self.trace,
184            Level::DEBUG => self.debug,
185            Level::INFO => self.info,
186            Level::WARN => self.warn,
187            Level::ERROR => self.error,
188        }
189    }
190}
191
192impl Default for LevelMap {
193    fn default() -> Self {
194        Self::apple()
195    }
196}
197
198/// Failure to create an Apple unified-log layer.
199#[derive(Debug)]
200pub enum OsLogError {
201    /// The subsystem contained an interior NUL byte.
202    InvalidSubsystem(NulError),
203    /// The category contained an interior NUL byte.
204    InvalidCategory(NulError),
205    /// Apple unexpectedly returned a null log handle.
206    CreateFailed,
207}
208
209impl fmt::Display for OsLogError {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        match self {
212            Self::InvalidSubsystem(_) => f.write_str("os_log subsystem contains a NUL byte"),
213            Self::InvalidCategory(_) => f.write_str("os_log category contains a NUL byte"),
214            Self::CreateFailed => f.write_str("Apple os_log_create returned a null handle"),
215        }
216    }
217}
218
219impl std::error::Error for OsLogError {
220    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
221        match self {
222            Self::InvalidSubsystem(err) | Self::InvalidCategory(err) => Some(err),
223            Self::CreateFailed => None,
224        }
225    }
226}
227
228/// A composable [`tracing_subscriber::Layer`] that writes to Apple unified
229/// logging.
230pub struct OsLogLayer {
231    log: Arc<LogHandle>,
232    layer_id: u64,
233    privacy: Privacy,
234    span_mode: SpanMode,
235    level_map: LevelMap,
236    include_target: bool,
237    include_span_context: bool,
238    max_message_bytes: usize,
239}
240
241impl OsLogLayer {
242    /// Create a layer for one fixed Apple subsystem/category pair.
243    ///
244    /// Apple caches these pairs for the lifetime of the process, so callers
245    /// should create a small, fixed set rather than derive categories from
246    /// request or span data.
247    pub fn new(subsystem: impl AsRef<str>, category: impl AsRef<str>) -> Result<Self, OsLogError> {
248        let subsystem = CString::new(subsystem.as_ref()).map_err(OsLogError::InvalidSubsystem)?;
249        let category = CString::new(category.as_ref()).map_err(OsLogError::InvalidCategory)?;
250
251        // SAFETY: both pointers are valid NUL-terminated strings for the
252        // duration of this call. The shim returns the retained os_log_t as an
253        // opaque pointer.
254        let raw = unsafe { ffi::rama_apple_oslog_create(subsystem.as_ptr(), category.as_ptr()) };
255        let raw = NonNull::new(raw).ok_or(OsLogError::CreateFailed)?;
256
257        Ok(Self {
258            log: Arc::new(LogHandle(raw)),
259            layer_id: next_layer_id(),
260            privacy: Privacy::default(),
261            span_mode: SpanMode::default(),
262            level_map: LevelMap::default(),
263            include_target: true,
264            include_span_context: false,
265            max_message_bytes: DEFAULT_MAX_MESSAGE_BYTES,
266        })
267    }
268
269    rama_utils::macros::generate_set_and_with! {
270        /// Set the privacy applied to all dynamic text emitted by this layer.
271        pub fn privacy(mut self, privacy: Privacy) -> Self {
272            self.privacy = privacy;
273            self
274        }
275    }
276
277    rama_utils::macros::generate_set_and_with! {
278        /// Configure native signpost export for spans.
279        ///
280        /// Defaults to [`SpanMode::Signposts`].
281        pub fn span_mode(mut self, span_mode: SpanMode) -> Self {
282            self.span_mode = span_mode;
283            self
284        }
285    }
286
287    rama_utils::macros::generate_set_and_with! {
288        /// Set the tracing-to-Apple level mapping.
289        pub fn level_map(mut self, level_map: LevelMap) -> Self {
290            self.level_map = level_map;
291            self
292        }
293    }
294
295    rama_utils::macros::generate_set_and_with! {
296        /// Include or omit the tracing target in event and signpost messages.
297        pub fn target(mut self, include_target: bool) -> Self {
298            self.include_target = include_target;
299            self
300        }
301    }
302
303    rama_utils::macros::generate_set_and_with! {
304        /// Include or omit the event's explicit/contextual span path and fields.
305        pub fn span_context(mut self, include_span_context: bool) -> Self {
306            self.include_span_context = include_span_context;
307            self
308        }
309    }
310
311    rama_utils::macros::generate_set_and_with! {
312        /// Bound the formatted dynamic payload.
313        ///
314        /// Apple caps persisted dynamic content at roughly 1 KiB. Values
315        /// below three bytes are raised to three so truncation can be represented
316        /// by `...`.
317        pub fn max_message_bytes(mut self, max_message_bytes: usize) -> Self {
318            self.max_message_bytes = if max_message_bytes < MIN_MESSAGE_BYTES {
319                MIN_MESSAGE_BYTES
320            } else {
321                max_message_bytes
322            };
323            self
324        }
325    }
326
327    fn format_span(&self, state: &SpanState) -> Vec<u8> {
328        let mut output = BoundedString::new(self.max_message_bytes);
329        if self.include_target {
330            _ = write!(output, "[{}] ", state.metadata.target());
331        }
332        output.push_str(state.metadata.name());
333        if !state.fields.is_empty() {
334            output.push_str(" ");
335            output.push_bounded(&state.fields);
336        }
337        output.into_c_message()
338    }
339
340    fn format_event<S>(&self, event: &Event<'_>, ctx: &Context<'_, S>) -> Vec<u8>
341    where
342        S: Subscriber + for<'lookup> LookupSpan<'lookup>,
343    {
344        let mut message = BoundedString::new(self.max_message_bytes);
345        let mut fields = BoundedString::new(self.max_message_bytes);
346        event.record(&mut FieldVisitor::event(&mut message, &mut fields));
347
348        let mut output = BoundedString::new(self.max_message_bytes);
349        if self.include_target {
350            _ = write!(output, "[{}] ", event.metadata().target());
351        }
352
353        if !message.is_empty() {
354            output.push_bounded(&message);
355        }
356        if !fields.is_empty() {
357            if !message.is_empty() {
358                output.push_str(" ");
359            }
360            output.push_bounded(&fields);
361        }
362
363        if self.include_span_context {
364            let mut wrote_span = false;
365            if let Some(scope) = ctx.event_scope(event) {
366                for span in scope.from_root() {
367                    let extensions = span.extensions();
368                    let Some(states) = extensions.get::<SpanStates>() else {
369                        continue;
370                    };
371                    let Some(state) = states.0.get(&self.layer_id) else {
372                        continue;
373                    };
374
375                    if !wrote_span {
376                        if !output.is_empty() {
377                            output.push_str(" ");
378                        }
379                        output.push_str("spans=[");
380                        wrote_span = true;
381                    } else {
382                        output.push_str(" > ");
383                    }
384
385                    output.push_str(state.metadata.name());
386                    if !state.fields.is_empty() {
387                        output.push_str("{");
388                        output.push_bounded(&state.fields);
389                        output.push_str("}");
390                    }
391                }
392            }
393            if wrote_span {
394                output.push_str("]");
395            }
396        }
397
398        output.into_c_message()
399    }
400}
401
402impl Clone for OsLogLayer {
403    fn clone(&self) -> Self {
404        Self {
405            log: Arc::clone(&self.log),
406            layer_id: next_layer_id(),
407            privacy: self.privacy,
408            span_mode: self.span_mode,
409            level_map: self.level_map,
410            include_target: self.include_target,
411            include_span_context: self.include_span_context,
412            max_message_bytes: self.max_message_bytes,
413        }
414    }
415}
416
417impl fmt::Debug for OsLogLayer {
418    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
419        f.debug_struct("OsLogLayer")
420            .field("layer_id", &self.layer_id)
421            .field("privacy", &self.privacy)
422            .field("span_mode", &self.span_mode)
423            .field("level_map", &self.level_map)
424            .field("include_target", &self.include_target)
425            .field("include_span_context", &self.include_span_context)
426            .field("max_message_bytes", &self.max_message_bytes)
427            .finish_non_exhaustive()
428    }
429}
430
431impl<S> Layer<S> for OsLogLayer
432where
433    S: Subscriber + for<'lookup> LookupSpan<'lookup>,
434{
435    fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
436        let signpost_enabled = self.span_mode == SpanMode::Signposts && self.log.signpost_enabled();
437        if !self.include_span_context && !signpost_enabled {
438            return;
439        }
440
441        let Some(span) = ctx.span(id) else {
442            return;
443        };
444
445        let mut fields = BoundedString::new(self.max_message_bytes);
446        attrs.record(&mut FieldVisitor::fields(&mut fields));
447
448        let signpost_id = if signpost_enabled {
449            let signpost_id = self.log.signpost_id_generate();
450            if is_valid_signpost_id(signpost_id) {
451                Some(signpost_id)
452            } else {
453                None
454            }
455        } else {
456            None
457        };
458
459        let state = SpanState {
460            metadata: attrs.metadata(),
461            fields,
462            signpost_id,
463        };
464
465        if let Some(signpost_id) = signpost_id {
466            let message = self.format_span(&state);
467            self.log.signpost_begin(signpost_id, &message, self.privacy);
468        }
469
470        let mut extensions = span.extensions_mut();
471        if let Some(states) = extensions.get_mut::<SpanStates>() {
472            states.0.insert(self.layer_id, state);
473        } else {
474            let mut states = HashMap::default();
475            states.insert(self.layer_id, state);
476            extensions.insert(SpanStates(states));
477        }
478    }
479
480    fn on_record(&self, id: &Id, values: &Record<'_>, ctx: Context<'_, S>) {
481        let Some(span) = ctx.span(id) else {
482            return;
483        };
484        let mut extensions = span.extensions_mut();
485        let Some(states) = extensions.get_mut::<SpanStates>() else {
486            return;
487        };
488        let Some(state) = states.0.get_mut(&self.layer_id) else {
489            return;
490        };
491
492        values.record(&mut FieldVisitor::fields(&mut state.fields));
493    }
494
495    fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
496        let os_type = self.level_map.get(*event.metadata().level());
497        if !self.log.enabled(os_type) {
498            return;
499        }
500
501        let message = self.format_event(event, &ctx);
502        self.log.emit(os_type, &message, self.privacy);
503    }
504
505    fn on_close(&self, id: Id, ctx: Context<'_, S>) {
506        let Some(span) = ctx.span(&id) else {
507            return;
508        };
509        let mut extensions = span.extensions_mut();
510        let Some(states) = extensions.get_mut::<SpanStates>() else {
511            return;
512        };
513        let Some(state) = states.0.remove(&self.layer_id) else {
514            return;
515        };
516
517        if let Some(signpost_id) = state.signpost_id {
518            let message = self.format_span(&state);
519            self.log.signpost_end(signpost_id, &message, self.privacy);
520        }
521    }
522}
523
524struct LogHandle(NonNull<c_void>);
525
526// SAFETY: Apple documents os_log_t values as process-wide logging handles;
527// os_log calls are safe to make concurrently from different threads.
528unsafe impl Send for LogHandle {}
529// SAFETY: see the Send implementation above. The handle is immutable and all
530// operations are delegated to Apple's thread-safe unified logging runtime.
531unsafe impl Sync for LogHandle {}
532
533impl LogHandle {
534    fn enabled(&self, os_type: OsLogType) -> bool {
535        // SAFETY: self.0 is a retained os_log_t and os_type has one of Apple's
536        // documented os_log_type_t values.
537        unsafe { ffi::rama_apple_oslog_enabled(self.0.as_ptr(), os_type as u8) != 0 }
538    }
539
540    fn emit(&self, os_type: OsLogType, message: &[u8], privacy: Privacy) {
541        debug_assert_eq!(message.last(), Some(&0));
542        // SAFETY: message is NUL-terminated and lives for the synchronous shim
543        // call; self.0 is a valid os_log_t.
544        unsafe {
545            ffi::rama_apple_oslog_emit(
546                self.0.as_ptr(),
547                os_type as u8,
548                message.as_ptr().cast::<c_char>(),
549                privacy as u8,
550            );
551        }
552    }
553
554    fn signpost_enabled(&self) -> bool {
555        // SAFETY: self.0 is a valid os_log_t. The shim also performs the OS
556        // availability check before touching signpost APIs.
557        unsafe { ffi::rama_apple_oslog_signpost_enabled(self.0.as_ptr()) != 0 }
558    }
559
560    fn signpost_id_generate(&self) -> u64 {
561        // SAFETY: self.0 is a valid os_log_t and the shim availability-checks
562        // the signpost API.
563        unsafe { ffi::rama_apple_oslog_signpost_id_generate(self.0.as_ptr()) }
564    }
565
566    fn signpost_begin(&self, signpost_id: u64, message: &[u8], privacy: Privacy) {
567        debug_assert!(is_valid_signpost_id(signpost_id));
568        debug_assert_eq!(message.last(), Some(&0));
569        // SAFETY: the ID came from Apple for this handle, message is
570        // NUL-terminated, and the shim performs the availability check.
571        unsafe {
572            ffi::rama_apple_oslog_signpost_begin(
573                self.0.as_ptr(),
574                signpost_id,
575                message.as_ptr().cast::<c_char>(),
576                privacy as u8,
577            );
578        }
579    }
580
581    fn signpost_end(&self, signpost_id: u64, message: &[u8], privacy: Privacy) {
582        debug_assert!(is_valid_signpost_id(signpost_id));
583        debug_assert_eq!(message.last(), Some(&0));
584        // SAFETY: this matches a begin emitted by this handle, message is
585        // NUL-terminated, and the shim availability-checks the API.
586        unsafe {
587            ffi::rama_apple_oslog_signpost_end(
588                self.0.as_ptr(),
589                signpost_id,
590                message.as_ptr().cast::<c_char>(),
591                privacy as u8,
592            );
593        }
594    }
595}
596
597impl Drop for LogHandle {
598    fn drop(&mut self) {
599        // SAFETY: os_log_create returned this retained handle, and Arc ensures
600        // it is released exactly once after the last layer clone is dropped.
601        unsafe { ffi::rama_apple_oslog_release(self.0.as_ptr()) };
602    }
603}
604
605struct SpanStates(HashMap<u64, SpanState>);
606
607struct SpanState {
608    metadata: &'static Metadata<'static>,
609    fields: BoundedString,
610    signpost_id: Option<u64>,
611}
612
613struct FieldVisitor<'a> {
614    message: Option<&'a mut BoundedString>,
615    fields: &'a mut BoundedString,
616}
617
618impl<'a> FieldVisitor<'a> {
619    fn event(message: &'a mut BoundedString, fields: &'a mut BoundedString) -> Self {
620        Self {
621            message: Some(message),
622            fields,
623        }
624    }
625
626    fn fields(fields: &'a mut BoundedString) -> Self {
627        Self {
628            message: None,
629            fields,
630        }
631    }
632
633    fn record_value(&mut self, field: &Field, write_value: impl FnOnce(&mut BoundedString, bool)) {
634        if field.name() == "message"
635            && let Some(message) = self.message.as_deref_mut()
636        {
637            write_value(message, true);
638            return;
639        }
640
641        if !self.fields.is_empty() {
642            self.fields.push_str(" ");
643        }
644        self.fields.push_str(field.name());
645        self.fields.push_str("=");
646        write_value(self.fields, false);
647    }
648}
649
650impl Visit for FieldVisitor<'_> {
651    fn record_f64(&mut self, field: &Field, value: f64) {
652        self.record_value(field, |output, _| _ = write!(output, "{value}"));
653    }
654
655    fn record_i64(&mut self, field: &Field, value: i64) {
656        self.record_value(field, |output, _| _ = write!(output, "{value}"));
657    }
658
659    fn record_u64(&mut self, field: &Field, value: u64) {
660        self.record_value(field, |output, _| _ = write!(output, "{value}"));
661    }
662
663    fn record_i128(&mut self, field: &Field, value: i128) {
664        self.record_value(field, |output, _| _ = write!(output, "{value}"));
665    }
666
667    fn record_u128(&mut self, field: &Field, value: u128) {
668        self.record_value(field, |output, _| _ = write!(output, "{value}"));
669    }
670
671    fn record_bool(&mut self, field: &Field, value: bool) {
672        self.record_value(field, |output, _| _ = write!(output, "{value}"));
673    }
674
675    fn record_str(&mut self, field: &Field, value: &str) {
676        self.record_value(field, |output, is_message| {
677            if is_message {
678                output.push_str(value);
679            } else {
680                _ = write!(output, "{value:?}");
681            }
682        });
683    }
684
685    fn record_bytes(&mut self, field: &Field, value: &[u8]) {
686        self.record_value(field, |output, _| _ = write!(output, "{value:?}"));
687    }
688
689    fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
690        self.record_value(field, |output, _| _ = write!(output, "{value}"));
691    }
692
693    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
694        self.record_value(field, |output, _| _ = write!(output, "{value:?}"));
695    }
696}
697
698#[derive(Debug)]
699struct BoundedString {
700    value: String,
701    max_bytes: usize,
702    truncated: bool,
703}
704
705impl BoundedString {
706    fn new(max_bytes: usize) -> Self {
707        Self {
708            value: String::with_capacity(max_bytes.min(256)),
709            max_bytes,
710            truncated: false,
711        }
712    }
713
714    fn is_empty(&self) -> bool {
715        self.value.is_empty()
716    }
717
718    fn as_str(&self) -> &str {
719        &self.value
720    }
721
722    fn push_str(&mut self, value: &str) {
723        _ = self.write_str(value);
724    }
725
726    fn push_bounded(&mut self, value: &Self) {
727        self.push_str(value.as_str());
728        self.truncated |= value.truncated;
729    }
730
731    fn into_c_message(mut self) -> Vec<u8> {
732        if self.truncated {
733            let keep = self.max_bytes.saturating_sub(MIN_MESSAGE_BYTES);
734            truncate_utf8(&mut self.value, keep);
735            self.value.push_str("...");
736        }
737
738        if self.value.contains('\0') {
739            let mut escaped = Self::new(self.max_bytes);
740            for part in self.value.split_inclusive('\0') {
741                if let Some(without_nul) = part.strip_suffix('\0') {
742                    escaped.push_str(without_nul);
743                    escaped.push_str("\\0");
744                } else {
745                    escaped.push_str(part);
746                }
747            }
748            return escaped.into_c_message();
749        }
750
751        let mut bytes = self.value.into_bytes();
752        bytes.push(0);
753        bytes
754    }
755}
756
757impl fmt::Write for BoundedString {
758    fn write_str(&mut self, value: &str) -> fmt::Result {
759        let remaining = self.max_bytes.saturating_sub(self.value.len());
760        if value.len() <= remaining {
761            self.value.push_str(value);
762            return Ok(());
763        }
764
765        let mut end = remaining;
766        while end > 0 && !value.is_char_boundary(end) {
767            end -= 1;
768        }
769        self.value.push_str(&value[..end]);
770        self.truncated = true;
771        Ok(())
772    }
773}
774
775fn truncate_utf8(value: &mut String, max_bytes: usize) {
776    if value.len() <= max_bytes {
777        return;
778    }
779
780    let mut end = max_bytes;
781    while end > 0 && !value.is_char_boundary(end) {
782        end -= 1;
783    }
784    value.truncate(end);
785}
786
787fn next_layer_id() -> u64 {
788    NEXT_LAYER_ID.fetch_add(1, Ordering::Relaxed)
789}
790
791const fn is_valid_signpost_id(signpost_id: u64) -> bool {
792    signpost_id != OS_SIGNPOST_ID_NULL && signpost_id != OS_SIGNPOST_ID_INVALID
793}
794
795mod ffi {
796    use std::ffi::{c_char, c_void};
797
798    unsafe extern "C" {
799        pub(super) fn rama_apple_oslog_create(
800            subsystem: *const c_char,
801            category: *const c_char,
802        ) -> *mut c_void;
803        pub(super) fn rama_apple_oslog_release(log: *mut c_void);
804        pub(super) fn rama_apple_oslog_enabled(log: *mut c_void, os_type: u8) -> u8;
805        pub(super) fn rama_apple_oslog_emit(
806            log: *mut c_void,
807            os_type: u8,
808            message: *const c_char,
809            is_public: u8,
810        );
811
812        pub(super) fn rama_apple_oslog_signpost_enabled(log: *mut c_void) -> u8;
813        pub(super) fn rama_apple_oslog_signpost_id_generate(log: *mut c_void) -> u64;
814        pub(super) fn rama_apple_oslog_signpost_begin(
815            log: *mut c_void,
816            signpost_id: u64,
817            message: *const c_char,
818            is_public: u8,
819        );
820        pub(super) fn rama_apple_oslog_signpost_end(
821            log: *mut c_void,
822            signpost_id: u64,
823            message: *const c_char,
824            is_public: u8,
825        );
826    }
827}
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832    use crate::telemetry::tracing::{self, subscriber::layer::SubscriberExt as _};
833    use std::sync::RwLock;
834
835    struct FormattingCapture {
836        formatter: OsLogLayer,
837        events: Arc<RwLock<Vec<String>>>,
838    }
839
840    impl<S> Layer<S> for FormattingCapture
841    where
842        S: Subscriber + for<'lookup> LookupSpan<'lookup>,
843    {
844        fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
845            <OsLogLayer as Layer<S>>::on_new_span(&self.formatter, attrs, id, ctx);
846        }
847
848        fn on_record(&self, id: &Id, values: &Record<'_>, ctx: Context<'_, S>) {
849            <OsLogLayer as Layer<S>>::on_record(&self.formatter, id, values, ctx);
850        }
851
852        fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
853            let message = self.formatter.format_event(event, &ctx);
854            let message = String::from_utf8(message[..message.len() - 1].to_vec()).unwrap();
855            self.events.write().unwrap().push(message);
856        }
857
858        fn on_close(&self, id: Id, ctx: Context<'_, S>) {
859            <OsLogLayer as Layer<S>>::on_close(&self.formatter, id, ctx);
860        }
861    }
862
863    #[test]
864    fn signposts_are_enabled_by_default() {
865        assert_eq!(SpanMode::default(), SpanMode::Signposts);
866    }
867
868    #[test]
869    fn level_maps_are_explicit_about_faults() {
870        assert_eq!(LevelMap::apple().get(Level::ERROR), OsLogType::Error);
871        assert_eq!(
872            LevelMap::persistent_info().get(Level::INFO),
873            OsLogType::Default
874        );
875        assert_eq!(
876            LevelMap::tracing_oslog_compatible().get(Level::ERROR),
877            OsLogType::Fault
878        );
879    }
880
881    #[test]
882    fn bounded_message_is_utf8_safe_and_nul_free() {
883        let mut message = BoundedString::new(8);
884        message.push_str("ééééé");
885        let message = message.into_c_message();
886
887        assert_eq!(message.last(), Some(&0));
888        std::str::from_utf8(&message[..message.len() - 1]).unwrap();
889        assert!(message.len() <= 9);
890
891        let mut message = BoundedString::new(16);
892        message.push_str("left\0right");
893        let message = message.into_c_message();
894        assert_eq!(&message[..message.len() - 1], b"left\\0right");
895    }
896
897    #[test]
898    fn invalid_subsystem_and_category_are_errors() {
899        assert!(matches!(
900            OsLogLayer::new("bad\0subsystem", "category"),
901            Err(OsLogError::InvalidSubsystem(_))
902        ));
903        assert!(matches!(
904            OsLogLayer::new("com.example", "bad\0category"),
905            Err(OsLogError::InvalidCategory(_))
906        ));
907    }
908
909    #[test]
910    fn explicit_parents_records_and_multiple_layers_do_not_panic() {
911        let first = OsLogLayer::new("org.plabayo.rama.test", "first")
912            .unwrap()
913            .with_privacy(Privacy::Public)
914            .with_level_map(LevelMap::persistent_info())
915            .with_span_mode(SpanMode::Signposts)
916            .with_span_context(true);
917        let second = first.clone().with_target(false);
918        let subscriber = tracing::subscriber::registry().with(first).with(second);
919        let dispatch = tracing::Dispatch::new(subscriber);
920
921        tracing::dispatcher::with_default(&dispatch, || {
922            let span = tracing::info_span!("request", request.id = tracing::field::Empty);
923            span.record("request.id", 42_u64);
924            tracing::info!(parent: &span, answer = 42, "explicit parent");
925            tracing::info!(parent: None, "explicit root");
926        });
927    }
928
929    #[test]
930    fn event_formatting_uses_explicit_scope_and_late_records() {
931        let events = Arc::new(RwLock::new(Vec::new()));
932        let formatter = OsLogLayer::new("org.plabayo.rama.test", "format")
933            .unwrap()
934            .with_target(false)
935            .with_span_context(true);
936        let capture = FormattingCapture {
937            formatter,
938            events: Arc::clone(&events),
939        };
940        let dispatch = tracing::Dispatch::new(tracing::subscriber::registry().with(capture));
941
942        tracing::dispatcher::with_default(&dispatch, || {
943            let span = tracing::info_span!("request", request.id = tracing::field::Empty);
944            span.record("request.id", 42_u64);
945            tracing::info!(parent: &span, answer = 42, "explicit parent");
946            tracing::info!(parent: None, "explicit root");
947        });
948
949        let events = events.read().unwrap();
950        assert_eq!(events.len(), 2);
951        assert!(events[0].contains("explicit parent answer=42"));
952        assert!(events[0].contains("spans=[request{request.id=42}]"));
953        assert_eq!(events[1], "explicit root");
954    }
955}