Skip to main content

lattice_config/
plugin_options.rs

1// `linkme`'s distributed slices use `link_section` to aggregate items at link
2// time. The `options!` macro expansion below emits such a declaration; allow the
3// workspace's `unsafe_code = "deny"` lint locally with the same safety rationale
4// documented in `option_decl.rs`, `group.rs`, and `core_options.rs`.
5#![allow(unsafe_code)]
6
7//! Plugin observability options (`plugin.*`). PO.4.3: `plugin.trace-level` sets
8//! the global default boundary-trace verbosity the host's `PluginTracer` gates on
9//! (design `docs/dev/architecture/plugin-observability.md` §7). The loader
10//! observes `Event::OptionChanged` and pushes the new level into the tracer live
11//! (PO.3's per-plugin gate republish reaches the hot path on the next keystroke).
12//!
13//! The value type mirrors `lattice_plugin_host::TraceLevel`, duplicated here
14//! because `lattice-config` is foundational and cannot depend on the plugin host;
15//! the loader bridges the two by the option's string form (the labels match).
16
17use crate::option_type::{EnumeratedValue, OptionType};
18
19/// `plugin.trace-level` — the global default plugin boundary-trace verbosity.
20/// Ordered least→most verbose; a record is kept when its level ≤ this gate.
21/// `info` (the default) drops per-call traces — authors opt a plugin up to
22/// `debug` / `trace` (globally here, or per-plugin from the `:plugins` view).
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum PluginTraceLevel {
25    /// Silence every plugin's trace entirely.
26    Off,
27    /// Traps only.
28    Error,
29    /// Guest errors and traps.
30    Warn,
31    /// The default: crash/lifecycle signal only, no per-call traces.
32    #[default]
33    Info,
34    /// Every host↔guest call, timed.
35    Debug,
36    /// Every call with argument / result detail.
37    Trace,
38}
39
40impl PluginTraceLevel {
41    /// The on-disk / `:set` spelling (`off` … `trace`). Deliberately
42    /// identical to `lattice_plugin_host::TraceLevel`'s labels: the
43    /// loader bridges the two types through this string.
44    pub fn label(&self) -> &'static str {
45        match self {
46            PluginTraceLevel::Off => "off",
47            PluginTraceLevel::Error => "error",
48            PluginTraceLevel::Warn => "warn",
49            PluginTraceLevel::Info => "info",
50            PluginTraceLevel::Debug => "debug",
51            PluginTraceLevel::Trace => "trace",
52        }
53    }
54
55    /// One-line description of this level for the `:set` value
56    /// completion marginalia.
57    pub fn doc(&self) -> &'static str {
58        match self {
59            PluginTraceLevel::Off => "Silence all plugin traces",
60            PluginTraceLevel::Error => "Traps only",
61            PluginTraceLevel::Warn => "Guest errors and traps",
62            PluginTraceLevel::Info => "Crash/lifecycle only (no per-call traces)",
63            PluginTraceLevel::Debug => "Every host↔guest call, timed",
64            PluginTraceLevel::Trace => "Every call with argument/result detail",
65        }
66    }
67
68    /// Every level, least → most verbose (the closed value set).
69    pub fn all() -> [PluginTraceLevel; 6] {
70        [
71            PluginTraceLevel::Off,
72            PluginTraceLevel::Error,
73            PluginTraceLevel::Warn,
74            PluginTraceLevel::Info,
75            PluginTraceLevel::Debug,
76            PluginTraceLevel::Trace,
77        ]
78    }
79
80    /// Parse a [`Self::label`] back into a level. Exact match; any
81    /// other input is an `Err` naming the accepted forms.
82    ///
83    /// # Examples
84    ///
85    /// ```
86    /// use lattice_config::PluginTraceLevel;
87    ///
88    /// assert_eq!(PluginTraceLevel::parse_label("debug"), Ok(PluginTraceLevel::Debug));
89    /// assert_eq!(PluginTraceLevel::default().label(), "info");
90    /// assert!(PluginTraceLevel::parse_label("verbose").is_err());
91    /// ```
92    pub fn parse_label(s: &str) -> Result<Self, String> {
93        match s {
94            "off" => Ok(PluginTraceLevel::Off),
95            "error" => Ok(PluginTraceLevel::Error),
96            "warn" => Ok(PluginTraceLevel::Warn),
97            "info" => Ok(PluginTraceLevel::Info),
98            "debug" => Ok(PluginTraceLevel::Debug),
99            "trace" => Ok(PluginTraceLevel::Trace),
100            other => Err(format!(
101                "plugin.trace-level: expected `off`, `error`, `warn`, `info`, `debug`, or `trace`, got `{other}`"
102            )),
103        }
104    }
105}
106
107impl OptionType for PluginTraceLevel {
108    fn parse(s: &str) -> Result<Self, String> {
109        PluginTraceLevel::parse_label(s)
110    }
111    fn format(&self) -> String {
112        self.label().to_string()
113    }
114    fn type_label() -> &'static str {
115        "plugin-trace-level"
116    }
117    fn enumerate() -> Option<Vec<&'static str>> {
118        Some(PluginTraceLevel::all().iter().map(|v| v.label()).collect())
119    }
120
121    /// TC.1: closed — `parse` accepts these forms and nothing else, so
122    /// the schema is an `enum` and `:customize` can offer a picker.
123    fn enumerate_is_exhaustive() -> bool {
124        true
125    }
126    fn enumerate_with_docs() -> Option<Vec<EnumeratedValue>> {
127        Some(
128            PluginTraceLevel::all()
129                .iter()
130                .map(|v| EnumeratedValue {
131                    form: v.label(),
132                    doc: v.doc(),
133                })
134                .collect(),
135        )
136    }
137}
138
139crate::options! {
140    group = crate::Plugin;
141
142    /// Global default plugin boundary-trace verbosity. `off` / `error` / `warn` /
143    /// `info` (default) / `debug` / `trace`. `info` and below carry only
144    /// crash/lifecycle signal — no per-call traces (the keystroke hot path stays
145    /// free). Raise to `debug` to trace every host↔guest call; per-plugin
146    /// overrides live in the `:plugins` view (`T`).
147    #[name("plugin.trace-level")]
148    pub PluginTraceLevelOption: PluginTraceLevel = PluginTraceLevel::Info;
149}
150
151#[cfg(test)]
152mod tests {
153    #![allow(clippy::unwrap_used, clippy::panic)]
154    use crate::{ConfigRegistry, PluginTraceLevel, PluginTraceLevelOption};
155
156    fn reg() -> ConfigRegistry {
157        let r = ConfigRegistry::new();
158        r.init_from_linkme();
159        r
160    }
161
162    #[test]
163    fn default_is_info() {
164        let r = reg();
165        assert_eq!(
166            *r.get_typed::<PluginTraceLevelOption>().unwrap(),
167            PluginTraceLevel::Info
168        );
169    }
170
171    #[test]
172    fn set_debug_parses() {
173        let r = reg();
174        r.parse_and_set_command("plugin.trace-level=debug").unwrap();
175        assert_eq!(
176            *r.get_typed::<PluginTraceLevelOption>().unwrap(),
177            PluginTraceLevel::Debug
178        );
179    }
180
181    #[test]
182    fn bad_value_errors() {
183        let r = reg();
184        assert!(r.parse_and_set_command("plugin.trace-level=loud").is_err());
185    }
186
187    #[test]
188    fn label_round_trips_every_level() {
189        for lvl in PluginTraceLevel::all() {
190            assert_eq!(PluginTraceLevel::parse_label(lvl.label()), Ok(lvl));
191        }
192    }
193}