use super::*;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct DbConfig {
pub deploy: BackendDeployConfig,
pub host: String,
pub port: u16,
pub database: String,
pub role: String,
pub password: String,
pub ssl_mode: String,
pub pooler_dsn: String,
pub direct_dsn: String,
pub max_open_conns: i32,
pub max_idle_conns: i32,
pub conn_max_lifetime_secs: u64,
pub conn_max_idle_secs: u64,
#[serde(default = "five_i32")]
pub min_connections: i32,
#[serde(default = "ten_u64")]
pub acquire_timeout_secs: u64,
}
impl DbConfig {
pub fn is_configured(&self) -> bool {
!self.host.is_empty() && !self.database.is_empty() && !self.role.is_empty()
}
pub fn effective_ssl_mode(&self) -> &str {
if !self.ssl_mode.is_empty() {
return &self.ssl_mode;
}
match self.deploy.mode {
DeployMode::Cloud => "require",
DeployMode::SelfHosted => "prefer",
}
}
pub fn masked_log(&self) -> String {
format!(
"host={} db={} role={} ssl={} deploy={}",
self.host,
self.database,
self.role,
self.effective_ssl_mode(),
self.deploy.mode.as_str(),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct RedisConfig {
pub deploy: BackendDeployConfig,
pub host: String,
pub port: u16,
pub password: String,
pub database: u8,
pub pool_size: u32,
pub min_idle_conns: u32,
pub max_retries: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dsn: Option<String>,
pub tls_enabled: bool,
pub default_ttl_secs: u64,
pub mode: String,
pub sentinel_master: String,
}
impl RedisConfig {
pub fn is_configured(&self) -> bool {
!self.host.is_empty() || self.dsn.as_ref().map(|d| !d.is_empty()).unwrap_or(false)
}
pub fn effective_tls(&self) -> bool {
if self.tls_enabled {
return true;
}
self.deploy.tls_required()
}
pub fn masked_log(&self) -> String {
format!(
"host={}:{} db={} tls={} mode={} deploy={}",
self.host,
self.port,
self.database,
self.effective_tls(),
self.mode,
self.deploy.mode.as_str(),
)
}
pub fn effective_port(&self) -> u16 {
if self.port > 0 { self.port } else { 6379 }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct QdrantConfig {
pub deploy: BackendDeployConfig,
pub host: String,
pub grpc_port: u16,
pub http_port: u16,
pub api_key: String,
pub tls_enabled: bool,
pub default_dimension: u32,
pub default_distance: String,
pub max_retries: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
impl QdrantConfig {
pub fn is_configured(&self) -> bool {
!self.host.is_empty() || self.url.as_ref().map(|u| !u.is_empty()).unwrap_or(false)
}
pub fn effective_tls(&self) -> bool {
if self.tls_enabled {
return true;
}
self.deploy.tls_required()
}
pub fn masked_log(&self) -> String {
format!(
"host={} grpc={} http={} tls={} dim={} dist={} deploy={}",
self.host,
self.effective_grpc_port(),
self.effective_http_port(),
self.effective_tls(),
self.default_dimension,
self.default_distance,
self.deploy.mode.as_str(),
)
}
pub fn effective_grpc_port(&self) -> u16 {
if self.grpc_port > 0 {
self.grpc_port
} else {
6334
}
}
pub fn effective_http_port(&self) -> u16 {
if self.http_port > 0 {
self.http_port
} else {
6333
}
}
pub fn effective_distance(&self) -> &str {
if self.default_distance.is_empty() {
"Cosine"
} else {
&self.default_distance
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct MinioConfig {
pub deploy: BackendDeployConfig,
pub endpoint: String,
pub access_key: String,
pub secret_key: String,
pub use_ssl: bool,
pub bucket_prefix: String,
pub region: String,
pub max_retries: u32,
pub multipart_part_size_bytes: u64,
}
impl MinioConfig {
pub fn is_configured(&self) -> bool {
!self.endpoint.is_empty()
}
pub fn effective_ssl(&self) -> bool {
if self.use_ssl {
return true;
}
self.deploy.tls_required()
}
pub fn masked_log(&self) -> String {
format!(
"endpoint={} region={} ssl={} prefix={} deploy={}",
self.endpoint,
self.effective_region(),
self.effective_ssl(),
self.bucket_prefix,
self.deploy.mode.as_str(),
)
}
pub fn effective_region(&self) -> &str {
if self.region.is_empty() {
"us-east-1"
} else {
&self.region
}
}
pub fn qualified_bucket(&self, logical_name: &str) -> String {
format!("{}{}", self.bucket_prefix, logical_name)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct FailoverConfig {
pub enabled: bool,
pub failure_threshold: u32,
pub cooldown: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AuditSinkKind {
#[default]
None,
Stdout,
File,
Kafka,
Postgres,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct AuditSinkConfig {
pub kind: AuditSinkKind,
pub file_path: Option<String>,
pub kafka_topic: Option<String>,
pub kafka_brokers: Option<String>,
pub pg_table: Option<String>,
pub min_severity: String,
}
impl Default for AuditSinkConfig {
fn default() -> Self {
Self {
kind: AuditSinkKind::default(),
file_path: None,
kafka_topic: None,
kafka_brokers: None,
pg_table: None,
min_severity: "info".to_string(),
}
}
}
impl AuditSinkConfig {
pub fn from_env() -> Self {
let kind = match std::env::var("UDB_AUDIT_SINK")
.unwrap_or_default()
.to_lowercase()
.as_str()
{
"stdout" => AuditSinkKind::Stdout,
"file" => AuditSinkKind::File,
"kafka" => AuditSinkKind::Kafka,
"postgres" | "pg" => AuditSinkKind::Postgres,
_ => AuditSinkKind::None,
};
let min_severity = std::env::var("UDB_AUDIT_MIN_SEVERITY")
.ok()
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| "info".to_string());
Self {
kind,
file_path: std::env::var("UDB_AUDIT_FILE_PATH")
.ok()
.filter(|v| !v.is_empty()),
kafka_topic: std::env::var("UDB_AUDIT_KAFKA_TOPIC")
.ok()
.filter(|v| !v.is_empty()),
kafka_brokers: std::env::var("UDB_AUDIT_KAFKA_BROKERS")
.ok()
.filter(|v| !v.is_empty()),
pg_table: std::env::var("UDB_AUDIT_PG_TABLE")
.ok()
.filter(|v| !v.is_empty()),
min_severity,
}
}
pub fn merge_env(&mut self) {
if let Ok(raw) = std::env::var("UDB_AUDIT_SINK") {
self.kind = match raw.to_lowercase().as_str() {
"stdout" => AuditSinkKind::Stdout,
"file" => AuditSinkKind::File,
"kafka" => AuditSinkKind::Kafka,
"postgres" | "pg" => AuditSinkKind::Postgres,
_ => AuditSinkKind::None,
};
}
if let Some(v) = std::env::var("UDB_AUDIT_MIN_SEVERITY")
.ok()
.filter(|v| !v.is_empty())
{
self.min_severity = v;
}
if let Some(v) = std::env::var("UDB_AUDIT_FILE_PATH")
.ok()
.filter(|v| !v.is_empty())
{
self.file_path = Some(v);
}
if let Some(v) = std::env::var("UDB_AUDIT_KAFKA_TOPIC")
.ok()
.filter(|v| !v.is_empty())
{
self.kafka_topic = Some(v);
}
if let Some(v) = std::env::var("UDB_AUDIT_KAFKA_BROKERS")
.ok()
.filter(|v| !v.is_empty())
{
self.kafka_brokers = Some(v);
}
if let Some(v) = std::env::var("UDB_AUDIT_PG_TABLE")
.ok()
.filter(|v| !v.is_empty())
{
self.pg_table = Some(v);
}
}
pub fn is_active(&self) -> bool {
self.kind != AuditSinkKind::None
}
pub fn validate(&self) -> Vec<String> {
let mut errors = Vec::new();
match self.kind {
AuditSinkKind::File if self.file_path.is_none() => {
errors
.push("UDB_AUDIT_SINK=file requires UDB_AUDIT_FILE_PATH to be set".to_string());
}
AuditSinkKind::Kafka if self.kafka_topic.is_none() => {
errors.push(
"UDB_AUDIT_SINK=kafka requires UDB_AUDIT_KAFKA_TOPIC to be set".to_string(),
);
}
AuditSinkKind::Kafka if self.kafka_brokers.is_none() => {
errors.push(
"UDB_AUDIT_SINK=kafka requires UDB_AUDIT_KAFKA_BROKERS to be set".to_string(),
);
}
AuditSinkKind::Postgres if self.pg_table.is_none() => {
errors.push(
"UDB_AUDIT_SINK=postgres requires UDB_AUDIT_PG_TABLE to be set".to_string(),
);
}
_ => {}
}
match self.min_severity.as_str() {
"" | "info" | "warn" | "error" => {}
other => errors.push(format!(
"UDB_AUDIT_MIN_SEVERITY='{other}' is not valid; use 'info', 'warn', or 'error'"
)),
}
errors
}
}