use eyre::eyre;
use itertools::Itertools;
use std::net::SocketAddr;
use std::num::NonZeroU32;
use std::time::Duration;
use std::{
path::{Path, PathBuf},
sync::{Arc, RwLock},
};
use crate::{
mar::{MarConfig, MarStagingCleanType},
metrics::system_metrics::SystemMetricConfig,
};
use crate::{
network::{NetworkClient, NetworkConfig},
util::{DiskBacked, UnwrapOrDie, UpdateStatus},
};
use crate::util::disk_size::DiskSize;
#[cfg(test)]
pub use self::config_file::ConnectionCheckProtocol;
#[cfg(target_os = "linux")]
pub use self::config_file::{CoredumpCaptureStrategy, CoredumpCompression};
use self::device_info::DeviceInfoValue;
#[cfg(test)]
pub use self::device_info::MockDeviceInfoDefaults;
pub use self::{
config_file::{
ConnectivityMonitorConfig, ConnectivityMonitorTarget, JsonConfigs, LogFilterConfig,
LogFilterRule, LogRuleAction, LogSource, LogToMetricRule, MemfaultdConfig, SessionConfig,
StorageConfig, SyslogConfig,
},
device_config::{DeviceConfig, Resolution, Sampling},
device_info::{DeviceInfo, DeviceInfoDefaultsImpl, DeviceInfoWarning},
};
use crate::mar::{MarEntryBuilder, Metadata};
use eyre::{Context, Result};
mod config_file;
pub use config_file::{
LevelMappingConfig, LevelMappingRegex, LinuxCustomTraceConfig, LinuxCustomTraceLogCompression,
PersistStorageConfig, TraceFilter,
};
mod device_config;
pub use device_config::{DeviceConfigLogging, DeviceConfigLoggingFilters};
mod device_info;
mod messages;
mod utils;
pub use messages::DeviceConfigUpdateMessage;
const FALLBACK_SOFTWARE_VERSION: &str = "0.0.0-memfault-unknown";
const FALLBACK_SOFTWARE_TYPE: &str = "memfault-unknown";
pub struct Config {
pub device_info: DeviceInfo,
pub config_file: MemfaultdConfig,
pub config_file_path: PathBuf,
pub cached_device_config: Arc<RwLock<DiskBacked<DeviceConfig>>>,
}
const LOGS_SUBDIRECTORY: &str = "logs";
pub const MAR_STAGING_SUBDIRECTORY: &str = "mar";
const DEVICE_CONFIG_FILE: &str = "device_config.json";
const COREDUMP_RATE_LIMITER_FILENAME: &str = "coredump_rate_limit";
const TRACE_RATE_LIMITER_FILENAME: &str = "trace_rate_limit";
impl Config {
pub const DEFAULT_CONFIG_PATH: &'static str = "/etc/memfaultd.conf";
pub fn read_from_system(
user_config: Option<&Path>,
warnings_handle_fn: impl Fn(&DeviceInfoWarning),
) -> Result<Self> {
let config_file = user_config.unwrap_or_else(|| Path::new(Self::DEFAULT_CONFIG_PATH));
let config = MemfaultdConfig::load(config_file).wrap_err(eyre!(
"Unable to read config file {}",
&config_file.display()
))?;
let (device_info, warnings) =
DeviceInfo::load().wrap_err(eyre!("Unable to load device info"))?;
warnings.iter().for_each(warnings_handle_fn);
let device_config = DiskBacked::from_path(&Self::device_config_path_from_config(&config));
Ok(Self {
device_info,
config_file: config,
config_file_path: config_file.to_owned(),
cached_device_config: Arc::new(RwLock::new(device_config)),
})
}
pub fn refresh_device_config(&self, client: &impl NetworkClient) -> Result<UpdateStatus> {
let response = client.fetch_device_config()?;
let confirm_version = match response.data.completed {
Some(v) if v == response.data.revision => None,
_ => Some(response.data.revision),
};
let new_config: DeviceConfig = response.into();
let update_status = self
.cached_device_config
.write()
.unwrap_or_die()
.set(new_config)?;
if let Some(revision) = confirm_version {
let mar_staging = self.mar_tmp_staging_path();
let mar_config = MarConfig::from(self);
MarEntryBuilder::new(&mar_staging)?
.set_metadata(Metadata::new_device_config(revision))
.save(&NetworkConfig::from(self), &mar_config)?;
}
Ok(update_status)
}
pub fn tmp_dir(&self) -> PathBuf {
match self.config_file.tmp_dir {
Some(ref tmp_dir) => tmp_dir.clone(),
None => self.config_file.persist_dir.clone(),
}
.into()
}
pub fn persist_dir(&self) -> PathBuf {
self.config_file.persist_dir.clone().into()
}
pub fn tmp_dir_max_size(&self) -> DiskSize {
DiskSize::new_capacity(self.config_file.tmp_dir_max_usage as u64)
}
pub fn tmp_dir_min_headroom(&self) -> DiskSize {
DiskSize {
bytes: self.config_file.tmp_dir_min_headroom as u64,
inodes: self.config_file.tmp_dir_min_inodes as u64,
}
}
pub fn coredump_rate_limiter_file_path(&self) -> PathBuf {
self.tmp_dir().join(COREDUMP_RATE_LIMITER_FILENAME)
}
pub fn trace_rate_limiter_file_path(&self) -> PathBuf {
self.tmp_dir().join(TRACE_RATE_LIMITER_FILENAME)
}
pub fn logs_path(&self) -> PathBuf {
self.tmp_dir().join(LOGS_SUBDIRECTORY)
}
pub fn mar_tmp_staging_path(&self) -> PathBuf {
self.tmp_dir().join(MAR_STAGING_SUBDIRECTORY)
}
pub fn mar_logs_clean_type(&self) -> MarStagingCleanType {
self.mar_persist_storage_config()
.and_then(|persist_config| persist_config.logs.then_some(MarStagingCleanType::Persist))
.unwrap_or(MarStagingCleanType::Tmp)
}
pub fn mar_persist_staging_path(&self) -> PathBuf {
self.config_file.persist_dir.join(MAR_STAGING_SUBDIRECTORY)
}
pub fn mar_persist_storage_config(&self) -> Option<&PersistStorageConfig> {
self.config_file.persist_storage.as_ref()
}
fn device_config_path_from_config(config_file: &MemfaultdConfig) -> PathBuf {
config_file.persist_dir.join(DEVICE_CONFIG_FILE)
}
pub fn device_config_path(&self) -> PathBuf {
Self::device_config_path_from_config(&self.config_file)
}
pub fn sampling(&self) -> Sampling {
if self.config_file.enable_dev_mode {
Sampling::development()
} else {
self.device_config().sampling
}
}
fn device_config(&self) -> DeviceConfig {
self.cached_device_config
.read()
.unwrap_or_die()
.get()
.clone()
}
pub fn software_version(&self) -> &str {
match (
&self.device_info.software_version,
&self.config_file.software_version,
) {
(Some(DeviceInfoValue::Configured(sw_version)), _) => sw_version.as_ref(),
(None, Some(sw_version)) => sw_version.as_ref(),
(Some(DeviceInfoValue::Default(_)), Some(sw_version)) => sw_version.as_ref(),
(Some(DeviceInfoValue::Default(sw_version)), None) => sw_version.as_ref(),
(None, None) => FALLBACK_SOFTWARE_VERSION,
}
}
pub fn software_type(&self) -> &str {
match (
&self.device_info.software_type,
&self.config_file.software_type,
) {
(Some(DeviceInfoValue::Configured(software_type)), _) => software_type.as_ref(),
(None, Some(software_type)) => software_type.as_ref(),
(Some(DeviceInfoValue::Default(_)), Some(software_type)) => software_type.as_ref(),
(Some(DeviceInfoValue::Default(software_type)), None) => software_type.as_ref(),
(None, None) => FALLBACK_SOFTWARE_TYPE,
}
}
pub fn mar_entry_max_age(&self) -> Duration {
self.config_file.mar.mar_entry_max_age
}
pub fn mar_entry_max_count(&self) -> usize {
self.config_file.mar.mar_entry_max_count
}
pub fn battery_monitor_periodic_update_enabled(&self) -> bool {
self.config_file.battery_monitor.is_some()
}
pub fn battery_monitor_battery_info_command(&self) -> Option<&str> {
self.config_file
.battery_monitor
.as_ref()
.and_then(|monitor| monitor.battery_info_command.as_ref())
.map(|command| command.as_str())
}
pub fn battery_monitor_auto_mode(&self) -> bool {
self.config_file
.battery_monitor
.as_ref()
.is_some_and(|monitor| monitor.auto)
}
pub fn battery_monitor_interval(&self) -> Duration {
match self.config_file.battery_monitor.as_ref() {
Some(battery_config) => battery_config.interval_seconds,
None => Duration::from_secs(0),
}
}
pub fn connectivity_monitor_config(&self) -> Option<&ConnectivityMonitorConfig> {
self.config_file.connectivity_monitor.as_ref()
}
pub fn session_configs(&self) -> Option<&Vec<SessionConfig>> {
self.config_file.sessions.as_ref()
}
pub fn statsd_server_enabled(&self) -> bool {
self.config_file.metrics.statsd_server.is_some()
}
pub fn statsd_server_address(&self) -> Result<SocketAddr> {
match &self.config_file.metrics.statsd_server {
Some(statsd_server_config) => Ok(statsd_server_config.bind_address),
None => Err(eyre!("No StatsD server bind_address configured!")),
}
}
pub fn statsd_server_legacy_gauge_aggregation_enabled(&self) -> bool {
match &self.config_file.metrics.statsd_server {
None => false,
Some(statsd_server_config) => statsd_server_config
.legacy_gauge_aggregation
.unwrap_or(false),
}
}
pub fn statsd_server_legacy_key_names_enabled(&self) -> bool {
self.config_file
.metrics
.statsd_server
.as_ref()
.and_then(|server| server.legacy_key_names)
.unwrap_or(false)
}
pub fn hrt_enabled(&self) -> bool {
self.config_file.metrics.high_resolution_telemetry.enable
}
pub fn hrt_max_samples_per_min(&self) -> NonZeroU32 {
self.config_file
.metrics
.high_resolution_telemetry
.max_samples_per_minute
}
pub fn builtin_system_metric_collection_enabled(&self) -> bool {
self.config_file.metrics.system_metric_collection.enable
}
pub fn system_metric_config(&self) -> SystemMetricConfig {
self.config_file.metrics.system_metric_collection.clone()
}
pub fn log_extraction_config(&self) -> &LevelMappingConfig {
&self.config_file.logs.level_mapping
}
pub fn log_extra_attributes(&self) -> Vec<String> {
let fluentd_extra_attr = &self.config_file.fluent_bit.extra_fluentd_attributes;
let logs_extra_attr = &self.config_file.logs.extra_attributes;
fluentd_extra_attr
.iter()
.chain(logs_extra_attr.iter())
.dedup()
.cloned()
.collect()
}
pub fn log_max_buffered_lines(&self) -> usize {
let fluent_bit_max = self.config_file.fluent_bit.max_buffered_lines;
let log_max = self.config_file.logs.max_buffered_lines;
fluent_bit_max.unwrap_or(log_max)
}
pub fn linux_custom_trace_config(&self) -> LinuxCustomTraceConfig {
self.config_file.custom_trace.unwrap_or_default()
}
pub fn linux_custom_trace_log_compression(&self) -> LinuxCustomTraceLogCompression {
self.config_file
.custom_trace
.map(|config| config.log_compression)
.unwrap_or(LinuxCustomTraceLogCompression::Gzip)
}
}
#[cfg(test)]
impl Config {
pub fn test_fixture() -> Self {
Config {
device_info: DeviceInfo::test_fixture(),
config_file: MemfaultdConfig::test_fixture(),
config_file_path: PathBuf::from("test_fixture.conf"),
cached_device_config: Arc::new(RwLock::new(DiskBacked::from_path(&PathBuf::from(
"/dev/null",
)))),
}
}
pub fn test_fixture_with_info_overrides(software_version: &str, software_type: &str) -> Self {
Config {
device_info: DeviceInfo::test_fixture_with_overrides(software_version, software_type),
config_file: MemfaultdConfig::test_fixture(),
config_file_path: PathBuf::from("test_fixture.conf"),
cached_device_config: Arc::new(RwLock::new(DiskBacked::from_path(&PathBuf::from(
"/dev/null",
)))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{fs::create_dir_all, path::PathBuf};
use rstest::{fixture, rstest};
use crate::{
config::{device_info::DeviceInfoValue, Config},
mar::MarEntry,
network::{
DeviceConfigResponse, DeviceConfigResponseConfig, DeviceConfigResponseData,
DeviceConfigResponseResolution, MockNetworkClient,
},
util::path::AbsolutePath,
};
#[test]
fn tmp_dir_defaults_to_persist_dir() {
let config = Config::test_fixture();
assert_eq!(config.tmp_dir(), config.config_file.persist_dir);
}
#[test]
fn tmp_folder_set() {
let mut config = Config::test_fixture();
let abs_path = PathBuf::from("/my/abs/path");
config.config_file.tmp_dir = Some(AbsolutePath::try_from(abs_path.clone()).unwrap());
assert_eq!(config.tmp_dir(), abs_path);
}
#[test]
fn test_info_overrides_file() {
let config =
Config::test_fixture_with_info_overrides("1.0.0-overridden", "overridden-type");
assert_eq!(config.software_version(), "1.0.0-overridden");
assert_eq!(config.software_type(), "overridden-type");
}
#[rstest]
fn generate_mar_device_config_confirmation_when_needed(mut fixture: Fixture) {
fixture
.client
.expect_fetch_device_config()
.return_once(|| Ok(DEVICE_CONFIG_SAMPLE));
fixture
.config
.refresh_device_config(&fixture.client)
.unwrap();
assert_eq!(fixture.count_mar_entries(), 1);
}
#[rstest]
fn do_not_generate_mar_device_config_if_not_needed(mut fixture: Fixture) {
let mut device_config = DEVICE_CONFIG_SAMPLE;
device_config.data.completed = Some(device_config.data.revision);
fixture
.client
.expect_fetch_device_config()
.return_once(move || Ok(device_config));
fixture
.config
.refresh_device_config(&fixture.client)
.unwrap();
assert_eq!(fixture.count_mar_entries(), 0);
}
#[rstest]
#[case(Some(DeviceInfoValue::Configured("1.0.0".into())), None, "1.0.0")]
#[case(Some(DeviceInfoValue::Default("1.0.0".into())), None, "1.0.0")]
#[case(Some(DeviceInfoValue::Configured("1.0.0".into())), Some("2.0.0"), "1.0.0")]
#[case(Some(DeviceInfoValue::Default("1.0.0".into())), Some("2.0.0"), "2.0.0")]
#[case(None, Some("2.0.0"), "2.0.0")]
#[case(None, None, FALLBACK_SOFTWARE_VERSION)]
fn software_version_precedence(
#[case] device_info_swv: Option<DeviceInfoValue>,
#[case] config_swv: Option<&str>,
#[case] expected: &str,
) {
let mut config = Config::test_fixture();
config.device_info.software_version = device_info_swv;
config.config_file.software_version = config_swv.map(String::from);
assert_eq!(config.software_version(), expected);
}
#[rstest]
#[case(Some(DeviceInfoValue::Configured("test".into())), None, "test")]
#[case(Some(DeviceInfoValue::Default("test".into())), None, "test")]
#[case(Some(DeviceInfoValue::Configured("test".into())), Some("prod"), "test")]
#[case(Some(DeviceInfoValue::Default("test".into())), Some("prod"), "prod")]
#[case(None, Some("prod"), "prod")]
#[case(None, None, FALLBACK_SOFTWARE_TYPE)]
fn software_type_precedence(
#[case] device_info_swv: Option<DeviceInfoValue>,
#[case] config_swv: Option<&str>,
#[case] expected: &str,
) {
let mut config = Config::test_fixture();
config.device_info.software_type = device_info_swv;
config.config_file.software_type = config_swv.map(String::from);
assert_eq!(config.software_type(), expected);
}
#[rstest]
#[case::both_set(
vec!["fluentd"],
vec!["logs"],
vec!["fluentd", "logs"])]
#[case::fluentd_set(vec!["fluentd"], vec![], vec!["fluentd"])]
#[case::logs_set(vec![], vec!["logs"], vec!["logs"])]
#[case::neither_set(vec![], vec![], vec![])]
#[case::duplicates(
vec!["fluentd", "logs"],
vec!["logs"],
vec!["fluentd", "logs"])]
fn log_attr_merge(
#[case] fluentd_attr: Vec<&str>,
#[case] log_attr: Vec<&str>,
#[case] expected_attr: Vec<&str>,
) {
let mut config = Config::test_fixture();
config.config_file.fluent_bit.extra_fluentd_attributes =
fluentd_attr.into_iter().map(|s| s.to_string()).collect();
config.config_file.logs.extra_attributes =
log_attr.into_iter().map(|s| s.to_string()).collect();
let mut actual_attr = config.log_extra_attributes();
actual_attr.sort();
assert_eq!(actual_attr, expected_attr);
}
#[rstest]
#[case(Some(42), 25, 42)]
#[case(None, 25, 25)]
fn log_max_buffered_precedence(
#[case] fluent_bit_max: Option<usize>,
#[case] log_max: usize,
#[case] expected: usize,
) {
let mut config = Config::test_fixture();
config.config_file.fluent_bit.max_buffered_lines = fluent_bit_max;
config.config_file.logs.max_buffered_lines = log_max;
let actual_max = config.log_max_buffered_lines();
assert_eq!(actual_max, expected);
}
#[rstest]
#[case(Some(true), true)]
#[case(None, false)]
#[case(Some(false), false)]
fn legacy_key_names(#[case] legacy_key_names_enabled: Option<bool>, #[case] expected: bool) {
let mut config = Config::test_fixture();
config
.config_file
.metrics
.statsd_server
.as_mut()
.unwrap()
.legacy_key_names = legacy_key_names_enabled;
assert_eq!(config.statsd_server_legacy_key_names_enabled(), expected);
}
#[rstest]
#[case(
Some(crate::config::LinuxCustomTraceLogCompression::Gzip),
crate::config::LinuxCustomTraceLogCompression::Gzip
)]
#[case(
Some(crate::config::LinuxCustomTraceLogCompression::Zlib),
crate::config::LinuxCustomTraceLogCompression::Zlib
)]
#[case(
Some(crate::config::LinuxCustomTraceLogCompression::None),
crate::config::LinuxCustomTraceLogCompression::None
)]
#[case(None, crate::config::LinuxCustomTraceLogCompression::Gzip)] fn linux_custom_trace_log_compression(
#[case] compression_type: Option<LinuxCustomTraceLogCompression>,
#[case] expected: crate::config::LinuxCustomTraceLogCompression,
) {
let mut config = Config::test_fixture();
config.config_file.custom_trace =
compression_type.map(|log_compression| crate::config::LinuxCustomTraceConfig {
log_compression,
rate_limit_count: 5,
rate_limit_duration: Duration::from_secs(3600),
});
assert_eq!(config.linux_custom_trace_log_compression(), expected);
}
#[test]
fn linux_custom_trace_config_is_optional() {
let mut config = Config::test_fixture();
config.config_file.custom_trace = None;
assert_eq!(
config.linux_custom_trace_log_compression(),
LinuxCustomTraceLogCompression::Gzip
);
}
#[test]
fn linux_custom_trace_config_with_gzip_compression() {
let mut config = Config::test_fixture();
config.config_file.custom_trace = Some(LinuxCustomTraceConfig {
log_compression: LinuxCustomTraceLogCompression::Gzip,
rate_limit_count: 5,
rate_limit_duration: Duration::from_secs(3600),
});
assert_eq!(
config.linux_custom_trace_log_compression(),
LinuxCustomTraceLogCompression::Gzip
);
}
#[test]
fn linux_custom_trace_config_with_zlib_compression() {
let mut config = Config::test_fixture();
config.config_file.custom_trace = Some(LinuxCustomTraceConfig {
log_compression: LinuxCustomTraceLogCompression::Zlib,
rate_limit_count: 5,
rate_limit_duration: Duration::from_secs(3600),
});
assert_eq!(
config.linux_custom_trace_log_compression(),
LinuxCustomTraceLogCompression::Zlib
);
}
#[test]
fn linux_custom_trace_config_with_no_compression() {
let mut config = Config::test_fixture();
config.config_file.custom_trace = Some(LinuxCustomTraceConfig {
log_compression: LinuxCustomTraceLogCompression::None,
rate_limit_count: 5,
rate_limit_duration: Duration::from_secs(3600),
});
assert_eq!(
config.linux_custom_trace_log_compression(),
LinuxCustomTraceLogCompression::None
);
}
#[rstest]
#[case(None, LinuxCustomTraceLogCompression::Gzip)]
#[case(
Some(LinuxCustomTraceLogCompression::Gzip),
LinuxCustomTraceLogCompression::Gzip
)]
#[case(
Some(LinuxCustomTraceLogCompression::Zlib),
LinuxCustomTraceLogCompression::Zlib
)]
#[case(
Some(LinuxCustomTraceLogCompression::None),
LinuxCustomTraceLogCompression::None
)]
fn linux_custom_trace_config_defaults_and_overrides(
#[case] compression_setting: Option<LinuxCustomTraceLogCompression>,
#[case] expected: LinuxCustomTraceLogCompression,
) {
let mut config = Config::test_fixture();
config.config_file.custom_trace =
compression_setting.map(|log_compression| LinuxCustomTraceConfig {
log_compression,
rate_limit_count: 5,
rate_limit_duration: Duration::from_secs(3600),
});
assert_eq!(config.linux_custom_trace_log_compression(), expected);
}
#[test]
fn linux_custom_trace_config_serialization_roundtrip() {
use serde_json;
let original_config = LinuxCustomTraceConfig {
log_compression: LinuxCustomTraceLogCompression::Gzip,
rate_limit_count: 5,
rate_limit_duration: Duration::from_secs(3600),
};
let json = serde_json::to_string(&original_config).unwrap();
let deserialized_config: LinuxCustomTraceConfig = serde_json::from_str(&json).unwrap();
assert_eq!(
original_config.log_compression,
deserialized_config.log_compression
);
let compression_types = vec![
LinuxCustomTraceLogCompression::Gzip,
LinuxCustomTraceLogCompression::Zlib,
LinuxCustomTraceLogCompression::None,
];
for compression_type in compression_types {
let config = LinuxCustomTraceConfig {
log_compression: compression_type,
rate_limit_count: 5,
rate_limit_duration: Duration::from_secs(3600),
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: LinuxCustomTraceConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config, deserialized);
}
}
struct Fixture {
config: Config,
_tmp_dir: tempfile::TempDir,
client: MockNetworkClient,
}
#[fixture]
fn fixture() -> Fixture {
Fixture::new()
}
impl Fixture {
fn new() -> Self {
let tmp_dir = tempfile::tempdir().unwrap();
let mut config = Config::test_fixture();
config.config_file.persist_dir = tmp_dir.path().to_path_buf().try_into().unwrap();
create_dir_all(config.mar_tmp_staging_path()).unwrap();
Self {
config,
_tmp_dir: tmp_dir,
client: MockNetworkClient::new(),
}
}
fn count_mar_entries(self) -> usize {
MarEntry::iterate_from_container(&self.config.mar_tmp_staging_path())
.unwrap()
.count()
}
}
#[rstest]
#[case(Some(true))]
#[case(Some(false))]
#[case(None)]
fn mar_logs_clean_type(#[case] is_persisted: Option<bool>) {
let mut config = Config::test_fixture();
let persist_storage = is_persisted.map(|logs| PersistStorageConfig {
min_headroom: 1,
max_usage: 2,
min_inodes: 3,
reboots: false,
coredumps: false,
metrics: false,
logs,
});
config.config_file.persist_storage = persist_storage;
let clean_type = is_persisted
.and_then(|coredumps| coredumps.then_some(MarStagingCleanType::Persist))
.unwrap_or(MarStagingCleanType::Tmp);
assert_eq!(config.mar_logs_clean_type(), clean_type);
}
const DEVICE_CONFIG_SAMPLE: DeviceConfigResponse = DeviceConfigResponse {
data: DeviceConfigResponseData {
completed: None,
revision: 42,
config: DeviceConfigResponseConfig {
memfault: crate::network::DeviceConfigResponseMemfault {
sampling: crate::network::DeviceConfigResponseSampling {
debugging_resolution: DeviceConfigResponseResolution::High,
logging_resolution: DeviceConfigResponseResolution::High,
monitoring_resolution: DeviceConfigResponseResolution::High,
},
data_upload_start_date: None,
memfaultd: None,
},
},
},
};
}