Unify control-plane TOML config serialization and shared telemetry settings

This commit is contained in:
lda committed 2026-10-10 16:08:24 +07:00 Verified
1 parent f34c1fe2d3
commit acecb57b50
4 files changed
+105 -128

No files matched your search

+44 -39
View File
@@ -4,7 +4,7 @@ use anyhow::{Context, Result};
use crate::cli::{InitConfigArgs, ServeArgs};
use crate::config::resolve::{load_file_config, normalize_public_url, resolve_path};
use crate::config::types::{DEFAULT_CONFIG, WritableConfig, WritableTelemetry};
use crate::config::types::{DEFAULT_CONFIG, FileConfig, FileTelemetryConfig};
pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
if args.stdout && args.config_file.is_some() {
@@ -44,10 +44,7 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
.unwrap_or_else(|| "<defaults>".to_string())
)
})?,
None => defaults
.bind
.parse()
.expect("static default bind should parse"),
None => defaults.bind,
},
};
let public_url = normalize_public_url(
@@ -90,29 +87,35 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
args.bootstrap_enroll_tokens.clone()
};
let body = WritableConfig {
data_dir,
bind: bind.to_string(),
public_url,
state_file,
command_timeout_ms: args
.command_timeout_ms
.or(base.command_timeout_ms)
.unwrap_or(defaults.command_timeout_ms)
.max(1),
enroll_token_ttl_seconds: args
.enroll_token_ttl_seconds
.or(base.enroll_token_ttl_seconds)
.unwrap_or(defaults.enroll_token_ttl_seconds)
.max(1),
observation_retention_seconds: args
.observation_retention_seconds
.or(base.observation_retention_seconds)
.unwrap_or(defaults.observation_retention_seconds),
pid_file,
ui_dist_dir,
bootstrap_enroll_tokens,
telemetry: WritableTelemetry {
let body = FileConfig {
data_dir: Some(data_dir),
bind: Some(bind.to_string()),
public_url: Some(public_url),
state_file: Some(state_file),
command_timeout_ms: Some(
args.command_timeout_ms
.or(base.command_timeout_ms)
.unwrap_or(defaults.command_timeout.as_millis() as u64)
.max(1),
),
enroll_token_ttl_seconds: Some(
args.enroll_token_ttl_seconds
.or(base.enroll_token_ttl_seconds)
.unwrap_or(defaults.enroll_token_ttl.as_secs())
.max(1),
),
observation_retention_seconds: Some(
args.observation_retention_seconds
.or(base.observation_retention_seconds)
.unwrap_or(defaults.observation_retention.as_secs()),
),
pid_file: Some(pid_file),
ui_dist_dir: Some(ui_dist_dir),
// Preserve the previous generated template's omission of an empty list.
// Explicit empty lists still round-trip through FileConfig unchanged.
bootstrap_enroll_tokens: (!bootstrap_enroll_tokens.is_empty())
.then_some(bootstrap_enroll_tokens),
telemetry: Some(FileTelemetryConfig {
console_filter: base_telemetry
.console_filter
.or_else(|| defaults.telemetry.console_filter.clone()),
@@ -130,16 +133,18 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
.clone()
.or(base_telemetry.otlp_endpoint)
.or_else(|| defaults.telemetry.otlp_endpoint.clone()),
service_name: args
.telemetry_service_name
.clone()
.or(base_telemetry.service_name)
.unwrap_or_else(|| defaults.telemetry.service_name.clone()),
json_logs: args
.telemetry_json_logs
.or(base_telemetry.json_logs)
.unwrap_or(defaults.telemetry.json_logs),
},
service_name: Some(
args.telemetry_service_name
.clone()
.or(base_telemetry.service_name)
.unwrap_or_else(|| defaults.telemetry.service_name.clone()),
),
json_logs: Some(
args.telemetry_json_logs
.or(base_telemetry.json_logs)
.unwrap_or(defaults.telemetry.json_logs),
),
}),
};
let rendered = toml::to_string_pretty(&body).context("failed to render config template")?;
@@ -332,7 +337,7 @@ json_logs = true
);
assert_eq!(
parsed["command_timeout_ms"].as_integer(),
Some(defaults.command_timeout_ms as i64)
Some(defaults.command_timeout.as_millis() as i64)
);
assert_eq!(
parsed["telemetry"]["service_name"].as_str(),
+8 -13
View File
@@ -33,10 +33,7 @@ impl DaemonConfig {
args.config_file.display()
)
})?,
None => defaults
.bind
.parse()
.expect("static default bind should parse"),
None => defaults.bind,
},
};
@@ -57,21 +54,21 @@ impl DaemonConfig {
let command_timeout = Duration::from_millis(
args.command_timeout_ms
.or(file.command_timeout_ms)
.unwrap_or(defaults.command_timeout_ms)
.unwrap_or(defaults.command_timeout.as_millis() as u64)
.max(1),
);
let enroll_token_ttl = Duration::from_secs(
args.enroll_token_ttl_seconds
.or(file.enroll_token_ttl_seconds)
.unwrap_or(defaults.enroll_token_ttl_seconds)
.unwrap_or(defaults.enroll_token_ttl.as_secs())
.max(1),
);
let observation_retention = Duration::from_secs(
args.observation_retention_seconds
.or(file.observation_retention_seconds)
.unwrap_or(defaults.observation_retention_seconds)
.unwrap_or(defaults.observation_retention.as_secs())
.max(1),
);
@@ -146,7 +143,7 @@ pub fn resolve_issue_token_settings(args: &IssueEnrollTokenArgs) -> Result<Issue
let ttl = Duration::from_secs(
args.ttl_seconds
.or(file.enroll_token_ttl_seconds)
.unwrap_or(defaults.enroll_token_ttl_seconds)
.unwrap_or(defaults.enroll_token_ttl.as_secs())
.max(1),
);
@@ -229,7 +226,8 @@ fn resolve_admin_public_url(
}
fn resolve_telemetry(file: Option<FileTelemetryConfig>) -> TelemetryConfig {
let mut out = TelemetryConfig::default();
// Service-specific defaults live in the CC template, not the shared type.
let mut out = DEFAULT_CONFIG.telemetry.clone();
if let Some(file) = file {
out.authorization_env = file.authorization_env;
out.console_filter = file.console_filter;
@@ -344,10 +342,7 @@ mod tests {
resolved.state_file,
defaults.data_dir.join(&defaults.state_file)
);
assert_eq!(
resolved.command_timeout,
std::time::Duration::from_millis(defaults.command_timeout_ms)
);
assert_eq!(resolved.command_timeout, defaults.command_timeout);
assert_eq!(
resolved.telemetry.service_name,
defaults.telemetry.service_name
+52 -66
View File
@@ -4,6 +4,7 @@ use std::sync::LazyLock;
use std::time::Duration;
use serde::{Deserialize, Serialize};
pub use wakey_core::telemetry::TelemetryConfig;
#[derive(Debug, Clone)]
pub struct DaemonConfig {
@@ -20,33 +21,9 @@ pub struct DaemonConfig {
pub telemetry: TelemetryConfig,
}
#[derive(Debug, Clone)]
pub struct TelemetryConfig {
pub console_filter: Option<String>,
pub otlp_log_filter: Option<String>,
pub trace_filter: Option<String>,
pub otlp_endpoint: Option<String>,
pub authorization_env: Option<String>,
pub service_name: String,
pub json_logs: bool,
}
impl Default for TelemetryConfig {
fn default() -> Self {
let defaults = &DEFAULT_CONFIG.telemetry;
Self {
console_filter: defaults.console_filter.clone(),
otlp_log_filter: defaults.otlp_log_filter.clone(),
trace_filter: defaults.trace_filter.clone(),
otlp_endpoint: defaults.otlp_endpoint.clone(),
authorization_env: defaults.authorization_env.clone(),
service_name: defaults.service_name.clone(),
json_logs: defaults.json_logs,
}
}
}
#[derive(Debug, Deserialize, Default)]
/// TOML DTO for both loading partial files and generating complete files.
/// Options preserve omission for precedence; runtime conversion stays separate.
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub(crate) struct FileConfig {
pub(crate) data_dir: Option<PathBuf>,
pub(crate) bind: Option<String>,
@@ -62,8 +39,10 @@ pub(crate) struct FileConfig {
pub(crate) telemetry: Option<FileTelemetryConfig>,
}
#[derive(Debug, Deserialize, Default)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub(crate) struct FileTelemetryConfig {
// Missing fields must remain distinguishable from explicit values when
// init-config merges an existing file with CLI overrides and defaults.
pub(crate) console_filter: Option<String>,
pub(crate) otlp_log_filter: Option<String>,
pub(crate) trace_filter: Option<String>,
@@ -73,50 +52,19 @@ pub(crate) struct FileTelemetryConfig {
pub(crate) json_logs: Option<bool>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct WritableConfig {
pub(crate) data_dir: PathBuf,
pub(crate) bind: String,
pub(crate) public_url: String,
pub(crate) state_file: PathBuf,
pub(crate) command_timeout_ms: u64,
pub(crate) enroll_token_ttl_seconds: u64,
pub(crate) observation_retention_seconds: u64,
pub(crate) pid_file: PathBuf,
pub(crate) ui_dist_dir: PathBuf,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) bootstrap_enroll_tokens: Vec<String>,
pub(crate) telemetry: WritableTelemetry,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct WritableTelemetry {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) console_filter: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) otlp_log_filter: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) trace_filter: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) otlp_endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) authorization_env: Option<String>,
pub(crate) service_name: String,
pub(crate) json_logs: bool,
}
pub(crate) static DEFAULT_CONFIG: LazyLock<WritableConfig> = LazyLock::new(|| WritableConfig {
// Relative template paths are resolved under data_dir by the runtime loader.
pub(crate) static DEFAULT_CONFIG: LazyLock<DaemonConfig> = LazyLock::new(|| DaemonConfig {
data_dir: crate::cli::DEFAULT_DATA_DIR.into(),
bind: "0.0.0.0:8080".to_string(),
bind: SocketAddr::from(([0, 0, 0, 0], 8080)),
public_url: "http://127.0.0.1:8080".to_string(),
state_file: "state.sqlite3".into(),
command_timeout_ms: 30_000,
enroll_token_ttl_seconds: 86_400,
observation_retention_seconds: 2_592_000,
command_timeout: Duration::from_millis(30_000),
enroll_token_ttl: Duration::from_secs(86_400),
observation_retention: Duration::from_secs(2_592_000),
pid_file: "wakey-control-plane.pid".into(),
ui_dist_dir: "ui/dist".into(),
bootstrap_enroll_tokens: Vec::new(),
telemetry: WritableTelemetry {
telemetry: TelemetryConfig {
console_filter: None,
otlp_log_filter: None,
trace_filter: None,
@@ -139,3 +87,41 @@ pub struct StateAccessSettings {
pub state_file: PathBuf,
pub public_url: Option<String>,
}
#[cfg(test)]
mod tests {
use super::{FileConfig, FileTelemetryConfig};
#[test]
fn file_dto_round_trip_preserves_omission_and_explicit_values() {
let omitted: FileConfig = toml::from_str("").unwrap();
assert_eq!(omitted, FileConfig::default());
assert_eq!(toml::to_string(&omitted).unwrap(), "");
let explicit = FileConfig {
bootstrap_enroll_tokens: Some(Vec::new()),
observation_retention_seconds: Some(0),
telemetry: Some(FileTelemetryConfig {
json_logs: Some(false),
console_filter: Some("info".into()),
otlp_log_filter: Some("debug".into()),
trace_filter: Some("info".into()),
..Default::default()
}),
..Default::default()
};
let rendered = toml::to_string_pretty(&explicit).unwrap();
let restored: FileConfig = toml::from_str(&rendered).unwrap();
assert_eq!(restored, explicit);
assert_ne!(restored, omitted);
}
#[test]
fn legacy_token_alias_serializes_with_canonical_name() {
let file: FileConfig = toml::from_str("enroll_tokens = []").unwrap();
assert_eq!(file.bootstrap_enroll_tokens, Some(Vec::new()));
let rendered = toml::to_string(&file).unwrap();
assert!(rendered.contains("bootstrap_enroll_tokens = []"));
assert!(!rendered.starts_with("enroll_tokens"));
}
}