Skip to main content

romance_core/addon/
observability.rs

1use crate::addon::Addon;
2use anyhow::Result;
3use std::path::Path;
4
5pub struct ObservabilityAddon;
6
7impl Addon for ObservabilityAddon {
8    fn name(&self) -> &str {
9        "observability"
10    }
11
12    fn check_prerequisites(&self, project_root: &Path) -> Result<()> {
13        super::check_romance_project(project_root)
14    }
15
16    fn is_already_installed(&self, project_root: &Path) -> bool {
17        project_root
18            .join("backend/src/middleware/request_id.rs")
19            .exists()
20    }
21
22    fn install(&self, project_root: &Path) -> Result<()> {
23        install_observability(project_root)
24    }
25
26    fn uninstall(&self, project_root: &Path) -> Result<()> {
27        use colored::Colorize;
28
29        println!("{}", "Uninstalling observability...".bold());
30
31        // Delete files
32        if super::remove_file_if_exists(
33            &project_root.join("backend/src/middleware/request_id.rs"),
34        )? {
35            println!("  {} backend/src/middleware/request_id.rs", "delete".red());
36        }
37        if super::remove_file_if_exists(
38            &project_root.join("backend/src/middleware/tracing_setup.rs"),
39        )? {
40            println!(
41                "  {} backend/src/middleware/tracing_setup.rs",
42                "delete".red()
43            );
44        }
45
46        // Remove lines from middleware/mod.rs
47        super::remove_line_from_file(
48            &project_root.join("backend/src/middleware/mod.rs"),
49            "request_id",
50        )?;
51        super::remove_line_from_file(
52            &project_root.join("backend/src/middleware/mod.rs"),
53            "tracing_setup",
54        )?;
55
56        // Remove request_id_layer from routes/mod.rs
57        super::remove_line_from_file(
58            &project_root.join("backend/src/routes/mod.rs"),
59            "request_id_layer",
60        )?;
61
62        // Clean up middleware module if security doesn't use it
63        let security_installed = project_root
64            .join("backend/src/middleware/security_headers.rs")
65            .exists();
66        if !security_installed {
67            super::remove_file_if_exists(&project_root.join("backend/src/middleware/mod.rs"))?;
68            let _ = std::fs::remove_dir(project_root.join("backend/src/middleware"));
69            super::remove_mod_from_main(project_root, "middleware")?;
70        }
71
72        // Regenerate AI context
73        crate::ai_context::regenerate(project_root).ok();
74
75        println!();
76        println!(
77            "{}",
78            "Observability uninstalled successfully.".green().bold()
79        );
80
81        Ok(())
82    }
83}
84
85fn install_observability(project_root: &Path) -> Result<()> {
86    use crate::template::TemplateEngine;
87    use crate::utils;
88    use colored::Colorize;
89    use tera::Context;
90
91    println!("{}", "Installing observability...".bold());
92
93    let engine = TemplateEngine::new()?;
94    let ctx = Context::new();
95
96    // Generate tracing setup
97    let content = engine.render("addon/observability/tracing_setup.rs.tera", &ctx)?;
98    utils::write_file(
99        &project_root.join("backend/src/middleware/tracing_setup.rs"),
100        &content,
101    )?;
102    println!(
103        "  {} backend/src/middleware/tracing_setup.rs",
104        "create".green()
105    );
106
107    // Generate request ID middleware
108    let content = engine.render("addon/observability/request_id.rs.tera", &ctx)?;
109    utils::write_file(
110        &project_root.join("backend/src/middleware/request_id.rs"),
111        &content,
112    )?;
113    println!(
114        "  {} backend/src/middleware/request_id.rs",
115        "create".green()
116    );
117
118    // Ensure middleware/mod.rs exists
119    let middleware_mod_path = project_root.join("backend/src/middleware/mod.rs");
120    if middleware_mod_path.exists() {
121        let content = std::fs::read_to_string(&middleware_mod_path)?;
122        let mut new_content = content.clone();
123        if !new_content.contains("mod tracing_setup;") {
124            new_content = format!("pub mod tracing_setup;\n{}", new_content);
125        }
126        if !new_content.contains("mod request_id;") {
127            new_content = format!("pub mod request_id;\n{}", new_content);
128        }
129        std::fs::write(&middleware_mod_path, new_content)?;
130    } else {
131        utils::write_file(
132            &middleware_mod_path,
133            "pub mod request_id;\npub mod tracing_setup;\n",
134        )?;
135    }
136
137    // Add mod middleware to main.rs if not present
138    super::add_mod_to_main(project_root, "middleware")?;
139
140    // Replace scaffold's tracing init with observability module's init_tracing()
141    let main_path = project_root.join("backend/src/main.rs");
142    let main_content = std::fs::read_to_string(&main_path)?;
143    if !main_content.contains("init_tracing()") {
144        // Find and replace the existing tracing_subscriber block
145        if main_content.contains("tracing_subscriber::registry()") {
146            // Replace the block from "tracing_subscriber::registry()" through ".init();"
147            let new_main = if let Some(start) = main_content.find("    tracing_subscriber::registry()") {
148                if let Some(init_pos) = main_content[start..].find(".init();") {
149                    let end = start + init_pos + ".init();".len();
150                    format!(
151                        "{}    crate::middleware::tracing_setup::init_tracing();{}",
152                        &main_content[..start],
153                        &main_content[end..]
154                    )
155                } else {
156                    main_content.clone()
157                }
158            } else {
159                main_content.clone()
160            };
161            // Also remove unused tracing_subscriber import if present
162            let new_main = new_main.replace(
163                "use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};\n",
164                "",
165            );
166            std::fs::write(&main_path, new_main)?;
167            println!("  {} backend/src/main.rs (replaced tracing init)", "update".green());
168        }
169    }
170
171    // Inject trace layer into routes
172    utils::insert_at_marker(
173        &project_root.join("backend/src/routes/mod.rs"),
174        "// === ROMANCE:MIDDLEWARE ===",
175        "        .layer(crate::middleware::request_id::request_id_layer())",
176    )?;
177
178    // Add dependencies
179    crate::generator::auth::insert_cargo_dependency(
180        &project_root.join("backend/Cargo.toml"),
181        &[
182            ("tower-http", r#"{ version = "0.6", features = ["cors", "trace", "request-id", "propagate-header"] }"#),
183        ],
184    )?;
185
186    // Add RUST_LOG to .env
187    super::append_env_var(
188        &project_root.join("backend/.env"),
189        "RUST_LOG=info",
190    )?;
191    super::append_env_var(
192        &project_root.join("backend/.env.example"),
193        "RUST_LOG=info",
194    )?;
195
196    println!();
197    println!(
198        "{}",
199        "Observability installed successfully!".green().bold()
200    );
201    println!("  Structured logging with request ID propagation enabled.");
202    println!("  Set RUST_LOG=debug for verbose logging.");
203
204    Ok(())
205}