ssg/core/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.
40///
41/// # Examples
42///
43/// ```rust
44/// use ssg::otel::init_if_enabled;
45///
46/// // Disabled ⇒ always returns false, never installs a subscriber.
47/// assert!(!init_if_enabled(false));
48/// ```
49pub fn init_if_enabled(enabled: bool) -> bool {
50 if !enabled {
51 return false;
52 }
53 real::init()
54}
55
56#[cfg(feature = "otel")]
57mod real {
58 use tracing_subscriber::{fmt::format::FmtSpan, prelude::*, EnvFilter};
59
60 pub(super) fn init() -> bool {
61 // Idempotent: if a subscriber is already installed (e.g.
62 // double `--trace` invocation in a script that re-enters),
63 // silently no-op.
64 let filter = EnvFilter::try_from_default_env()
65 .or_else(|_| EnvFilter::try_new("info"))
66 .unwrap_or_default();
67
68 let layer = tracing_subscriber::fmt::layer()
69 .json()
70 .with_span_events(FmtSpan::CLOSE)
71 .with_target(true);
72
73 let installed = tracing_subscriber::registry()
74 .with(filter)
75 .with(layer)
76 .try_init()
77 .is_ok();
78
79 if installed {
80 tracing::info!(
81 target = "ssg::otel",
82 "OpenTelemetry build tracing enabled (JSON to stdout)"
83 );
84 }
85 installed
86 }
87}
88
89#[cfg(not(feature = "otel"))]
90mod real {
91 /// When the `otel` feature is disabled the runtime is absent;
92 /// even if `--trace` is passed, we emit a warning via the
93 /// existing `log` facade and return `false`. The CLI flag is
94 /// still parsed so scripts work across feature-on/feature-off
95 /// builds without conditional logic.
96 pub(super) fn init() -> bool {
97 log::warn!(
98 "[--trace] requested but this binary was built without the \
99 `otel` feature. Rebuild with `cargo build --features otel` \
100 to enable build-pipeline tracing."
101 );
102 false
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn init_disabled_is_noop_returns_false() {
112 // Always returns false when the caller passes `false`,
113 // regardless of feature state.
114 assert!(!init_if_enabled(false));
115 }
116
117 #[cfg(not(feature = "otel"))]
118 #[test]
119 fn init_enabled_without_feature_warns_and_returns_false() {
120 // Without the feature compiled in, the second call also
121 // returns false (it logs a warning via `log` rather than
122 // panicking).
123 assert!(!init_if_enabled(true));
124 }
125}