diff --git a/.changeset/bootstrap-honest-status.md b/.changeset/bootstrap-honest-status.md new file mode 100644 index 0000000..31c57b6 --- /dev/null +++ b/.changeset/bootstrap-honest-status.md @@ -0,0 +1,7 @@ +--- +'@smooai/observability': minor +--- + +Rust bootstrap now reports whether it is actually EXPORTING, not just whether it +ran, and warns loudly when no OTLP endpoint is configured. `installed: true` with +no endpoint used to read as success while nothing left the process. diff --git a/rust/observability/src/bootstrap.rs b/rust/observability/src/bootstrap.rs index fe6eaab..0d31c8e 100644 --- a/rust/observability/src/bootstrap.rs +++ b/rust/observability/src/bootstrap.rs @@ -83,7 +83,18 @@ impl BootstrapEnv { #[derive(Clone)] pub struct BootstrapResult { /// Whether the bootstrap actually ran (false = disabled or already-installed-elsewhere). + /// + /// NOTE: `installed: true` only means bootstrap was not disabled. It does + /// NOT mean anything is being exported — check [`Self::exporting`] for + /// that. These were the same flag until 2026-08-15, and the conflation hid + /// a production service emitting nothing for months: it had no endpoint + /// configured, so the OTel SDK was never installed, yet bootstrap reported + /// success and the pod's own "self-emit enabled" log line printed anyway. pub installed: bool, + /// Whether an OTLP exporter was actually installed — i.e. whether spans, + /// metrics and logs have somewhere to go. False when no endpoint is + /// configured, in which case this SDK is a no-op for telemetry. + pub exporting: bool, /// OTel handle — flush/shutdown. `None` if no endpoint was configured. pub otel: Option, /// The capture client. Always present; capture-handler-only if no DSN. @@ -128,6 +139,7 @@ async fn build(env: BootstrapEnv) -> BootstrapResult { if env.disabled { return BootstrapResult { installed: false, + exporting: false, otel: None, client: Client::init(ClientOptions::default()), }; @@ -190,6 +202,17 @@ async fn build(env: BootstrapEnv) -> BootstrapResult { opts.token_provider = token_provider; Some(setup_otel_sdk(opts)) } else { + // The single most expensive silence this SDK can produce. With no + // endpoint, nothing is exported — but every other signal (the bootstrap + // return, the caller's own "observability enabled" log) still says + // healthy. Say it plainly instead, and name the variable to set. + warn(concat!( + "NO OTLP ENDPOINT CONFIGURED — telemetry is NOT being exported. ", + "Nothing this process emits will reach a collector. ", + "Set SMOOAI_OBSERVABILITY_ENDPOINT (or a per-signal ", + "OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS}_ENDPOINT), or set ", + "SMOOAI_OBSERVABILITY_DISABLED=true to make this silence deliberate." + )); None }; @@ -214,11 +237,37 @@ async fn build(env: BootstrapEnv) -> BootstrapResult { BootstrapResult { installed: true, + exporting: otel.is_some(), otel, client, } } + +#[cfg(test)] +mod exporting_status_tests { + use super::*; + + /// The inverse of the no-endpoint case: with an endpoint configured, the + /// result must claim it IS exporting. Without both halves asserted, a + /// regression that hard-codes either value passes. + #[tokio::test] + async fn an_endpoint_reports_exporting_true() { + let env = BootstrapEnv { + endpoint: Some("https://collector.example.com".to_string()), + service_name: Some("test-service".to_string()), + ..Default::default() + }; + let result = build(env).await; + assert!(result.installed); + assert!( + result.exporting, + "an endpoint was configured, so exporting must be true" + ); + assert!(result.otel.is_some()); + } +} + fn strip_trailing_slash(url: &str) -> String { url.trim_end_matches('/').to_string() } @@ -279,7 +328,14 @@ mod tests { ..Default::default() }; let result = build(env).await; - assert!(result.installed); + assert!(result.installed, "bootstrap ran"); + // …but it is NOT exporting, and the result now says so. This assertion + // is the whole point: `installed` alone used to be the only signal, and + // it reads as "everything is fine" while nothing leaves the process. + assert!( + !result.exporting, + "no endpoint configured must report exporting = false" + ); assert!(result.otel.is_none()); // capture client still usable result