Skip to main content

ssg/
otel.rs

1// Copyright © 2023 - 2026 Static Site Generator (SSG). All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! OpenTelemetry build-pipeline tracing scaffolding (issue #422).
5//!
6//! This module is intentionally a *scaffold*. The full deliverable
7//! ships in two phases:
8//!
9//! 1. **Phase A (this commit)** — `otel` Cargo feature, `--trace` CLI
10//!    flag, an `init_if_enabled` initialiser that attaches a
11//!    `tracing-subscriber` JSON formatter to stdout, and one demo
12//!    span around `pipeline::execute_build_pipeline` via
13//!    `#[tracing::instrument]`.
14//!
15//! 2. **Phase B (deferred follow-up)** — per-plugin spans inside
16//!    `PluginManager::run_*`, file-count + duration + peak-RSS
17//!    delta fields, OTLP/gRPC + Jaeger exporter wiring, and a
18//!    Grafana dashboard JSON. Phase B requires `tokio` to be
19//!    introduced; the rest of SSG is rayon-based, so that
20//!    architectural decision is deliberately deferred.
21//!
22//! When the `otel` feature is **off**, this module compiles to an
23//! empty stub — `init_if_enabled` is a no-op. Callers may invoke it
24//! unconditionally and it will simply do nothing.
25
26/// Initialises tracing if both:
27///
28/// 1. The crate was compiled with the `otel` feature, and
29/// 2. `enabled` is `true` (typically driven by the `--trace` CLI flag).
30///
31/// On `(true, true)`: installs a `tracing-subscriber` global
32/// dispatcher with JSON formatting to stdout, level filter from
33/// `RUST_LOG` (default `info`).
34///
35/// In any other case: returns immediately, no global state mutated.
36///
37/// # Returns
38///
39/// `true` if a subscriber was installed; `false` otherwise.
40pub fn init_if_enabled(enabled: bool) -> bool {
41    if !enabled {
42        return false;
43    }
44    real::init()
45}
46
47#[cfg(feature = "otel")]
48mod real {
49    use tracing_subscriber::{fmt::format::FmtSpan, prelude::*, EnvFilter};
50
51    pub(super) fn init() -> bool {
52        // Idempotent: if a subscriber is already installed (e.g.
53        // double `--trace` invocation in a script that re-enters),
54        // silently no-op.
55        let filter = EnvFilter::try_from_default_env()
56            .or_else(|_| EnvFilter::try_new("info"))
57            .unwrap_or_default();
58
59        let layer = tracing_subscriber::fmt::layer()
60            .json()
61            .with_span_events(FmtSpan::CLOSE)
62            .with_target(true);
63
64        let installed = tracing_subscriber::registry()
65            .with(filter)
66            .with(layer)
67            .try_init()
68            .is_ok();
69
70        if installed {
71            tracing::info!(
72                target = "ssg::otel",
73                "OpenTelemetry build tracing enabled (JSON to stdout)"
74            );
75        }
76        installed
77    }
78}
79
80#[cfg(not(feature = "otel"))]
81mod real {
82    /// When the `otel` feature is disabled the runtime is absent;
83    /// even if `--trace` is passed, we emit a warning via the
84    /// existing `log` facade and return `false`. The CLI flag is
85    /// still parsed so scripts work across feature-on/feature-off
86    /// builds without conditional logic.
87    pub(super) fn init() -> bool {
88        log::warn!(
89            "[--trace] requested but this binary was built without the \
90             `otel` feature. Rebuild with `cargo build --features otel` \
91             to enable build-pipeline tracing."
92        );
93        false
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn init_disabled_is_noop_returns_false() {
103        // Always returns false when the caller passes `false`,
104        // regardless of feature state.
105        assert!(!init_if_enabled(false));
106    }
107
108    #[cfg(not(feature = "otel"))]
109    #[test]
110    fn init_enabled_without_feature_warns_and_returns_false() {
111        // Without the feature compiled in, the second call also
112        // returns false (it logs a warning via `log` rather than
113        // panicking).
114        assert!(!init_if_enabled(true));
115    }
116}