Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions common/include/villas/hist.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#pragma once

#include <string>
#include <vector>

#include <jansson.h>
Expand Down Expand Up @@ -60,6 +61,9 @@ class Hist {
// Write the histogram in JSON format to file \p f.
int dumpJson(FILE *f) const;

std::string toPrometheusText(const std::string &metric_name,
const std::string &node_name) const;

// Build a libjansson / JSON object of the histogram.
json_t *toJson() const;

Expand Down
47 changes: 40 additions & 7 deletions common/lib/hist.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,16 @@ void Hist::put(double value) {
if (data.size()) {
if (total < warmup) {
// We are still in warmup phase... Waiting for more samples...
} else if (data.size() && total == warmup && warmup != 0) {
low = getMean() - 3 * getStddev();
high = getMean() + 3 * getStddev();
} else if (total == warmup) {
if (warmup != 0) {
low = getMean() - 3 * getStddev();
high = getMean() + 3 * getStddev();
} else {
low = -10;
high = 10;
}

resolution = (high - low) / data.size();
} else if (data.size() && (total == warmup) && (warmup == 0)) {
// There is no warmup phase
// TODO resolution = ?
} else {
idx_t idx = std::round((value - low) / resolution);

Expand Down Expand Up @@ -136,7 +139,7 @@ void Hist::plot(Logger logger) const {
table.header();

for (size_t i = 0; i < data.size(); i++) {
double value = low + (i)*resolution;
double value = low + (double)i * resolution;
Hist::cnt_t cnt = data[i];
int bar = cols[2].getWidth() * ((double)cnt / max);

Expand All @@ -150,6 +153,36 @@ void Hist::plot(Logger logger) const {
}
}

std::string Hist::toPrometheusText(const std::string &metric_name,
const std::string &node_name) const {
std::stringstream base;
base << "#TYPE HISTOGRAM " << metric_name;

// Needed because Prometheus understands quantiles.
cnt_t cumsum = 0;
for (size_t i = 0; i < data.size(); i++) {
double value = low + ((double)i + 0.5) * resolution;

if (cumsum <= UINTMAX_MAX - data[i]) {
cumsum += data[i];
} else {
cumsum = UINTMAX_MAX; // Avoid overflow
}

base << "\n"
<< metric_name << " {node=\"" << node_name << "\" le=\"" << value
<< "\"} " << cumsum;
}

base << "\n"
<< metric_name << " {node=\"" << node_name << "\" le=\"+Inf\"} " << total
<< "\n"
<< metric_name << "_count "
<< " {node=\"" << node_name << "\"} " << total;

return base.str();
}

char *Hist::dump() const {
char *buf = new char[128];
if (!buf)
Expand Down
1 change: 1 addition & 0 deletions lib/api/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ set(API_SRC
requests/node_stats.cpp
requests/node_stats_reset.cpp
requests/node_file.cpp
requests/metrics.cpp
requests/paths.cpp
requests/path_info.cpp
requests/path_action.cpp
Expand Down
70 changes: 70 additions & 0 deletions lib/api/requests/metrics.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* The metrics API ressource.
*
* Author: Steffen Vogel <post@steffenvogel.de>
* Author: Youssef Nakti <naktiyoussef@proton.me>
* SPDX-FileCopyrightText: 2014-2023 Institute for Automation of Complex Power Systems, RWTH Aachen University
* SPDX-License-Identifier: Apache-2.0
*/

#include <chrono>

#include <jansson.h>

#include <villas/api/request.hpp>
#include <villas/api/response.hpp>
#include <villas/api/session.hpp>
#include <villas/node.hpp>
#include <villas/stats.hpp>
#include <villas/super_node.hpp>
#include <villas/utils.hpp>

namespace villas {
namespace node {
namespace api {

class MetricsRequest : public Request {
public:
using Request::Request;

virtual Response *execute() {
if (method != Session::Method::GET) {
throw InvalidMethod(this);
Comment thread
stv0g marked this conversation as resolved.
}

if (body != nullptr) {
throw BadRequest("The metrics endpoint does not accept any body data");
}

std::stringstream ss;
NodeList node_list = session->getSuperNode()->getNodes();

for (Node *node : node_list) {
auto stats = node->getStats();
if (!stats)
continue;

std::string node_name = node->getNameShort();
for (auto &metric : Stats::metrics) {
std::string metric_name = metric.second.name;
std::replace(metric_name.begin(), metric_name.end(), '.', '_');
ss << stats->getHistogram(metric.first)
.toPrometheusText(metric_name, node->getNameShort())
<< "\n\n";
}
}

auto str = ss.str();
return new Response(session, HTTP_STATUS_OK, "text/plain; charset=UTF-8",
Buffer(str.c_str(), str.size()));
}
};

// Register API request
static char n[] = "metrics";
static char r[] = "/metrics";
static char d[] = "Get stats of all nodes in Prometheus metrics format";
static RequestPlugin<MetricsRequest, n, r, d> p;

} // namespace api
} // namespace node
} // namespace villas
158 changes: 158 additions & 0 deletions tests/integration/api-metrics.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env bash
#
# Integration test for remote API
#
# Author: Steffen Vogel <steffen.vogel@opal-rt.com>
# SPDX-FileCopyrightText: 2025 OPAL-RT Germany GmbH
# SPDX-License-Identifier: Apache-2.0

set -e

DIR=$(mktemp -d)
pushd ${DIR}

function finish {
popd
rm -rf ${DIR}
}
trap finish EXIT

cat > config.json <<EOF
{
"http": {
"port": 8080
},
"nodes": {
"node1": {
"type": "signal",

"signal": "sine",
"limit": 10,
"values": 1,

"in": {
"hooks": [
{
"type": "stats",
"warmup": 0,
"buckets": 5
}
]
}
}
},
"paths": [
{
"in": "node1"
}
]
}
EOF

cat <<EOF > expected
#TYPE HISTOGRAM rtp_jitter
rtp_jitter {node="node1" le="0"} 0
rtp_jitter {node="node1" le="0"} 0
rtp_jitter {node="node1" le="0"} 0
rtp_jitter {node="node1" le="0"} 0
rtp_jitter {node="node1" le="0"} 0
rtp_jitter {node="node1" le="+Inf"} 0
rtp_jitter_count {node="node1"} 0

#TYPE HISTOGRAM rtp_pkts_lost
rtp_pkts_lost {node="node1" le="0"} 0
rtp_pkts_lost {node="node1" le="0"} 0
rtp_pkts_lost {node="node1" le="0"} 0
rtp_pkts_lost {node="node1" le="0"} 0
rtp_pkts_lost {node="node1" le="0"} 0
rtp_pkts_lost {node="node1" le="+Inf"} 0
rtp_pkts_lost_count {node="node1"} 0

#TYPE HISTOGRAM rtp_loss_fraction
rtp_loss_fraction {node="node1" le="0"} 0
rtp_loss_fraction {node="node1" le="0"} 0
rtp_loss_fraction {node="node1" le="0"} 0
rtp_loss_fraction {node="node1" le="0"} 0
rtp_loss_fraction {node="node1" le="0"} 0
rtp_loss_fraction {node="node1" le="+Inf"} 0
rtp_loss_fraction_count {node="node1"} 0

#TYPE HISTOGRAM signal_cnt
signal_cnt {node="node1" le="-8"} 0
signal_cnt {node="node1" le="-4"} 0
signal_cnt {node="node1" le="0"} 0
signal_cnt {node="node1" le="4"} 9
signal_cnt {node="node1" le="8"} 9
signal_cnt {node="node1" le="+Inf"} 10
signal_cnt_count {node="node1"} 10

#TYPE HISTOGRAM age
age {node="node1" le="0"} 0
age {node="node1" le="0"} 0
age {node="node1" le="0"} 0
age {node="node1" le="0"} 0
age {node="node1" le="0"} 0
age {node="node1" le="+Inf"} 0
age_count {node="node1"} 0

#TYPE HISTOGRAM owd
owd {node="node1" le="-8"} 0
owd {node="node1" le="-4"} 0
owd {node="node1" le="0"} 0
owd {node="node1" le="4"} 8
owd {node="node1" le="8"} 8
owd {node="node1" le="+Inf"} 9
owd_count {node="node1"} 9

#TYPE HISTOGRAM gap_received
gap_received {node="node1" le="-8"} 0
gap_received {node="node1" le="-4"} 0
gap_received {node="node1" le="0"} 0
gap_received {node="node1" le="4"} 8
gap_received {node="node1" le="8"} 8
gap_received {node="node1" le="+Inf"} 9
gap_received_count {node="node1"} 9

#TYPE HISTOGRAM gap_sent
gap_sent {node="node1" le="-8"} 0
gap_sent {node="node1" le="-4"} 0
gap_sent {node="node1" le="0"} 0
gap_sent {node="node1" le="4"} 8
gap_sent {node="node1" le="8"} 8
gap_sent {node="node1" le="+Inf"} 9
gap_sent_count {node="node1"} 9

#TYPE HISTOGRAM reordered
reordered {node="node1" le="0"} 0
reordered {node="node1" le="0"} 0
reordered {node="node1" le="0"} 0
reordered {node="node1" le="0"} 0
reordered {node="node1" le="0"} 0
reordered {node="node1" le="+Inf"} 0
reordered_count {node="node1"} 0

#TYPE HISTOGRAM skipped
skipped {node="node1" le="0"} 0
skipped {node="node1" le="0"} 0
skipped {node="node1" le="0"} 0
skipped {node="node1" le="0"} 0
skipped {node="node1" le="0"} 0
skipped {node="node1" le="+Inf"} 0
skipped_count {node="node1"} 0

EOF

# Start VILLASnode instance with local config
villas node config.json &

# Wait for node to complete init
sleep 2

# Fetch config via API
curl -s http://localhost:8080/api/v2/metrics > metrics

# Shutdown VILLASnode
kill $!

# Check if metrics contain expected values
diff -u expected metrics
32 changes: 16 additions & 16 deletions tests/integration/api-nodes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,29 @@ cat > config.json <<EOF
},
"nodes": {
"testnode1": {
"type": "websocket",
"dummy": "value1"
"type": "websocket",
"dummy": "value1"
},
"testnode2": {
"type": "socket",
"dummy": "value2",
"type": "socket",
"dummy": "value2",

"in": {
"address": "*:12001",
"signals": [
{ "name": "sig1", "unit": "Volts", "type": "float", "init": 123.0 },
{ "name": "sig2", "unit": "Ampere", "type": "integer", "init": 123 }
]
},
"out": {
"address": "127.0.0.1:12000"
}
"in": {
"address": "*:12001",
"signals": [
{ "name": "sig1", "unit": "Volts", "type": "float", "init": 123.0 },
{ "name": "sig2", "unit": "Ampere", "type": "integer", "init": 123 }
]
},
"out": {
"address": "127.0.0.1:12000"
}
}
},
"paths": [
{
"in": "testnode2",
"out": "testnode1"
"in": "testnode2",
"out": "testnode1"
}
]
}
Expand Down
Loading