Skip to content
Merged
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ if(CODE_COVERAGE)
LCOV_ARGS --ignore-errors unused
EXCLUDE
"3rd/*"
"cmd/*"
"libs/*"
"${PROJECT_BINARY_DIR}/*"
"/usr/*"
Expand Down
48 changes: 48 additions & 0 deletions libs/visor_test/catch2/otel_helpers.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */

#pragma once

#include <cstdint>
#include <opentelemetry/proto/metrics/v1/metrics.pb.h>
#include <string>

namespace visor::test {

// Walk an OTLP ScopeMetrics and return the first int gauge data point of the
// named metric, or -1 if not found. All pktvisor metrics are emitted as gauges
// (see Metric::to_opentelemetry in src/Metrics.cpp), so this covers Counters,
// Rates, Cardinalities, and the like uniformly.
inline int64_t otel_gauge_value(const opentelemetry::proto::metrics::v1::ScopeMetrics &scope, const std::string &name)
{
for (int i = 0; i < scope.metrics_size(); ++i) {
const auto &m = scope.metrics(i);
if (m.name() == name && m.has_gauge() && m.gauge().data_points_size() > 0) {
return m.gauge().data_points(0).as_int();
}
}
return -1;
}

// Sum every int gauge data point of the named metric across all label sets.
// Useful for handlers (e.g. DNS v2, Net v2) that emit one data point per
// `direction` value — counters get sliced into per-direction series and a
// caller asking for the project total wants them summed.
inline int64_t otel_gauge_sum(const opentelemetry::proto::metrics::v1::ScopeMetrics &scope, const std::string &name)
{
int64_t total = 0;
bool found = false;
for (int i = 0; i < scope.metrics_size(); ++i) {
const auto &m = scope.metrics(i);
if (m.name() == name && m.has_gauge()) {
for (const auto &p : m.gauge().data_points()) {
total += p.as_int();
found = true;
}
}
}
return found ? total : -1;
}

}
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ add_executable(unit-tests-visor-core
tests/test_taps.cpp
tests/test_policies.cpp
tests/test_handlers.cpp
tests/test_module_plugins.cpp
)

target_include_directories(unit-tests-visor-core PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
Expand Down
40 changes: 40 additions & 0 deletions src/handlers/bgp/tests/test_bgp_layer.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/catch_test_visor.hpp>
#include <catch2/otel_helpers.hpp>

#include <opentelemetry/proto/metrics/v1/metrics.pb.h>
#include <sstream>

#include "PcapInputStream.h"
#include "BgpStreamHandler.h"
Expand Down Expand Up @@ -50,3 +54,39 @@ TEST_CASE("Parse BGP tests", "[pcap][bgp]")
CHECK(counters.total.value() == 9);

}

TEST_CASE("BGP to_prometheus and to_opentelemetry backends", "[pcap][bgp][backends]")
{
PcapInputStream stream{"pcap-test"};
stream.config_set("pcap_file", "tests/fixtures/bgp.pcap");
stream.config_set("bpf", "");

visor::Config c;
c.config_set<uint64_t>("num_periods", 1);
auto stream_proxy = stream.add_event_proxy(c);
BgpStreamHandler bgp_handler{"bgp-test", stream_proxy, &c};

bgp_handler.start();
stream.start();
bgp_handler.stop();
stream.stop();

// Counter values come from the existing parse test: total=9, OPEN=2,
// UPDATE=4, KEEPALIVE=3. They must round-trip identically through both
// backends.
std::stringstream prom;
bgp_handler.metrics()->bucket(0)->to_prometheus(prom, {});
auto prom_text = prom.str();
CHECK(prom_text.find("bgp_wire_packets_total{} 9") != std::string::npos);
CHECK(prom_text.find("bgp_wire_packets_open{} 2") != std::string::npos);
CHECK(prom_text.find("bgp_wire_packets_update{} 4") != std::string::npos);
CHECK(prom_text.find("bgp_wire_packets_keepalive{} 3") != std::string::npos);

opentelemetry::proto::metrics::v1::ScopeMetrics scope;
timespec start_ts{}, end_ts{};
bgp_handler.metrics()->bucket(0)->to_opentelemetry(scope, start_ts, end_ts, {});
using visor::test::otel_gauge_value;
CHECK(otel_gauge_value(scope, "bgp_wire_packets_total") == 9);
CHECK(otel_gauge_value(scope, "bgp_wire_packets_open") == 2);
CHECK(otel_gauge_value(scope, "bgp_wire_packets_keepalive") == 3);
}
39 changes: 39 additions & 0 deletions src/handlers/dhcp/tests/test_dhcp_layer.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/catch_test_visor.hpp>
#include <catch2/otel_helpers.hpp>

