added support for Collection Functions
new supported operations: min,max,sum,avg
new supported time scopes: interval,startup
added unit test to verify that each collection function returns correct
timestamp and value
Tested:
- POST/GET on telemetry features in bmcweb, no regression detected
- Using dbus API metric with collection function works as expected
Change-Id: Ib364c433915e07fd7a102f00109525362c40ab8a
Signed-off-by: Krzysztof Grobelny <krzysztof.grobelny@intel.com>
diff --git a/src/details/collection_function.cpp b/src/details/collection_function.cpp
new file mode 100644
index 0000000..d92fdf3
--- /dev/null
+++ b/src/details/collection_function.cpp
@@ -0,0 +1,172 @@
+#include "collection_function.hpp"
+
+#include <cmath>
+
+namespace details
+{
+
+class FunctionSingle : public CollectionFunction
+{
+ public:
+ ReadingItem calculate(const std::vector<ReadingItem>& readings,
+ uint64_t) const override
+ {
+ return readings.back();
+ }
+
+ ReadingItem calculateForStartupInterval(std::vector<ReadingItem>& readings,
+ uint64_t timestamp) const override
+ {
+ readings.assign({readings.back()});
+ return readings.back();
+ }
+};
+
+class FunctionMinimum : public CollectionFunction
+{
+ public:
+ ReadingItem calculate(const std::vector<ReadingItem>& readings,
+ uint64_t) const override
+ {
+ return *std::min_element(
+ readings.begin(), readings.end(),
+ [](const auto& left, const auto& right) {
+ return std::make_tuple(!std::isfinite(left.second),
+ left.second) <
+ std::make_tuple(!std::isfinite(right.second),
+ right.second);
+ });
+ }
+
+ ReadingItem calculateForStartupInterval(std::vector<ReadingItem>& readings,
+ uint64_t timestamp) const override
+ {
+ readings.assign({ReadingItem(calculate(readings, timestamp))});
+ return readings.back();
+ }
+};
+
+class FunctionMaximum : public CollectionFunction
+{
+ public:
+ ReadingItem calculate(const std::vector<ReadingItem>& readings,
+ uint64_t) const override
+ {
+ return *std::max_element(
+ readings.begin(), readings.end(),
+ [](const auto& left, const auto& right) {
+ return std::make_tuple(std::isfinite(left.second),
+ left.second) <
+ std::make_tuple(std::isfinite(right.second),
+ right.second);
+ });
+ }
+
+ ReadingItem calculateForStartupInterval(std::vector<ReadingItem>& readings,
+ uint64_t timestamp) const override
+ {
+ readings.assign({ReadingItem(calculate(readings, timestamp))});
+ return readings.back();
+ }
+};
+
+class FunctionAverage : public CollectionFunction
+{
+ public:
+ ReadingItem calculate(const std::vector<ReadingItem>& readings,
+ uint64_t timestamp) const override
+ {
+ auto valueSum = 0.0;
+ auto timeSum = uint64_t{0};
+ for (auto it = readings.begin(); it != std::prev(readings.end()); ++it)
+ {
+ if (std::isfinite(it->second))
+ {
+ const auto kt = std::next(it);
+ const auto duration = kt->first - it->first;
+ valueSum += it->second * duration;
+ timeSum += duration;
+ }
+ }
+
+ const auto duration = timestamp - readings.back().first;
+ valueSum += readings.back().second * duration;
+ timeSum += duration;
+
+ return ReadingItem{timestamp, valueSum / timeSum};
+ }
+
+ ReadingItem calculateForStartupInterval(std::vector<ReadingItem>& readings,
+ uint64_t timestamp) const override
+ {
+ auto result = calculate(readings, timestamp);
+ if (std::isfinite(result.second))
+ {
+ readings.assign({ReadingItem(readings.front().first, result.second),
+ ReadingItem(timestamp, readings.back().second)});
+ }
+ return result;
+ }
+};
+
+class FunctionSummation : public CollectionFunction
+{
+ public:
+ ReadingItem calculate(const std::vector<ReadingItem>& readings,
+ uint64_t timestamp) const override
+ {
+ auto valueSum = 0.0;
+ for (auto it = readings.begin(); it != std::prev(readings.end()); ++it)
+ {
+ if (std::isfinite(it->second))
+ {
+ const auto kt = std::next(it);
+ const auto duration = kt->first - it->first;
+ valueSum += it->second * duration;
+ }
+ }
+
+ const auto duration = timestamp - readings.back().first;
+ valueSum += readings.back().second * duration;
+
+ return ReadingItem{timestamp, valueSum};
+ }
+
+ ReadingItem calculateForStartupInterval(std::vector<ReadingItem>& readings,
+ uint64_t timestamp) const override
+ {
+ auto result = calculate(readings, timestamp);
+ if (std::isfinite(result.second) && timestamp > 0u)
+ {
+ readings.assign({ReadingItem(timestamp - 1u, result.second),
+ ReadingItem(timestamp, readings.back().second)});
+ }
+ return result;
+ }
+};
+
+std::shared_ptr<CollectionFunction>
+ makeCollectionFunction(OperationType operationType)
+{
+ using namespace std::string_literals;
+
+ switch (operationType)
+ {
+ case OperationType::single:
+ return std::make_shared<FunctionSingle>();
+ case OperationType::min:
+ return std::make_shared<FunctionMinimum>();
+ case OperationType::max:
+ return std::make_shared<FunctionMaximum>();
+ case OperationType::avg:
+ return std::make_shared<FunctionAverage>();
+ case OperationType::sum:
+ return std::make_shared<FunctionSummation>();
+ default:
+ throw std::runtime_error("op: "s +
+ utils::enumToString(operationType) +
+ " is not supported"s);
+ }
+}
+
+} // namespace details
diff --git a/src/details/collection_function.hpp b/src/details/collection_function.hpp
new file mode 100644
index 0000000..a8708e7
--- /dev/null
+++ b/src/details/collection_function.hpp
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "types/operation_type.hpp"
+
+#include <cstdint>
+#include <memory>
+#include <utility>
+#include <vector>
+
+namespace details
+{
+
+using ReadingItem = std::pair<uint64_t, double>;
+
+class CollectionFunction
+{
+ public:
+ virtual ~CollectionFunction() = default;
+
+ virtual ReadingItem calculate(const std::vector<ReadingItem>& readings,
+ uint64_t timestamp) const = 0;
+ virtual ReadingItem
+ calculateForStartupInterval(std::vector<ReadingItem>& readings,
+ uint64_t timestamp) const = 0;
+};
+
+std::shared_ptr<CollectionFunction> makeCollectionFunction(OperationType);
+
+} // namespace details
diff --git a/src/interfaces/clock.hpp b/src/interfaces/clock.hpp
new file mode 100644
index 0000000..0c355b7
--- /dev/null
+++ b/src/interfaces/clock.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <chrono>
+
+namespace interfaces
+{
+
+class Clock
+{
+ public:
+ using duration = std::chrono::steady_clock::time_point::duration;
+ using rep = std::chrono::steady_clock::time_point::rep;
+ using period = std::chrono::steady_clock::time_point::period;
+ using time_point = std::chrono::steady_clock::time_point;
+
+ virtual ~Clock() = default;
+
+ virtual time_point now() const noexcept = 0;
+ virtual uint64_t timestamp() const noexcept = 0;
+};
+
+} // namespace interfaces
diff --git a/src/interfaces/metric.hpp b/src/interfaces/metric.hpp
index 3279fd7..62a8f44 100644
--- a/src/interfaces/metric.hpp
+++ b/src/interfaces/metric.hpp
@@ -16,7 +16,7 @@
virtual ~Metric() = default;
virtual void initialize() = 0;
- virtual const std::vector<MetricValue>& getReadings() const = 0;
+ virtual std::vector<MetricValue> getReadings() const = 0;
virtual LabeledMetricParameters dumpConfiguration() const = 0;
};
diff --git a/src/metric.cpp b/src/metric.cpp
index 9dd4e67..04d3fe9 100644
--- a/src/metric.cpp
+++ b/src/metric.cpp
@@ -1,25 +1,138 @@
#include "metric.hpp"
+#include "details/collection_function.hpp"
#include "types/report_types.hpp"
#include "utils/labeled_tuple.hpp"
#include "utils/transform.hpp"
#include <algorithm>
+class Metric::CollectionData
+{
+ public:
+ using ReadingItem = details::ReadingItem;
+
+ virtual ~CollectionData() = default;
+
+ virtual ReadingItem update(uint64_t timestamp) = 0;
+ virtual ReadingItem update(uint64_t timestamp, double value) = 0;
+};
+
+class Metric::DataPoint : public Metric::CollectionData
+{
+ public:
+ ReadingItem update(uint64_t timestamp) override
+ {
+ return ReadingItem{lastTimestamp, lastReading};
+ }
+
+ ReadingItem update(uint64_t timestamp, double reading) override
+ {
+ lastTimestamp = timestamp;
+ lastReading = reading;
+ return update(timestamp);
+ }
+
+ private:
+ uint64_t lastTimestamp = 0u;
+ double lastReading = 0.0;
+};
+
+class Metric::DataInterval : public Metric::CollectionData
+{
+ public:
+ DataInterval(std::shared_ptr<details::CollectionFunction> function,
+ CollectionDuration duration) :
+ function(std::move(function)),
+ duration(duration)
+ {}
+
+ ReadingItem update(uint64_t timestamp) override
+ {
+ if (readings.size() > 0)
+ {
+ auto it = readings.begin();
+ for (auto kt = std::next(readings.rbegin()); kt != readings.rend();
+ ++kt)
+ {
+ const auto& [nextItemTimestamp, nextItemReading] =
+ *std::prev(kt);
+ if (timestamp >= nextItemTimestamp &&
+ static_cast<uint64_t>(timestamp - nextItemTimestamp) >
+ duration.t.count())
+ {
+ it = kt.base();
+ break;
+ }
+ }
+ readings.erase(readings.begin(), it);
+
+ if (timestamp > duration.t.count())
+ {
+ readings.front().first = std::max(
+ readings.front().first, timestamp - duration.t.count());
+ }
+ }
+
+ return function->calculate(readings, timestamp);
+ }
+
+ ReadingItem update(uint64_t timestamp, double reading) override
+ {
+ readings.emplace_back(timestamp, reading);
+ return update(timestamp);
+ }
+
+ private:
+ std::shared_ptr<details::CollectionFunction> function;
+ std::vector<ReadingItem> readings;
+ CollectionDuration duration;
+};
+
+class Metric::DataStartup : public Metric::CollectionData
+{
+ public:
+ DataStartup(std::shared_ptr<details::CollectionFunction> function) :
+ function(std::move(function))
+ {}
+
+ ReadingItem update(uint64_t timestamp) override
+ {
+ return function->calculateForStartupInterval(readings, timestamp);
+ }
+
+ ReadingItem update(uint64_t timestamp, double reading) override
+ {
+ readings.emplace_back(timestamp, reading);
+ return function->calculateForStartupInterval(readings, timestamp);
+ }
+
+ private:
+ std::shared_ptr<details::CollectionFunction> function;
+ std::vector<ReadingItem> readings;
+};
+
Metric::Metric(Sensors sensorsIn, OperationType operationTypeIn,
std::string idIn, std::string metadataIn,
CollectionTimeScope timeScopeIn,
- CollectionDuration collectionDurationIn) :
+ CollectionDuration collectionDurationIn,
+ std::unique_ptr<interfaces::Clock> clockIn) :
id(idIn),
metadata(metadataIn),
readings(sensorsIn.size(),
- MetricValue{std::move(idIn), std::move(metadataIn), 0., 0u}),
+ MetricValue{std::move(idIn), std::move(metadataIn), 0.0, 0u}),
sensors(std::move(sensorsIn)), operationType(operationTypeIn),
- timeScope(timeScopeIn), collectionDuration(collectionDurationIn)
+ collectionTimeScope(timeScopeIn), collectionDuration(collectionDurationIn),
+ collectionAlgorithms(makeCollectionData(sensors.size(), operationType,
+ collectionTimeScope,
+ collectionDuration)),
+ clock(std::move(clockIn))
{
- tryUnpackJsonMetadata();
+ attemptUnpackJsonMetadata();
}
+Metric::~Metric() = default;
+
void Metric::initialize()
{
for (const auto& sensor : sensors)
@@ -28,32 +141,40 @@
}
}
-const std::vector<MetricValue>& Metric::getReadings() const
+std::vector<MetricValue> Metric::getReadings() const
{
- return readings;
+ const auto timestamp = clock->timestamp();
+
+ auto resultReadings = readings;
+
+ for (size_t i = 0; i < resultReadings.size(); ++i)
+ {
+ std::tie(resultReadings[i].timestamp, resultReadings[i].value) =
+ collectionAlgorithms[i]->update(timestamp);
+ }
+
+ return resultReadings;
}
void Metric::sensorUpdated(interfaces::Sensor& notifier, uint64_t timestamp)
{
- MetricValue& mv = findMetric(notifier);
- mv.timestamp = timestamp;
+ findAssociatedData(notifier).update(timestamp);
}
void Metric::sensorUpdated(interfaces::Sensor& notifier, uint64_t timestamp,
double value)
{
- MetricValue& mv = findMetric(notifier);
- mv.timestamp = timestamp;
- mv.value = value;
+ findAssociatedData(notifier).update(timestamp, value);
}
-MetricValue& Metric::findMetric(interfaces::Sensor& notifier)
+Metric::CollectionData&
+ Metric::findAssociatedData(const interfaces::Sensor& notifier)
{
auto it = std::find_if(
sensors.begin(), sensors.end(),
[¬ifier](const auto& sensor) { return sensor.get() == ¬ifier; });
auto index = std::distance(sensors.begin(), it);
- return readings.at(index);
+ return *collectionAlgorithms.at(index);
}
LabeledMetricParameters Metric::dumpConfiguration() const
@@ -63,10 +184,50 @@
});
return LabeledMetricParameters(std::move(sensorPath), operationType, id,
- metadata, timeScope, collectionDuration);
+ metadata, collectionTimeScope,
+ collectionDuration);
}
-void Metric::tryUnpackJsonMetadata()
+std::vector<std::unique_ptr<Metric::CollectionData>>
+ Metric::makeCollectionData(size_t size, OperationType op,
+ CollectionTimeScope timeScope,
+ CollectionDuration duration)
+{
+ using namespace std::string_literals;
+
+ std::vector<std::unique_ptr<Metric::CollectionData>> result;
+
+ result.reserve(size);
+
+ switch (timeScope)
+ {
+ case CollectionTimeScope::interval:
+ std::generate_n(
+ std::back_inserter(result), size,
+ [cf = details::makeCollectionFunction(op), duration] {
+ return std::make_unique<DataInterval>(cf, duration);
+ });
+ break;
+ case CollectionTimeScope::point:
+ std::generate_n(std::back_inserter(result), size,
+ [] { return std::make_unique<DataPoint>(); });
+ break;
+ case CollectionTimeScope::startup:
+ std::generate_n(std::back_inserter(result), size,
+ [cf = details::makeCollectionFunction(op)] {
+ return std::make_unique<DataStartup>(cf);
+ });
+ break;
+ default:
+ throw std::runtime_error("timeScope: "s +
+ utils::enumToString(timeScope) +
+ " is not supported"s);
+ }
+
+ return result;
+}
+
+void Metric::attemptUnpackJsonMetadata()
{
using MetricMetadata =
utils::LabeledTuple<std::tuple<std::vector<std::string>>,
@@ -91,6 +252,6 @@
}
}
}
- catch (const nlohmann::json::parse_error&)
+ catch (const nlohmann::json::exception&)
{}
}
diff --git a/src/metric.hpp b/src/metric.hpp
index 08684da..dfed383 100644
--- a/src/metric.hpp
+++ b/src/metric.hpp
@@ -1,5 +1,6 @@
#pragma once
+#include "interfaces/clock.hpp"
#include "interfaces/metric.hpp"
#include "interfaces/sensor.hpp"
#include "interfaces/sensor_listener.hpp"
@@ -11,24 +12,36 @@
{
public:
Metric(Sensors sensors, OperationType operationType, std::string id,
- std::string metadata, CollectionTimeScope, CollectionDuration);
+ std::string metadata, CollectionTimeScope, CollectionDuration,
+ std::unique_ptr<interfaces::Clock>);
+ ~Metric();
void initialize() override;
- const std::vector<MetricValue>& getReadings() const override;
+ std::vector<MetricValue> getReadings() const override;
void sensorUpdated(interfaces::Sensor&, uint64_t) override;
void sensorUpdated(interfaces::Sensor&, uint64_t, double value) override;
LabeledMetricParameters dumpConfiguration() const override;
private:
- void tryUnpackJsonMetadata();
+ class CollectionData;
+ class DataPoint;
+ class DataInterval;
+ class DataStartup;
- MetricValue& findMetric(interfaces::Sensor&);
+ static std::vector<std::unique_ptr<CollectionData>>
+ makeCollectionData(size_t size, OperationType, CollectionTimeScope,
+ CollectionDuration);
+
+ void attemptUnpackJsonMetadata();
+ CollectionData& findAssociatedData(const interfaces::Sensor& notifier);
std::string id;
std::string metadata;
std::vector<MetricValue> readings;
Sensors sensors;
OperationType operationType;
- CollectionTimeScope timeScope;
+ CollectionTimeScope collectionTimeScope;
CollectionDuration collectionDuration;
+ std::vector<std::unique_ptr<CollectionData>> collectionAlgorithms;
+ std::unique_ptr<interfaces::Clock> clock;
};
diff --git a/src/report_factory.cpp b/src/report_factory.cpp
index fb6edf9..091739c 100644
--- a/src/report_factory.cpp
+++ b/src/report_factory.cpp
@@ -3,6 +3,7 @@
#include "metric.hpp"
#include "report.hpp"
#include "sensor.hpp"
+#include "utils/clock.hpp"
#include "utils/conversion.hpp"
#include "utils/dbus_mapper.hpp"
#include "utils/transform.hpp"
@@ -33,7 +34,8 @@
param.at_label<ts::OperationType>(), param.at_label<ts::Id>(),
param.at_label<ts::MetricMetadata>(),
param.at_label<ts::CollectionTimeScope>(),
- param.at_label<ts::CollectionDuration>());
+ param.at_label<ts::CollectionDuration>(),
+ std::make_unique<Clock>());
});
return std::make_unique<Report>(
diff --git a/src/types/collection_duration.hpp b/src/types/collection_duration.hpp
index ddeb9cc..2db8278 100644
--- a/src/types/collection_duration.hpp
+++ b/src/types/collection_duration.hpp
@@ -6,6 +6,7 @@
#include <nlohmann/json.hpp>
#include <chrono>
+#include <cstdint>
BOOST_STRONG_TYPEDEF(Milliseconds, CollectionDuration)
diff --git a/src/types/duration_type.hpp b/src/types/duration_type.hpp
new file mode 100644
index 0000000..69d3862
--- /dev/null
+++ b/src/types/duration_type.hpp
@@ -0,0 +1,6 @@
+#pragma once
+
+#include <chrono>
+#include <cstdint>
+
+using Milliseconds = std::chrono::duration<uint64_t, std::milli>;
diff --git a/src/utils/clock.hpp b/src/utils/clock.hpp
new file mode 100644
index 0000000..00fd8d0
--- /dev/null
+++ b/src/utils/clock.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "interfaces/clock.hpp"
+#include "types/duration_type.hpp"
+
+#include <chrono>
+
+class Clock : public interfaces::Clock
+{
+ public:
+ time_point now() const noexcept override
+ {
+ return std::chrono::steady_clock::now();
+ }
+
+ uint64_t timestamp() const noexcept override
+ {
+ return std::chrono::time_point_cast<Milliseconds>(now())
+ .time_since_epoch()
+ .count();
+ }
+};