use std::path::PathBuf;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub(crate) trait Merge {
fn merge(self, over: Self) -> Self;
}
fn merge_opt<T: Merge>(base: Option<T>, over: Option<T>) -> Option<T> {
match (base, over) {
(Some(base), Some(over)) => Some(base.merge(over)),
(base, over) => over.or(base),
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct RsigmaConfigPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub global: Option<GlobalPartial>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub daemon: Option<DaemonPartial>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub eval: Option<EvalPartial>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backtest: Option<BacktestPartial>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coverage: Option<CoveragePartial>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scorecard: Option<ScorecardPartial>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub visibility: Option<VisibilityPartial>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mcp: Option<McpPartial>,
}
impl Merge for RsigmaConfigPartial {
fn merge(self, over: Self) -> Self {
Self {
version: over.version.or(self.version),
global: merge_opt(self.global, over.global),
daemon: merge_opt(self.daemon, over.daemon),
eval: merge_opt(self.eval, over.eval),
backtest: merge_opt(self.backtest, over.backtest),
coverage: merge_opt(self.coverage, over.coverage),
scorecard: merge_opt(self.scorecard, over.scorecard),
visibility: merge_opt(self.visibility, over.visibility),
mcp: merge_opt(self.mcp, over.mcp),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct GlobalPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub log_format: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_format: Option<String>,
}
impl Merge for GlobalPartial {
fn merge(self, over: Self) -> Self {
Self {
log_format: over.log_format.or(self.log_format),
color: over.color.or(self.color),
output_format: over.output_format.or(self.output_format),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct DaemonPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rules: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pipelines: Option<Vec<PathBuf>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sources: Option<Vec<PathBuf>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enrichers: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api: Option<ApiPartial>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input: Option<InputPartial>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<OutputPartial>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub correlation: Option<CorrelationPartial>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state: Option<StatePartial>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub engine: Option<EnginePartial>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nats: Option<NatsPartial>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tap: Option<TapPartial>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tail: Option<TailPartial>,
}
impl Merge for DaemonPartial {
fn merge(self, over: Self) -> Self {
Self {
rules: over.rules.or(self.rules),
pipelines: over.pipelines.or(self.pipelines),
sources: over.sources.or(self.sources),
enrichers: over.enrichers.or(self.enrichers),
api: merge_opt(self.api, over.api),
input: merge_opt(self.input, over.input),
output: merge_opt(self.output, over.output),
correlation: merge_opt(self.correlation, over.correlation),
state: merge_opt(self.state, over.state),
engine: merge_opt(self.engine, over.engine),
nats: merge_opt(self.nats, over.nats),
tap: merge_opt(self.tap, over.tap),
tail: merge_opt(self.tail, over.tail),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct ApiPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub addr: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tls: Option<TlsPartial>,
}
impl Merge for ApiPartial {
fn merge(self, over: Self) -> Self {
Self {
addr: over.addr.or(self.addr),
tls: merge_opt(self.tls, over.tls),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct TlsPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cert: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_ca: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_plaintext: Option<bool>,
}
impl Merge for TlsPartial {
fn merge(self, over: Self) -> Self {
Self {
cert: over.cert.or(self.cert),
key: over.key.or(self.key),
client_ca: over.client_ca.or(self.client_ca),
min_version: over.min_version.or(self.min_version),
allow_plaintext: over.allow_plaintext.or(self.allow_plaintext),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct InputPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub syslog_tz: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub syslog_strip_bom: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub buffer_size: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub batch_size: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub jq: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub jsonpath: Option<String>,
}
impl Merge for InputPartial {
fn merge(self, over: Self) -> Self {
Self {
source: over.source.or(self.source),
format: over.format.or(self.format),
syslog_tz: over.syslog_tz.or(self.syslog_tz),
syslog_strip_bom: over.syslog_strip_bom.or(self.syslog_strip_bom),
buffer_size: over.buffer_size.or(self.buffer_size),
batch_size: over.batch_size.or(self.batch_size),
jq: over.jq.or(self.jq),
jsonpath: over.jsonpath.or(self.jsonpath),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct OutputPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sinks: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dlq: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub drain_timeout: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub include_event: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pretty: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retry_max: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backoff_base_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backoff_max_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub batch_max: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub batch_flush_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub webhooks: Option<Vec<String>>,
}
impl Merge for OutputPartial {
fn merge(self, over: Self) -> Self {
Self {
sinks: over.sinks.or(self.sinks),
dlq: over.dlq.or(self.dlq),
drain_timeout: over.drain_timeout.or(self.drain_timeout),
include_event: over.include_event.or(self.include_event),
pretty: over.pretty.or(self.pretty),
retry_max: over.retry_max.or(self.retry_max),
backoff_base_ms: over.backoff_base_ms.or(self.backoff_base_ms),
backoff_max_ms: over.backoff_max_ms.or(self.backoff_max_ms),
batch_max: over.batch_max.or(self.batch_max),
batch_flush_ms: over.batch_flush_ms.or(self.batch_flush_ms),
webhooks: over.webhooks.or(self.webhooks),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct CorrelationPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub suppress: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub event_mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_events: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_state_entries: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_group_entries: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp_fields: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp_fallback: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub no_detections: Option<bool>,
}
impl Merge for CorrelationPartial {
fn merge(self, over: Self) -> Self {
Self {
suppress: over.suppress.or(self.suppress),
action: over.action.or(self.action),
event_mode: over.event_mode.or(self.event_mode),
max_events: over.max_events.or(self.max_events),
max_state_entries: over.max_state_entries.or(self.max_state_entries),
max_group_entries: over.max_group_entries.or(self.max_group_entries),
timestamp_fields: over.timestamp_fields.or(self.timestamp_fields),
timestamp_fallback: over.timestamp_fallback.or(self.timestamp_fallback),
no_detections: over.no_detections.or(self.no_detections),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct StatePartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub db: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub save_interval: Option<u64>,
}
impl Merge for StatePartial {
fn merge(self, over: Self) -> Self {
Self {
db: over.db.or(self.db),
save_interval: over.save_interval.or(self.save_interval),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct EnginePartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bloom_prefilter: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bloom_max_bytes: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observe_fields: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observe_fields_max_keys: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_remote_include: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cross_rule_ac: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub match_detail: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub egress_policy: Option<String>,
}
impl Merge for EnginePartial {
fn merge(self, over: Self) -> Self {
Self {
bloom_prefilter: over.bloom_prefilter.or(self.bloom_prefilter),
bloom_max_bytes: over.bloom_max_bytes.or(self.bloom_max_bytes),
observe_fields: over.observe_fields.or(self.observe_fields),
observe_fields_max_keys: over
.observe_fields_max_keys
.or(self.observe_fields_max_keys),
allow_remote_include: over.allow_remote_include.or(self.allow_remote_include),
cross_rule_ac: over.cross_rule_ac.or(self.cross_rule_ac),
match_detail: over.match_detail.or(self.match_detail),
egress_policy: over.egress_policy.or(self.egress_policy),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct NatsPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub consumer_group: Option<String>,
}
impl Merge for NatsPartial {
fn merge(self, over: Self) -> Self {
Self {
consumer_group: over.consumer_group.or(self.consumer_group),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct TapPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub buffer_events: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_sessions: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_duration: Option<String>,
}
impl Merge for TapPartial {
fn merge(self, over: Self) -> Self {
Self {
enabled: over.enabled.or(self.enabled),
buffer_events: over.buffer_events.or(self.buffer_events),
max_sessions: over.max_sessions.or(self.max_sessions),
max_duration: over.max_duration.or(self.max_duration),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct TailPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub buffer_events: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_sessions: Option<usize>,
}
impl Merge for TailPartial {
fn merge(self, over: Self) -> Self {
Self {
enabled: over.enabled.or(self.enabled),
buffer_events: over.buffer_events.or(self.buffer_events),
max_sessions: over.max_sessions.or(self.max_sessions),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct EvalPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rules: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pipelines: Option<Vec<PathBuf>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_format: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub syslog_tz: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub syslog_strip_bom: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fail_on_detection: Option<bool>,
}
impl Merge for EvalPartial {
fn merge(self, over: Self) -> Self {
Self {
rules: over.rules.or(self.rules),
pipelines: over.pipelines.or(self.pipelines),
input_format: over.input_format.or(self.input_format),
syslog_tz: over.syslog_tz.or(self.syslog_tz),
syslog_strip_bom: over.syslog_strip_bom.or(self.syslog_strip_bom),
fail_on_detection: over.fail_on_detection.or(self.fail_on_detection),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct BacktestPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rules: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub corpus: Option<Vec<PathBuf>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expectations: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unexpected: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pipelines: Option<Vec<PathBuf>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_format: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub syslog_tz: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub syslog_strip_bom: Option<bool>,
}
impl Merge for BacktestPartial {
fn merge(self, over: Self) -> Self {
Self {
rules: over.rules.or(self.rules),
corpus: over.corpus.or(self.corpus),
expectations: over.expectations.or(self.expectations),
unexpected: over.unexpected.or(self.unexpected),
pipelines: over.pipelines.or(self.pipelines),
input_format: over.input_format.or(self.input_format),
syslog_tz: over.syslog_tz.or(self.syslog_tz),
syslog_strip_bom: over.syslog_strip_bom.or(self.syslog_strip_bom),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct CoveragePartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rules: Option<Vec<PathBuf>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub atomics: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub baseline: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub targets: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fail_on_gaps: Option<bool>,
}
impl Merge for CoveragePartial {
fn merge(self, over: Self) -> Self {
Self {
rules: over.rules.or(self.rules),
atomics: over.atomics.or(self.atomics),
baseline: over.baseline.or(self.baseline),
targets: over.targets.or(self.targets),
fail_on_gaps: over.fail_on_gaps.or(self.fail_on_gaps),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct ScorecardPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backtest: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coverage: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metrics: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metrics_window: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub triage: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub report: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fail_on: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_precision: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tune_max_precision: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retire_max_precision: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_volume: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stale_window: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_fp_ratio: Option<f64>,
}
impl Merge for ScorecardPartial {
fn merge(self, over: Self) -> Self {
Self {
backtest: over.backtest.or(self.backtest),
coverage: over.coverage.or(self.coverage),
metrics: over.metrics.or(self.metrics),
metrics_window: over.metrics_window.or(self.metrics_window),
triage: over.triage.or(self.triage),
report: over.report.or(self.report),
fail_on: over.fail_on.or(self.fail_on),
min_precision: over.min_precision.or(self.min_precision),
tune_max_precision: over.tune_max_precision.or(self.tune_max_precision),
retire_max_precision: over.retire_max_precision.or(self.retire_max_precision),
min_volume: over.min_volume.or(self.min_volume),
stale_window: over.stale_window.or(self.stale_window),
max_fp_ratio: over.max_fp_ratio.or(self.max_fp_ratio),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct VisibilityPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mapping: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fail_on_blind_spots: Option<bool>,
}
impl Merge for VisibilityPartial {
fn merge(self, over: Self) -> Self {
Self {
mapping: over.mapping.or(self.mapping),
fail_on_blind_spots: over.fail_on_blind_spots.or(self.fail_on_blind_spots),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
pub(crate) struct McpPartial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub http_addr: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lint_config: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rules_dir: Option<PathBuf>,
}
impl Merge for McpPartial {
fn merge(self, over: Self) -> Self {
Self {
http_addr: over.http_addr.or(self.http_addr),
lint_config: over.lint_config.or(self.lint_config),
rules_dir: over.rules_dir.or(self.rules_dir),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merge_prefers_higher_layer_per_field() {
let base = RsigmaConfigPartial {
version: Some(1),
daemon: Some(DaemonPartial {
rules: Some(PathBuf::from("/etc/rsigma/rules")),
api: Some(ApiPartial {
addr: Some("0.0.0.0:9090".into()),
tls: None,
}),
..Default::default()
}),
..Default::default()
};
let over = RsigmaConfigPartial {
daemon: Some(DaemonPartial {
api: Some(ApiPartial {
addr: Some("127.0.0.1:8080".into()),
tls: None,
}),
..Default::default()
}),
..Default::default()
};
let merged = base.merge(over);
let daemon = merged.daemon.expect("daemon section");
assert_eq!(daemon.rules, Some(PathBuf::from("/etc/rsigma/rules")));
assert_eq!(
daemon.api.expect("api section").addr,
Some("127.0.0.1:8080".into())
);
assert_eq!(merged.version, Some(1));
}
#[test]
fn mcp_section_parses_and_merges() {
let base: RsigmaConfigPartial = yaml_serde::from_str(
"mcp:\n http_addr: 127.0.0.1:9100\n rules_dir: /etc/rsigma/rules\n",
)
.expect("parses mcp section");
let over: RsigmaConfigPartial =
yaml_serde::from_str("mcp:\n rules_dir: /override/rules\n").expect("parses override");
let merged = base.merge(over);
let mcp = merged.mcp.expect("mcp section");
assert_eq!(mcp.http_addr.as_deref(), Some("127.0.0.1:9100"));
assert_eq!(mcp.rules_dir, Some(PathBuf::from("/override/rules")));
}
#[test]
fn merge_keeps_base_when_over_is_none() {
let base = RsigmaConfigPartial {
global: Some(GlobalPartial {
log_format: Some("json".into()),
..Default::default()
}),
..Default::default()
};
let merged = base.merge(RsigmaConfigPartial::default());
assert_eq!(
merged.global.expect("global").log_format,
Some("json".into())
);
}
}