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);
+ }
+ }
+}