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