#include <opentelemetry/proto/metrics/v1/metrics.pb.h>
#include <sstream>

#include "DhcpStreamHandler.h"
#include "PcapInputStream.h"
Expand Down Expand Up @@ -92,3 +96,38 @@ TEST_CASE("Parse DHCP V6 tests", "[pcap][dhcp]")
CHECK(j["top_clients"][0]["name"] == nullptr);
CHECK(j["top_servers"][0]["name"] == "08:00:27:d4:10:bb/fe80::a00:27ff:fed4:10bb");
}

TEST_CASE("DHCP to_prometheus and to_opentelemetry backends", "[pcap][dhcp][backends]")
{
PcapInputStream stream{"pcap-test"};
stream.config_set("pcap_file", "tests/fixtures/dhcp-flow.pcap");
stream.config_set("bpf", "");

visor::Config c;
c.config_set<uint64_t>("num_periods", 1);
auto stream_proxy = stream.add_event_proxy(c);
DhcpStreamHandler dhcp_handler{"dhcp-test", stream_proxy, &c};

dhcp_handler.start();
stream.start();
dhcp_handler.stop();
stream.stop();

// Counter values come from "Parse DHCP tests": DISCOVER=1, OFFER=1,
// REQUEST=3, ACK=3. Round-trip through both backends.
std::stringstream prom;
dhcp_handler.metrics()->bucket(0)->to_prometheus(prom, {});
auto prom_text = prom.str();
CHECK(prom_text.find("dhcp_wire_packets_discover{} 1") != std::string::npos);
CHECK(prom_text.find("dhcp_wire_packets_offer{} 1") != std::string::npos);
CHECK(prom_text.find("dhcp_wire_packets_request{} 3") != std::string::npos);
CHECK(prom_text.find("dhcp_wire_packets_ack{} 3") != std::string::npos);

opentelemetry::proto::metrics::v1::ScopeMetrics scope;
timespec start_ts{}, end_ts{};
dhcp_handler.metrics()->bucket(0)->to_opentelemetry(scope, start_ts, end_ts, {});
using visor::test::otel_gauge_value;
CHECK(otel_gauge_value(scope, "dhcp_wire_packets_discover") == 1);
CHECK(otel_gauge_value(scope, "dhcp_wire_packets_request") == 3);
CHECK(otel_gauge_value(scope, "dhcp_wire_packets_ack") == 3);
}
83 changes: 83 additions & 0 deletions src/handlers/dns/v1/tests/test_dns_layer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/catch_test_visor.hpp>

#include <catch2/otel_helpers.hpp>
#include <opentelemetry/proto/metrics/v1/metrics.pb.h>
#include <sstream>

#include "DnsStreamHandler.h"
#include "GeoDB.h"
#include "PcapInputStream.h"
Expand Down Expand Up @@ -1053,3 +1057,82 @@ TEST_CASE("DNS Filters: only_rcode with predicate", "[pcap][dns][filter]")
nlohmann::json j;
dns_handler_2.metrics()->bucket(0)->to_json(j);
}

