use serde::{Deserialize, Serialize};
use crate::SCHEMA_VERSION_V1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Config")]
#[serde(deny_unknown_fields)]
pub struct AgentConfig {
pub schema_version: String,
#[serde(default)]
pub agent: AgentSection,
#[serde(default)]
pub control_plane: ControlPlaneSection,
#[serde(default)]
pub paths: PathsSection,
#[serde(default)]
pub execution: ExecutionSection,
#[serde(default)]
pub telemetry: TelemetrySection,
#[serde(default)]
pub discovery: DiscoverySection,
}
impl AgentConfig {
pub fn new(
agent: AgentSection,
control_plane: ControlPlaneSection,
paths: PathsSection,
execution: ExecutionSection,
) -> Self {
Self {
schema_version: SCHEMA_VERSION_V1.to_string(),
agent,
control_plane,
paths,
execution,
telemetry: TelemetrySection::default(),
discovery: DiscoverySection::default(),
}
}
pub fn with_telemetry(mut self, telemetry: TelemetrySection) -> Self {
self.telemetry = telemetry;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Config")]
#[serde(deny_unknown_fields)]
pub struct DiscoverySection {
#[serde(default = "default_discovery_host_enabled")]
pub host_enabled: bool,
#[serde(default = "default_discovery_network_enabled")]
pub network_enabled: bool,
#[serde(default = "default_discovery_endpoint_enabled")]
pub endpoint_enabled: bool,
#[serde(default)]
pub process_enabled: bool,
#[serde(default)]
pub container_enabled: bool,
}
impl Default for DiscoverySection {
fn default() -> Self {
Self {
host_enabled: default_discovery_host_enabled(),
network_enabled: default_discovery_network_enabled(),
endpoint_enabled: default_discovery_endpoint_enabled(),
process_enabled: true,
container_enabled: false,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Config")]
#[serde(deny_unknown_fields)]
pub struct AgentSection {
#[serde(default)]
pub agent_id: Option<String>,
#[serde(default)]
pub environment_id: Option<String>,
#[serde(default)]
pub instance_name: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Config")]
#[serde(deny_unknown_fields)]
pub struct ControlPlaneSection {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub endpoint: Option<String>,
#[serde(default)]
pub enrollment_token: Option<String>,
#[serde(default)]
pub credential_request: Option<String>,
#[serde(default)]
pub credential_id: Option<String>,
#[serde(default)]
pub credential_expires_at: Option<String>,
#[serde(default)]
pub tls_mode: Option<String>,
#[serde(default)]
pub trust_bundle: Option<String>,
#[serde(default)]
pub auth_mode: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Config")]
#[serde(deny_unknown_fields)]
pub struct PathsSection {
#[serde(default = "default_root_dir")]
pub root_dir: String,
#[serde(default = "default_run_dir")]
pub run_dir: String,
#[serde(default = "default_state_dir")]
pub state_dir: String,
#[serde(default = "default_log_dir")]
pub log_dir: String,
}
impl Default for PathsSection {
fn default() -> Self {
Self {
root_dir: default_root_dir(),
run_dir: default_run_dir(),
state_dir: default_state_dir(),
log_dir: default_log_dir(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Config")]
#[serde(deny_unknown_fields)]
pub struct ExecutionSection {
#[serde(default = "default_max_running_actions")]
pub max_running_actions: u32,
#[serde(default = "default_cancel_grace_ms")]
pub cancel_grace_ms: u64,
#[serde(default = "default_stdout_limit_bytes")]
pub default_stdout_limit_bytes: u64,
#[serde(default = "default_stderr_limit_bytes")]
pub default_stderr_limit_bytes: u64,
}
impl Default for ExecutionSection {
fn default() -> Self {
Self {
max_running_actions: default_max_running_actions(),
cancel_grace_ms: default_cancel_grace_ms(),
default_stdout_limit_bytes: default_stdout_limit_bytes(),
default_stderr_limit_bytes: default_stderr_limit_bytes(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Config")]
#[serde(deny_unknown_fields)]
pub struct TelemetrySection {
#[serde(default)]
pub logs: LogsSection,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Config")]
#[serde(deny_unknown_fields)]
pub struct LogsSection {
#[serde(default)]
pub file_inputs: Vec<LogFileInputSection>,
#[serde(default)]
pub file_inputs_file: Option<String>,
#[serde(default = "default_logs_buffer_bytes")]
pub in_memory_buffer_bytes: u64,
#[serde(default = "default_max_line_bytes")]
pub max_line_bytes: u64,
#[serde(default = "default_max_read_bytes_per_tick")]
pub max_read_bytes_per_tick: u64,
#[serde(default = "default_max_lines_per_tick")]
pub max_lines_per_tick: u64,
#[serde(default = "default_spool_max_bytes")]
pub spool_max_bytes: u64,
#[serde(default = "default_spool_over_limit")]
pub spool_over_limit: String,
#[serde(default = "default_logs_spool_dir")]
pub spool_dir: String,
#[serde(default)]
pub output: LogsOutputSection,
}
impl Default for LogsSection {
fn default() -> Self {
Self {
file_inputs: Vec::new(),
file_inputs_file: None,
in_memory_buffer_bytes: default_logs_buffer_bytes(),
max_line_bytes: default_max_line_bytes(),
max_read_bytes_per_tick: default_max_read_bytes_per_tick(),
max_lines_per_tick: default_max_lines_per_tick(),
spool_max_bytes: default_spool_max_bytes(),
spool_over_limit: default_spool_over_limit(),
spool_dir: default_logs_spool_dir(),
output: LogsOutputSection::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Config")]
#[serde(deny_unknown_fields)]
pub struct LogsOutputSection {
#[serde(default = "default_logs_output_enabled")]
pub enabled: bool,
#[serde(default = "default_logs_output_kind")]
pub kind: String,
#[serde(default)]
pub file: LogsFileOutputSection,
#[serde(default)]
pub tcp: LogsTcpOutputSection,
}
impl Default for LogsOutputSection {
fn default() -> Self {
Self {
enabled: default_logs_output_enabled(),
kind: default_logs_output_kind(),
file: LogsFileOutputSection::default(),
tcp: LogsTcpOutputSection::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Config")]
#[serde(deny_unknown_fields)]
pub struct LogsFileOutputSection {
#[serde(default = "default_logs_output_file")]
pub path: String,
}
impl Default for LogsFileOutputSection {
fn default() -> Self {
Self {
path: default_logs_output_file(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Config")]
#[serde(deny_unknown_fields)]
pub struct LogsTcpOutputSection {
#[serde(default = "default_logs_output_tcp_addr")]
pub addr: String,
#[serde(default = "default_logs_output_tcp_port")]
pub port: u16,
#[serde(default = "default_logs_output_tcp_framing")]
pub framing: String,
}
impl Default for LogsTcpOutputSection {
fn default() -> Self {
Self {
addr: default_logs_output_tcp_addr(),
port: default_logs_output_tcp_port(),
framing: default_logs_output_tcp_framing(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
#[jumo(kind = "struct", domain = "Discovery", module = "Discovery.Config")]
#[serde(deny_unknown_fields)]
pub struct LogFileInputSection {
pub input_id: String,
pub path: String,
#[serde(default = "default_startup_position")]
pub startup_position: String,
#[serde(default = "default_multiline_mode")]
pub multiline_mode: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LogFileInputsFile {
pub file_inputs: Vec<LogFileInputSection>,
}
fn default_logs_buffer_bytes() -> u64 {
1_048_576
}
fn default_max_line_bytes() -> u64 {
1_048_576
}
fn default_max_read_bytes_per_tick() -> u64 {
4_194_304
}
fn default_max_lines_per_tick() -> u64 {
4096
}
fn default_spool_max_bytes() -> u64 {
268_435_456
}
fn default_spool_over_limit() -> String {
"pause".to_string()
}
fn default_root_dir() -> String {
".".to_string()
}
fn default_run_dir() -> String {
"run".to_string()
}
fn default_state_dir() -> String {
"state".to_string()
}
fn default_log_dir() -> String {
"log".to_string()
}
fn default_max_running_actions() -> u32 {
1
}
fn default_cancel_grace_ms() -> u64 {
5_000
}
fn default_stdout_limit_bytes() -> u64 {
1_048_576
}
fn default_stderr_limit_bytes() -> u64 {
1_048_576
}
fn default_logs_spool_dir() -> String {
"state/spool/logs".to_string()
}
fn default_logs_output_file() -> String {
"log/wist-records.ndjson".to_string()
}
fn default_logs_output_enabled() -> bool {
true
}
fn default_logs_output_kind() -> String {
"file".to_string()
}
fn default_logs_output_tcp_addr() -> String {
"127.0.0.1".to_string()
}
fn default_logs_output_tcp_port() -> u16 {
9000
}
fn default_logs_output_tcp_framing() -> String {
"line".to_string()
}
fn default_multiline_mode() -> String {
"none".to_string()
}
fn default_startup_position() -> String {
"tail".to_string()
}
fn default_discovery_host_enabled() -> bool {
true
}
fn default_discovery_network_enabled() -> bool {
true
}
fn default_discovery_endpoint_enabled() -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_file_input_without_startup_position_starts_from_the_tail() {
let input: LogFileInputSection =
serde_json::from_str(r#"{"input_id":"app","path":"/var/log/app.log"}"#)
.expect("decode");
assert_eq!(input.startup_position, "tail");
assert_eq!(input.multiline_mode, "none");
}
#[test]
fn logs_output_rejects_unknown_keys() {
assert!(serde_json::from_str::<LogsOutputSection>(r#"{"enabled":true,"nope":1}"#).is_err());
}
#[test]
fn an_output_section_without_enabled_is_on() {
let output: LogsOutputSection = serde_json::from_str(r#"{"kind":"tcp"}"#).expect("decode");
assert!(output.enabled);
assert!(LogsOutputSection::default().enabled);
}
#[test]
fn an_explicit_disabled_output_is_respected() {
let output: LogsOutputSection =
serde_json::from_str(r#"{"enabled":false,"kind":"tcp"}"#).expect("decode");
assert!(!output.enabled);
assert_eq!(output.kind, "tcp");
}
#[test]
fn an_explicit_startup_position_still_wins() {
let input: LogFileInputSection = serde_json::from_str(
r#"{"input_id":"app","path":"/var/log/app.log","startup_position":"head"}"#,
)
.expect("decode");
assert_eq!(input.startup_position, "head");
}
#[test]
fn a_discovery_section_omitted_whole_defaults_process_on_but_an_inline_omission_defaults_it_off()
{
assert!(DiscoverySection::default().process_enabled);
assert!(DiscoverySection::default().host_enabled);
let inline: DiscoverySection = serde_json::from_str("{}").expect("decode");
assert!(
!inline.process_enabled,
"段内省略 process_enabled 必须是 false"
);
assert!(inline.host_enabled, "段内省略 host_enabled 仍是 true");
let whole_missing: AgentConfig =
serde_json::from_str(r#"{"schema_version":"v1"}"#).expect("decode");
assert!(whole_missing.discovery.process_enabled);
let empty_section: AgentConfig =
serde_json::from_str(r#"{"schema_version":"v1","discovery":{}}"#).expect("decode");
assert!(!empty_section.discovery.process_enabled);
}
#[test]
fn agent_config_rejects_unknown_fields() {
assert!(
serde_json::from_str::<AgentConfig>(r#"{"schema_version":"v1","nope":1}"#).is_err()
);
}
#[test]
fn agent_config_new_fills_the_version_and_default_sections() {
let config = AgentConfig::new(
AgentSection::default(),
ControlPlaneSection::default(),
PathsSection::default(),
ExecutionSection::default(),
);
assert_eq!(config.schema_version, SCHEMA_VERSION_V1);
assert_eq!(config.telemetry, TelemetrySection::default());
assert_eq!(config.discovery, DiscoverySection::default());
}
}