1use eyre::{eyre, Context};
5use log::warn;
6use serde::{Deserialize, Serialize};
7use std::net::IpAddr;
8use std::time::Duration;
9use std::{
10 collections::{HashMap, HashSet},
11 num::NonZeroU32,
12};
13use std::{net::SocketAddr, path::PathBuf};
14
15use crate::metrics::{system_metrics::SystemMetricConfig, MetricStringKey, SessionName};
16use crate::util::*;
17use crate::util::{path::AbsolutePath, serialization::*};
18use crate::{mar::CompressionAlgorithm, util::disk_size::DiskSize};
19
20#[derive(Serialize, Deserialize, Debug)]
21pub struct MemfaultdConfig {
22 pub persist_dir: AbsolutePath,
23 pub tmp_dir: Option<AbsolutePath>,
24 #[serde(rename = "tmp_dir_min_headroom_kib", with = "kib_to_usize")]
25 pub tmp_dir_min_headroom: usize,
26 pub tmp_dir_min_inodes: usize,
27 #[serde(rename = "tmp_dir_max_usage_kib", with = "kib_to_usize")]
28 pub tmp_dir_max_usage: usize,
29 #[serde(rename = "upload_interval_seconds", with = "seconds_to_duration")]
30 pub upload_interval: Duration,
31 #[serde(rename = "heartbeat_interval_seconds", with = "seconds_to_duration")]
32 pub heartbeat_interval: Duration,
33 pub enable_data_collection: bool,
34 pub enable_dev_mode: bool,
35 pub software_version: Option<String>,
36 pub software_type: Option<String>,
37 pub project_key: String,
38 pub base_url: String,
39 pub swupdate: SwUpdateConfig,
40 pub reboot: RebootConfig,
41 pub coredump: CoredumpConfig,
42 #[serde(rename = "fluent-bit")]
43 pub fluent_bit: FluentBitConfig,
44 pub logs: LogsConfig,
45 pub mar: MarConfig,
46 pub http_server: HttpServerConfig,
47 pub battery_monitor: Option<BatteryMonitorConfig>,
48 pub connectivity_monitor: Option<ConnectivityMonitorConfig>,
49 pub sessions: Option<Vec<SessionConfig>>,
50 pub metrics: MetricReportConfig,
51 pub custom_trace: Option<LinuxCustomTraceConfig>,
52 pub persist_storage: Option<PersistStorageConfig>,
53}
54
55#[derive(Serialize, Deserialize, Debug)]
56pub struct SwUpdateConfig {
57 pub input_file: PathBuf,
58 pub output_file: PathBuf,
59}
60
61#[derive(Serialize, Deserialize, Debug)]
62pub struct RebootConfig {
63 pub last_reboot_reason_file: PathBuf,
64 pub capture_pstore: bool,
65}
66
67#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
68pub enum CoredumpCompression {
69 #[serde(rename = "gzip")]
70 Gzip,
71 #[serde(rename = "none")]
72 None,
73}
74
75#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
76#[serde(tag = "type")]
77pub enum CoredumpCaptureStrategy {
78 #[serde(rename = "threads")]
79 Threads {
81 #[serde(rename = "max_thread_size_kib", with = "kib_to_usize")]
82 max_thread_size: usize,
83 },
84 #[serde(rename = "kernel_selection")]
85 KernelSelection,
88 #[serde(rename = "stacktrace")]
89 Stacktrace,
91}
92
93#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
94pub struct LinuxCustomTraceConfig {
95 pub log_compression: LinuxCustomTraceLogCompression,
96 pub rate_limit_count: u32,
97 #[serde(rename = "rate_limit_duration_seconds", with = "seconds_to_duration")]
98 pub rate_limit_duration: Duration,
99}
100
101#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
102#[serde(tag = "type")]
103pub enum TraceFilter {
104 #[serde(rename = "executable_name")]
105 ExecutableName { name: String },
106 #[serde(rename = "executable_path")]
107 ExecutablePath { path: String },
108}
109
110impl Default for LinuxCustomTraceConfig {
111 fn default() -> Self {
112 Self {
113 log_compression: LinuxCustomTraceLogCompression::Gzip,
114 rate_limit_count: 5,
115 rate_limit_duration: Duration::from_secs(3600),
116 }
117 }
118}
119
120#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)]
121pub enum LinuxCustomTraceLogCompression {
122 #[serde(rename = "gzip")]
123 Gzip,
124 #[serde(rename = "zlib")]
125 Zlib,
126 #[serde(rename = "none")]
127 None,
128}
129
130impl From<LinuxCustomTraceLogCompression> for CompressionAlgorithm {
131 fn from(value: LinuxCustomTraceLogCompression) -> CompressionAlgorithm {
132 match value {
133 LinuxCustomTraceLogCompression::Gzip => CompressionAlgorithm::Gzip,
134 LinuxCustomTraceLogCompression::Zlib => CompressionAlgorithm::Zlib,
135 LinuxCustomTraceLogCompression::None => CompressionAlgorithm::None,
136 }
137 }
138}
139
140#[derive(Serialize, Deserialize, Debug, Clone)]
141pub struct CoredumpConfig {
142 pub compression: CoredumpCompression,
143 #[serde(rename = "coredump_max_size_kib", with = "kib_to_usize")]
144 pub coredump_max_size: usize,
145 pub rate_limit_count: u32,
146 #[serde(rename = "rate_limit_duration_seconds", with = "seconds_to_duration")]
147 pub rate_limit_duration: Duration,
148 pub capture_strategy: CoredumpCaptureStrategy,
149 pub log_lines: usize,
150 pub filters: Option<Vec<TraceFilter>>,
151}
152
153#[derive(Serialize, Deserialize, Debug)]
154pub struct FluentBitConfig {
155 pub extra_fluentd_attributes: Vec<String>,
156 pub bind_address: SocketAddr,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub max_buffered_lines: Option<usize>,
159 pub max_connections: usize,
160}
161
162#[derive(Serialize, Deserialize, Debug)]
163pub struct HttpServerConfig {
164 pub bind_address: SocketAddr,
165}
166
167#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
168pub struct SyslogConfig {
169 pub bind_address: SocketAddr,
170}
171
172#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
173pub enum LogSource {
174 #[serde(rename = "fluent-bit")]
175 FluentBit,
176 #[serde(rename = "journald")]
177 Journald,
178 #[serde(rename = "syslog")]
179 Syslog(SyslogConfig),
180}
181
182#[derive(Serialize, Deserialize, Debug)]
183pub struct LogsConfig {
184 #[serde(rename = "rotate_size_kib", with = "kib_to_usize")]
185 pub rotate_size: usize,
186 #[serde(rename = "rotate_after_seconds", with = "seconds_to_duration")]
187 pub rotate_after: Duration,
188 #[serde(with = "number_to_compression")]
189 pub compression_level: Compression,
190 pub max_lines_per_minute: NonZeroU32,
191 pub log_to_metrics: Option<LogToMetricsConfig>,
192 pub storage: StorageConfig,
193 pub source: LogSource,
194 pub level_mapping: LevelMappingConfig,
195 pub extra_attributes: Vec<String>,
196 pub max_buffered_lines: usize,
197 pub filtering: Option<LogFilterConfig>,
198}
199
200#[derive(Clone, Serialize, Deserialize, Debug)]
201pub struct LogFilterConfig {
202 pub default_action: LogRuleAction,
203 pub rules: Vec<LogFilterRule>,
204}
205
206impl Default for LogFilterConfig {
207 fn default() -> Self {
208 LogFilterConfig {
209 rules: vec![],
210 default_action: LogRuleAction::Include,
211 }
212 }
213}
214
215#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, Debug, Eq)]
216#[serde(rename_all = "lowercase")]
217pub enum LogRuleAction {
218 Pass,
219 Include,
220 Exclude,
221}
222
223#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
224pub struct LogFilterRule {
225 pub service: Option<String>,
226 pub counter_name: Option<String>,
227 pub pattern: Option<String>,
228 pub level: Option<String>,
229 pub extra_fields: Option<HashMap<String, String>>,
230 pub action: Option<LogRuleAction>,
231}
232
233#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
234pub enum StorageConfig {
235 #[serde(rename = "disabled")]
236 Disabled,
237 #[serde(rename = "persist")]
238 Persist,
239}
240
241#[derive(Serialize, Deserialize, Debug)]
242pub struct LogToMetricsConfig {
243 pub rules: Vec<LogToMetricRule>,
244}
245
246#[derive(Serialize, Deserialize, Debug, Clone)]
247#[serde(tag = "type")]
248pub enum LogToMetricRule {
249 #[serde(rename = "count_matching")]
250 CountMatching {
251 pattern: String,
253 metric_name: String,
254 #[serde(default)]
256 filter: HashMap<String, String>,
257 },
258}
259
260#[derive(Serialize, Deserialize, Debug)]
261pub struct MarConfig {
262 #[serde(rename = "mar_file_max_size_kib", with = "kib_to_usize")]
263 pub mar_file_max_size: usize,
264 #[serde(rename = "mar_entry_max_age_seconds", with = "seconds_to_duration")]
265 pub mar_entry_max_age: Duration,
266 pub mar_entry_max_count: usize,
267}
268
269#[derive(Serialize, Deserialize, Debug)]
270pub struct BatteryMonitorConfig {
271 pub battery_info_command: Option<String>,
272 pub auto: bool,
273 #[serde(with = "seconds_to_duration")]
274 pub interval_seconds: Duration,
275}
276
277#[derive(Serialize, Deserialize, Debug)]
278pub struct ConnectivityMonitorConfig {
279 #[serde(with = "seconds_to_duration")]
280 pub interval_seconds: Duration,
281 pub targets: Vec<ConnectivityMonitorTarget>,
282 #[serde(
283 with = "seconds_to_duration",
284 default = "default_connection_check_timeout"
285 )]
286 pub timeout_seconds: Duration,
287}
288fn default_connection_check_timeout() -> Duration {
289 Duration::from_secs(10)
290}
291
292#[derive(Serialize, Deserialize, Debug)]
293pub struct MetricReportConfig {
294 pub enable_daily_heartbeats: bool,
295 pub system_metric_collection: SystemMetricConfig,
296 pub statsd_server: Option<StatsDServerConfig>,
297 pub high_resolution_telemetry: HrtConfig,
298 #[serde(default)]
299 pub min_max_metrics: Vec<MetricStringKey>,
300}
301
302#[derive(Serialize, Deserialize, Debug)]
303pub struct HrtConfig {
304 pub enable: bool,
305 pub max_samples_per_minute: NonZeroU32,
306}
307
308#[derive(Serialize, Deserialize, Debug, Clone)]
309pub struct ConnectivityMonitorTarget {
310 #[serde(default = "default_connection_check_protocol")]
311 pub protocol: ConnectionCheckProtocol,
312 pub host: IpAddr,
313 pub port: u16,
314}
315
316#[derive(Serialize, Deserialize, Debug, Clone)]
317#[serde(rename_all = "lowercase")]
318pub enum ConnectionCheckProtocol {
319 Tcp,
320}
321
322fn default_connection_check_protocol() -> ConnectionCheckProtocol {
323 ConnectionCheckProtocol::Tcp
324}
325
326#[derive(Serialize, Clone, Deserialize, Debug)]
327pub struct SessionConfig {
328 pub name: SessionName,
329 pub captured_metrics: HashSet<MetricStringKey>,
330}
331
332#[derive(Serialize, Deserialize, Debug)]
333pub struct StatsDServerConfig {
334 pub bind_address: SocketAddr,
335 pub legacy_gauge_aggregation: Option<bool>,
336 pub legacy_key_names: Option<bool>,
337}
338
339#[derive(Clone, Serialize, Deserialize, Debug)]
340pub struct LevelMappingConfig {
341 pub enable: bool,
342 pub regex: Option<LevelMappingRegex>,
343}
344
345#[derive(Clone, Serialize, Deserialize, Debug)]
346pub struct LevelMappingRegex {
347 pub emergency: Option<String>,
348 pub alert: Option<String>,
349 pub critical: Option<String>,
350 pub error: Option<String>,
351 pub warning: Option<String>,
352 pub notice: Option<String>,
353 pub info: Option<String>,
354 pub debug: Option<String>,
355}
356
357#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
358pub struct PersistStorageConfig {
359 #[serde(rename = "min_headroom_kib", with = "kib_to_usize")]
360 pub min_headroom: usize,
361 #[serde(rename = "max_usage_kib", with = "kib_to_usize")]
362 pub max_usage: usize,
363 pub min_inodes: usize,
364 pub reboots: bool,
365 pub coredumps: bool,
366 pub metrics: bool,
367 pub logs: bool,
368}
369
370impl PersistStorageConfig {
371 pub fn max_total_size(&self) -> DiskSize {
372 DiskSize::new_capacity(self.max_usage as u64)
373 }
374
375 pub fn min_headroom(&self) -> DiskSize {
376 DiskSize {
377 bytes: self.min_headroom as u64,
378 inodes: self.min_inodes as u64,
379 }
380 }
381}
382
383use flate2::Compression;
384use serde_json::Value;
385use std::fs;
386use std::path::Path;
387
388use crate::config::utils::{
389 filter_path_is_valid, software_type_is_valid, software_version_is_valid,
390};
391
392pub struct JsonConfigs {
393 pub base: Value,
395 pub runtime: Value,
397}
398
399impl MemfaultdConfig {
400 pub fn load(config_path: &Path) -> eyre::Result<MemfaultdConfig> {
401 let JsonConfigs {
402 base: mut config_json,
403 runtime,
404 } = Self::parse_configs(config_path)?;
405 Self::merge_into(&mut config_json, runtime);
406
407 let config: MemfaultdConfig = serde_json::from_value(config_json)?;
409
410 let mut validation_errors = vec![];
411 if let Some(software_version) = &config.software_version {
412 if let Err(e) = software_version_is_valid(software_version) {
413 validation_errors.push(format!(" Invalid value for \"software_version\": {}", e));
414 }
415 }
416 if let Some(software_type) = &config.software_type {
417 if let Err(e) = software_type_is_valid(software_type) {
418 validation_errors.push(format!(" Invalid value for \"software_type\": {}", e));
419 }
420 }
421 if let Some(filters) = &config.coredump.filters {
422 filters.iter().for_each(|filter| {
423 if let TraceFilter::ExecutablePath { path } = filter {
424 if let Err(e) = filter_path_is_valid(path) {
425 warn!("Invalid coredump filter path: {}", e);
426 }
427 }
428 });
429 }
430
431 match validation_errors.is_empty() {
432 true => Ok(config),
433 false => Err(eyre::eyre!("\n{}", validation_errors.join("\n"))),
434 }
435 }
436
437 pub fn parse_configs(config_path: &Path) -> eyre::Result<JsonConfigs> {
439 let mut base: Value = Self::parse(include_str!("../../builtin.conf"))
441 .wrap_err("Error parsing built-in configuration file")?;
442
443 let user_config = Self::parse(std::fs::read_to_string(config_path)?.as_str())
445 .wrap_err(eyre!("Error reading {}", config_path.display()))?;
446
447 Self::merge_into(&mut base, user_config);
449
450 let runtime_config_path = Self::runtime_config_path_from_json(&base)?;
452 let runtime = if runtime_config_path.exists() {
453 Self::parse(fs::read_to_string(&runtime_config_path)?.as_str()).wrap_err(eyre!(
454 "Error reading runtime configuration {}",
455 runtime_config_path.display()
456 ))?
457 } else {
458 Value::Object(serde_json::Map::new())
459 };
460
461 Ok(JsonConfigs { base, runtime })
462 }
463
464 pub fn set_and_write_bool_to_runtime_config(&self, key: &str, value: bool) -> eyre::Result<()> {
466 let config_string = match fs::read_to_string(self.runtime_config_path()) {
467 Ok(config_string) => config_string,
468 Err(e) => {
469 if e.kind() == std::io::ErrorKind::NotFound {
470 "{}".to_string()
471 } else {
472 return Err(eyre::eyre!("Failed to read runtime config: {}", e));
473 }
474 }
475 };
476
477 let mut config_val = Self::parse(&config_string)?;
478 config_val[key] = Value::Bool(value);
479
480 self.write_value_to_runtime_config(config_val)
481 }
482
483 fn write_value_to_runtime_config(&self, value: Value) -> eyre::Result<()> {
487 let runtime_config_path = self.runtime_config_path();
488 fs::write(runtime_config_path, value.to_string())?;
489
490 Ok(())
491 }
492
493 pub fn runtime_config_path(&self) -> PathBuf {
494 PathBuf::from(self.persist_dir.clone()).join("runtime.conf")
495 }
496
497 fn parse(config_string: &str) -> eyre::Result<Value> {
499 let json_text = string::remove_comments(config_string);
500 let json: Value = serde_json::from_str(json_text.as_str())?;
501 if !json.is_object() {
502 return Err(eyre::eyre!("Configuration should be a JSON object."));
503 }
504 Ok(json)
505 }
506
507 fn merge_into(dest: &mut Value, src: Value) {
509 assert!(dest.is_object());
510 if let Value::Object(src_map) = src {
511 for (key, value) in src_map {
512 if let Some(obj) = dest.get_mut(&key) {
513 if obj.is_object() {
514 MemfaultdConfig::merge_into(obj, value);
515 continue;
516 }
517 }
518 dest[&key] = value;
519 }
520 } else if let Value::Null = src {
521 *dest = Value::Null
522 }
523 }
524
525 pub fn generate_tmp_filename(&self, filename: &str) -> PathBuf {
526 let tmp_dir = self.tmp_dir.as_ref().unwrap_or(&self.persist_dir);
528 PathBuf::from(tmp_dir.clone()).join(filename)
529 }
530
531 pub fn generate_persist_filename(&self, filename: &str) -> PathBuf {
532 PathBuf::from(self.persist_dir.clone()).join(filename)
533 }
534
535 fn runtime_config_path_from_json(config: &Value) -> eyre::Result<PathBuf> {
537 let mut persist_dir = PathBuf::from(
538 config["persist_dir"]
539 .as_str()
540 .ok_or(eyre::eyre!("Config['persist_dir'] must be a string."))?,
541 );
542 persist_dir.push("runtime.conf");
543 Ok(persist_dir)
544 }
545}
546
547#[cfg(test)]
548impl MemfaultdConfig {
549 pub fn test_fixture() -> Self {
550 use std::fs::write;
551 use tempfile::tempdir;
552
553 let tmp = tempdir().unwrap();
554 let config_path = tmp.path().join("memfaultd.conf");
555 write(&config_path, "{}").unwrap();
556 MemfaultdConfig::load(&config_path).unwrap()
557 }
558}
559
560#[cfg(test)]
561mod test {
562 use insta::{assert_json_snapshot, with_settings};
563 use rstest::rstest;
564
565 use super::*;
566
567 use crate::test_utils::set_snapshot_suffix;
568
569 #[test]
570 fn test_merge() {
571 let mut c =
572 serde_json::from_str(r#"{ "node": { "value": true, "valueB": false } }"#).unwrap();
573 let j = serde_json::from_str(r#"{ "node2": "xxx" }"#).unwrap();
574
575 MemfaultdConfig::merge_into(&mut c, j);
576
577 assert_eq!(
578 serde_json::to_string(&c).unwrap(),
579 r#"{"node":{"value":true,"valueB":false},"node2":"xxx"}"#
580 );
581 }
582
583 #[test]
584 fn test_merge_overwrite() {
585 let mut c =
586 serde_json::from_str(r#"{ "node": { "value": true, "valueB": false } }"#).unwrap();
587 let j = serde_json::from_str(r#"{ "node": { "value": false }}"#).unwrap();
588
589 MemfaultdConfig::merge_into(&mut c, j);
590
591 assert_eq!(
592 serde_json::to_string(&c).unwrap(),
593 r#"{"node":{"value":false,"valueB":false}}"#
594 );
595 }
596
597 #[test]
598 fn test_merge_overwrite_nested() {
599 let mut c = serde_json::from_str(
600 r#"{ "node": { "value": true, "valueB": false, "valueC": { "a": 1, "b": 2 } } }"#,
601 )
602 .unwrap();
603 let j = serde_json::from_str(r#"{ "node": { "valueC": { "b": 42 } }}"#).unwrap();
604
605 MemfaultdConfig::merge_into(&mut c, j);
606
607 assert_eq!(
608 serde_json::to_string(&c).unwrap(),
609 r#"{"node":{"value":true,"valueB":false,"valueC":{"a":1,"b":42}}}"#
610 );
611 }
612
613 #[test]
614 fn test_merge_overwrite_with_null() {
615 let mut c = serde_json::from_str(
616 r#"{ "node": { "value": true, "valueB": false, "valueC": { "a": 1, "b": 2 } } }"#,
617 )
618 .unwrap();
619 let j = serde_json::from_str(r#"{ "node": { "valueC": null }}"#).unwrap();
620
621 MemfaultdConfig::merge_into(&mut c, j);
622
623 assert_eq!(
624 serde_json::to_string(&c).unwrap(),
625 r#"{"node":{"value":true,"valueB":false,"valueC":null}}"#
626 );
627 }
628
629 #[rstest]
630 #[case("empty_object")]
631 #[case("with_partial_logs")]
632 #[case("without_coredump_compression")]
633 #[case("with_coredump_capture_strategy_threads")]
634 #[case("with_log_to_metrics_rules")]
635 #[case("with_connectivity_monitor")]
636 #[case("with_sessions")]
637 #[case("metrics_config")]
638 #[case("log_filters")]
639 #[case("syslog")]
640 #[case("with_linux_custom_trace_gzip")]
641 #[case("with_linux_custom_trace_zlib")]
642 #[case("with_linux_custom_trace_none")]
643 #[case("with_persist_storage_config")]
644 #[case("with_battery_monitor")]
645 #[case("with_min_max_metrics")]
646 fn can_parse_test_files(#[case] name: &str) {
647 let input_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
648 .join("src/config/test-config")
649 .join(name)
650 .with_extension("json");
651 let content = MemfaultdConfig::load(&input_path).unwrap();
653 with_settings!({sort_maps => true}, {
656 assert_json_snapshot!(name, content);
657 });
658 }
659
660 #[rstest]
661 #[case("with_invalid_path")]
662 #[case("with_invalid_swt_swv")]
663 #[case("with_sessions_invalid_metric_name")]
664 #[case("with_sessions_invalid_session_name")]
665 fn will_reject_bad_config(#[case] name: &str) {
666 let input_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
667 .join("src/config/test-config")
668 .join(name)
669 .with_extension("json");
670 let result = MemfaultdConfig::load(&input_path);
671 assert!(result.is_err());
672 }
673
674 #[rstest]
675 #[case("no_file", None)]
676 #[case("empty_object", Some("{}"))]
677 #[case("other_key", Some(r#"{"key2":false}"#))]
678 fn test_set_and_write_bool_to_runtime_config(
679 #[case] test_name: &str,
680 #[case] config_string: Option<&str>,
681 ) {
682 let mut config = MemfaultdConfig::test_fixture();
683 let temp_data_dir = tempfile::tempdir().unwrap();
684 config.persist_dir = AbsolutePath::try_from(temp_data_dir.path().to_path_buf()).unwrap();
685
686 if let Some(config_string) = config_string {
687 std::fs::write(config.runtime_config_path(), config_string).unwrap();
688 }
689
690 config
691 .set_and_write_bool_to_runtime_config("key", true)
692 .unwrap();
693
694 let disk_config_string = std::fs::read_to_string(config.runtime_config_path()).unwrap();
695
696 set_snapshot_suffix!("{}", test_name);
697 insta::assert_json_snapshot!(disk_config_string);
698 }
699}