Skip to content

Commit dc23737

Browse files
committed
feat: add startup config output
1 parent 35a5c61 commit dc23737

File tree

3 files changed

+299
-40
lines changed

3 files changed

+299
-40
lines changed

crates/inferadb-management-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod metrics;
1616
pub mod ratelimit;
1717
pub mod repository;
1818
pub mod repository_context;
19+
pub mod startup;
1920
pub mod webhook_client;
2021

2122
pub use auth::{PasswordHasher, hash_password, verify_password};
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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

Comments
 (0)