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
diff --git a/health_metric_config.hpp b/health_metric_config.hpp
new file mode 100644
index 0000000..2b0a578
--- /dev/null
+++ b/health_metric_config.hpp
@@ -0,0 +1,94 @@
+#pragma once
+
+#include <xyz/openbmc_project/Common/Threshold/server.hpp>
+
+#include <chrono>
+#include <limits>
+#include <map>
+#include <string>
+#include <vector>
+
+namespace phosphor::health::metric
+{
+
+using ThresholdIntf =
+    sdbusplus::server::xyz::openbmc_project::common::Threshold;
+
+enum class Type
+{
+    cpu,
+    memory,
+    storage,
+    inode,
+    unknown
+};
+
+enum class SubType
+{
+    // CPU subtypes
+    cpuKernel,
+    cpuTotal,
+    cpuUser,
+    // Memory subtypes
+    memoryAvailable,
+    memoryBufferedAndCached,
+    memoryFree,
+    memoryShared,
+    memoryTotal,
+    // Storage subtypes
+    storageReadWrite,
+    NA
+};
+
+namespace config
+{
+
+using namespace std::literals::chrono_literals;
+
+struct Threshold
+{
+    double value = defaults::value;
+    bool log = false;
+    std::string target = defaults::target;
+
+    using map_t =
+        std::map<std::tuple<ThresholdIntf::Type, ThresholdIntf::Bound>,
+                 Threshold>;
+
+    struct defaults
+    {
+        static constexpr auto value = std::numeric_limits<double>::quiet_NaN();
+        static constexpr auto target = "";
+    };
+};
+
+struct HealthMetric
+{
+    /** @brief The name of the metric. */
+    std::string name = "unnamed";
+    /** @brief The metric subtype. */
+    SubType subType = SubType::NA;
+    /** @brief The collection frequency for the metric. */
+    std::chrono::seconds collectionFreq = defaults::frequency;
+    /** @brief The window size for the metric. */
+    size_t windowSize = defaults::windowSize;
+    /** @brief The threshold configs for the metric. */
+    Threshold::map_t thresholds{};
+    /** @brief The path for filesystem metric */
+    std::string path = defaults::path;
+
+    using map_t = std::map<Type, std::vector<HealthMetric>>;
+
+    struct defaults
+    {
+        static constexpr auto frequency = 1s;
+        static constexpr auto windowSize = 1;
+        static constexpr auto path = "";
+    };
+};
+
+/** @brief Get the health metric configs. */
+auto getHealthMetricConfigs() -> HealthMetric::map_t;
+
+} // namespace config
+} // namespace phosphor::health::metric
diff --git a/meson.build b/meson.build
index 5bbdfc2..27e2bff 100644
--- a/meson.build
+++ b/meson.build
@@ -8,17 +8,27 @@
     meson_version: '>=1.1.1',
 )
 
+phosphor_logging_dep = dependency('phosphor-logging')
+phosphor_dbus_interfaces_dep = dependency('phosphor-dbus-interfaces')
+sdbusplus_dep = dependency('sdbusplus')
+sdeventplus_dep = dependency('sdeventplus')
+nlohmann_json_dep = dependency('nlohmann_json', include_type: 'system')
+base_deps = [
+    phosphor_logging_dep,
+    phosphor_dbus_interfaces_dep,
+    sdbusplus_dep,
+    sdeventplus_dep,
+    nlohmann_json_dep
+]
+
 executable(
     'health-monitor',
     [
         'healthMonitor.cpp',
+        'health_metric_config.cpp',
     ],
     dependencies: [
-        dependency('phosphor-dbus-interfaces'),
-        dependency('phosphor-logging'),
-        dependency('sdbusplus'),
-        dependency('sdeventplus'),
-        dependency('nlohmann_json', include_type: 'system')
+        base_deps
     ],
     install: true,
     install_dir: get_option('bindir')
@@ -45,3 +55,7 @@
   configuration: conf_data,
   install: true,
   install_dir: systemd.get_variable('systemdsystemunitdir'))
