Skip to main content

obs_core/
lib.rs

1#![forbid(unsafe_code)]
2#![warn(rust_2024_compatibility, missing_docs, missing_debug_implementations)]
3// Tests routinely use `.unwrap()` for clarity; production code uses `?`.
4#![cfg_attr(
5    test,
6    allow(
7        clippy::unwrap_used,
8        clippy::expect_used,
9        clippy::panic,
10        clippy::indexing_slicing
11    )
12)]
13
14//! Runtime engine for the obs SDK — the spine that user code emits into.
15//!
16//! Phase-3 surface (specs/91-impl-plan.md tasks 3.1–3.15):
17//!
18//! - [`callsite`] — `ObsCallsite`, atomic-`Interest` cache (spec 11 § 2).
19//! - [`observer`][mod@observer] — three-tier resolution + per-tier worker pool + `StandardObserver`
20//!   (spec 11 §§ 3, 4 + 6.4).
21//! - [`registry`] — schema registry + `ScrubbedEnvelope` (spec 14).
22//! - [`envelope`] — envelope builder + projection helpers (spec 11 § 5).
23//! - [`scope`] — `obs::scope!` / `obs::context!` runtime support (spec 13 §§ 2, 3, 6).
24//! - [`instrumented`] — `Instrumented<F>` future adapter and `Instrument` / `WithObserver` traits
25//!   (spec 13 § 3).
26//! - [`sampling`] — head sampler (spec 13 § 6).
27//! - [`filter`] — EnvFilter-shaped `obs::Filter` (spec 13 § 7).
28//! - [`audit_spool`] — AUDIT-tier disk spool (spec 11 § 6.4).
29//! - [`panic_hook`] — `install_panic_hook` (spec 11 § 6.1).
30//! - [`span_trace`] — `obs::SpanTrace` (spec 13 § 9).
31//! - [`sink`] — sink trait + writers (spec 20).
32
33pub mod audit_spool;
34// NB: previously `aux` — renamed because `aux` is a reserved Windows
35// filename (`aux.rs` silently omitted from published tarballs on
36// win32; flagged by `cargo publish`). The `aux` module path is still
37// re-exported below for backwards compatibility.
38pub mod callsite;
39pub mod codegen_helpers;
40pub mod config;
41pub mod config_watcher;
42pub mod emit;
43pub mod envelope;
44pub mod filter;
45pub mod forensic;
46pub mod instrumented;
47pub mod metric;
48pub mod observer;
49pub mod panic_hook;
50pub mod propagator;
51pub mod registry;
52pub mod resource;
53pub mod sampling;
54pub mod scope;
55pub(crate) mod self_events;
56/// Public re-exports for self-event helpers consumed by sinks/middleware
57/// that emit them on behalf of the runtime (e.g. OTLP trace sink emits
58/// `ObsSpanPairOrphaned` after pair_timeout). Spec 93 P1-2 + P1-7.
59pub mod self_events_public {
60    pub use crate::self_events::{
61        emit_callsite_hash_collision_pub as emit_callsite_hash_collision,
62        emit_label_cardinality_high_pub as emit_label_cardinality_high,
63        emit_oversized_label_dropped_pub as emit_label_oversized,
64        emit_span_pair_orphaned_pub as emit_span_pair_orphaned,
65    };
66}
67pub mod sink;
68pub mod span_trace;
69#[cfg(feature = "test")]
70pub mod test;
71pub mod wire;
72
73#[doc(hidden)]
74#[cfg(feature = "test")]
75pub mod __macro_deps {
76    pub use serde_json;
77}
78
79/// Cap an external string at `max_bytes` (UTF-8-safe boundary), append
80/// the `…<truncated:N>` suffix when the input was clipped, and emit
81/// one `ObsLabelOversized` self-event for telemetry. Returns the
82/// (possibly truncated) string. Spec 95 § 3.10 / P2-AH.
83///
84/// Boundary callers (HTTP middleware, bridge field visitor) should
85/// run this on every untrusted string before it lands in
86/// `env.labels` or the typed payload.
87#[must_use]
88pub fn cap_external_string(field: &'static str, raw: String, max_bytes: u16) -> String {
89    let max = max_bytes as usize;
90    if raw.len() <= max {
91        return raw;
92    }
93    let original_size = raw.len() as u64;
94    // Find the largest UTF-8-safe truncation point ≤ max.
95    let mut end = max;
96    while end > 0 && !raw.is_char_boundary(end) {
97        end -= 1;
98    }
99    let mut out = String::with_capacity(end + 24);
100    out.push_str(&raw[..end]);
101    out.push_str(&format!("…<truncated:{}>", original_size));
102    self_events::emit_oversized_label_dropped_pub(field, original_size, out.len() as u64);
103    out
104}
105
106pub use callsite::ObsCallsite;
107/// Back-compat alias: the module used to be called `aux`. Kept as a
108/// re-export so downstream `use obs_core::aux::…` paths keep working.
109#[doc(hidden)]
110pub use codegen_helpers as aux;
111pub use codegen_helpers::{BuildableTo, EnumCount, FieldCapture, SpanCtx, SpanFrame};
112pub use config::{EventsConfig, SamplingConfig};
113pub use config_watcher::{ConfigWatcher, DEFAULT_DEBOUNCE};
114pub use emit::Emit;
115pub use envelope::{Envelope, EventSchema, FieldMeta, FieldRole};
116pub use filter::Filter;
117pub use instrumented::{Instrument, Instrumented, WithObserver};
118pub use metric::{MetricEmitter, NoopMetricEmitter};
119pub use obs_proto::{
120    ENVELOPE_FORMAT_VER,
121    obs::v1::{ObsBatch, ObsEnvelope},
122};
123pub use obs_types::{
124    Cardinality, Classification, FieldKind, MetricKind, SamplingReason, Severity, Tier,
125};
126pub use observer::{
127    BuildError, InMemoryHandle, InMemoryObserver, NoopObserver, Observer, StandardObserver,
128    StandardObserverBuilder, ThreadObserverGuard, WeakObserver, WorkerCounters, install_observer,
129    install_observer_arc, observer, observer_weak, with_observer_task, with_observer_task_sync,
130    with_observer_thread_local, with_test_observer,
131};
132pub use panic_hook::install_panic_hook;
133pub use propagator::{
134    ObsTraceCtx, W3cPropagator, extract_w3c, fresh_span_id, fresh_trace_id, inject_w3c,
135    status_class,
136};
137pub use registry::{
138    ArrowEventSchema, ArrowField, ArrowLeafType, ArrowSchemaModel, ArrowStructBuilder,
139    CallsiteRecord, CallsiteSource, DecodeError, ENVELOPE_COLUMNS, EVENT_SCHEMAS,
140    EventSchemaErased, ObsCallsiteRegistry, OtelAttributeView, OtlpValue, SchemaRegistry,
141    ScrubError, ScrubbedEnvelope, callsite_id, scrub_payload,
142};
143pub use resource::ResourceAttrs;
144pub use sampling::{SamplingDecision, decide as sample_decide};
145pub use scope::{ScopeField, ScopeFrame, ScopeFrameBuilder, ScopeGuard, ScopeKind};
146pub use sink::{
147    FanOutSink, FormatterStyle, InMemorySink, LevelSplitWriter, MakeWriter, NdjsonFileSink,
148    NonBlockingWriter, NoopSink, RollingFileWriter, RollingFileWriterBuilder, RollingPolicy, Sink,
149    StderrWriter, StdoutSink, StdoutWriter, TeeWriter, WorkerGuard,
150};
151pub use span_trace::SpanTrace;
152
153/// Re-exports for code generated by `obs-macros::derive(Event)` and
154/// `obs-build`. End users should not depend on these directly.
155#[doc(hidden)]
156pub mod __private {
157    pub use std::sync::OnceLock;
158
159    pub use buffa;
160    pub use bytes::BytesMut;
161    pub use linkme;
162    /// Proto Tier enum re-export so macros can reach it through the
163    /// `__private` namespace without forcing user code to depend on
164    /// `obs-proto` directly.
165    pub use obs_proto::obs::v1::Severity as ProtoSeverity;
166    pub use obs_proto::{
167        __private::*,
168        obs::v1::{SamplingReason as ProtoSamplingReason, Tier as ProtoTier},
169    };
170    pub use obs_types::*;
171    pub use secrecy;
172    pub use serde_json;
173
174    pub use crate::{
175        callsite::ObsCallsite,
176        codegen_helpers::{BuildableTo, EnumCount, FieldCapture, SpanCtx, SpanFrame},
177        forensic::{ForensicLimiter, ensure_limiter, try_acquire_forensic},
178        registry::{EVENT_SCHEMAS, EventSchemaErased, Sealed},
179        scope::{ScopeField, ScopeGuard, ScopeKind},
180        wire::BuffaEncodeField,
181    };
182
183    /// L011 helper: const-time test that a string starts with a known
184    /// prefix.
185    #[must_use]
186    #[allow(clippy::indexing_slicing)]
187    pub const fn starts_with_obs(name: &str, prefix: &str) -> bool {
188        let n = name.as_bytes();
189        let p = prefix.as_bytes();
190        if n.len() < p.len() {
191            return false;
192        }
193        let mut i = 0;
194        while i < p.len() {
195            if n[i] != p[i] {
196                return false;
197            }
198            i += 1;
199        }
200        true
201    }
202}