Skip to main content

hermit/
liteinst_record.rs

1//! Bounded complete records using stock tracing field formatting.
2//!
3//! The event envelope matches tracing-subscriber's default Full format with
4//! ANSI disabled by default and optionally enabled by the caller. Non-ANSI
5//! fields use the public DefaultFields visitor unchanged.
6//! This Layer owns per-call event storage and bounded span caches, avoiding the
7//! fmt Layer's destructible TLS output buffer and unbounded initial span cache.
8//! A failed update preserves the previous cache; errors poison later records.
9//! A sink accepts one complete record or returns an error and is never retried.
10//!
11//! This does not make Registry or EnvFilter teardown-safe: creating/entering
12//! spans or using dynamic span filters after their TLS teardown remains subject
13//! to those dependencies' lifetime requirements. A contextual event after
14//! Registry TLS destruction can lose its current span without a local error;
15//! the public Context API cannot distinguish this from having no entered span.
16//! Such emissions must not be treated as complete-log evidence. This module
17//! provides neither capture completion nor deterministic record ordering.
18//! This preparation is not wired into backend verification.
19
20use std::cell::Cell;
21use std::fmt;
22use std::io;
23use std::io::Write;
24use std::sync::Arc;
25use std::sync::Condvar;
26use std::sync::Mutex;
27use std::sync::OnceLock;
28
29use tracing::Event;
30use tracing::Level;
31use tracing::Subscriber;
32use tracing::field::Field;
33use tracing::field::Visit;
34use tracing::span::Attributes;
35use tracing::span::Id;
36use tracing::span::Record;
37use tracing_log::NormalizeEvent;
38use tracing_subscriber::EnvFilter;
39use tracing_subscriber::Layer;
40use tracing_subscriber::field::RecordFields;
41use tracing_subscriber::field::VisitOutput;
42use tracing_subscriber::fmt::FormattedFields;
43use tracing_subscriber::fmt::MakeWriter;
44use tracing_subscriber::fmt::format::DefaultFields;
45use tracing_subscriber::fmt::format::DefaultVisitor;
46use tracing_subscriber::fmt::format::FormatFields;
47use tracing_subscriber::fmt::format::Writer;
48use tracing_subscriber::fmt::time::FormatTime;
49use tracing_subscriber::fmt::time::SystemTime;
50use tracing_subscriber::layer::Context;
51use tracing_subscriber::layer::SubscriberExt;
52use tracing_subscriber::registry::LookupSpan;
53
54/// Provisional per-event payload budget, not derived from measured workloads.
55/// Measure complete INFO record sizes before selecting production limits or
56/// wiring this preparation into backend verification.
57pub const EVENT_BYTES: usize = 1024 * 1024;
58/// Provisional per-span payload budget, not derived from measured workloads.
59/// Each successfully formatted span retains the full reserved capacity until
60/// its cache is replaced or the span closes, even when its fields are short.
61/// Measure field sizes and live-span memory costs before production wiring.
62pub const SPAN_BYTES: usize = 64 * 1024;
63
64/// Formatter payload budgets, not a bound on callback allocations, registry or
65/// filter bookkeeping, allocator overhead, live spans or concurrent emitters.
66/// Each formatting call reserves its full event or span budget fallibly. Span
67/// caches retain that allocation, including unused capacity; an update briefly
68/// holds both the old cache and its replacement. An allocator may reserve more
69/// than requested, but only the configured payload limit may be written.
70#[derive(Clone, Copy, Debug)]
71pub struct FormatterLimits {
72    event_bytes: usize,
73    span_bytes: usize,
74}
75
76impl FormatterLimits {
77    pub fn new(event_bytes: usize, span_bytes: usize) -> io::Result<Self> {
78        let envelope = span_bytes
79            .checked_mul(2)
80            .and_then(|spans| spans.checked_add(event_bytes))
81            .and_then(|bytes| bytes.checked_add(8));
82        if event_bytes == 0
83            || span_bytes == 0
84            || envelope.is_none_or(|bytes| bytes > isize::MAX as usize)
85        {
86            return Err(io::Error::new(
87                io::ErrorKind::InvalidInput,
88                "invalid formatter limits",
89            ));
90        }
91        Ok(Self {
92            event_bytes,
93            span_bytes,
94        })
95    }
96}
97
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum BufferKind {
100    Event,
101    Span,
102}
103
104/// A sticky failure, not a capture-completion or deterministic-order verdict.
105#[derive(Debug)]
106pub enum RecordFailure {
107    Formatting,
108    Sink(io::Error),
109    Reentrant,
110    Unwinding,
111    Size { buffer: BufferKind, limit: usize },
112    Allocation,
113}
114
115impl fmt::Display for RecordFailure {
116    fn fmt(&self, writer: &mut fmt::Formatter<'_>) -> fmt::Result {
117        match self {
118            Self::Formatting => writer.write_str("host record formatting failed"),
119            Self::Sink(error) => write!(writer, "host record sink failed: {error}"),
120            Self::Reentrant => writer.write_str("reentrant host record publication"),
121            Self::Unwinding => writer.write_str("host record formatting or publication unwound"),
122            Self::Size { buffer, limit } => {
123                write!(writer, "{buffer:?} formatter exceeded {limit} bytes")
124            }
125            Self::Allocation => writer.write_str("formatter storage allocation failed"),
126        }
127    }
128}
129
130impl std::error::Error for RecordFailure {
131    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
132        match self {
133            Self::Sink(error) => Some(error),
134            _ => None,
135        }
136    }
137}
138
139/// Retain this independently of the subscriber, including through its Drop.
140///
141/// This reports formatter/publication failures, not Registry or EnvFilter
142/// lifecycle completeness. In particular, losing Registry's current-span TLS
143/// can omit context without setting this status. `None` cannot certify a
144/// complete log; the caller must establish those dependency lifetimes too.
145/// Sink calls already in flight may finish after another emitter fails. This
146/// status is not a publication barrier: quiesce all emitters before checking
147/// the final result, and refuse comparison if any failure was recorded.
148#[derive(Clone, Default)]
149pub struct RecordStatus(
150    Arc<Mutex<Option<Arc<RecordFailure>>>>,
151    Arc<OnceLock<Box<dyn Fn() + Send + Sync>>>,
152    Arc<Condvar>,
153);
154
155impl RecordStatus {
156    pub fn failure(&self) -> Option<Arc<RecordFailure>> {
157        self.0
158            .lock()
159            .unwrap_or_else(|error| error.into_inner())
160            .clone()
161    }
162
163    fn fail(&self, failure: RecordFailure) {
164        let mut stored = self.0.lock().unwrap_or_else(|error| error.into_inner());
165        if stored.is_none() {
166            *stored = Some(Arc::new(failure));
167            drop(stored);
168            // Wake initialization waiters before a user failure callback can
169            // wait for their completion. The retained failure is already set.
170            self.2.notify_all();
171            if let Some(notify) = self.1.get() {
172                notify();
173            }
174        } else {
175            drop(stored);
176            drop(failure);
177        }
178    }
179
180    fn wake_waiters(&self) {
181        // Pair with snapshot's predicate check and Condvar wait under this
182        // mutex, so a completed initialization cannot race a waiter to sleep.
183        let _stored = self.0.lock().unwrap_or_else(|error| error.into_inner());
184        self.2.notify_all();
185    }
186
187    fn formatting(&self, action: impl FnOnce() -> fmt::Result) -> fmt::Result {
188        let attempt = Attempt(Some(self));
189        let result = action();
190        attempt.complete();
191        if result.is_err() {
192            self.fail(RecordFailure::Formatting);
193        }
194        result
195    }
196}
197
198struct Attempt<'status>(Option<&'status RecordStatus>);
199
200impl Attempt<'_> {
201    fn complete(mut self) {
202        // A successful callback may run from Drop during an unrelated panic.
203        // Only unwinding out of this callback is a formatter or sink failure.
204        self.0 = None;
205    }
206}
207
208impl Drop for Attempt<'_> {
209    fn drop(&mut self) {
210        if let Some(status) = self.0
211            && std::thread::panicking()
212        {
213            status.fail(RecordFailure::Unwinding);
214        }
215    }
216}
217
218struct BoundedBuffer<'status> {
219    bytes: Vec<u8>,
220    limit: usize,
221    failed: bool,
222    kind: BufferKind,
223    status: &'status RecordStatus,
224}
225
226fn reserve_payload(
227    bytes: &mut Vec<u8>,
228    limit: usize,
229) -> Result<(), std::collections::TryReserveError> {
230    #[cfg(test)]
231    tests::before_payload_allocation()?;
232    bytes.try_reserve_exact(limit)
233}
234
235impl<'status> BoundedBuffer<'status> {
236    fn new(
237        limit: usize,
238        kind: BufferKind,
239        status: &'status RecordStatus,
240    ) -> Result<Self, fmt::Error> {
241        let mut bytes = Vec::new();
242        if reserve_payload(&mut bytes, limit).is_err() {
243            status.fail(RecordFailure::Allocation);
244            return Err(fmt::Error);
245        }
246        Ok(Self {
247            bytes,
248            limit,
249            failed: false,
250            kind,
251            status,
252        })
253    }
254
255    fn text(&self) -> &str {
256        std::str::from_utf8(&self.bytes).expect("only complete UTF-8 strings were copied")
257    }
258
259    fn into_string(self) -> String {
260        String::from_utf8(self.bytes).expect("only complete UTF-8 strings were copied")
261    }
262}
263
264impl fmt::Write for BoundedBuffer<'_> {
265    fn write_str(&mut self, text: &str) -> fmt::Result {
266        if self.failed {
267            return Err(fmt::Error);
268        }
269        if self
270            .bytes
271            .len()
272            .checked_add(text.len())
273            .is_none_or(|end| end > self.limit)
274        {
275            self.failed = true;
276            self.status.fail(RecordFailure::Size {
277                buffer: self.kind,
278                limit: self.limit,
279            });
280            return Err(fmt::Error);
281        }
282        // The full payload budget was reserved fallibly before formatting;
283        // appending within that limit cannot allocate or touch unused bytes.
284        self.bytes.extend_from_slice(text.as_bytes());
285        Ok(())
286    }
287}
288
289// Cell has no destructor. Each list entry belongs to a live call's stack,
290// rather than a Vec whose TLS destructor can run before late logging.
291thread_local! {
292    static ACTIVE: Cell<*const Active> = const { Cell::new(std::ptr::null()) };
293}
294
295struct Active {
296    identity: usize,
297    previous: *const Active,
298}
299
300struct RestoreActive(*const Active);
301
302impl Drop for RestoreActive {
303    fn drop(&mut self) {
304        ACTIVE.set(self.0);
305    }
306}
307
308fn with_active<T>(
309    status: &RecordStatus,
310    action: impl FnOnce() -> Result<T, fmt::Error>,
311) -> Result<T, fmt::Error> {
312    if status.failure().is_some() {
313        return Err(fmt::Error);
314    }
315    let identity = Arc::as_ptr(&status.0) as usize;
316    let previous = ACTIVE.get();
317    let mut current = previous;
318    while !current.is_null() {
319        // SAFETY: ACTIVE contains only entries in enclosing with_active calls
320        // on this thread. Each entry stays in its stack slot until action has
321        // returned or unwound; RestoreActive unlinks it before that slot dies.
322        let active = unsafe { &*current };
323        if active.identity == identity {
324            status.fail(RecordFailure::Reentrant);
325            return Err(fmt::Error);
326        }
327        current = active.previous;
328    }
329    let active = Active { identity, previous };
330    ACTIVE.set(&raw const active);
331    let restore = RestoreActive(previous);
332    let result = action();
333    drop(restore);
334    result
335}
336
337// Keep the standard cache wrapper; only this Layer writes its payload.
338struct CheckedFields;
339
340// Extensions are keyed by Rust type, so two RecordLayers cannot each insert a
341// FormattedFields<CheckedFields>. Retain a separate entry for each layer instead.
342// The Arc stays in the cache as well as the layer: replacing a layer while a
343// span lives cannot reuse an old allocation identity and inherit stale fields.
344#[derive(Default)]
345struct SpanCaches(Vec<(Arc<()>, Arc<SpanCache>)>);
346
347impl SpanCaches {
348    fn get(&self, identity: &Arc<()>) -> Option<&Arc<SpanCache>> {
349        self.0
350            .iter()
351            .find(|(owner, _)| Arc::ptr_eq(owner, identity))
352            .map(|(_, fields)| fields)
353    }
354
355    fn get_or_insert(&mut self, identity: &Arc<()>) -> Arc<SpanCache> {
356        if let Some(cache) = self.get(identity) {
357            return cache.clone();
358        }
359        let cache = Arc::new(SpanCache::default());
360        self.0.push((identity.clone(), cache.clone()));
361        cache
362    }
363}
364
365#[derive(Default)]
366struct PendingUpdates {
367    fields: Arc<FormattedFields<CheckedFields>>,
368    // Empty updates add spaces only if initial fields will be nonempty.
369    // Saturating at limit + 1 is enough to detect every eventual overflow.
370    leading_empty: usize,
371}
372
373#[derive(Default)]
374struct CacheState {
375    fields: Arc<FormattedFields<CheckedFields>>,
376    pending: Option<PendingUpdates>,
377}
378
379#[derive(Default)]
380struct SpanCache(Mutex<CacheState>);
381
382impl SpanCache {
383    fn snapshot(
384        &self,
385        status: &RecordStatus,
386    ) -> Result<Arc<FormattedFields<CheckedFields>>, fmt::Error> {
387        let mut failure = status.0.lock().unwrap_or_else(|error| error.into_inner());
388        loop {
389            if failure.is_some() {
390                return Err(fmt::Error);
391            }
392            {
393                let state = self.0.lock().unwrap_or_else(|error| error.into_inner());
394                if state.pending.is_none() {
395                    return Ok(state.fields.clone());
396                }
397            }
398            // No Registry or cache lock crosses this wait. Same-thread
399            // formatter reentry has already failed in with_active; another
400            // thread must see complete initial fields before its event emits.
401            failure = status
402                .2
403                .wait(failure)
404                .unwrap_or_else(|error| error.into_inner());
405        }
406    }
407
408    fn initialize(self: &Arc<Self>, status: &RecordStatus) -> Initializing {
409        #[cfg(test)]
410        tests::before_span_initialization();
411        let mut state = self.0.lock().unwrap_or_else(|error| error.into_inner());
412        assert!(
413            state.pending.is_none(),
414            "span initialized twice concurrently"
415        );
416        state.pending = Some(PendingUpdates::default());
417        Initializing {
418            cache: self.clone(),
419            status: status.clone(),
420        }
421    }
422}
423
424struct Initializing {
425    cache: Arc<SpanCache>,
426    status: RecordStatus,
427}
428
429impl Drop for Initializing {
430    fn drop(&mut self) {
431        // Also runs when allocation, formatting, or the final merge fails or
432        // panics. Keep the last readable fields and discard unfinished updates.
433        self.cache
434            .0
435            .lock()
436            .unwrap_or_else(|error| error.into_inner())
437            .pending = None;
438        self.status.wake_waiters();
439    }
440}
441
442struct RecordLayer<Timer, Sink> {
443    identity: Arc<()>,
444    timer: Timer,
445    timestamp: bool,
446    ansi: bool,
447    limits: FormatterLimits,
448    status: RecordStatus,
449    writer: RecordWriterFactory<Sink>,
450}
451
452impl<Timer, Sink> RecordLayer<Timer, Sink> {
453    fn format_span<Fields: RecordFields>(
454        &self,
455        fields: Fields,
456    ) -> Result<BoundedBuffer<'_>, fmt::Error> {
457        let mut buffer =
458            BoundedBuffer::new(self.limits.span_bytes, BufferKind::Span, &self.status)?;
459        // User fields may log or update other spans. Format exactly once without
460        // holding Registry extensions or a cache lock. The latest committed
461        // prefix is merged only after this call returns.
462        self.status
463            .formatting(|| format_fields(Writer::new(&mut buffer), fields, self.ansi))?;
464        if self.status.failure().is_some() {
465            return Err(fmt::Error);
466        }
467        Ok(buffer)
468    }
469
470    fn commit_span(
471        &self,
472        current: &mut Arc<FormattedFields<CheckedFields>>,
473        mut buffer: BoundedBuffer<'_>,
474        append: bool,
475    ) -> Result<(), RecordFailure> {
476        if append && !current.fields.is_empty() {
477            let prefix = current.fields.len() + 1;
478            let length = buffer.bytes.len();
479            let Some(combined) = prefix
480                .checked_add(length)
481                .filter(|combined| *combined <= buffer.limit)
482            else {
483                // The caller releases extensions before reporting the failure:
484                // its notification callback may itself emit a record.
485                return Err(RecordFailure::Size {
486                    buffer: BufferKind::Span,
487                    limit: buffer.limit,
488                });
489            };
490            // The entire payload capacity was reserved before user formatting.
491            // Move the fragment within that allocation, preserving stock's
492            // separating space even when the new fragment is empty.
493            buffer.bytes.resize(combined, 0);
494            buffer.bytes.copy_within(..length, prefix);
495            buffer.bytes[..current.fields.len()].copy_from_slice(current.fields.as_bytes());
496            buffer.bytes[current.fields.len()] = b' ';
497        }
498        *current = Arc::new(FormattedFields::new(buffer.into_string()));
499        Ok(())
500    }
501
502    fn commit_update(
503        &self,
504        cache: &SpanCache,
505        buffer: BoundedBuffer<'_>,
506    ) -> Result<(), RecordFailure> {
507        let mut state = cache.0.lock().unwrap_or_else(|error| error.into_inner());
508        if let Some(pending) = &mut state.pending {
509            let empty = pending.fields.is_empty() && buffer.bytes.is_empty();
510            self.commit_span(&mut pending.fields, buffer, true)?;
511            if empty {
512                pending.leading_empty = (pending.leading_empty + 1).min(self.limits.span_bytes + 1);
513            }
514            Ok(())
515        } else {
516            self.commit_span(&mut state.fields, buffer, true)
517        }
518    }
519
520    fn commit_initial(
521        &self,
522        cache: &SpanCache,
523        mut buffer: BoundedBuffer<'_>,
524    ) -> Result<(), RecordFailure> {
525        let mut state = cache.0.lock().unwrap_or_else(|error| error.into_inner());
526        let pending = state.pending.as_ref().expect("initialization is active");
527        let spaces = if buffer.bytes.is_empty() {
528            0
529        } else {
530            pending.leading_empty + usize::from(!pending.fields.is_empty())
531        };
532        let combined = buffer
533            .bytes
534            .len()
535            .checked_add(spaces)
536            .and_then(|length| length.checked_add(pending.fields.fields.len()))
537            .filter(|length| *length <= buffer.limit);
538        let Some(combined) = combined else {
539            return Err(RecordFailure::Size {
540                buffer: BufferKind::Span,
541                limit: buffer.limit,
542            });
543        };
544        // Initial fields precede every update, even when the update finished
545        // first. Both payloads were reserved fallibly before user formatting.
546        buffer.bytes.resize(buffer.bytes.len() + spaces, b' ');
547        buffer
548            .bytes
549            .extend_from_slice(pending.fields.fields.as_bytes());
550        debug_assert_eq!(buffer.bytes.len(), combined);
551        state.fields = Arc::new(FormattedFields::new(buffer.into_string()));
552        state.pending = None;
553        Ok(())
554    }
555}
556
557impl<Registry, Timer, Sink> Layer<Registry> for RecordLayer<Timer, Sink>
558where
559    Registry: Subscriber + for<'lookup> LookupSpan<'lookup>,
560    Timer: FormatTime + 'static,
561    Sink: Fn(&[u8]) -> io::Result<()> + 'static,
562{
563    fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, context: Context<'_, Registry>) {
564        let span = context.span(id).expect("new span is registered");
565        let (cache, initializing) = {
566            let mut extensions = span.extensions_mut();
567            if extensions.get_mut::<SpanCaches>().is_none() {
568                extensions.insert(SpanCaches::default());
569            }
570            let caches = extensions.get_mut::<SpanCaches>().unwrap();
571            // An inner Layer may expose the ID and record fields before this
572            // callback starts. Stock formatting keeps that existing cache and
573            // does not format the initial attributes again.
574            if caches.get(&self.identity).is_some() {
575                return;
576            }
577            let cache = caches.get_or_insert(&self.identity);
578            // Publish the initializing state with the cache. Otherwise another
579            // thread can observe an empty ready cache before formatting starts.
580            let initializing = cache.initialize(&self.status);
581            (cache, initializing)
582        };
583        let _ = with_active(&self.status, || {
584            let _initializing = initializing;
585            let buffer = self.format_span(attrs)?;
586            self.commit_initial(&cache, buffer).map_err(|failure| {
587                self.status.fail(failure);
588                fmt::Error
589            })
590        });
591    }
592
593    fn on_record(&self, id: &Id, fields: &Record<'_>, context: Context<'_, Registry>) {
594        let _ = with_active(&self.status, || {
595            let span = context.span(id).expect("recorded span is registered");
596            let (cache, initializing) = {
597                let mut extensions = span.extensions_mut();
598                if extensions.get_mut::<SpanCaches>().is_none() {
599                    extensions.insert(SpanCaches::default());
600                }
601                let caches = extensions.get_mut::<SpanCaches>().unwrap();
602                if let Some(cache) = caches.get(&self.identity) {
603                    (cache.clone(), None)
604                } else {
605                    let cache = caches.get_or_insert(&self.identity);
606                    // A record callback can create the first cache too. Mark
607                    // it pending before publication so concurrent events wait
608                    // for its complete fields, just as during on_new_span.
609                    let initializing = cache.initialize(&self.status);
610                    (cache, Some(initializing))
611                }
612            };
613            let _initializing = initializing;
614            let buffer = self.format_span(fields)?;
615            let result = if _initializing.is_some() {
616                self.commit_initial(&cache, buffer)
617            } else {
618                self.commit_update(&cache, buffer)
619            };
620            result.map_err(|failure| {
621                self.status.fail(failure);
622                fmt::Error
623            })
624        });
625    }
626
627    fn on_event(&self, event: &Event<'_>, context: Context<'_, Registry>) {
628        let record = with_active(&self.status, || {
629            let mut buffer =
630                BoundedBuffer::new(self.limits.event_bytes, BufferKind::Event, &self.status)?;
631            self.status.formatting(|| {
632                let mut writer = Writer::new(&mut buffer);
633                // Match stock Full's timestamp styling, fallback and padding.
634                if self.timestamp {
635                    if self.ansi {
636                        writer.write_str("\x1b[2m")?;
637                    }
638                    if self.timer.format_time(&mut writer).is_err() {
639                        writer.write_str("<unknown time>")?;
640                    }
641                    if self.ansi {
642                        writer.write_str("\x1b[0m")?;
643                    }
644                    writer.write_char(' ')?;
645                }
646                let normalized = event.normalized_metadata();
647                let metadata = normalized.as_ref().unwrap_or_else(|| event.metadata());
648                if self.ansi {
649                    let (color, level) = match *metadata.level() {
650                        Level::TRACE => ("\x1b[35m", "TRACE"),
651                        Level::DEBUG => ("\x1b[34m", "DEBUG"),
652                        Level::INFO => ("\x1b[32m", " INFO"),
653                        Level::WARN => ("\x1b[33m", " WARN"),
654                        Level::ERROR => ("\x1b[31m", "ERROR"),
655                    };
656                    write_styled(&mut writer, color, level, true)?;
657                    writer.write_char(' ')?;
658                } else {
659                    write!(writer, "{:>5} ", metadata.level())?;
660                }
661                if let Some(scope) = context.event_scope(event) {
662                    let mut seen = false;
663                    for span in scope.from_root() {
664                        write_styled(&mut writer, "\x1b[1m", span.metadata().name(), self.ansi)?;
665                        let fields = span
666                            .extensions()
667                            .get::<SpanCaches>()
668                            .and_then(|caches| caches.get(&self.identity))
669                            .cloned();
670                        // Keep a complete immutable cache version while writing;
671                        // even a size-failure notification runs without Registry
672                        // extension locks held.
673                        if let Some(cache) = fields {
674                            let fields = cache.snapshot(&self.status)?;
675                            if !fields.is_empty() {
676                                write_styled(&mut writer, "\x1b[1m", "{", self.ansi)?;
677                                write!(writer, "{fields}")?;
678                                write_styled(&mut writer, "\x1b[1m", "}", self.ansi)?;
679                            }
680                        }
681                        write_styled(&mut writer, "\x1b[2m", ":", self.ansi)?;
682                        seen = true;
683                    }
684                    if seen {
685                        writer.write_char(' ')?;
686                    }
687                }
688                write_styled(&mut writer, "\x1b[2m", metadata.target(), self.ansi)?;
689                write_styled(&mut writer, "\x1b[2m", ":", self.ansi)?;
690                writer.write_char(' ')?;
691                format_fields(writer.by_ref(), event, self.ansi)?;
692                writeln!(writer)
693            })?;
694            if self.status.failure().is_some() {
695                return Err(fmt::Error);
696            }
697            Ok(buffer)
698        });
699        if let Ok(record) = record {
700            let _ = self
701                .writer
702                .write_record(record.text().as_bytes(), self.limits.event_bytes);
703        }
704    }
705}
706
707fn write_styled(writer: &mut Writer<'_>, prefix: &str, text: &str, ansi: bool) -> fmt::Result {
708    if ansi {
709        writer.write_str(prefix)?;
710    }
711    writer.write_str(text)?;
712    if ansi {
713        writer.write_str("\x1b[0m")?;
714    }
715    Ok(())
716}
717
718fn format_fields(writer: Writer<'_>, fields: impl RecordFields, ansi: bool) -> fmt::Result {
719    if !ansi {
720        return DefaultFields::new().format_fields(writer, fields);
721    }
722    let mut visitor = AnsiVisitor {
723        writer,
724        is_empty: true,
725        result: Ok(()),
726    };
727    fields.record(&mut visitor);
728    visitor.result
729}
730
731// tracing-subscriber 0.3.23 keeps Writer::with_ansi and the DefaultVisitor's
732// styles private. Follow its Full/DefaultVisitor rules here without an extra
733// fmt Layer, TLS buffer or intermediate allocation. Message escaping still
734// uses the public DefaultVisitor. Independent stock subscribers test the bytes.
735struct AnsiVisitor<'a> {
736    writer: Writer<'a>,
737    is_empty: bool,
738    result: fmt::Result,
739}
740
741impl Visit for AnsiVisitor<'_> {
742    fn record_str(&mut self, field: &Field, value: &str) {
743        if field.name() == "message" {
744            self.record_debug(field, &format_args!("{value}"));
745        } else {
746            self.record_debug(field, &value);
747        }
748    }
749
750    fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
751        if let Some(source) = value.source() {
752            self.record_debug(
753                field,
754                &format_args!(
755                    "{} \x1b[3m{}\x1b[0m\x1b[3m.sources\x1b[0m\x1b[2m=\x1b[0m{}",
756                    Sanitized(value),
757                    field.name(),
758                    ErrorSources(source),
759                ),
760            );
761        } else {
762            self.record_debug(field, &format_args!("{}", Sanitized(value)));
763        }
764    }
765
766    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
767        if self.result.is_err() || field.name().starts_with("log.") {
768            return;
769        }
770        if field.name() == "message" {
771            let mut visitor = DefaultVisitor::new(self.writer.by_ref(), self.is_empty);
772            visitor.record_debug(field, value);
773            self.result = visitor.finish();
774        } else {
775            self.result = (|| {
776                if !self.is_empty {
777                    self.writer.write_char(' ')?;
778                }
779                let name = field.name().strip_prefix("r#").unwrap_or(field.name());
780                write_styled(&mut self.writer, "\x1b[3m", name, true)?;
781                write_styled(&mut self.writer, "\x1b[2m", "=", true)?;
782                write!(self.writer, "{value:?}")
783            })();
784        }
785        self.is_empty = false;
786    }
787}
788
789// Error values and sources use stock's streaming ESC/C1 escaping. Named Debug
790// values deliberately keep their bytes; applying this to all values would
791// change stock's output. This wrapper neither buffers nor revisits the value.
792struct Sanitized<T>(T);
793
794impl<T: fmt::Display> fmt::Display for Sanitized<T> {
795    fn fmt(&self, writer: &mut fmt::Formatter<'_>) -> fmt::Result {
796        struct EscapingWriter<'a, 'b>(&'a mut fmt::Formatter<'b>);
797        impl fmt::Write for EscapingWriter<'_, '_> {
798            fn write_str(&mut self, text: &str) -> fmt::Result {
799                for ch in text.chars() {
800                    match ch {
801                        '\x1b' => self.0.write_str("\\x1b")?,
802                        '\x07' => self.0.write_str("\\x07")?,
803                        '\x08' => self.0.write_str("\\x08")?,
804                        '\x0c' => self.0.write_str("\\x0c")?,
805                        '\x7f' => self.0.write_str("\\x7f")?,
806                        '\u{80}'..='\u{9f}' => write!(self.0, "\\u{{{:x}}}", ch as u32)?,
807                        _ => fmt::Write::write_char(self.0, ch)?,
808                    }
809                }
810                Ok(())
811            }
812        }
813        fmt::Write::write_fmt(&mut EscapingWriter(writer), format_args!("{}", self.0))
814    }
815}
816
817impl<T: fmt::Display> fmt::Debug for Sanitized<T> {
818    fn fmt(&self, writer: &mut fmt::Formatter<'_>) -> fmt::Result {
819        fmt::Display::fmt(self, writer)
820    }
821}
822
823struct ErrorSources<'a>(&'a (dyn std::error::Error + 'static));
824
825impl fmt::Display for ErrorSources<'_> {
826    fn fmt(&self, writer: &mut fmt::Formatter<'_>) -> fmt::Result {
827        let mut list = writer.debug_list();
828        let mut current = Some(self.0);
829        while let Some(error) = current {
830            list.entry(&Sanitized(error));
831            current = error.source();
832        }
833        list.finish()
834    }
835}
836
837struct RecordWriter<'sink, Sink> {
838    sink: &'sink Sink,
839    status: &'sink RecordStatus,
840}
841
842impl<Sink: Fn(&[u8]) -> io::Result<()>> Write for RecordWriter<'_, Sink> {
843    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
844        self.write_all(bytes).map(|()| bytes.len())
845    }
846
847    fn write_all(&mut self, bytes: &[u8]) -> io::Result<()> {
848        with_active(self.status, || {
849            let attempt = Attempt(Some(self.status));
850            let result = (self.sink)(bytes);
851            attempt.complete();
852            match result {
853                Ok(()) => Ok(()),
854                Err(error) => {
855                    self.status.fail(RecordFailure::Sink(error));
856                    Err(fmt::Error)
857                }
858            }
859        })
860        .map_err(|_| io::Error::other("complete record publication failed"))
861    }
862
863    fn flush(&mut self) -> io::Result<()> {
864        Ok(())
865    }
866}
867
868struct RecordWriterFactory<Sink> {
869    sink: Sink,
870    status: RecordStatus,
871}
872
873impl<Sink: Fn(&[u8]) -> io::Result<()>> RecordWriterFactory<Sink> {
874    fn write_record(&self, bytes: &[u8], limit: usize) -> io::Result<()> {
875        if bytes.len() > limit {
876            self.status.fail(RecordFailure::Size {
877                buffer: BufferKind::Event,
878                limit,
879            });
880            return Err(io::Error::new(
881                io::ErrorKind::InvalidData,
882                "oversized formatted record",
883            ));
884        }
885        self.make_writer().write_all(bytes)
886    }
887}
888
889impl<'sink, Sink: Fn(&[u8]) -> io::Result<()> + 'sink> MakeWriter<'sink>
890    for RecordWriterFactory<Sink>
891{
892    type Writer = RecordWriter<'sink, Sink>;
893
894    fn make_writer(&'sink self) -> Self::Writer {
895        RecordWriter {
896            sink: &self.sink,
897            status: &self.status,
898        }
899    }
900}
901
902/// Prepare without installing a subscriber or opening a destination.
903///
904/// `filter` must already be resolved by the caller. Records use the standard
905/// Full format with timestamps and generated ANSI disabled. Stock sanitizes
906/// the `message` field and error values; arbitrary named Debug fields preserve
907/// their original bytes, including escape sequences.
908/// Inspect the retained status after emitter quiescence, including teardown;
909/// absence of a local failure alone is not a complete-log or parity verdict.
910/// The callback is invoked once on the first local failure, after storing it.
911///
912/// The emitted byte stream is not self-describing on failure: it can be an
913/// incomplete prefix with no marker. A consumer feeding it to `detcore::logdiff`
914/// must ensure any failure refuses comparison, either through retained failure
915/// state or an in-band marker recognized by the comparator as truncation. The
916/// bytes alone cannot qualify a comparison. Transport wiring must enforce this
917/// before this preparation can be used for backend verification.
918pub fn record_subscriber_with_failure<Sink>(
919    filter: EnvFilter,
920    limits: FormatterLimits,
921    sink: Sink,
922    failure: impl Fn() + Send + Sync + 'static,
923) -> (impl Subscriber + Send + Sync, RecordStatus)
924where
925    Sink: Fn(&[u8]) -> io::Result<()> + Send + Sync + 'static,
926{
927    let status = RecordStatus::default();
928    let _ = status.1.set(Box::new(failure));
929    subscriber_with_status(filter, limits, sink, SystemTime, true, status)
930}
931
932/// Prepare a complete-record layer without installing it or selecting events.
933///
934/// Apply [`Layer::with_filter`] to each layer independently: the public logger
935/// can use its resolved [`EnvFilter`] while private evidence uses a fixed INFO
936/// selector. A global public filter would also suppress private evidence.
937/// Each layer owns its formatter limits, span fields and retained failure state.
938/// Each live span retains a full `span_bytes` allocation per layer; two layers
939/// with the default limits retain 128 KiB per span. Measurements before wiring
940/// must account for layer count and cache versions held by in-flight records.
941/// The completion, teardown and transport requirements of
942/// [`record_subscriber_with_failure`] apply equally to this constructor.
943pub fn record_layer_with_failure<Registry, Sink>(
944    limits: FormatterLimits,
945    sink: Sink,
946    failure: impl Fn() + Send + Sync + 'static,
947) -> (impl Layer<Registry> + Send + Sync, RecordStatus)
948where
949    Registry: Subscriber + for<'lookup> LookupSpan<'lookup>,
950    Sink: Fn(&[u8]) -> io::Result<()> + Send + Sync + 'static,
951{
952    record_layer_with_failure_and_ansi(limits, sink, failure, false)
953}
954
955/// Prepare a complete-record layer with an explicit ANSI formatting policy.
956///
957/// `ansi` is resolved by the host caller before constructing the layer, never
958/// by inspecting terminal state while formatting. `true` matches stock Full
959/// styling; `false` preserves [`record_layer_with_failure`]'s default bytes.
960/// Both event and span limits count all emitted bytes, including escape codes.
961/// The filtering, failure and completion requirements of that constructor
962/// still apply. This option does not install a layer or change CLI output.
963pub fn record_layer_with_failure_and_ansi<Registry, Sink>(
964    limits: FormatterLimits,
965    sink: Sink,
966    failure: impl Fn() + Send + Sync + 'static,
967    ansi: bool,
968) -> (impl Layer<Registry> + Send + Sync, RecordStatus)
969where
970    Registry: Subscriber + for<'lookup> LookupSpan<'lookup>,
971    Sink: Fn(&[u8]) -> io::Result<()> + Send + Sync + 'static,
972{
973    let status = RecordStatus::default();
974    let _ = status.1.set(Box::new(failure));
975    (
976        RecordLayer {
977            ansi,
978            ..layer_with_status(limits, sink, SystemTime, true, status.clone())
979        },
980        status,
981    )
982}
983
984fn subscriber_with_status<Sink, Timer>(
985    filter: EnvFilter,
986    limits: FormatterLimits,
987    sink: Sink,
988    timer: Timer,
989    timestamp: bool,
990    status: RecordStatus,
991) -> (impl Subscriber + Send + Sync, RecordStatus)
992where
993    Sink: Fn(&[u8]) -> io::Result<()> + Send + Sync + 'static,
994    Timer: FormatTime + Send + Sync + 'static,
995{
996    let layer = layer_with_status(limits, sink, timer, timestamp, status.clone());
997    (
998        tracing_subscriber::registry().with(layer).with(filter),
999        status,
1000    )
1001}
1002
1003fn layer_with_status<Sink, Timer>(
1004    limits: FormatterLimits,
1005    sink: Sink,
1006    timer: Timer,
1007    timestamp: bool,
1008    status: RecordStatus,
1009) -> RecordLayer<Timer, Sink> {
1010    RecordLayer {
1011        identity: Arc::new(()),
1012        timer,
1013        timestamp,
1014        ansi: false,
1015        limits,
1016        status: status.clone(),
1017        writer: RecordWriterFactory {
1018            sink,
1019            status: status.clone(),
1020        },
1021    }
1022}
1023
1024#[cfg(test)]
1025mod tests;