Skip to main content

degenbot_cli/
sinks.rs

1//! The telemetry sinks + boot order (ADR-043, ADR-051 D2/D9).
2//!
3//! The `#[pymodule]` init registers symbols only; the Python driver installs
4//! its subscriber in `driver_boot()`. This module installs the console's
5//! subscriber with the standalone-Rust-consumer default, so a one-shot
6//! command is silent at `info` unless an explicit `RUST_LOG` says otherwise:
7//!
8//! 1. **Typed config first.** `BotConfigLoader` (schema defaults + the
9//!    standard file layer + `DEGENBOT_*` env) is installed into the typed
10//!    holder BEFORE any subscriber exists, so `telemetry.log_level` /
11//!    `telemetry.diag` are the values the filter resolution actually sees. An
12//!    invalid config is a loud boot refusal (exit 2), exactly as Python's
13//!    module init refuses.
14//! 2. **The tracing registry.** A `fmt` layer on stderr filtered by
15//!    [`resolve_filters`](degenbot_bot::telemetry::resolve_filters) with the
16//!    Python-driver wiring default (`info`) - the same resolution the binding
17//!    boots, so an explicit `RUST_LOG` wins verbatim on this sink too - plus
18//!    the [`progress`](crate::progress) bar layer (ADR-051 D9).
19//! 3. **The shared ADR-043 contracts**: `warn_retired_env_names`,
20//!    `install_panic_hook`. The worker-census boot table is NOT dumped here —
21//!    it belongs to the drivers' boot prelude, not a one-shot command.
22//!
23//! OTLP/metrics are deliberately NOT booted here: a console invocation is a
24//! short-lived process, not a scrape target, and the Python driver's OTLP layer
25//! belongs to the long-running bot process. `telemetry.otel` is still honored as
26//! config (the typed loader refuses a bad value) - it just has no sink to gate.
27
28use std::sync::Arc;
29
30use degenbot_bot::telemetry as bot_telemetry;
31use tracing_subscriber::layer::{Layer as _, SubscriberExt as _};
32use tracing_subscriber::EnvFilter;
33
34use crate::progress::{self, Layer as ProgressLayer, Painter};
35
36/// The process-lifetime telemetry handles. Dropping it clears the progress bar.
37#[derive(Debug)]
38#[must_use = "the boot guard must outlive the command run"]
39pub struct TelemetryBoot {
40    painter: Arc<Painter>,
41}
42
43impl Drop for TelemetryBoot {
44    fn drop(&mut self) {
45        self.painter.finish();
46    }
47}
48
49/// Boot the console's telemetry sinks (see the module docs for the order).
50///
51/// # Errors
52///
53/// The refusal message when the typed configuration is invalid - the process
54/// cannot honor a policy it does not have, so the boot is refused exactly as
55/// the Python module init refuses it.
56pub fn boot() -> Result<TelemetryBoot, String> {
57    // Step 1: the typed config. The loader is the ONE env-reading site.
58    match degenbot_config::BotConfigLoader::new()
59        .with_standard_file_paths()
60        .load()
61    {
62        Ok(loaded) => {
63            // First-wins, mirroring the Python boot path.
64            let _ = degenbot_bot::bot_core::stance::install(Arc::new(loaded.config));
65        }
66        Err(error) => return Err(format!("invalid configuration - boot refused: {error}")),
67    }
68
69    // Step 2: the console filter. The module init no longer installs a
70    // subscriber, so THIS install is the process's one telemetry surface and
71    // it resolves with the standalone-Rust-consumer default (ADR-043 §6):
72    // silent at `info`, loud through an explicit RUST_LOG. The Python driver
73    // resolves ITS console filter in driver_boot() with its own default.
74    let plan = bot_telemetry::resolve_filters(bot_telemetry::CONSOLE_WIRING_DEFAULT_RUST);
75    let painter = Arc::new(Painter::from_draw_target(progress::stderr_opt()));
76    let console_filter = EnvFilter::new(&plan.console);
77    let subscriber = tracing_subscriber::registry()
78        .with(
79            tracing_subscriber::fmt::layer()
80                .compact()
81                .with_writer(std::io::stderr)
82                .with_filter(console_filter.clone()),
83        )
84        .with(ProgressLayer::new(Arc::clone(&painter)).with_filter(console_filter));
85    let installed = tracing::subscriber::set_global_default(subscriber).is_ok();
86
87    // Step 3: the shared ADR-043 section 2/5 contracts. The census boot
88    // table belongs to the drivers' boot prelude — a one-shot command arms
89    // BOOT_DUMPED by dumping it, which would re-classify every later
90    // registration as a late-registration notice; the register() entries
91    // still document this process's spawn sites either way.
92    degenbot_core::telemetry::warn_retired_env_names();
93    degenbot_core::telemetry::install_panic_hook();
94
95    degenbot_core::op_debug!(
96        domain = pump,
97        console = %plan.console,
98        "telemetry boot complete"
99    );
100    if !installed {
101        degenbot_core::op_warn!(
102            domain = pump,
103            "global tracing subscriber already installed; console wiring left untouched"
104        );
105    }
106
107    Ok(TelemetryBoot { painter })
108}