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}