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}