clnrm_core/telemetry/
cli_helpers.rs

1//! CLI Telemetry Helpers
2//!
3//! Provides builder pattern helpers for emitting CLI command telemetry
4//! that conforms to registry schemas.
5
6use std::time::Instant;
7use tracing::{info_span, Span};
8
9/// Builder for CLI initialization span (clnrm init)
10pub struct CliInitSpanBuilder {
11    project_path: String,
12    exists_before: bool,
13    force_used: bool,
14}
15
16impl CliInitSpanBuilder {
17    pub fn new(project_path: String, exists_before: bool, force_used: bool) -> Self {
18        Self {
19            project_path,
20            exists_before,
21            force_used,
22        }
23    }
24
25    pub fn start(self) -> CliInitSpan {
26        let span = info_span!(
27            "clnrm.cli.init",
28            cli.command = "init",
29            cli.version = env!("CARGO_PKG_VERSION"),
30            project.path = %self.project_path,
31            project.exists_before = self.exists_before,
32            force.used = self.force_used,
33        );
34
35        CliInitSpan {
36            span,
37            start_time: Instant::now(),
38        }
39    }
40}
41
42pub struct CliInitSpan {
43    span: Span,
44    start_time: Instant,
45}
46
47impl CliInitSpan {
48    pub fn finish(
49        self,
50        success: bool,
51        config_generated: bool,
52        config_path: Option<String>,
53        files_created: usize,
54        error: Option<(String, String)>,
55    ) {
56        let duration_ms = self.start_time.elapsed().as_secs_f64() * 1000.0;
57
58        let _enter = self.span.enter();
59
60        // Required attributes
61        self.span.record("operation.success", success);
62        self.span.record("config.generated", config_generated);
63        self.span.record("operation.duration_ms", duration_ms);
64
65        // Recommended attributes
66        if let Some(path) = config_path {
67            self.span.record("config.path", path.as_str());
68        }
69        self.span.record("files.created", files_created as i64);
70
71        // Conditional error attributes
72        if let Some((error_type, error_message)) = error {
73            self.span.record("error.type", error_type.as_str());
74            self.span.record("error.message", error_message.as_str());
75        }
76    }
77}
78
79/// Builder for CLI plugins span (clnrm plugins)
80pub struct CliPluginsSpanBuilder;
81
82impl Default for CliPluginsSpanBuilder {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88impl CliPluginsSpanBuilder {
89    pub fn new() -> Self {
90        Self
91    }
92
93    pub fn start(self) -> CliPluginsSpan {
94        let span = info_span!(
95            "clnrm.cli.plugins",
96            cli.command = "plugins",
97            cli.version = env!("CARGO_PKG_VERSION"),
98        );
99
100        CliPluginsSpan {
101            span,
102            start_time: Instant::now(),
103        }
104    }
105}
106
107pub struct CliPluginsSpan {
108    span: Span,
109    start_time: Instant,
110}
111
112impl CliPluginsSpan {
113    pub fn finish(
114        self,
115        success: bool,
116        plugins_discovered: usize,
117        plugins_builtin: usize,
118        plugins_custom: usize,
119        plugins_by_type: Option<String>,
120        error: Option<(String, String)>,
121    ) {
122        let duration_ms = self.start_time.elapsed().as_secs_f64() * 1000.0;
123
124        let _enter = self.span.enter();
125
126        // Required attributes
127        self.span.record("operation.success", success);
128        self.span
129            .record("plugins.discovered", plugins_discovered as i64);
130        self.span.record("operation.duration_ms", duration_ms);
131
132        // Recommended attributes
133        self.span.record("plugins.builtin", plugins_builtin as i64);
134        self.span.record("plugins.custom", plugins_custom as i64);
135
136        if let Some(json) = plugins_by_type {
137            self.span.record("plugins.by_type", json.as_str());
138        }
139
140        // Conditional error attributes
141        if let Some((error_type, error_message)) = error {
142            self.span.record("error.type", error_type.as_str());
143            self.span.record("error.message", error_message.as_str());
144        }
145    }
146}
147
148/// Builder for CLI health span (clnrm health)
149pub struct CliHealthSpanBuilder {
150    verbose: bool,
151}
152
153impl CliHealthSpanBuilder {
154    pub fn new(verbose: bool) -> Self {
155        Self { verbose }
156    }
157
158    pub fn start(self) -> CliHealthSpan {
159        let span = info_span!(
160            "clnrm.cli.health",
161            cli.command = "health",
162            cli.version = env!("CARGO_PKG_VERSION"),
163            verbose.enabled = self.verbose,
164        );
165
166        CliHealthSpan {
167            span,
168            start_time: Instant::now(),
169        }
170    }
171}
172
173pub struct CliHealthSpan {
174    span: Span,
175    start_time: Instant,
176}
177
178/// Parameters for finishing a health check span
179#[derive(Debug, Default)]
180pub struct HealthCheckResult {
181    pub success: bool,
182    pub overall: String,
183    pub checks_total: usize,
184    pub checks_passed: usize,
185    pub checks_failed: usize,
186    pub docker_available: bool,
187    pub docker_version: Option<String>,
188    pub docker_type: Option<String>,
189    pub weaver_available: bool,
190    pub weaver_version: Option<String>,
191    pub error: Option<(String, String)>,
192}
193
194impl HealthCheckResult {
195    pub fn builder() -> HealthCheckResultBuilder {
196        HealthCheckResultBuilder::default()
197    }
198}
199
200/// Builder for HealthCheckResult
201#[derive(Debug, Default)]
202pub struct HealthCheckResultBuilder {
203    result: HealthCheckResult,
204}
205
206impl HealthCheckResultBuilder {
207    pub fn success(mut self, success: bool) -> Self {
208        self.result.success = success;
209        self
210    }
211
212    pub fn overall(mut self, overall: impl Into<String>) -> Self {
213        self.result.overall = overall.into();
214        self
215    }
216
217    pub fn checks(mut self, total: usize, passed: usize, failed: usize) -> Self {
218        self.result.checks_total = total;
219        self.result.checks_passed = passed;
220        self.result.checks_failed = failed;
221        self
222    }
223
224    pub fn docker(
225        mut self,
226        available: bool,
227        version: Option<String>,
228        dtype: Option<String>,
229    ) -> Self {
230        self.result.docker_available = available;
231        self.result.docker_version = version;
232        self.result.docker_type = dtype;
233        self
234    }
235
236    pub fn weaver(mut self, available: bool, version: Option<String>) -> Self {
237        self.result.weaver_available = available;
238        self.result.weaver_version = version;
239        self
240    }
241
242    pub fn error(mut self, error_type: String, error_message: String) -> Self {
243        self.result.error = Some((error_type, error_message));
244        self
245    }
246
247    pub fn build(self) -> HealthCheckResult {
248        self.result
249    }
250}
251
252impl CliHealthSpan {
253    /// Finish the health span with a single parameter object
254    pub fn finish(self, result: HealthCheckResult) {
255        let duration_ms = self.start_time.elapsed().as_secs_f64() * 1000.0;
256
257        let _enter = self.span.enter();
258
259        // Required attributes
260        self.span.record("operation.success", result.success);
261        self.span.record("health.overall", result.overall.as_str());
262        self.span
263            .record("health.checks_total", result.checks_total as i64);
264        self.span
265            .record("health.checks_passed", result.checks_passed as i64);
266        self.span
267            .record("health.checks_failed", result.checks_failed as i64);
268        self.span
269            .record("docker.available", result.docker_available);
270        self.span.record("operation.duration_ms", duration_ms);
271
272        // Recommended attributes
273        if let Some(version) = result.docker_version {
274            self.span.record("docker.version", version.as_str());
275        }
276        if let Some(dtype) = result.docker_type {
277            self.span.record("docker.type", dtype.as_str());
278        }
279        self.span
280            .record("weaver.available", result.weaver_available);
281        if let Some(version) = result.weaver_version {
282            self.span.record("weaver.version", version.as_str());
283        }
284
285        // Conditional error attributes
286        if let Some((error_type, error_message)) = result.error {
287            self.span.record("error.type", error_type.as_str());
288            self.span.record("error.message", error_message.as_str());
289        }
290    }
291}
292
293/// Builder for CLI self-test span (clnrm self-test)
294pub struct CliSelfTestSpanBuilder {
295    suite: Option<String>,
296}
297
298impl CliSelfTestSpanBuilder {
299    pub fn new(suite: Option<String>) -> Self {
300        Self { suite }
301    }
302
303    pub fn start(self) -> CliSelfTestSpan {
304        let suite_name = self.suite.as_deref().unwrap_or("all");
305
306        let span = info_span!(
307            "clnrm.cli.self_test",
308            cli.command = "self-test",
309            cli.version = env!("CARGO_PKG_VERSION"),
310            test.suite = suite_name,
311        );
312
313        CliSelfTestSpan {
314            span,
315            start_time: Instant::now(),
316        }
317    }
318}
319
320pub struct CliSelfTestSpan {
321    span: Span,
322    start_time: Instant,
323}
324
325impl CliSelfTestSpan {
326    pub fn finish(
327        self,
328        success: bool,
329        tests_total: usize,
330        tests_passed: usize,
331        tests_failed: usize,
332        error: Option<(String, String)>,
333    ) {
334        let duration_ms = self.start_time.elapsed().as_secs_f64() * 1000.0;
335
336        let _enter = self.span.enter();
337
338        // Required attributes
339        self.span.record("operation.success", success);
340        self.span.record("test.count", tests_total as i64);
341        self.span.record("test.passed", tests_passed as i64);
342        self.span.record("test.failed", tests_failed as i64);
343        self.span.record("operation.duration_ms", duration_ms);
344
345        // Conditional error attributes
346        if let Some((error_type, error_message)) = error {
347            self.span.record("error.type", error_type.as_str());
348            self.span.record("error.message", error_message.as_str());
349        }
350    }
351}