add health_metric_collection implementation

Add the interface and implementation for health_metric_collection which
encapsulates various health_metrics.
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

Add test_health_metric_collection gtest for unit testing.

Change-Id: Ia0b9fbc6bec4850735c7eb74dcd5c40fc47c568c
Signed-off-by: Jagpal Singh Gill <paligill@gmail.com>
diff --git a/health_metric_collection.cpp b/health_metric_collection.cpp
new file mode 100644
index 0000000..1fc0757
--- /dev/null
+++ b/health_metric_collection.cpp
@@ -0,0 +1,248 @@
+#include "health_metric_collection.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+
+#include <fstream>
+#include <numeric>
+#include <unordered_map>
+
+extern "C"
+{
+#include <sys/statvfs.h>
+}
+
+PHOSPHOR_LOG2_USING;
+
+namespace phosphor::health::metric::collection
+{
+
+auto HealthMetricCollection::readCPU() -> bool
+{
+    enum CPUStatsIndex
+    {
+        userIndex = 0,
+        niceIndex,
+        systemIndex,
+        idleIndex,
+        iowaitIndex,
+        irqIndex,
+        softirqIndex,
+        stealIndex,
+        guestUserIndex,
+        guestNiceIndex,
+        maxIndex
+    };
+    constexpr auto procStat = "/proc/stat";
+    std::ifstream fileStat(procStat);
+    if (!fileStat.is_open())
+    {
+        error("Unable to open {PATH} for reading CPU stats", "PATH", procStat);
+        return false;
+    }
+
+    std::string firstLine, labelName;
+    std::size_t timeData[CPUStatsIndex::maxIndex] = {0};
+
+    std::getline(fileStat, firstLine);
+    std::stringstream ss(firstLine);
+    ss >> labelName;
+
+    if (labelName.compare("cpu"))
+    {
+        error("CPU data not available");
+        return false;
+    }
+
+    for (auto idx = 0; idx < CPUStatsIndex::maxIndex; idx++)
+    {
+        if (!(ss >> timeData[idx]))
+        {
+            error("CPU data not correct");
+            return false;
+        }
+    }
+
+    for (auto& config : configs)
+    {
+        uint64_t activeTime = 0, activeTimeDiff = 0, totalTime = 0,
+                 totalTimeDiff = 0;
+        double activePercValue = 0;
+
+        if (config.subType == MetricIntf::SubType::cpuTotal)
+        {
+            activeTime = timeData[CPUStatsIndex::userIndex] +
+                         timeData[CPUStatsIndex::niceIndex] +
+                         timeData[CPUStatsIndex::systemIndex] +
+                         timeData[CPUStatsIndex::irqIndex] +
+                         timeData[CPUStatsIndex::softirqIndex] +
+                         timeData[CPUStatsIndex::stealIndex] +
+                         timeData[CPUStatsIndex::guestUserIndex] +
+                         timeData[CPUStatsIndex::guestNiceIndex];
+        }
+        else if (config.subType == MetricIntf::SubType::cpuKernel)
+        {
+            activeTime = timeData[CPUStatsIndex::systemIndex];
+        }
+        else if (config.subType == MetricIntf::SubType::cpuUser)
+        {
+            activeTime = timeData[CPUStatsIndex::userIndex];
+        }
+
+        totalTime = std::accumulate(std::begin(timeData), std::end(timeData),
+                                    decltype(totalTime){0});
+
+        activeTimeDiff = activeTime - preActiveTime[config.subType];
+        totalTimeDiff = totalTime - preTotalTime[config.subType];
+
+        /* Store current active and total time for next calculation */
+        preActiveTime[config.subType] = activeTime;
+        preTotalTime[config.subType] = totalTime;
+
+        activePercValue = (100.0 * activeTimeDiff) / totalTimeDiff;
+        debug("CPU Metric {SUBTYPE}: {VALUE}", "SUBTYPE",
+              std::to_underlying(config.subType), "VALUE",
+              (double)activePercValue);
+        /* For CPU, both user and monitor uses percentage values */
+        metrics[config.subType]->update(
+            MValue(activePercValue, activePercValue));
+    }
+    return true;
+}
+
+auto HealthMetricCollection::readMemory() -> bool
+{
+    constexpr auto procMeminfo = "/proc/meminfo";
+    std::ifstream memInfo(procMeminfo);
+    if (!memInfo.is_open())
+    {
+        error("Unable to open {PATH} for reading Memory stats", "PATH",
+              procMeminfo);
+        return false;
+    }
+    std::string line;
+    std::unordered_map<MetricIntf::SubType, double> memoryValues;
+
+    while (std::getline(memInfo, line))
+    {
+        std::string name;
+        double value;
+        std::istringstream iss(line);
+
+        if (!(iss >> name >> value))
+        {
+            continue;
+        }
+        if (name.starts_with("MemAvailable"))
+        {
+            memoryValues[MetricIntf::SubType::memoryAvailable] = value;
+        }
+        else if (name.starts_with("MemFree"))
+        {
+            memoryValues[MetricIntf::SubType::memoryFree] = value;
+        }
+        else if (name.starts_with("Buffers") || name.starts_with("Cached"))
+        {
+            memoryValues[MetricIntf::SubType::memoryBufferedAndCached] += value;
+        }
+        else if (name.starts_with("MemTotal"))
+        {
+            memoryValues[MetricIntf::SubType::memoryTotal] = value;
+        }
+        else if (name.starts_with("Shmem"))
+        {
+            memoryValues[MetricIntf::SubType::memoryShared] = value;
+        }
+    }
+
+    for (auto& config : configs)
+    {
+        auto absoluteValue = memoryValues.at(config.subType);
+        auto memoryTotal = memoryValues.at(MetricIntf::SubType::memoryTotal);
+        double percentValue = (memoryTotal - absoluteValue) / memoryTotal * 100;
+        absoluteValue = absoluteValue * 1000;
+        debug("Memory Metric {SUBTYPE}: {VALUE}, {PERCENT}", "SUBTYPE",
+              std::to_underlying(config.subType), "VALUE", absoluteValue,
+              "PERCENT", percentValue);
+        metrics[config.subType]->update(MValue(absoluteValue, percentValue));
+    }
+    return true;
+}
+
+auto HealthMetricCollection::readStorage() -> bool
+{
+    for (auto& config : configs)
+    {
+        struct statvfs buffer;
+        if (statvfs(config.path.c_str(), &buffer) != 0)
+        {
+            auto e = errno;
+            error("Error from statvfs: {ERROR}, path: {PATH}", "ERROR",
+                  strerror(e), "PATH", config.path);
+            continue;
+        }
+        double total = buffer.f_blocks * (buffer.f_frsize / 1024);
+        double available = buffer.f_bfree * (buffer.f_frsize / 1024);
+        double availablePercent = ((available / total) * 100);
+
+        debug("Storage Metric {SUBTYPE}: {TOTAL} {AVAIL} {AVAIL_PERCENT}",
+              "SUBTYPE", std::to_underlying(config.subType), "TOTAL", total,
+              "AVAIL", available, "AVAIL_PERCENT", availablePercent);
+        metrics[config.subType]->update(MValue(available, availablePercent));
+    }
+    return true;
+}
+
+void HealthMetricCollection::read()
+{
+    switch (type)
+    {
+        case MetricIntf::Type::cpu:
+        {
+            if (!readCPU())
+            {
+                error("Failed to read CPU health metric");
+            }
+            break;
+        }
+        case MetricIntf::Type::memory:
+        {
+            if (!readMemory())
+            {
+                error("Failed to read memory health metric");
+            }
+            break;
+        }
+        case MetricIntf::Type::storage:
+        {
+            if (!readStorage())
+            {
+                error("Failed to read storage health metric");
+            }
+            break;
+        }
+        default:
+        {
+            error("Unknown health metric type {TYPE}", "TYPE",
+                  std::to_underlying(type));
+            break;
+        }
+    }
+}
+
+void HealthMetricCollection::create(const MetricIntf::paths_t& bmcPaths)
+{
+    metrics.clear();
+
+    for (auto& config : configs)
+    {
+        /* TODO: Remove this after adding iNode support */
+        if (config.subType == MetricIntf::SubType::NA)
+        {
+            continue;
+        }
+        metrics[config.subType] = std::make_unique<MetricIntf::HealthMetric>(
+            bus, type, config, bmcPaths);
+    }
+}
+
+} // namespace phosphor::health::metric::collection
diff --git a/health_metric_collection.hpp b/health_metric_collection.hpp
new file mode 100644
index 0000000..51a92e3
--- /dev/null
+++ b/health_metric_collection.hpp
@@ -0,0 +1,53 @@
+#pragma once
+
+#include "health_metric.hpp"
+
+namespace phosphor::health::metric::collection
+{
+namespace ConfigIntf = phosphor::health::metric::config;
+namespace MetricIntf = phosphor::health::metric;
+
+using configs_t = std::vector<ConfigIntf::HealthMetric>;
+
+class HealthMetricCollection
+{
+  public:
+    HealthMetricCollection(sdbusplus::bus_t& bus, MetricIntf::Type type,
+                           const configs_t& configs,
+                           MetricIntf::paths_t& bmcPaths) :
+        bus(bus),
+        type(type), configs(configs)
+    {
+        create(bmcPaths);
+    }
+
+    /** @brief Read the health metric collection from the system */
+    void read();
+
+  private:
+    using map_t = std::unordered_map<MetricIntf::SubType,
+                                     std::unique_ptr<MetricIntf::HealthMetric>>;
+    using time_map_t = std::unordered_map<MetricIntf::SubType, uint64_t>;
+    /** @brief Create a new health metric collection object */
+    void create(const MetricIntf::paths_t& bmcPaths);
+    /** @brief Read the CPU */
+    auto readCPU() -> bool;
+    /** @brief Read the memory */
+    auto readMemory() -> bool;
+    /** @brief Read the storage */
+    auto readStorage() -> bool;
+    /** @brief D-Bus bus connection */
+    sdbusplus::bus_t& bus;
+    /** @brief Metric type */
+    MetricIntf::Type type;
+    /** @brief Health metric configs */
+    const configs_t& configs;
+    /** @brief Map of health metrics by subtype */
+    map_t metrics;
+    /** @brief Map for active time by subtype */
+    time_map_t preActiveTime;
+    /** @brief Map for total time by subtype */
+    time_map_t preTotalTime;
+};
+
+} // namespace phosphor::health::metric::collection
diff --git a/meson.build b/meson.build
index 1abe217..09ef147 100644
--- a/meson.build
+++ b/meson.build
@@ -28,6 +28,7 @@
         'health_metric_config.cpp',
         'health_metric.cpp',
         'health_utils.cpp',
