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