TEST_CASE("dns to_prometheus and to_opentelemetry backends", "[pcap][dns][backends]")
{
visor::input::pcap::PcapInputStream stream{"pcap-test"};
stream.config_set("pcap_file", "tests/fixtures/dns_ipv4_udp.pcap");
stream.config_set("bpf", "");

visor::Config c;
c.config_set<uint64_t>("num_periods", 1);
auto stream_proxy = stream.add_event_proxy(c);
visor::handler::dns::DnsStreamHandler handler{"dns-test", stream_proxy, &c};

handler.start();
stream.start();
handler.stop();
stream.stop();

// Counter values match the existing "Parse DNS UDP IPv4 tests" case:
// UDP=140, IPv4=140, queries=70, replies=70.
std::stringstream prom;
handler.metrics()->bucket(0)->to_prometheus(prom, {});
auto prom_text = prom.str();
CHECK(prom_text.find("dns_wire_packets_udp{} 140") != std::string::npos);
CHECK(prom_text.find("dns_wire_packets_ipv4{} 140") != std::string::npos);
CHECK(prom_text.find("dns_wire_packets_queries{} 70") != std::string::npos);
CHECK(prom_text.find("dns_wire_packets_replies{} 70") != std::string::npos);

opentelemetry::proto::metrics::v1::ScopeMetrics scope;
timespec start_ts{}, end_ts{};
handler.metrics()->bucket(0)->to_opentelemetry(scope, start_ts, end_ts, {});
using visor::test::otel_gauge_value;
CHECK(otel_gauge_value(scope, "dns_wire_packets_udp") == 140);
CHECK(otel_gauge_value(scope, "dns_wire_packets_queries") == 70);
CHECK(otel_gauge_value(scope, "dns_wire_packets_replies") == 70);
}

TEST_CASE("DNS v1 process_dns_layer(l3,l4,QR) shallow overload", "[dns][unit]")
{
// Exercises the no-payload overload of DnsMetricsBucket::process_dns_layer
// that's used when full packet info isn't available (e.g. filter pre-pass).
PcapInputStream stream{"pcap-test"};
stream.config_set("pcap_file", "tests/fixtures/dns_udp_tcp_random.pcap");
stream.config_set("bpf", "");

visor::Config c;
c.config_set<uint64_t>("num_periods", 1);
auto stream_proxy = stream.add_event_proxy(c);
visor::handler::dns::DnsStreamHandler handler{"dns-unit", stream_proxy, &c};
handler.start();

auto *bucket = const_cast<visor::handler::dns::DnsMetricsBucket *>(handler.metrics()->bucket(0));
// Snapshot counters before our direct calls so we can assert deltas
// independent of what the existing PCAP feed already produced.
using visor::test::otel_gauge_value;
auto snapshot = [&](const std::string &name) {
opentelemetry::proto::metrics::v1::ScopeMetrics s;
timespec st{}, et{};
bucket->to_opentelemetry(s, st, et, {});
return otel_gauge_value(s, name);
};
auto q0 = snapshot("dns_wire_packets_queries");
auto r0 = snapshot("dns_wire_packets_replies");
auto u0 = snapshot("dns_wire_packets_udp");
auto t0 = snapshot("dns_wire_packets_tcp");

// Two UDP queries, one UDP response, one TCP query → +3 queries, +1 reply,
// +3 udp, +1 tcp.
bucket->process_dns_layer(pcpp::UDP, visor::handler::dns::Protocol::PCPP_UDP, visor::lib::dns::QR::query);
bucket->process_dns_layer(pcpp::UDP, visor::handler::dns::Protocol::PCPP_UDP, visor::lib::dns::QR::query);
bucket->process_dns_layer(pcpp::UDP, visor::handler::dns::Protocol::PCPP_UDP, visor::lib::dns::QR::response);
bucket->process_dns_layer(pcpp::TCP, visor::handler::dns::Protocol::PCPP_TCP, visor::lib::dns::QR::query);

CHECK(snapshot("dns_wire_packets_queries") == q0 + 3);
CHECK(snapshot("dns_wire_packets_replies") == r0 + 1);
CHECK(snapshot("dns_wire_packets_udp") == u0 + 3);
CHECK(snapshot("dns_wire_packets_tcp") == t0 + 1);

handler.stop();
}
85 changes: 85 additions & 0 deletions src/handlers/dns/v2/tests/test_dns_layer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/catch_test_visor.hpp>

#include <catch2/otel_helpers.hpp>
#include <opentelemetry/proto/metrics/v1/metrics.pb.h>
#include <sstream>

#include "DnsStreamHandler.h"
#include "GeoDB.h"
#include "PcapInputStream.h"
Expand Down Expand Up @@ -974,3 +978,84 @@ TEST_CASE("DNS invalid config", "[dns][filter][config]")
dns_handler.config_set<bool>("invalid_config", true);
REQUIRE_THROWS_WITH(dns_handler.start(), "invalid_config is an invalid/unsupported config or filter. The valid configs/filters are: exclude_noerror, only_rcode, only_dnssec_response, answer_count, only_qtype, only_qname, only_qname_suffix, geoloc_notfound, asn_notfound, dnstap_msg_type, public_suffix_list, recorded_stream, xact_ttl_secs, xact_ttl_ms, deep_sample_rate, num_periods, topn_count, topn_percentile_threshold");
}

