use std::{env, io::Write};
const BANNER: &str = include_str!("../assets/startup_banner.txt");
const VINU_PURPLE: &str = "\x1b[38;2;180;124;255m";
const RESET: &str = "\x1b[0m";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StartupBanner<'a> {
pub service_name: &'a str,
pub description: &'a str,
pub version: &'a str,
pub environment: &'a str,
}
pub fn runtime_environment() -> String {
["APP_ENV", "RUST_ENV", "ENVIRONMENT", "NODE_ENV"]
.iter()
.find_map(|key| env::var(key).ok().filter(|value| !value.trim().is_empty()))
.unwrap_or_else(|| "production".to_string())
}
pub fn print(config: StartupBanner<'_>) {
let stdout = std::io::stdout();
let mut output = stdout.lock();
let _ = write(&mut output, config);
}
fn write(mut output: impl Write, config: StartupBanner<'_>) -> std::io::Result<()> {
if !env_flag_enabled("STARTUP_BANNER", true) {
return Ok(());
}
let color = color_enabled();
write_banner(&mut output, color)?;
writeln!(output)?;
if color {
write!(output, "{RESET}")?;
}
writeln!(output, "Service : {}", config.service_name)?;
writeln!(output, "Description : {}", config.description)?;
writeln!(output, "Version : {}", config.version)?;
write!(output, "Environment : {}", config.environment)?;
writeln!(output)
}
fn write_banner(mut output: impl Write, color: bool) -> std::io::Result<()> {
for line in BANNER.trim_matches('\n').lines() {
if color {
write!(output, "{VINU_PURPLE}")?;
}
writeln!(output, "{line}")?;
}
Ok(())
}
fn color_enabled() -> bool {
if env::var_os("NO_COLOR").is_some() {
return false;
}
!env::var("LOG_FORMAT")
.map(|format| format.eq_ignore_ascii_case("json"))
.unwrap_or(false)
}
fn env_flag_enabled(name: &str, default: bool) -> bool {
match env::var(name) {
Ok(value) => !matches!(
value.trim().to_ascii_lowercase().as_str(),
"0" | "false" | "off" | "no"
),
Err(_) => default,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, OnceLock};
fn env_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
fn clear_env() {
for key in [
"APP_ENV",
"RUST_ENV",
"ENVIRONMENT",
"NODE_ENV",
"STARTUP_BANNER",
"NO_COLOR",
"LOG_FORMAT",
] {
env::remove_var(key);
}
}
fn config() -> StartupBanner<'static> {
StartupBanner {
service_name: "cex-test",
description: "Test service",
version: "1.2.3",
environment: "test",
}
}
#[test]
fn environment_uses_priority_order_and_default() {
let _guard = env_lock().lock().unwrap();
clear_env();
assert_eq!(runtime_environment(), "production");
env::set_var("NODE_ENV", "node");
env::set_var("ENVIRONMENT", "environment");
env::set_var("RUST_ENV", "rust");
env::set_var("APP_ENV", "app");
assert_eq!(runtime_environment(), "app");
}
#[test]
fn disabled_banner_writes_nothing() {
let _guard = env_lock().lock().unwrap();
clear_env();
env::set_var("STARTUP_BANNER", "no");
let mut output = Vec::new();
write(&mut output, config()).unwrap();
assert!(output.is_empty());
}
#[test]
fn banner_contains_metadata_without_log_prefixes() {
let _guard = env_lock().lock().unwrap();
clear_env();
env::set_var("NO_COLOR", "1");
let mut output = Vec::new();
write(&mut output, config()).unwrap();
let output = String::from_utf8(output).unwrap();
assert!(output.starts_with(BANNER));
assert!(output.contains("\n\nService : cex-test\n"));
assert!(output.contains("Description : Test service\n"));
assert!(output.contains("Version : 1.2.3\n"));
assert!(output.ends_with("Environment : test\n"));
assert!(!output.contains("ThreadId"));
assert!(!output.contains(" INFO "));
}
#[test]
fn banner_uses_vinu_purple_by_default() {
let _guard = env_lock().lock().unwrap();
clear_env();
let mut output = Vec::new();
write(&mut output, config()).unwrap();
let output = String::from_utf8(output).unwrap();
assert!(output.starts_with(VINU_PURPLE));
assert_eq!(
BANNER.trim_matches('\n').lines().count(),
output.matches(VINU_PURPLE).count()
);
assert!(output.contains(&format!("{RESET}Service")));
assert!(!output.ends_with("\x1b[0m\n"));
}
#[test]
fn json_output_disables_color() {
let _guard = env_lock().lock().unwrap();
clear_env();
env::set_var("LOG_FORMAT", "JSON");
let mut output = Vec::new();
write(&mut output, config()).unwrap();
assert!(!String::from_utf8(output).unwrap().contains("\x1b["));
}
}