+
+if get_option('tests').allowed()
+  subdir('test')
+endif
diff --git a/meson.options b/meson.options
new file mode 100644
index 0000000..0fc2767
--- /dev/null
+++ b/meson.options
@@ -0,0 +1 @@
+option('tests', type: 'feature', description: 'Build tests')
diff --git a/test/meson.build b/test/meson.build
new file mode 100644
index 0000000..4df0d46
--- /dev/null
+++ b/test/meson.build
@@ -0,0 +1,38 @@
+gtest_dep = dependency('gtest', main: true, disabler: true, required: false)
+gmock_dep = dependency('gmock', disabler: true, required: false)
+if not gtest_dep.found() or not gmock_dep.found()
+    gtest_proj = import('cmake').subproject('googletest', required: false)
+    if gtest_proj.found()
+        gtest_dep = declare_dependency(
+            dependencies: [
+                dependency('threads'),
+                gtest_proj.dependency('gtest'),
+                gtest_proj.dependency('gtest_main'),
+            ]
+        )
+        gmock_dep = gtest_proj.dependency('gmock')
+    else
+        assert(
+            not get_option('tests').enabled(),
+            'Googletest is required if tests are enabled'
+        )
+    endif
+endif
+
+test(
+    'test_health_metric_config',
+    executable(
+        'test_health_metric_config',
+        'test_health_metric_config.cpp',
+        '../health_metric_config.cpp',
+        dependencies: [
+            gtest_dep,
+            gmock_dep,
+            phosphor_logging_dep,
+            phosphor_dbus_interfaces_dep,
+            sdbusplus_dep,
+            nlohmann_json_dep
+        ],
+        include_directories: '../',
+    )
+)
diff --git a/test/test_health_metric_config.cpp b/test/test_health_metric_config.cpp
new file mode 100644
index 0000000..471bf2b
--- /dev/null
+++ b/test/test_health_metric_config.cpp
@@ -0,0 +1,73 @@
+#include "health_metric_config.hpp"
+
+#include <sdbusplus/test/sdbus_mock.hpp>
+
+#include <iostream>
+#include <set>
+#include <utility>
+
+#include <gtest/gtest.h>
+
+using namespace phosphor::health;
+using namespace phosphor::health::metric::config;
+
+constexpr auto minConfigSize = 1;
+
+TEST(HealthMonitorConfigTest, TestConfigSize)
+{
+    auto healthMetricConfigs = getHealthMetricConfigs();
+    EXPECT_GE(healthMetricConfigs.size(), minConfigSize);
+}
+
+bool isValidSubType(metric::Type type, metric::SubType subType)
+{
+    std::cout << "Metric Type: " << std::to_underlying(type)
+              << " Metric SubType: " << std::to_underlying(subType)
+              << std::endl;
+
+    using set_t = std::set<metric::SubType>;
+
+    switch (type)
+    {
+        case metric::Type::cpu:
+            return set_t{metric::SubType::cpuTotal, metric::SubType::cpuKernel,
+                         metric::SubType::cpuUser}
+                .contains(subType);
+
+        case metric::Type::memory:
+            return set_t{metric::SubType::memoryAvailable,
+                         metric::SubType::memoryBufferedAndCached,
+                         metric::SubType::memoryFree,
+                         metric::SubType::memoryShared,
+                         metric::SubType::memoryTotal}
+                .contains(subType);
+
+        case metric::Type::storage:
+            return set_t{metric::SubType::storageReadWrite, metric::SubType::NA}
+                .contains(subType);
+
+        case metric::Type::inode:
+            return set_t{metric::SubType::NA}.contains(subType);
+
+        default:
+            return false;
+    }
+}
+
+TEST(HealthMonitorConfigTest, TestConfigValues)
+{
+    auto healthMetricConfigs = getHealthMetricConfigs();
+    for (const auto& [type, configs] : healthMetricConfigs)
+    {
+        EXPECT_NE(type, metric::Type::unknown);
+        EXPECT_GE(configs.size(), minConfigSize);
+        for (const auto& config : configs)
+        {
+            EXPECT_NE(config.name, std::string(""));
+            EXPECT_TRUE(isValidSubType(type, config.subType));
+            EXPECT_GE(config.collectionFreq, HealthMetric::defaults::frequency);
+            EXPECT_GE(config.windowSize, HealthMetric::defaults::windowSize);
+            EXPECT_GE(config.thresholds.size(), minConfigSize);
+        }
+    }
+}