TEST_CASE("dnsv2 to_prometheus and to_opentelemetry backends", "[pcap][dnsv2][backends]")
{
visor::input::pcap::PcapInputStream stream{"pcap-test"};
stream.config_set("pcap_file", "tests/fixtures/dns_ipv4_udp.pcap");
stream.config_set("bpf", "");

visor::Config c;
c.config_set<uint64_t>("num_periods", 1);
auto stream_proxy = stream.add_event_proxy(c);
visor::handler::dns::v2::DnsStreamHandler handler{"dnsv2-test", stream_proxy, &c};

handler.start();
stream.start();
handler.stop();
stream.stop();

// DNS v2 slices counters by `direction` label (in/out/unknown), so we
// sum across all data points to get the project total. The fixture has
// 70 query/reply pairs over UDP IPv4 → 70 xacts total.
std::stringstream prom;
handler.metrics()->bucket(0)->to_prometheus(prom, {});
CHECK(prom.str().find("dns_xacts{") != std::string::npos);

opentelemetry::proto::metrics::v1::ScopeMetrics scope;
timespec start_ts{}, end_ts{};
handler.metrics()->bucket(0)->to_opentelemetry(scope, start_ts, end_ts, {});
using visor::test::otel_gauge_sum;
CHECK(otel_gauge_sum(scope, "dns_xacts") == 70);
CHECK(otel_gauge_sum(scope, "dns_udp_xacts") == 70);
CHECK(otel_gauge_sum(scope, "dns_ipv4_xacts") == 70);
}

TEST_CASE("DNS v2 specialized_merge aggregates two buckets", "[dns][unit]")
{
auto run = [](const std::string &name,
std::shared_ptr<visor::input::pcap::PcapInputStream> &stream,
std::shared_ptr<visor::Config> &c,
std::shared_ptr<visor::handler::dns::v2::DnsStreamHandler> &h) {
stream = std::make_shared<visor::input::pcap::PcapInputStream>(name + "-stream");
stream->config_set("pcap_file", std::string("tests/fixtures/dns_udp_tcp_random.pcap"));
stream->config_set("bpf", std::string(""));
c = std::make_shared<visor::Config>();
c->config_set<uint64_t>("num_periods", 1);
auto proxy = stream->add_event_proxy(*c);
h = std::make_shared<visor::handler::dns::v2::DnsStreamHandler>(name, proxy, c.get());
h->start();
stream->start();
h->stop();
stream->stop();
};

std::shared_ptr<visor::input::pcap::PcapInputStream> s1, s2;
std::shared_ptr<visor::Config> c1, c2;
std::shared_ptr<visor::handler::dns::v2::DnsStreamHandler> h1, h2;
run("dns-merge-1", s1, c1, h1);
run("dns-merge-2", s2, c2, h2);

auto *target = const_cast<visor::handler::dns::v2::DnsMetricsBucket *>(h1->metrics()->bucket(0));

// Capture per-bucket counters before merging so we can assert the sum.
// DNS v2 emits per-direction; sum across data points.
using visor::test::otel_gauge_sum;
auto snapshot_xacts = [](const visor::handler::dns::v2::DnsMetricsBucket *b) {
opentelemetry::proto::metrics::v1::ScopeMetrics s;
timespec st{}, et{};
b->to_opentelemetry(s, st, et, {});
return otel_gauge_sum(s, "dns_xacts");
};
auto pre_b1 = snapshot_xacts(h1->metrics()->bucket(0));
auto pre_b2 = snapshot_xacts(h2->metrics()->bucket(0));
REQUIRE(pre_b1 > 0);
REQUIRE(pre_b2 > 0);

REQUIRE_NOTHROW(target->specialized_merge(*h2->metrics()->bucket(0), visor::Metric::Aggregate::DEFAULT));

opentelemetry::proto::metrics::v1::ScopeMetrics scope_after;
timespec st{}, et{};
target->to_opentelemetry(scope_after, st, et, {});
CHECK(otel_gauge_sum(scope_after, "dns_xacts") == pre_b1 + pre_b2);
}
Loading
Loading