lattice_plugin_trace/
format.rs1use lattice_plugin_host::{Direction, PluginSeam, PluginTraceRecord, TraceLevel, TraceOutcome};
14
15pub const SHARED_BUFFER_NAME: &str = "*plugin-trace*";
17pub const TRACE_MODE_ID: &str = "plugin-trace-mode";
18
19pub fn per_plugin_buffer_name(plugin: &str) -> String {
24 format!("*plugin-trace:{plugin}*")
25}
26
27pub fn parse_per_plugin_name(buffer_name: &str) -> Option<&str> {
31 buffer_name
32 .strip_prefix("*plugin-trace:")
33 .and_then(|rest| rest.strip_suffix('*'))
34}
35
36fn level_tag(level: TraceLevel) -> String {
39 format!("{:<5}", level.as_str())
40}
41
42fn fuel_short(fuel: u64) -> String {
45 if fuel >= 1000 {
46 format!("{:.1}k", fuel as f64 / 1000.0)
47 } else {
48 fuel.to_string()
49 }
50}
51
52fn outcome_cell(level: TraceLevel, outcome: &TraceOutcome) -> String {
57 match outcome {
58 TraceOutcome::Trap { kind, .. } => format!("trap({kind})"),
59 TraceOutcome::Denied { capability } => format!("denied({capability})"),
60 TraceOutcome::Ok { .. } if level == TraceLevel::Warn => "guest-err".to_string(),
61 TraceOutcome::Ok { micros, fuel_delta } => {
62 let mut s = format!("ok {micros}µs");
63 if *fuel_delta > 0 {
64 s.push_str(&format!(", {} fuel", fuel_short(*fuel_delta)));
65 }
66 s
67 }
68 }
69}
70
71fn direction_arrow(direction: Direction) -> &'static str {
74 match direction {
75 Direction::HostImport => "«", Direction::GuestExport => "»", }
78}
79
80pub fn format_trace_line(record: &PluginTraceRecord) -> String {
89 if record.seam == PluginSeam::Logging {
90 let message = record.detail.as_deref().unwrap_or("");
91 let context = record.call.as_ref();
92 return if context.is_empty() {
93 format!(
94 "{lvl} [plugin:{id}] logging: {message}",
95 lvl = level_tag(record.level),
96 id = record.plugin,
97 )
98 } else {
99 format!(
100 "{lvl} [plugin:{id}] logging {context}: {message}",
101 lvl = level_tag(record.level),
102 id = record.plugin,
103 )
104 };
105 }
106 format!(
107 "{lvl} [plugin:{id}] {seam} {dir}{call} → {outcome}",
108 lvl = level_tag(record.level),
109 id = record.plugin,
110 seam = record.seam.as_str(),
111 dir = direction_arrow(record.direction),
112 call = record.call,
113 outcome = outcome_cell(record.level, &record.outcome),
114 )
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use lattice_plugin_host::PluginSeam;
121
122 fn rec(level: TraceLevel, outcome: TraceOutcome) -> PluginTraceRecord {
123 PluginTraceRecord {
124 plugin: 3,
125 seam: PluginSeam::Grammar,
126 direction: Direction::GuestExport,
127 call: "apply-motion".into(),
128 level,
129 outcome,
130 detail: None,
131 }
132 }
133
134 #[test]
135 fn a_success_shows_level_plugin_seam_call_and_timing() {
136 let line = format_trace_line(&rec(
137 TraceLevel::Debug,
138 TraceOutcome::Ok {
139 micros: 34,
140 fuel_delta: 0,
141 },
142 ));
143 assert_eq!(line, "debug [plugin:3] grammar »apply-motion → ok 34µs");
144 }
145
146 #[test]
147 fn fuel_is_appended_and_shortened_when_nonzero() {
148 let line = format_trace_line(&rec(
149 TraceLevel::Debug,
150 TraceOutcome::Ok {
151 micros: 12,
152 fuel_delta: 1234,
153 },
154 ));
155 assert!(line.ends_with("→ ok 12µs, 1.2k fuel"), "got {line}");
156 }
157
158 #[test]
159 fn a_warn_ok_reads_as_guest_err() {
160 let line = format_trace_line(&rec(
161 TraceLevel::Warn,
162 TraceOutcome::Ok {
163 micros: 0,
164 fuel_delta: 0,
165 },
166 ));
167 assert!(line.starts_with("warn "), "got {line}");
168 assert!(line.ends_with("→ guest-err"), "got {line}");
169 }
170
171 #[test]
172 fn a_trap_shows_its_kind() {
173 let line = format_trace_line(&rec(
174 TraceLevel::Error,
175 TraceOutcome::Trap {
176 kind: "fuel".to_string(),
177 func: "apply-motion".to_string(),
178 },
179 ));
180 assert!(line.starts_with("error "), "got {line}");
181 assert!(line.ends_with("→ trap(fuel)"), "got {line}");
182 }
183
184 #[test]
185 fn a_denied_shows_the_capability() {
186 let mut r = rec(
187 TraceLevel::Error,
188 TraceOutcome::Denied {
189 capability: "fs".to_string(),
190 },
191 );
192 r.direction = Direction::HostImport;
193 let line = format_trace_line(&r);
194 assert!(
195 line.contains("«apply-motion"),
196 "guest→host glyph, got {line}"
197 );
198 assert!(line.ends_with("→ denied(fs)"), "got {line}");
199 }
200
201 fn log_rec(context: &str, message: &str) -> PluginTraceRecord {
202 PluginTraceRecord {
203 plugin: 3,
204 seam: PluginSeam::Logging,
205 direction: Direction::HostImport,
206 call: context.to_string().into(),
207 level: TraceLevel::Info,
208 outcome: TraceOutcome::Ok {
209 micros: 0,
210 fuel_delta: 0,
211 },
212 detail: Some(message.to_string()),
213 }
214 }
215
216 #[test]
217 fn a_logging_record_renders_the_message_not_the_outcome() {
218 let line = format_trace_line(&log_rec("parser", "reindexed 40 files"));
220 assert_eq!(line, "info [plugin:3] logging parser: reindexed 40 files");
221 }
222
223 #[test]
224 fn a_logging_record_without_context_omits_the_category() {
225 let line = format_trace_line(&log_rec("", "hello"));
226 assert_eq!(line, "info [plugin:3] logging: hello");
227 }
228
229 #[test]
230 fn per_plugin_name_round_trips() {
231 let name = per_plugin_buffer_name("fuzzy-finder");
232 assert_eq!(name, "*plugin-trace:fuzzy-finder*");
233 assert_eq!(parse_per_plugin_name(&name), Some("fuzzy-finder"));
234 assert_eq!(parse_per_plugin_name(SHARED_BUFFER_NAME), None);
236 assert_eq!(parse_per_plugin_name("*scratch*"), None);
237 }
238}