add health_metric_config implementation
Add the health_metric_config interface and implementation for
phosphor-health-monitor. This interface will be used in the
re-write of phosphor-health-monitor.
This change is in relation to following design and D-Bus interface
update-
https://gerrit.openbmc.org/c/openbmc/docs/+/64917
https://gerrit.openbmc.org/c/openbmc/phosphor-dbus-interfaces/+/64914
gtest added for UT.
Change-Id: Ic40faafbb57597023cc70036428d46ee69a895a2
Signed-off-by: Jagpal Singh Gill <paligill@gmail.com>
diff --git a/health_metric_config.cpp b/health_metric_config.cpp
new file mode 100644
index 0000000..8320f25
--- /dev/null
+++ b/health_metric_config.cpp
@@ -0,0 +1,248 @@
+#include "config.h"
+
+#include "health_metric_config.hpp"
+
+#include <nlohmann/json.hpp>
+#include <phosphor-logging/lg2.hpp>
+
+#include <fstream>
+#include <unordered_map>
+#include <utility>
+
+PHOSPHOR_LOG2_USING;
+
+namespace phosphor::health::metric::config
+{
+
+using json = nlohmann::json;
+
+// Default health metric config
+extern json defaultHealthMetricConfig;
+
+// Valid thresholds from config
+static const auto validThresholdTypes =
+ std::unordered_map<std::string, ThresholdIntf::Type>{
+ {"Critical", ThresholdIntf::Type::Critical},
+ {"Warning", ThresholdIntf::Type::Warning}};
+
+// Valid metrics from config
+static const auto validTypes =
+ std::unordered_map<std::string, Type>{{"CPU", Type::cpu},
+ {"Memory", Type::memory},
+ {"Storage", Type::storage},
+ {"Inode", Type::inode}};
+
+// Valid submetrics from config
+static const auto validSubTypes = std::unordered_map<std::string, SubType>{
+ {"CPU", SubType::cpuTotal},
+ {"CPU_User", SubType::cpuUser},
+ {"CPU_Kernel", SubType::cpuKernel},
+ {"Memory", SubType::memoryTotal},
+ {"Memory_Free", SubType::memoryFree},
+ {"Memory_Available", SubType::memoryAvailable},
+ {"Memory_Shared", SubType::memoryShared},
+ {"Memory_Buffered_And_Cached", SubType::memoryBufferedAndCached},
+ {"Storage_RW", SubType::storageReadWrite}};
+
+/** Deserialize a Threshold from JSON. */
+void from_json(const json& j, Threshold& self)
+{
+ self.value = j.value("Value", 100.0);
+ self.log = j.value("Log", false);
+ self.target = j.value("Target", Threshold::defaults::target);
+}
+
+/** Deserialize a HealthMetric from JSON. */
+void from_json(const json& j, HealthMetric& self)
+{
+ self.collectionFreq = std::chrono::seconds(j.value(
+ "Frequency",
+ std::chrono::seconds(HealthMetric::defaults::frequency).count()));
+
+ self.windowSize = j.value("Window_size",
+ HealthMetric::defaults::windowSize);
+ // Path is only valid for storage
+ self.path = j.value("Path", "");
+
+ auto thresholds = j.find("Threshold");
+ if (thresholds == j.end())
+ {
+ return;
+ }
+
+ for (auto& [key, value] : thresholds->items())
+ {
+ if (!validThresholdTypes.contains(key))
+ {
+ warning("Invalid ThresholdType: {TYPE}", "TYPE", key);
+ continue;
+ }
+
+ auto config = value.template get<Threshold>();
+
+ // ThresholdIntf::Bound::Upper is the only use case for
+ // ThresholdIntf::Bound
+ self.thresholds.emplace(std::make_tuple(validThresholdTypes.at(key),
+ ThresholdIntf::Bound::Upper),
+ config);
+ }
+}
+
+json parseConfigFile(std::string configFile)
+{
+ std::ifstream jsonFile(configFile);
+ if (!jsonFile.is_open())
+ {
+ info("config JSON file not found: {PATH}", "PATH", configFile);
+ return {};
+ }
+
+ try
+ {
+ return json::parse(jsonFile, nullptr, true);
+ }
+ catch (const json::parse_error& e)
+ {
+ error("Failed to parse JSON config file {PATH}: {ERROR}", "PATH",
+ configFile, "ERROR", e);
+ }
+
+ return {};
+}
+
+void printConfig(HealthMetric::map_t& configs)
+{
+ for (auto& [type, configList] : configs)
+ {
+ for (auto& config : configList)
+ {
+ debug(
+ "MTYPE={MTYPE}, MNAME={MNAME} MSTYPE={MSTYPE} PATH={PATH}, FREQ={FREQ}, WSIZE={WSIZE}",
+ "MTYPE", std::to_underlying(type), "MNAME", config.name,
+ "MSTYPE", std::to_underlying(config.subType), "PATH",
+ config.path, "FREQ", config.collectionFreq.count(), "WSIZE",
+ config.windowSize);
+
+ for (auto& [key, threshold] : config.thresholds)
+ {
+ debug(
+ "THRESHOLD TYPE={TYPE} THRESHOLD BOUND={BOUND} VALUE={VALUE} LOG={LOG} TARGET={TARGET}",
+ "TYPE", std::to_underlying(get<ThresholdIntf::Type>(key)),
+ "BOUND", std::to_underlying(get<ThresholdIntf::Bound>(key)),
+ "VALUE", threshold.value, "LOG", threshold.log, "TARGET",
+ threshold.target);
+ }
+ }
+ }
+}
+
+auto getHealthMetricConfigs() -> HealthMetric::map_t
+{
+ json mergedConfig(defaultHealthMetricConfig);
+
+ if (auto platformConfig = parseConfigFile(HEALTH_CONFIG_FILE);
+ !platformConfig.empty())
+ {
+ mergedConfig.merge_patch(platformConfig);
+ }
+
+ HealthMetric::map_t configs = {};
+ for (auto& [name, metric] : mergedConfig.items())
+ {
+ static constexpr auto nameDelimiter = "_";
+ std::string typeStr = name.substr(0, name.find_first_of(nameDelimiter));
+
+ auto type = validTypes.find(typeStr);
+ if (type == validTypes.end())
+ {
+ warning("Invalid metric type: {TYPE}", "TYPE", typeStr);
+ continue;
+ }
+
+ auto config = metric.template get<HealthMetric>();
+
+ auto subType = validSubTypes.find(name);
+ config.subType = (subType != validSubTypes.end() ? subType->second
+ : SubType::NA);
+
+ configs[type->second].emplace_back(std::move(config));
+ }
+ printConfig(configs);
+ return configs;
+}
+
+json defaultHealthMetricConfig = R"({
+ "CPU": {
+ "Frequency": 1,
+ "Window_size": 120,
+ "Threshold": {
+ "Critical": {
+ "Value": 90.0,
+ "Log": true,
+ "Target": ""
+ },
+ "Warning": {
+ "Value": 80.0,
+ "Log": false,
+ "Target": ""
+ }
+ }
+ },
+ "CPU_User": {
+ "Frequency": 1,
+ "Window_size": 120,
+ "Threshold": {
+ "Critical": {
+ "Value": 90.0,
+ "Log": true,
+ "Target": ""
+ },
+ "Warning": {
+ "Value": 80.0,
+ "Log": false,
+ "Target": ""
+ }
+ }
+ },
+ "CPU_Kernel": {
+ "Frequency": 1,
+ "Window_size": 120,
+ "Threshold": {
+ "Critical": {
+ "Value": 90.0,
+ "Log": true,
+ "Target": ""
+ },
+ "Warning": {
+ "Value": 80.0,
+ "Log": false,
+ "Target": ""
+ }
+ }
+ },
+ "Memory_Available": {
+ "Frequency": 1,
+ "Window_size": 120,
+ "Threshold": {
+ "Critical": {
+ "Value": 85.0,
+ "Log": true,
+ "Target": ""
+ }
+ }
+ },
+ "Storage_RW": {
+ "Path": "/run/initramfs/rw",
+ "Frequency": 1,
+ "Window_size": 120,
+ "Threshold": {
+ "Critical": {
+ "Value": 85.0,
+ "Log": true,
+ "Target": ""
+ }
+ }
+ }
+})"_json;
+
+} // namespace phosphor::health::metric::config