add health_metric implementation
Add the interface and implementation for the health_metric to be used
in the rewrite for 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: Iffcc52f9dff712890377b1222fd7e7d5d6661eaf
Signed-off-by: Jagpal Singh Gill <paligill@gmail.com>
diff --git a/health_metric.cpp b/health_metric.cpp
new file mode 100644
index 0000000..2c82fea
--- /dev/null
+++ b/health_metric.cpp
@@ -0,0 +1,216 @@
+#include "health_metric.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+
+#include <numeric>
+#include <unordered_map>
+
+PHOSPHOR_LOG2_USING;
+
+namespace phosphor::health::metric
+{
+
+using association_t = std::tuple<std::string, std::string, std::string>;
+
+auto HealthMetric::getPath(SubType subType) -> std::string
+{
+ std::string path;
+ switch (subType)
+ {
+ case SubType::cpuTotal:
+ {
+ return std::string(BmcPath) + "/" + PathIntf::total_cpu;
+ }
+ case SubType::cpuKernel:
+ {
+ return std::string(BmcPath) + "/" + PathIntf::kernel_cpu;
+ }
+ case SubType::cpuUser:
+ {
+ return std::string(BmcPath) + "/" + PathIntf::user_cpu;
+ }
+ case SubType::memoryAvailable:
+ {
+ return std::string(BmcPath) + "/" + PathIntf::available_memory;
+ }
+ case SubType::memoryBufferedAndCached:
+ {
+ return std::string(BmcPath) + "/" +
+ PathIntf::buffered_and_cached_memory;
+ }
+ case SubType::memoryFree:
+ {
+ return std::string(BmcPath) + "/" + PathIntf::free_memory;
+ }
+ case SubType::memoryShared:
+ {
+ return std::string(BmcPath) + "/" + PathIntf::shared_memory;
+ }
+ case SubType::memoryTotal:
+ {
+ return std::string(BmcPath) + "/" + PathIntf::total_memory;
+ }
+ case SubType::storageReadWrite:
+ {
+ return std::string(BmcPath) + "/" + PathIntf::read_write_storage;
+ }
+ default:
+ {
+ error("Invalid Memory metric {TYPE}", "TYPE",
+ std::to_underlying(subType));
+ return "";
+ }
+ }
+}
+
+void HealthMetric::initProperties()
+{
+ switch (config.subType)
+ {
+ case SubType::cpuTotal:
+ case SubType::cpuKernel:
+ case SubType::cpuUser:
+ {
+ ValueIntf::unit(ValueIntf::Unit::Percent, true);
+ ValueIntf::minValue(0.0, true);
+ ValueIntf::maxValue(100.0, true);
+ break;
+ }
+ case SubType::memoryAvailable:
+ case SubType::memoryBufferedAndCached:
+ case SubType::memoryFree:
+ case SubType::memoryShared:
+ case SubType::memoryTotal:
+ case SubType::storageReadWrite:
+ default:
+ {
+ ValueIntf::unit(ValueIntf::Unit::Bytes, true);
+ ValueIntf::minValue(0.0, true);
+ }
+ }
+ ValueIntf::value(std::numeric_limits<double>::quiet_NaN());
+
+ using bound_map_t = std::map<ThresholdIntf::Bound, double>;
+ std::map<ThresholdIntf::Type, bound_map_t> thresholds;
+ for (const auto& [key, value] : config.thresholds)
+ {
+ auto type = std::get<ThresholdIntf::Type>(key);
+ auto bound = std::get<ThresholdIntf::Bound>(key);
+ auto threshold = thresholds.find(type);
+ if (threshold == thresholds.end())
+ {
+ bound_map_t bounds;
+ bounds.emplace(bound, value.value);
+ thresholds.emplace(type, bounds);
+ }
+ else
+ {
+ threshold->second.emplace(bound, value.value);
+ }
+ }
+ ThresholdIntf::value(thresholds);
+}
+
+void HealthMetric::checkThreshold(ThresholdIntf::Type type,
+ ThresholdIntf::Bound bound, double value)
+{
+ auto threshold = std::make_tuple(type, bound);
+ auto thresholds = ThresholdIntf::value();
+
+ if (thresholds.contains(type) && thresholds[type].contains(bound))
+ {
+ auto thresholdValue = thresholds[type][bound];
+ auto assertions = ThresholdIntf::asserted();
+ if (value > thresholdValue)
+ {
+ if (!assertions.contains(threshold))
+ {
+ assertions.insert(threshold);
+ ThresholdIntf::asserted(assertions);
+ ThresholdIntf::assertionChanged(type, bound, true, value);
+ auto tConfig = config.thresholds.at(threshold);
+ if (tConfig.log)
+ {
+ error(
+ "ASSERT: Health Metric {METRIC} crossed {TYPE} upper threshold",
+ "METRIC", config.name, "TYPE",
+ sdbusplus::message::convert_to_string(type));
+ startUnit(bus, tConfig.target);
+ }
+ }
+ return;
+ }
+ else if (assertions.contains(threshold))
+ {
+ assertions.erase(threshold);
+ ThresholdIntf::asserted(assertions);
+ ThresholdIntf::assertionChanged(type, bound, false, value);
+ if (config.thresholds.find(threshold)->second.log)
+ {
+ info(
+ "DEASSERT: Health Metric {METRIC} is below {TYPE} upper threshold",
+ "METRIC", config.name, "TYPE",
+ sdbusplus::message::convert_to_string(type));
+ }
+ }
+ }
+}
+
+void HealthMetric::checkThresholds(double value)
+{
+ if (!ThresholdIntf::value().empty())
+ {
+ for (auto type :
+ {ThresholdIntf::Type::HardShutdown,
+ ThresholdIntf::Type::SoftShutdown,
+ ThresholdIntf::Type::PerformanceLoss,
+ ThresholdIntf::Type::Critical, ThresholdIntf::Type::Warning})
+ {
+ checkThreshold(type, ThresholdIntf::Bound::Upper, value);
+ }
+ }
+}
+
+void HealthMetric::update(MValue value)
+{
+ // Maintain window size for metric
+ if (history.size() >= config.windowSize)
+ {
+ history.pop_front();
+ }
+ history.push_back(value.user);
+
+ if (history.size() < config.windowSize)
+ {
+ // Wait for the metric to have enough samples to calculate average
+ info("Not enough samples to calculate average");
+ return;
+ }
+
+ double average = (std::accumulate(history.begin(), history.end(), 0.0)) /
+ history.size();
+ ValueIntf::value(average);
+ checkThresholds(value.monitor);
+}
+
+void HealthMetric::create(const paths_t& bmcPaths)
+{
+ info("Create Health Metric: {METRIC}", "METRIC", config.name);
+ initProperties();
+
+ std::vector<association_t> associations;
+ static constexpr auto forwardAssociation = "measuring";
+ static constexpr auto reverseAssociation = "measured_by";
+ for (const auto& bmcPath : bmcPaths)
+ {
+ /*
+ * This metric is "measuring" the health for the BMC at bmcPath
+ * The BMC at bmcPath is "measured_by" this metric.
+ */
+ associations.push_back(
+ {forwardAssociation, reverseAssociation, bmcPath});
+ }
+ AssociationIntf::associations(associations);
+}
+
+} // namespace phosphor::health::metric
diff --git a/health_metric.hpp b/health_metric.hpp
new file mode 100644
index 0000000..e0c75f9
--- /dev/null
+++ b/health_metric.hpp
@@ -0,0 +1,79 @@
+#pragma once
+
+#include "health_metric_config.hpp"
+#include "health_utils.hpp"
+
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/Inventory/Item/Bmc/server.hpp>
+#include <xyz/openbmc_project/Metric/Value/server.hpp>
+
+#include <deque>
+#include <tuple>
+
+namespace phosphor::health::metric
+{
+
+using phosphor::health::utils::paths_t;
+using phosphor::health::utils::startUnit;
+using AssociationIntf =
+ sdbusplus::xyz::openbmc_project::Association::server::Definitions;
+using ValueIntf = sdbusplus::xyz::openbmc_project::Metric::server::Value;
+using PathIntf =
+ sdbusplus::common::xyz::openbmc_project::metric::Value::namespace_path;
+static constexpr auto BmcPath =
+ sdbusplus::common::xyz::openbmc_project::metric::Value::bmc;
+using BmcIntf = sdbusplus::xyz::openbmc_project::Inventory::Item::server::Bmc;
+using MetricIntf =
+ sdbusplus::server::object_t<ValueIntf, ThresholdIntf, AssociationIntf>;
+
+struct MValue
+{
+ /** @brief Value for end user consumption */
+ double user;
+ /** @brief Value for threshold monitor */
+ double monitor;
+};
+
+class HealthMetric : public MetricIntf
+{
+ public:
+ HealthMetric() = delete;
+ HealthMetric(const HealthMetric&) = delete;
+ HealthMetric(HealthMetric&&) = delete;
+ virtual ~HealthMetric() = default;
+
+ HealthMetric(sdbusplus::bus::bus& bus, phosphor::health::metric::Type type,
+ const config::HealthMetric& config, const paths_t& bmcPaths) :
+ MetricIntf(bus, getPath(config.subType).c_str(), action::defer_emit),
+ bus(bus), type(type), config(config)
+ {
+ create(bmcPaths);
+ this->emit_object_added();
+ }
+
+ /** @brief Update the health metric with the given value */
+ void update(MValue value);
+
+ private:
+ /** @brief Create a new health metric object */
+ void create(const paths_t& bmcPaths);
+ /** @brief Init properties for the health metric object */
+ void initProperties();
+ /** @brief Check specified threshold for the given value */
+ void checkThreshold(ThresholdIntf::Type type, ThresholdIntf::Bound bound,
+ double value);
+ /** @brief Check all thresholds for the given value */
+ void checkThresholds(double value);
+ /** @brief Get the object path for the given subtype */
+ auto getPath(SubType subType) -> std::string;
+ /** @brief D-Bus bus connection */
+ sdbusplus::bus::bus& bus;
+ /** @brief Metric type */
+ phosphor::health::metric::Type type;
+ /** @brief Metric configuration */
+ const config::HealthMetric config;
+ /** @brief Window for metric history */
+ std::deque<double> history;
+};
+
+} // namespace phosphor::health::metric
diff --git a/health_metric_config.cpp b/health_metric_config.cpp
index 8320f25..2c0c78c 100644
--- a/health_metric_config.cpp
+++ b/health_metric_config.cpp
@@ -5,6 +5,7 @@
#include <nlohmann/json.hpp>
#include <phosphor-logging/lg2.hpp>
+#include <cmath>
#include <fstream>
#include <unordered_map>
#include <utility>
@@ -79,6 +80,10 @@
}
auto config = value.template get<Threshold>();
+ if (!std::isfinite(config.value))
+ {
+ throw std::invalid_argument("Invalid threshold value");
+ }
// ThresholdIntf::Bound::Upper is the only use case for
// ThresholdIntf::Bound
diff --git a/health_utils.cpp b/health_utils.cpp
new file mode 100644
index 0000000..163fc8c
--- /dev/null
+++ b/health_utils.cpp
@@ -0,0 +1,23 @@
+#include "health_utils.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+
+PHOSPHOR_LOG2_USING;
+
+namespace phosphor::health::utils
+{
+
+void startUnit(sdbusplus::bus_t& bus, const std::string& sysdUnit)
+{
+ if (sysdUnit.empty())
+ {
+ return;
+ }
+ sdbusplus::message_t msg = bus.new_method_call(
+ "org.freedesktop.systemd1", "/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager", "StartUnit");
+ msg.append(sysdUnit, "replace");
+ bus.call_noreply(msg);
+}
+
+} // namespace phosphor::health::utils
diff --git a/health_utils.hpp b/health_utils.hpp
new file mode 100644
index 0000000..b89d8e5
--- /dev/null
+++ b/health_utils.hpp
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/sdbus.hpp>
+
+#include <vector>
+
+namespace phosphor::health::utils
+{
+
+using paths_t = std::vector<std::string>;
+
+/** @brief Start a systemd unit */
+void startUnit(sdbusplus::bus_t& bus, const std::string& sysdUnit);
+
+} // namespace phosphor::health::utils
diff --git a/meson.build b/meson.build
index 27e2bff..1abe217 100644
--- a/meson.build
+++ b/meson.build
@@ -26,6 +26,8 @@
[
'healthMonitor.cpp',
'health_metric_config.cpp',
+ 'health_metric.cpp',
+ 'health_utils.cpp',
],
dependencies: [
base_deps
diff --git a/test/meson.build b/test/meson.build
index 4df0d46..f240bc0 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -36,3 +36,21 @@
include_directories: '../',
)
)
+
+test(
+ 'test_health_metric',
+ executable(
+ 'test_health_metric',
+ 'test_health_metric.cpp',
+ '../health_metric.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.cpp b/test/test_health_metric.cpp
new file mode 100644
index 0000000..9e9f1f5
--- /dev/null
+++ b/test/test_health_metric.cpp
@@ -0,0 +1,96 @@
+#include "health_metric.hpp"
+
+#include <sdbusplus/test/sdbus_mock.hpp>
+#include <xyz/openbmc_project/Metric/Value/server.hpp>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace ConfigIntf = phosphor::health::metric::config;
+using PathIntf =
+ sdbusplus::server::xyz::openbmc_project::metric::Value::namespace_path;
+using namespace phosphor::health::metric;
+using namespace phosphor::health::utils;
+
+using ::testing::_;
+using ::testing::InSequence;
+using ::testing::Invoke;
+using ::testing::IsNull;
+using ::testing::NotNull;
+using ::testing::Pair;
+using ::testing::StrEq;
+
+class HealthMetricTest : 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";
+ const std::set<std::string> properties = {"Value", "MaxValue", "MinValue",
+ "Unit"};
+ const std::string objPath = std::string(PathIntf::value) + "/bmc/" +
+ PathIntf::kernel_cpu;
+ ConfigIntf::HealthMetric config;
+
+ void SetUp() override
+ {
+ config.name = "CPU_Kernel";
+ config.subType = SubType::cpuKernel;
+ config.collectionFreq = ConfigIntf::HealthMetric::defaults::frequency;
+ config.windowSize = 1;
+ config.thresholds = {
+ {{ThresholdIntf::Type::Critical, ThresholdIntf::Bound::Upper},
+ {.value = 90.0, .log = true, .target = ""}},
+ {{ThresholdIntf::Type::Warning, ThresholdIntf::Bound::Upper},
+ {.value = 80.0, .log = false, .target = ""}}};
+ config.path = "";
+ }
+};
+
+TEST_F(HealthMetricTest, TestMetricUnmockedObjectAddRemove)
+{
+ sdbusplus::bus::bus unmockedBus = sdbusplus::bus::new_bus();
+ unmockedBus.request_name(busName);
+ auto metric = std::make_unique<HealthMetric>(unmockedBus, Type::cpu, config,
+ paths_t());
+}
+
+TEST_F(HealthMetricTest, TestMetricThresholdChange)
+{
+ sdbusplus::server::manager_t objManager(bus, objPath.c_str());
+ bus.request_name(busName);
+ const auto thresholdProperties = std::set<std::string>{"Value", "Asserted"};
+
+ EXPECT_CALL(sdbusMock, sd_bus_emit_properties_changed_strv(
+ IsNull(), StrEq(objPath),
+ StrEq(ValueIntf::interface), NotNull()))
+ .WillRepeatedly(Invoke(
+ [&]([[maybe_unused]] sd_bus* bus, [[maybe_unused]] const char* path,
+ [[maybe_unused]] const char* interface, const char** names) {
+ EXPECT_THAT(properties, testing::Contains(names[0]));
+ return 0;
+ }));
+ EXPECT_CALL(sdbusMock, sd_bus_emit_properties_changed_strv(
+ IsNull(), StrEq(objPath),
+ StrEq(ThresholdIntf::interface), NotNull()))
+ .WillRepeatedly(Invoke(
+ [&]([[maybe_unused]] sd_bus* bus, [[maybe_unused]] const char* path,
+ [[maybe_unused]] const char* interface, const char** names) {
+ EXPECT_THAT(thresholdProperties, testing::Contains(names[0]));
+ return 0;
+ }));
+ EXPECT_CALL(sdbusMock,
+ sd_bus_message_new_signal(_, _, StrEq(objPath),
+ StrEq(ThresholdIntf::interface),
+ StrEq("AssertionChanged")))
+ .Times(4);
+
+ auto metric = std::make_unique<HealthMetric>(bus, Type::cpu, config,
+ paths_t());
+ // Exceed the critical threshold
+ metric->update(MValue(1200, 95.0));
+ // Go below critical threshold but above warning threshold
+ metric->update(MValue(1200, 85.0));
+ // Go below warning threshold
+ metric->update(MValue(1200, 75.0));
+}