Skip to main content

cuenv_events/
lib.rs

1//! Structured event system for cuenv.
2//!
3//! This crate provides a unified event system that enables multiple UI frontends
4//! (CLI, TUI, Web) to subscribe to a single event stream. Events are emitted using
5//! tracing macros and captured by a custom tracing Layer.
6//!
7//! # Architecture
8//!
9//! ```text
10//! ┌─────────────────────────────────────────────────────────────────────────┐
11//! │                           cuenv-events crate                            │
12//! │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  ┌─────────────┐ │
13//! │  │ Event Schema │  │ EventBus     │  │ Tracing Layer│  │ Renderers   │ │
14//! │  │ (typed)      │  │ (broadcast)  │  │ (capture)    │  │ (CLI/JSON)  │ │
15//! │  └──────────────┘  └──────────────┘  └──────────────┘  └─────────────┘ │
16//! └─────────────────────────────────────────────────────────────────────────┘
17//! ```
18//!
19//! # Usage
20//!
21//! ```rust,ignore
22//! use cuenv_events::{EventBus, CuenvEventLayer, emit_task_started};
23//! use tracing_subscriber::layer::SubscriberExt;
24//! use tracing_subscriber::util::SubscriberInitExt;
25//!
26//! // Create event bus and layer
27//! let bus = EventBus::new();
28//! let layer = CuenvEventLayer::new(bus.sender().inner);
29//!
30//! // Initialize tracing with the layer
31//! tracing_subscriber::registry()
32//!     .with(layer)
33//!     .init();
34//!
35//! // Emit events using macros
36//! emit_task_started!("build", "cargo build", false);
37//! ```
38
39pub mod bus;
40pub mod event;
41pub mod layer;
42pub mod metadata;
43pub mod redaction;
44pub mod renderers;
45
46// Re-exports for convenience
47pub use bus::{EventBus, EventReceiver, EventSender, SendError};
48pub use event::{
49    CiEvent, CommandEvent, CuenvEvent, EventCategory, EventSource, InteractiveEvent, OutputEvent,
50    Stream, SystemEvent, TaskEvent,
51};
52pub use layer::CuenvEventLayer;
53pub use metadata::{MetadataContext, correlation_id, set_correlation_id};
54pub use redaction::{REDACTED_PLACEHOLDER, redact, register_secret, register_secrets};
55pub use renderers::{CliRenderer, JsonRenderer};
56
57// ============================================================================
58// Emit Macros
59// ============================================================================
60
61/// Emit a task started event.
62///
63/// # Example
64/// ```rust,ignore
65/// emit_task_started!("build", "cargo build", true);
66/// ```
67#[macro_export]
68macro_rules! emit_task_started {
69    ($name:expr, $command:expr, $hermetic:expr) => {
70        ::tracing::info!(
71            target: "cuenv::task",
72            event_type = "task.started",
73            task_name = %$name,
74            command = %$command,
75            hermetic = $hermetic,
76        )
77    };
78}
79
80/// Emit a task cache hit event.
81///
82/// # Example
83/// ```rust,ignore
84/// emit_task_cache_hit!("build", "abc123");
85/// ```
86#[macro_export]
87macro_rules! emit_task_cache_hit {
88    ($name:expr, $cache_key:expr) => {
89        ::tracing::info!(
90            target: "cuenv::task",
91            event_type = "task.cache_hit",
92            task_name = %$name,
93            cache_key = %$cache_key,
94        )
95    };
96}
97
98/// Emit a task cache miss event.
99#[macro_export]
100macro_rules! emit_task_cache_miss {
101    ($name:expr) => {
102        ::tracing::info!(
103            target: "cuenv::task",
104            event_type = "task.cache_miss",
105            task_name = %$name,
106        )
107    };
108}
109
110/// Emit a task output event.
111///
112/// # Example
113/// ```rust,ignore
114/// emit_task_output!("build", "stdout", "Compiling...");
115/// ```
116#[macro_export]
117macro_rules! emit_task_output {
118    ($name:expr, $stream:expr, $content:expr) => {
119        ::tracing::info!(
120            target: "cuenv::task",
121            event_type = "task.output",
122            task_name = %$name,
123            stream = $stream,
124            content = %$content,
125        )
126    };
127}
128
129/// Emit a task completed event.
130///
131/// # Example
132/// ```rust,ignore
133/// emit_task_completed!("build", true, Some(0), 1234);
134/// ```
135#[macro_export]
136macro_rules! emit_task_completed {
137    ($name:expr, $success:expr, $exit_code:expr, $duration_ms:expr) => {
138        ::tracing::info!(
139            target: "cuenv::task",
140            event_type = "task.completed",
141            task_name = %$name,
142            success = $success,
143            exit_code = ?$exit_code,
144            duration_ms = $duration_ms,
145        )
146    };
147}
148
149/// Emit a task group started event.
150#[macro_export]
151macro_rules! emit_task_group_started {
152    ($name:expr, $sequential:expr, $task_count:expr) => {
153        ::tracing::info!(
154            target: "cuenv::task",
155            event_type = "task.group_started",
156            task_name = %$name,
157            sequential = $sequential,
158            task_count = $task_count,
159        )
160    };
161}
162
163/// Emit a task group completed event.
164#[macro_export]
165macro_rules! emit_task_group_completed {
166    ($name:expr, $success:expr, $duration_ms:expr) => {
167        ::tracing::info!(
168            target: "cuenv::task",
169            event_type = "task.group_completed",
170            task_name = %$name,
171            success = $success,
172            duration_ms = $duration_ms,
173        )
174    };
175}
176
177// CI Events
178
179/// Emit a CI context detected event.
180#[macro_export]
181macro_rules! emit_ci_context {
182    ($provider:expr, $event_type:expr, $ref_name:expr) => {
183        ::tracing::info!(
184            target: "cuenv::ci",
185            event_type = "ci.context_detected",
186            provider = %$provider,
187            ci_event_type = %$event_type,
188            ref_name = %$ref_name,
189        )
190    };
191}
192
193/// Emit a CI changed files found event.
194#[macro_export]
195macro_rules! emit_ci_changed_files {
196    ($count:expr) => {
197        ::tracing::info!(
198            target: "cuenv::ci",
199            event_type = "ci.changed_files",
200            count = $count,
201        )
202    };
203}
204
205/// Emit a CI projects discovered event.
206#[macro_export]
207macro_rules! emit_ci_projects_discovered {
208    ($count:expr) => {
209        ::tracing::info!(
210            target: "cuenv::ci",
211            event_type = "ci.projects_discovered",
212            count = $count,
213        )
214    };
215}
216
217/// Emit a CI project skipped event.
218#[macro_export]
219macro_rules! emit_ci_project_skipped {
220    ($path:expr, $reason:expr) => {
221        ::tracing::info!(
222            target: "cuenv::ci",
223            event_type = "ci.project_skipped",
224            path = %$path,
225            reason = %$reason,
226        )
227    };
228}
229
230/// Emit a CI task executing event.
231#[macro_export]
232macro_rules! emit_ci_task_executing {
233    ($project:expr, $task:expr) => {
234        ::tracing::info!(
235            target: "cuenv::ci",
236            event_type = "ci.task_executing",
237            project = %$project,
238            task = %$task,
239        )
240    };
241}
242
243/// Emit a CI task result event.
244#[macro_export]
245macro_rules! emit_ci_task_result {
246    ($project:expr, $task:expr, $success:expr) => {
247        ::tracing::info!(
248            target: "cuenv::ci",
249            event_type = "ci.task_result",
250            project = %$project,
251            task = %$task,
252            success = $success,
253        )
254    };
255    ($project:expr, $task:expr, $success:expr, $error:expr) => {
256        ::tracing::info!(
257            target: "cuenv::ci",
258            event_type = "ci.task_result",
259            project = %$project,
260            task = %$task,
261            success = $success,
262            error = %$error,
263        )
264    };
265}
266
267/// Emit a CI report generated event.
268#[macro_export]
269macro_rules! emit_ci_report {
270    ($path:expr) => {
271        ::tracing::info!(
272            target: "cuenv::ci",
273            event_type = "ci.report_generated",
274            path = %$path,
275        )
276    };
277}
278
279// Command Events
280
281/// Emit a command started event.
282#[macro_export]
283macro_rules! emit_command_started {
284    ($command:expr) => {
285        ::tracing::info!(
286            target: "cuenv::command",
287            event_type = "command.started",
288            command = %$command,
289        )
290    };
291    ($command:expr, $args:expr) => {
292        ::tracing::info!(
293            target: "cuenv::command",
294            event_type = "command.started",
295            command = %$command,
296            args = ?$args,
297        )
298    };
299}
300
301/// Emit a command progress event.
302#[macro_export]
303macro_rules! emit_command_progress {
304    ($command:expr, $progress:expr, $message:expr) => {
305        ::tracing::info!(
306            target: "cuenv::command",
307            event_type = "command.progress",
308            command = %$command,
309            progress = $progress,
310            message = %$message,
311        )
312    };
313}
314
315/// Emit a command completed event.
316#[macro_export]
317macro_rules! emit_command_completed {
318    ($command:expr, $success:expr, $duration_ms:expr) => {
319        ::tracing::info!(
320            target: "cuenv::command",
321            event_type = "command.completed",
322            command = %$command,
323            success = $success,
324            duration_ms = $duration_ms,
325        )
326    };
327}
328
329// Interactive Events
330
331/// Emit a prompt requested event.
332#[macro_export]
333macro_rules! emit_prompt_requested {
334    ($prompt_id:expr, $message:expr, $options:expr) => {
335        ::tracing::info!(
336            target: "cuenv::interactive",
337            event_type = "interactive.prompt_requested",
338            prompt_id = %$prompt_id,
339            message = %$message,
340            options = ?$options,
341        )
342    };
343}
344
345/// Emit a prompt resolved event.
346#[macro_export]
347macro_rules! emit_prompt_resolved {
348    ($prompt_id:expr, $response:expr) => {
349        ::tracing::info!(
350            target: "cuenv::interactive",
351            event_type = "interactive.prompt_resolved",
352            prompt_id = %$prompt_id,
353            response = %$response,
354        )
355    };
356}
357
358/// Emit a wait progress event.
359#[macro_export]
360macro_rules! emit_wait_progress {
361    ($target:expr, $elapsed_secs:expr) => {
362        ::tracing::info!(
363            target: "cuenv::interactive",
364            event_type = "interactive.wait_progress",
365            task_name = %$target,
366            elapsed_secs = $elapsed_secs,
367        )
368    };
369}
370
371// System Events
372
373/// Emit a supervisor log event.
374#[macro_export]
375macro_rules! emit_supervisor_log {
376    ($tag:expr, $message:expr) => {
377        ::tracing::info!(
378            target: "cuenv::system",
379            event_type = "system.supervisor_log",
380            tag = %$tag,
381            message = %$message,
382        )
383    };
384}
385
386/// Emit a system shutdown event.
387#[macro_export]
388macro_rules! emit_shutdown {
389    () => {
390        ::tracing::info!(
391            target: "cuenv::system",
392            event_type = "system.shutdown",
393        )
394    };
395}
396
397// Output Events
398
399/// Emit a stdout output event.
400#[macro_export]
401macro_rules! emit_stdout {
402    ($content:expr) => {
403        ::tracing::info!(
404            target: "cuenv::output",
405            event_type = "output.stdout",
406            content = %$content,
407        )
408    };
409}
410
411/// Emit a stderr output event.
412#[macro_export]
413macro_rules! emit_stderr {
414    ($content:expr) => {
415        ::tracing::info!(
416            target: "cuenv::output",
417            event_type = "output.stderr",
418            content = %$content,
419        )
420    };
421}
422
423/// Print to stdout with automatic secret redaction (with newline).
424///
425/// Use this instead of `println!` when output might contain secrets.
426/// This function applies `redact()` to the input before printing,
427/// ensuring any registered secrets are replaced with `*_*`.
428#[allow(clippy::print_stdout)]
429pub fn println_redacted(content: &str) {
430    println!("{}", redact(content));
431}
432
433/// Print to stdout with automatic secret redaction (no newline).
434///
435/// Use this instead of `print!` when output might contain secrets.
436#[allow(clippy::print_stdout)]
437pub fn print_redacted(content: &str) {
438    print!("{}", redact(content));
439}
440
441#[cfg(test)]
442#[allow(clippy::cognitive_complexity)]
443mod tests {
444    use super::*;
445    use tokio::sync::mpsc;
446    use tracing_subscriber::layer::SubscriberExt;
447
448    fn with_test_subscriber(f: impl FnOnce()) {
449        let (tx, _rx) = mpsc::unbounded_channel();
450        let layer = CuenvEventLayer::new(tx);
451        let subscriber = tracing_subscriber::registry().with(layer);
452        tracing::subscriber::with_default(subscriber, f);
453    }
454
455    #[tokio::test]
456    async fn test_task_macros_compile() {
457        with_test_subscriber(|| {
458            emit_task_started!("build", "cargo build", true);
459            emit_task_cache_hit!("build", "abc123");
460            emit_task_cache_miss!("test");
461            emit_task_output!("build", "stdout", "output");
462            emit_task_completed!("build", true, Some(0), 1000_u64);
463            emit_task_group_started!("all", false, 3_usize);
464            emit_task_group_completed!("all", true, 5000_u64);
465        });
466    }
467
468    #[tokio::test]
469    async fn test_ci_macros_compile() {
470        with_test_subscriber(|| {
471            emit_ci_context!("github", "push", "main");
472            emit_ci_changed_files!(10_usize);
473            emit_ci_projects_discovered!(3_usize);
474            emit_ci_project_skipped!("/path", "no tasks");
475            emit_ci_task_executing!("/path", "build");
476            emit_ci_task_result!("/path", "build", true);
477            emit_ci_task_result!("/path", "test", false, "assertion failed");
478            emit_ci_report!("/path/report.json");
479        });
480    }
481
482    #[tokio::test]
483    async fn test_command_macros_compile() {
484        with_test_subscriber(|| {
485            emit_command_started!("env");
486            emit_command_started!("task", vec!["build".to_string()]);
487            emit_command_progress!("env", 0.5_f32, "loading");
488            emit_command_completed!("env", true, 100_u64);
489        });
490    }
491
492    #[tokio::test]
493    async fn test_misc_macros_compile() {
494        with_test_subscriber(|| {
495            emit_prompt_requested!("p1", "Continue?", vec!["yes", "no"]);
496            emit_prompt_resolved!("p1", "yes");
497            emit_wait_progress!("hook", 5_u64);
498            emit_supervisor_log!("supervisor", "started");
499            emit_shutdown!();
500            emit_stdout!("hello");
501            emit_stderr!("error");
502        });
503    }
504}