|
| 1 | +//! Startup display utilities for InferaDB services |
| 2 | +//! |
| 3 | +//! Provides consistent, structured startup output across all InferaDB binaries. |
| 4 | +//! Includes banner display and configuration summary formatting. |
| 5 | +
|
| 6 | +use std::io::IsTerminal; |
| 7 | + |
| 8 | +/// Service information for the startup banner |
| 9 | +#[derive(Debug, Clone)] |
| 10 | +pub struct ServiceInfo { |
| 11 | + /// Service name (e.g., "InferaDB Management API") |
| 12 | + pub name: &'static str, |
| 13 | + /// Version string |
| 14 | + pub version: &'static str, |
| 15 | + /// Environment (development, staging, production) |
| 16 | + pub environment: String, |
| 17 | +} |
| 18 | + |
| 19 | +/// A single configuration entry for display |
| 20 | +#[derive(Debug, Clone)] |
| 21 | +pub struct ConfigEntry { |
| 22 | + /// Category/group name |
| 23 | + pub category: &'static str, |
| 24 | + /// Configuration key |
| 25 | + pub key: &'static str, |
| 26 | + /// Configuration value (already formatted as string) |
| 27 | + pub value: String, |
| 28 | + /// Whether this is a sensitive value that should be masked |
| 29 | + pub sensitive: bool, |
| 30 | +} |
| 31 | + |
| 32 | +impl ConfigEntry { |
| 33 | + /// Create a new configuration entry |
| 34 | + pub fn new(category: &'static str, key: &'static str, value: impl ToString) -> Self { |
| 35 | + Self { category, key, value: value.to_string(), sensitive: false } |
| 36 | + } |
| 37 | + |
| 38 | + /// Create a sensitive configuration entry (value will be masked) |
| 39 | + pub fn sensitive(category: &'static str, key: &'static str, value: impl ToString) -> Self { |
| 40 | + Self { category, key, value: value.to_string(), sensitive: true } |
| 41 | + } |
| 42 | + |
| 43 | + /// Mark an entry as sensitive |
| 44 | + pub fn as_sensitive(mut self) -> Self { |
| 45 | + self.sensitive = true; |
| 46 | + self |
| 47 | + } |
| 48 | +} |
| 49 | + |
| 50 | +/// Builder for creating a structured startup display |
| 51 | +pub struct StartupDisplay { |
| 52 | + service: ServiceInfo, |
| 53 | + entries: Vec<ConfigEntry>, |
| 54 | + use_ansi: bool, |
| 55 | +} |
| 56 | + |
| 57 | +impl StartupDisplay { |
| 58 | + /// Create a new startup display builder |
| 59 | + pub fn new(service: ServiceInfo) -> Self { |
| 60 | + Self { service, entries: Vec::new(), use_ansi: std::io::stdout().is_terminal() } |
| 61 | + } |
| 62 | + |
| 63 | + /// Set whether to use ANSI colors |
| 64 | + pub fn with_ansi(mut self, use_ansi: bool) -> Self { |
| 65 | + self.use_ansi = use_ansi; |
| 66 | + self |
| 67 | + } |
| 68 | + |
| 69 | + /// Add a configuration entry |
| 70 | + pub fn entry(mut self, entry: ConfigEntry) -> Self { |
| 71 | + self.entries.push(entry); |
| 72 | + self |
| 73 | + } |
| 74 | + |
| 75 | + /// Add multiple configuration entries |
| 76 | + pub fn entries(mut self, entries: impl IntoIterator<Item = ConfigEntry>) -> Self { |
| 77 | + self.entries.extend(entries); |
| 78 | + self |
| 79 | + } |
| 80 | + |
| 81 | + /// Display the startup banner and configuration summary |
| 82 | + pub fn display(&self) { |
| 83 | + self.print_banner(); |
| 84 | + self.print_config_summary(); |
| 85 | + } |
| 86 | + |
| 87 | + fn print_banner(&self) { |
| 88 | + let (dim, reset, bold, cyan) = if self.use_ansi { |
| 89 | + ("\x1b[2m", "\x1b[0m", "\x1b[1m", "\x1b[36m") |
| 90 | + } else { |
| 91 | + ("", "", "", "") |
| 92 | + }; |
| 93 | + |
| 94 | + // Simple, clean banner |
| 95 | + println!(); |
| 96 | + println!("{dim}┌─────────────────────────────────────────────────────────────┐{reset}"); |
| 97 | + println!( |
| 98 | + "{dim}│{reset} {bold}{cyan}{name:^55}{reset} {dim}│{reset}", |
| 99 | + name = self.service.name |
| 100 | + ); |
| 101 | + println!( |
| 102 | + "{dim}│{reset} {version:^55} {dim}│{reset}", |
| 103 | + version = format!("v{}", self.service.version) |
| 104 | + ); |
| 105 | + println!("{dim}└─────────────────────────────────────────────────────────────┘{reset}"); |
| 106 | + println!(); |
| 107 | + } |
| 108 | + |
| 109 | + fn print_config_summary(&self) { |
| 110 | + if self.entries.is_empty() { |
| 111 | + return; |
| 112 | + } |
| 113 | + |
| 114 | + let (dim, reset, bold, green, yellow) = if self.use_ansi { |
| 115 | + ("\x1b[2m", "\x1b[0m", "\x1b[1m", "\x1b[32m", "\x1b[33m") |
| 116 | + } else { |
| 117 | + ("", "", "", "", "") |
| 118 | + }; |
| 119 | + |
| 120 | + // Group entries by category |
| 121 | + let mut categories: Vec<(&str, Vec<&ConfigEntry>)> = Vec::new(); |
| 122 | + for entry in &self.entries { |
| 123 | + if let Some((_, entries)) = |
| 124 | + categories.iter_mut().find(|(cat, _)| *cat == entry.category) |
| 125 | + { |
| 126 | + entries.push(entry); |
| 127 | + } else { |
| 128 | + categories.push((entry.category, vec![entry])); |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + // Calculate column width for alignment |
| 133 | + let max_key_len = self.entries.iter().map(|e| e.key.len()).max().unwrap_or(20).max(20); |
| 134 | + |
| 135 | + println!("{bold}Configuration:{reset}"); |
| 136 | + println!(); |
| 137 | + |
| 138 | + for (category, entries) in categories { |
| 139 | + println!(" {dim}[{category}]{reset}"); |
| 140 | + for entry in entries { |
| 141 | + let display_value = if entry.sensitive { |
| 142 | + format!("{yellow}********{reset}") |
| 143 | + } else { |
| 144 | + format!("{green}{}{reset}", entry.value) |
| 145 | + }; |
| 146 | + println!( |
| 147 | + " {key:<width$} {value}", |
| 148 | + key = entry.key, |
| 149 | + width = max_key_len, |
| 150 | + value = display_value |
| 151 | + ); |
| 152 | + } |
| 153 | + println!(); |
| 154 | + } |
| 155 | + } |
| 156 | +} |
| 157 | + |
| 158 | +/// Log a startup phase header |
| 159 | +/// |
| 160 | +/// Use this to clearly delineate initialization phases in the logs. |
| 161 | +pub fn log_phase(phase: &str) { |
| 162 | + tracing::info!(""); |
| 163 | + tracing::info!("━━━ {} ━━━", phase); |
| 164 | +} |
| 165 | + |
| 166 | +/// Log a successful initialization step |
| 167 | +pub fn log_initialized(component: &str) { |
| 168 | + tracing::info!("✓ {} initialized", component); |
| 169 | +} |
| 170 | + |
| 171 | +/// Log a skipped initialization step |
| 172 | +pub fn log_skipped(component: &str, reason: &str) { |
| 173 | + tracing::info!("○ {} skipped: {}", component, reason); |
| 174 | +} |
| 175 | + |
| 176 | +/// Log that the service is ready to accept connections |
| 177 | +pub fn log_ready(service: &str, addresses: &[(&str, &str)]) { |
| 178 | + tracing::info!(""); |
| 179 | + tracing::info!("━━━ {} Ready ━━━", service); |
| 180 | + for (name, addr) in addresses { |
| 181 | + tracing::info!(" {} → {}", name, addr); |
| 182 | + } |
| 183 | + tracing::info!(""); |
| 184 | +} |
| 185 | + |
| 186 | +#[cfg(test)] |
| 187 | +mod tests { |
| 188 | + use super::*; |
| 189 | + |
| 190 | + #[test] |
| 191 | + fn test_config_entry_creation() { |
| 192 | + let entry = ConfigEntry::new("Server", "port", 8080); |
| 193 | + assert_eq!(entry.category, "Server"); |
| 194 | + assert_eq!(entry.key, "port"); |
| 195 | + assert_eq!(entry.value, "8080"); |
| 196 | + assert!(!entry.sensitive); |
| 197 | + } |
| 198 | + |
| 199 | + #[test] |
| 200 | + fn test_sensitive_entry() { |
| 201 | + let entry = ConfigEntry::sensitive("Auth", "secret", "my-secret"); |
| 202 | + assert!(entry.sensitive); |
| 203 | + |
| 204 | + let entry2 = ConfigEntry::new("Auth", "key", "value").as_sensitive(); |
| 205 | + assert!(entry2.sensitive); |
| 206 | + } |
| 207 | + |
| 208 | + #[test] |
| 209 | + fn test_startup_display_builder() { |
| 210 | + let service = |
| 211 | + ServiceInfo { name: "Test Service", version: "0.1.0", environment: "test".to_string() }; |
| 212 | + |
| 213 | + let display = StartupDisplay::new(service) |
| 214 | + .with_ansi(false) |
| 215 | + .entry(ConfigEntry::new("Server", "host", "0.0.0.0")) |
| 216 | + .entry(ConfigEntry::new("Server", "port", 8080)); |
| 217 | + |
| 218 | + assert_eq!(display.entries.len(), 2); |
| 219 | + assert!(!display.use_ansi); |
| 220 | + } |
| 221 | + |
| 222 | + #[test] |
| 223 | + fn test_startup_display_entries_batch() { |
| 224 | + let service = |
| 225 | + ServiceInfo { name: "Test Service", version: "0.1.0", environment: "test".to_string() }; |
| 226 | + |
| 227 | + let entries = vec![ |
| 228 | + ConfigEntry::new("Server", "host", "0.0.0.0"), |
| 229 | + ConfigEntry::new("Server", "port", 8080), |
| 230 | + ConfigEntry::new("Storage", "backend", "memory"), |
| 231 | + ]; |
| 232 | + |
| 233 | + let display = StartupDisplay::new(service).entries(entries); |
| 234 | + |
| 235 | + assert_eq!(display.entries.len(), 3); |
| 236 | + } |
| 237 | +} |
0 commit comments