+        'health_metric_collection.cpp',
     ],
     dependencies: [
         base_deps
diff --git a/test/meson.build b/test/meson.build
index f240bc0..c4f8993 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -54,3 +54,23 @@
         include_directories: '../',
     )
 )
+
+test(
+    'test_health_metric_collection',
+    executable(
+        'test_health_metric_collection',
+        'test_health_metric_collection.cpp',
+        '../health_metric_collection.cpp',
+        '../health_metric.cpp',
+        '../health_metric_config.cpp',
+        '../health_utils.cpp',
+        dependencies: [
+            gtest_dep,
+            gmock_dep,
+            phosphor_logging_dep,
+            phosphor_dbus_interfaces_dep,
+            sdbusplus_dep
+        ],
+        include_directories: '../',
+    )
+)
diff --git a/test/test_health_metric_collection.cpp b/test/test_health_metric_collection.cpp
new file mode 100644
index 0000000..2f4b1b9
--- /dev/null
+++ b/test/test_health_metric_collection.cpp
@@ -0,0 +1,160 @@
+#include "health_metric_collection.hpp"
+
+#include <sdbusplus/test/sdbus_mock.hpp>
+#include <xyz/openbmc_project/Metric/Value/server.hpp>
+
+#include <gtest/gtest.h>
+
+namespace ConfigIntf = phosphor::health::metric::config;
+namespace MetricIntf = phosphor::health::metric;
+namespace CollectionIntf = phosphor::health::metric::collection;
+
+using PathInterface =
+    sdbusplus::common::xyz::openbmc_project::metric::Value::namespace_path;
+using ThresholdIntf =
+    sdbusplus::server::xyz::openbmc_project::common::Threshold;
+using ::testing::Invoke;
+using ::testing::IsNull;
+using ::testing::NotNull;
+using ::testing::StrEq;
+
+class HealthMetricCollectionTest : public ::testing::Test
+{
+  public:
+    sdbusplus::SdBusMock sdbusMock;
+    sdbusplus::bus::bus bus = sdbusplus::get_mocked_new(&sdbusMock);
+
+    static constexpr auto busName = "xyz.openbmc_project.test.HealthMon";
+    static constexpr auto objPath = "/xyz/openbmc_project/sdbusplus/test";
+    const std::string valueInterface =
+        sdbusplus::common::xyz::openbmc_project::metric::Value::interface;
+    const std::string thresholdInterface =
+        sdbusplus::common::xyz::openbmc_project::common::Threshold::interface;
+    ConfigIntf::HealthMetric::map_t configs;
+
+    void SetUp() override
+    {
+        sdbusplus::server::manager_t objManager(bus, objPath);
+        bus.request_name(busName);
+
+        configs = ConfigIntf::getHealthMetricConfigs();
+        EXPECT_THAT(configs.size(), testing::Ge(1));
+        // Update the health metric window size to 1 and path for test purposes
+        for (auto& [key, values] : configs)
+        {
+            for (auto& config : values)
+            {
+                config.windowSize = 1;
+                if (key == MetricIntf::Type::storage &&
+                    config.subType == MetricIntf::SubType::storageReadWrite)
+                {
+                    config.path = "/tmp";
+                }
+            }
+        }
+    }
+
+    void updateThreshold(double value)
+    {
+        for (auto& [key, values] : configs)
+        {
+            for (auto& config : values)
+            {
+                for (auto& threshold : config.thresholds)
+                {
+                    threshold.second.value = value;
+                }
+            }
+        }
+    }
+
+    void createCollection()
+    {
+        std::map<MetricIntf::Type,
+                 std::unique_ptr<CollectionIntf::HealthMetricCollection>>
+            collections;
+        MetricIntf::paths_t bmcPaths = {};
+        for (const auto& [type, collectionConfig] : configs)
+        {
+            collections[type] =
+                std::make_unique<CollectionIntf::HealthMetricCollection>(
+                    bus, type, collectionConfig, bmcPaths);
+            collections[type]->read();
+        }
+    }
+};
+
+TEST_F(HealthMetricCollectionTest, TestCreation)
+{
+    // Change threshold value to 100 to avoid threshold assertion
+    updateThreshold(100);
+
+    EXPECT_CALL(sdbusMock,
+                sd_bus_emit_properties_changed_strv(
+                    IsNull(), NotNull(), StrEq(valueInterface), NotNull()))
+        .WillRepeatedly(Invoke(
+            [&]([[maybe_unused]] sd_bus* bus, [[maybe_unused]] const char* path,
+                [[maybe_unused]] const char* interface, const char** names) {
+        // Test no signal generation for metric init properties
+        const std::set<std::string> metricInitProperties = {"MaxValue",
+                                                            "MinValue", "Unit"};
+        EXPECT_THAT(metricInitProperties,
+                    testing::Not(testing::Contains(names[0])));
+        // Test signal generated for Value property set
+        const std::set<std::string> metricSetProperties = {"Value"};
+        EXPECT_THAT(metricSetProperties, testing::Contains(names[0]));
+        return 0;
+    }));
+
+    EXPECT_CALL(sdbusMock,
+                sd_bus_emit_properties_changed_strv(
+                    IsNull(), NotNull(), StrEq(thresholdInterface), NotNull()))
+        .WillRepeatedly(Invoke(
+            [&]([[maybe_unused]] sd_bus* bus, [[maybe_unused]] const char* path,
+                [[maybe_unused]] const char* interface, const char** names) {
+        // Test no signal generation for threshold init properties
+        const std::set<std::string> thresholdProperties = {"Value", "Asserted"};
+        EXPECT_THAT(thresholdProperties,
+                    testing::Not(testing::Contains(names[0])));
+        return 0;
+    }));
+
+    createCollection();
+}
+
+TEST_F(HealthMetricCollectionTest, TestThresholdAsserted)
+{
+    // Change threshold value to 0 to trigger threshold assertion
+    updateThreshold(0);
+
+    // Test metric value property change
+    EXPECT_CALL(sdbusMock,
+                sd_bus_emit_properties_changed_strv(
+                    IsNull(), NotNull(), StrEq(valueInterface), NotNull()))
+        .WillRepeatedly(Invoke(
+            [&]([[maybe_unused]] sd_bus* bus, [[maybe_unused]] const char* path,
+                [[maybe_unused]] const char* interface, const char** names) {
+        EXPECT_THAT("Value", StrEq(names[0]));
+        return 0;
+    }));
+
+    // Test threshold asserted property change
+    EXPECT_CALL(sdbusMock,
+                sd_bus_emit_properties_changed_strv(
+                    IsNull(), NotNull(), StrEq(thresholdInterface), NotNull()))
+        .WillRepeatedly(Invoke(
+            [&]([[maybe_unused]] sd_bus* bus, [[maybe_unused]] const char* path,
+                [[maybe_unused]] const char* interface, const char** names) {
+        EXPECT_THAT("Asserted", StrEq(names[0]));
+        return 0;
+    }));
+
+    // Test AssertionChanged signal generation
+    EXPECT_CALL(sdbusMock,
+                sd_bus_message_new_signal(IsNull(), NotNull(), NotNull(),
+                                          StrEq(thresholdInterface),
+                                          StrEq("AssertionChanged")))
+        .Times(11);
+
+    createCollection();
+}