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}