diff --git a/src/combinator.cpp b/src/combinator.cpp index c0aa5d6..d058e97 100644 --- a/src/combinator.cpp +++ b/src/combinator.cpp @@ -18,6 +18,7 @@ // You should have received a copy of the GNU General Public License // along with metricq-combinator. If not, see . #include "combinator.hpp" +#include "display_expression.hpp" #include #include @@ -25,7 +26,7 @@ #include -#include +#include using Log = metricq::logger::nitro::Log; @@ -93,6 +94,17 @@ void Combinator::on_transformer_config(const metricq::json& config) // Register the combined metric as a new source metric auto& metric = (*this)[combined_name]; + try + { + metric.metadata["displayExpression"] = displayExpression(combined_expression); + } + catch (const std::runtime_error& e) + { + Log::error() << fmt::format( + "Failed to create the Display Expression, metric: {}, error: {}", metric.id(), + e.what()); + } + if (combined_config.count("chunk_size")) { auto chunk_size = combined_config["chunk_size"].get(); diff --git a/src/display_expression.hpp b/src/display_expression.hpp new file mode 100644 index 0000000..470a8a1 --- /dev/null +++ b/src/display_expression.hpp @@ -0,0 +1,174 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +inline std::string toFormattedString(double value) +{ + // Special case for integers + if (std::round(value) == value) + { + return std::to_string(static_cast(value)); + } + + std::ostringstream oss; + oss << std::defaultfloat << std::setprecision(15); + oss << value; + std::string str = oss.str(); + + if (str.find('e') != std::string::npos) + { + oss.str(""); + oss << std::fixed << std::setprecision(15); + oss << value; + str = oss.str(); + } + + size_t decimal_pos = str.find('.'); + if (decimal_pos != std::string::npos) + { + // Remove trailing zeros + str.erase(str.find_last_not_of('0') + 1, std::string::npos); + + if (str.back() == '.') + { + str.pop_back(); + } + + // Handle floating point representation issues + if (str.length() > decimal_pos + 15) + { + str = str.substr(0, decimal_pos + 15); + // Remove trailing zeros again + str.erase(str.find_last_not_of('0') + 1, std::string::npos); + if (str.back() == '.') + { + str.pop_back(); + } + } + } + + return str; +} + +inline std::string handleBasicExpression(const nlohmann::json& expression) +{ + if (expression.is_number()) + { + const auto value = expression.get(); + + return toFormattedString(value); + } + + if (expression.is_string()) + { + return expression.get(); + } + + throw std::invalid_argument("Expression is not a basic type (number or string)!"); +} + +inline std::string handleOperatorExpression(const std::string& operation, + const std::string& leftStr, const std::string& rightStr) +{ + if (operation.size() > 1) + { + throw std::invalid_argument("Invalid operator length!"); + } + + switch (operation[0]) + { + case '+': + case '-': + case '*': + case '/': + return "(" + leftStr + " " + operation + " " + rightStr + ")"; + default: + throw std::invalid_argument("Invalid operator: " + operation); + } +} + +inline std::string handleCombinationExpression(const std::string& operation, + const std::vector& inputs) +{ + static const std::unordered_set validAggregates = { "sum", "min", "max" }; + + if (validAggregates.find(operation) == validAggregates.end()) + { + throw std::invalid_argument("Invalid aggregate operation: " + operation); + } + + if (inputs.empty()) + { + throw std::invalid_argument("Aggregate operation missing inputs!"); + } + + auto input = std::accumulate(std::next(inputs.begin()), inputs.end(), inputs[0], + [](std::string a, const std::string& b) { return a + ", " + b; }); + + return operation + "[" + input + "]"; +} + +inline std::string buildExpression(const nlohmann::json& expression) +{ + if (expression.is_number() || expression.is_string()) + { + return handleBasicExpression(expression); + } + + if (!expression.is_object() || !expression.contains("operation")) + { + throw std::invalid_argument("Unknown expression format!"); + } + + std::string operation = expression.value("operation", ""); + + if (operation == "throttle") + { + if (!expression.contains("input")) + { + throw std::invalid_argument("Throttle does not contain a input"); + } + return handleBasicExpression(expression["input"]); + } + + if (expression.contains("left") && expression.contains("right")) + { + std::string leftStr = buildExpression(expression["left"]); + std::string rightStr = buildExpression(expression["right"]); + return handleOperatorExpression(operation, leftStr, rightStr); + } + + if (expression.contains("inputs")) + { + if (!expression["inputs"].is_array()) + { + throw std::invalid_argument("Inputs must be an array!"); + } + + std::vector inputStrings; + for (const auto& input : expression["inputs"]) + { + inputStrings.push_back(buildExpression(input)); + } + return handleCombinationExpression(operation, inputStrings); + } + + throw std::invalid_argument("Unsupported operation type: " + operation); +} + +inline std::string displayExpression(const nlohmann::json& expression) +{ + std::string result = buildExpression(expression); + if (!result.empty() && result.front() == '(' && result.back() == ')') + { + result = result.substr(1, result.size() - 2); + } + + return result; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5a8a92a..2cb260e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,8 +1,8 @@ -add_executable(metricq-combinator.test_single_derivation test_single_derivation.cpp) -add_test(metricq-combinator.test_single_derivation metricq-combinator.test_single_derivation) +macro(add_test_case test_name) + add_executable(${test_name} ${test_name}.cpp) + add_test(${test_name} ${test_name}) + target_link_libraries(${test_name} PRIVATE metricq-combinator-lib) +endmacro() -target_link_libraries( - metricq-combinator.test_single_derivation - PRIVATE - metricq-combinator-lib -) +add_test_case(test_single_derivation) +add_test_case(test_display_expression) diff --git a/tests/test_display_expression.cpp b/tests/test_display_expression.cpp new file mode 100644 index 0000000..5181751 --- /dev/null +++ b/tests/test_display_expression.cpp @@ -0,0 +1,110 @@ +#include "../src/display_expression.hpp" +#include +#include + +struct TestMakeString +{ + double input; + std::string expected; +}; + +struct TestDisplayExpression +{ + nlohmann::json input; + std::string expected; +}; + +void runMakeStringTests() +{ + std::vector stringTestCases = { { 100, "100" }, + { 10.0, "10" }, + { 10.3, "10.3" }, + { 10.25, "10.25" }, + { 0.125, "0.125" }, + { 0.123456789, "0.123456789" }, + { 123456789, "123456789" }, + { 123456789.01234, "123456789.01234" } }; + + for (const auto& testCase : stringTestCases) + { + std::string result = toFormattedString(testCase.input); + if (result != testCase.expected) + { + std::cerr << "Test case " << testCase.input << " failed:\n"; + std::cerr << "Expected: " << testCase.expected << "\n"; + std::cerr << "Got: " << result << "\n"; + exit(1); + } + } + std::cout << "All toFormattedString test cases passed successfully!" << std::endl; +} + +void testExpression() +{ + std::vector testCases; + + testCases.emplace_back(TestDisplayExpression{ + { { "expression", + { { "operation", "*" }, + { "left", 5 }, + { "right", { { "operation", "-" }, { "left", 45 }, { "right", 3 } } } } } }, + "5 * (45 - 3)" }); + + testCases.emplace_back(TestDisplayExpression{ + { { "expression", + { { "operation", "*" }, + { "left", { { "operation", "+" }, { "left", 1 }, { "right", 2 } } }, + { "right", + { { "operation", "-" }, { "left", 10 }, { "right", "dummy.source" } } } } } }, + "(1 + 2) * (10 - dummy.source)" }); + + testCases.emplace_back(TestDisplayExpression{ + { { "expression", + { { "operation", "-" }, + { "left", + { { "operation", "+" }, + { "left", 15.3 }, + { "right", { { "operation", "min" }, { "inputs", { 42, 24, 8, 12 } } } } } }, + { "right", + { { "operation", "throttle" }, + { "cooldown_period", "42" }, + { "input", 8 } } } } } }, + "(15.3 + min[42, 24, 8, 12]) - 8" }); + + // Test the cases + for (const auto& testCase : testCases) + { + + auto expression = testCase.input["expression"]; + std::string result = displayExpression(expression); + + // Compare with expected result + if (result != testCase.expected) // comparing with the expected output + { + std::cerr << "Test case " << testCase.input << " failed:\n"; + std::cerr << "Expected: " << testCase.expected << "\n"; + std::cerr << "Got: " << result << "\n"; + return; + } + } + + std::cout << "All displayExpression test cases passed successfully!" << std::endl; +} + +int main() +{ + try + { + testExpression(); + runMakeStringTests(); + + std::cout << "All tests passed successfully!" << std::endl; + } + catch (const std::exception& e) + { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + return 0; +}