Repository: stackrox/collector
Pull request #2065: Use RAII on CollectorService and system-inspector/Service
Author: login: Molter73
Created: 2025-03-19T16:18:01Z | Merged: 2025-03-31T08:49:23Z

## Description

This patch replaces some init methods in favour of using constructors for initializing objects in an approach closer to RAII. It also moves some logic for the CollectorService::RunForever method to the constructor and destructor of this class and moves some variables from this method to be members of the class.

Some other minor cleanups have also been applied:
- Define all default constructors, assignment operators and destructors following the rule of 5.
- Uniform formatting of cpp and header files in editorconfig.
- Generalization of handlers to be used by Civetweb server.

## Checklist
- [x] Investigated and inspected CI test results
- [ ] Updated documentation accordingly

**Automated testing**
  - [ ] Added unit tests
  - [x] Added integration tests
  - [ ] Added regression tests

If any of these don't apply, please comment below.

## Testing Performed

- [x] Manually test the prometheus endpoint, ensure stats are still being exported correctly.
- [x] Manually test the civet endpoints, ensure functionality is still correct on each one.



Diff:
diff --git a/.editorconfig b/.editorconfig
index 4aeab816f3..b2c6e17cff 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -6,7 +6,7 @@ end_of_line = lf
 insert_final_newline = true
 trim_trailing_whitespace = true
 
-[collector/{*.cpp,lib/*.cpp,lib/*.h,test/*.cpp}]
+[collector/{**.cpp,**.h}]
 indent_style = space
 indent_size = 2
 
diff --git a/collector/collector.cpp b/collector/collector.cpp
index 2c0b1df6ac..76de031649 100644
--- a/collector/collector.cpp
+++ b/collector/collector.cpp
@@ -120,6 +120,23 @@ void initialChecks() {
   }
 }
 
+void RunService(CollectorConfig& config) {
+  auto& startup_diagnostics = StartupDiagnostics::GetInstance();
+  CollectorService collector(config, &g_control, &g_signum);
+
+  if (!collector.InitKernel()) {
+    startup_diagnostics.Log();
+    CLOG(FATAL) << "Failed to initialize collector kernel components.";
+  }
+
+  // output the GPL notice only once the kernel object has been found or downloaded
+  gplNotice();
+
+  startup_diagnostics.Log();
+
+  collector.RunForever();
+}
+
 int main(int argc, char** argv) {
   // Print system information before doing actual work.
   auto& host_info = HostInfo::Instance();
@@ -174,19 +191,7 @@ int main(int argc, char** argv) {
 
   config.grpc_channel = std::move(sensor_connection);
 
-  CollectorService collector(config, &g_control, &g_signum);
-
-  if (!SetupKernelDriver(collector, config)) {
-    startup_diagnostics.Log();
-    CLOG(FATAL) << "Failed to initialize collector kernel components.";
-  }
-
-  // output the GPL notice only once the kernel object has been found or downloaded
-  gplNotice();
-
-  startup_diagnostics.Log();
-
-  collector.RunForever();
+  RunService(config);
 
   CLOG(INFO) << "Collector exiting successfully!";
 
diff --git a/collector/lib/IntrospectionEndpoint.cpp b/collector/lib/CivetWrapper.cpp
similarity index 71%
rename from collector/lib/IntrospectionEndpoint.cpp
rename to collector/lib/CivetWrapper.cpp
index 0630a38aaf..c91309a4bd 100644
--- a/collector/lib/IntrospectionEndpoint.cpp
+++ b/collector/lib/CivetWrapper.cpp
@@ -1,10 +1,10 @@
-#include "IntrospectionEndpoint.h"
+#include "CivetWrapper.h"
 
 #include <sstream>
 
 namespace collector {
 
-QueryParams IntrospectionEndpoint::ParseParameters(const char* queryString) {
+QueryParams CivetWrapper::ParseParameters(const char* queryString) {
   QueryParams params;
 
   if (queryString == nullptr) {
@@ -26,7 +26,7 @@ QueryParams IntrospectionEndpoint::ParseParameters(const char* queryString) {
   return params;
 }
 
-std::optional<std::string> IntrospectionEndpoint::GetParameter(const QueryParams& params, const std::string& paramName) {
+std::optional<std::string> CivetWrapper::GetParameter(const QueryParams& params, const std::string& paramName) {
   return params.count(paramName) != 0 ? std::make_optional(params.at(paramName)) : std::nullopt;
 }
 
diff --git a/collector/lib/IntrospectionEndpoint.h b/collector/lib/CivetWrapper.h
similarity index 80%
rename from collector/lib/IntrospectionEndpoint.h
rename to collector/lib/CivetWrapper.h
index 17b41f2690..5db1983ddf 100644
--- a/collector/lib/IntrospectionEndpoint.h
+++ b/collector/lib/CivetWrapper.h
@@ -1,15 +1,17 @@
-#ifndef _INTROSPECTION_ENDPOINT_
-#define _INTROSPECTION_ENDPOINT_
+#ifndef CIVET_WRAPPER_H
+#define CIVET_WRAPPER_H
 
 #include <CivetServer.h>
 #include <optional>
 #include <unordered_map>
 
 namespace collector {
-
 using QueryParams = std::unordered_map<std::string, std::string>;
 
-class IntrospectionEndpoint : public CivetHandler {
+class CivetWrapper : public CivetHandler {
+ public:
+  virtual const std::string& GetBaseRoute() = 0;
+
  protected:
   static QueryParams ParseParameters(const char* queryString);
   static std::optional<std::string> GetParameter(const QueryParams& params, const std::string& paramName);
@@ -21,7 +23,6 @@ class IntrospectionEndpoint : public CivetHandler {
     return mg_send_http_error(conn, 400, err) >= 0;
   }
 };
-
 }  // namespace collector
 
-#endif  // _INTROSPECTION_ENDPOINT_
+#endif
diff --git a/collector/lib/CollectorRuntimeConfigInspector.h b/collector/lib/CollectorRuntimeConfigInspector.h
index bc46d311a3..c287e85990 100644
--- a/collector/lib/CollectorRuntimeConfigInspector.h
+++ b/collector/lib/CollectorRuntimeConfigInspector.h
@@ -3,21 +3,25 @@
 
 #include <json/writer.h>
 
+#include "CivetWrapper.h"
 #include "CollectorConfig.h"
-#include "IntrospectionEndpoint.h"
 
 namespace collector {
 
-class CollectorConfigInspector : public IntrospectionEndpoint {
+class CollectorConfigInspector : public CivetWrapper {
  public:
-  static const std::string kBaseRoute;
-
   CollectorConfigInspector(const CollectorConfig& config);
 
   // implementation of CivetHandler
   bool handleGet(CivetServer* server, struct mg_connection* conn) override;
 
+  const std::string& GetBaseRoute() override {
+    return kBaseRoute;
+  }
+
  private:
+  static const std::string kBaseRoute;
+
   const CollectorConfig& config_;
   std::string configToJson(bool& isError);
 };
diff --git a/collector/lib/CollectorService.cpp b/collector/lib/CollectorService.cpp
index a42e853f90..60f975d20e 100644
--- a/collector/lib/CollectorService.cpp
+++ b/collector/lib/CollectorService.cpp
@@ -1,125 +1,121 @@
 #include "CollectorService.h"
 
-#include "CollectionMethod.h"
-#include "ConfigLoader.h"
-#include "ContainerInfoInspector.h"
-
-extern "C" {
-#include <signal.h>
-}
-
 #include <memory>
 
-#include "CivetServer.h"
+#include "CollectionMethod.h"
 #include "CollectorRuntimeConfigInspector.h"
 #include "CollectorStatsExporter.h"
+#include "ConfigLoader.h"
 #include "ConnTracker.h"
-#include "Containers.h"
+#include "ContainerInfoInspector.h"
 #include "Diagnostics.h"
 #include "GRPCUtil.h"
 #include "GetStatus.h"
 #include "LogLevel.h"
+#include "NetworkSignalHandler.h"
 #include "NetworkStatusInspector.h"
 #include "NetworkStatusNotifier.h"
 #include "ProfilerHandler.h"
 #include "Utility.h"
-#include "prometheus/exposer.h"
 #include "system-inspector/Service.h"
 
-extern unsigned char g_bpf_drop_syscalls[];  // defined in libscap
-
 namespace collector {
 
+static const char* OPTIONS[] = {"listening_ports", "8080", nullptr};
+static const std::string PROMETHEUS_PORT = "9090";
+
 CollectorService::CollectorService(CollectorConfig& config, std::atomic<ControlValue>* control,
                                    const std::atomic<int>* signum)
-    : config_(config), control_(control), signum_(*signum) {
-  CLOG(INFO) << "Config: " << config;
-}
-
-void CollectorService::RunForever() {
-  // Start monitoring services.
-  // Some of these variables must remain in scope, so
-  // be cautious if decomposing to a separate function.
-  const char* options[] = {"listening_ports", "8080", 0};
-  CivetServer server(options);
-
-  std::shared_ptr<ConnectionTracker> conn_tracker;
-
-  GetStatus getStatus(config_.Hostname(), &system_inspector_);
+    : config_(config),
+      system_inspector_(config_),
+      control_(control),
+      signum_(*signum),
+      server_(OPTIONS),
+      registry_(std::make_shared<prometheus::Registry>()),
+      exposer_(PROMETHEUS_PORT),
+      exporter_(registry_, &config_, &system_inspector_),
+      config_loader_(config_) {
+  CLOG(INFO) << "Config: " << config_;
+
+  // Network tracking
+  if (!config_.grpc_channel || !config_.DisableNetworkFlows()) {
+    // In case if no GRPC is used, continue to setup networking infrasturcture
+    // with empty grpc_channel. NetworkConnectionInfoServiceComm will pick it
+    // up and use stdout instead.
+    if (config_.IsProcessesListeningOnPortsEnabled()) {
+      process_store_ = std::make_shared<ProcessStore>(&system_inspector_);
+    }
+    conn_scraper_ = std::make_shared<ConnScraper>(config_.HostProc(), process_store_);
+    conn_tracker_ = std::make_shared<ConnectionTracker>();
+    UnorderedSet<L4ProtoPortPair> ignored_l4proto_port_pairs(config_.IgnoredL4ProtoPortPairs());
+    conn_tracker_->UpdateIgnoredL4ProtoPortPairs(std::move(ignored_l4proto_port_pairs));
+    conn_tracker_->UpdateIgnoredNetworks(config_.IgnoredNetworks());
+    conn_tracker_->UpdateNonAggregatedNetworks(config_.NonAggregatedNetworks());
+
+    network_connection_info_service_comm_ = std::make_shared<NetworkConnectionInfoServiceComm>(config_.Hostname(), config_.grpc_channel);
+
+    auto total_reporter = config_.EnableConnectionStats() ? exporter_.GetConnectionsTotalReporter() : nullptr;
+    auto rate_reporter = config_.EnableConnectionStats() ? exporter_.GetConnectionsRateReporter() : nullptr;
+
+    net_status_notifier_ = std::make_unique<NetworkStatusNotifier>(
+        conn_scraper_,
+        conn_tracker_,
+        network_connection_info_service_comm_,
+        config_,
+        total_reporter,
+        rate_reporter);
+
+    auto network_signal_handler = std::make_unique<NetworkSignalHandler>(system_inspector_.GetInspector(), conn_tracker_, system_inspector_.GetUserspaceStats());
+    network_signal_handler->SetCollectConnectionStatus(config_.CollectConnectionStatus());
+    network_signal_handler->SetTrackSendRecv(config_.TrackingSendRecv());
+    system_inspector_.AddSignalHandler(std::move(network_signal_handler));
+  }
 
-  std::shared_ptr<prometheus::Registry> registry = std::make_shared<prometheus::Registry>();
+  // Initialize civetweb server handlers
+  civet_endpoints_.emplace_back(std::make_unique<GetStatus>(config_.Hostname(), &system_inspector_));
+  civet_endpoints_.emplace_back(std::make_unique<LogLevelHandler>());
+  civet_endpoints_.emplace_back(std::make_unique<ProfilerHandler>());
 
-  server.addHandler("/ready", getStatus);
-  LogLevelHandler setLogLevel;
-  server.addHandler("/loglevel", setLogLevel);
+  if (config.IsIntrospectionEnabled()) {
+    civet_endpoints_.emplace_back(std::make_unique<ContainerInfoInspector>(system_inspector_.GetContainerMetadataInspector()));
+    civet_endpoints_.emplace_back(std::make_unique<NetworkStatusInspector>(conn_tracker_));
+    civet_endpoints_.emplace_back(std::make_unique<CollectorConfigInspector>(config_));
+  }
 
-  ProfilerHandler profiler_handler;
-  server.addHandler(ProfilerHandler::kBaseRoute, profiler_handler);
+  for (const auto& endpoint : civet_endpoints_) {
+    server_.addHandler(endpoint->GetBaseRoute(), endpoint.get());
+  }
 
-  prometheus::Exposer exposer("9090");
-  exposer.RegisterCollectable(registry);
+  // Prometheus
+  exposer_.RegisterCollectable(registry_);
+}
 
-  CollectorStatsExporter exporter(registry, &config_, &system_inspector_);
-  ConfigLoader configLoader(config_);
-  configLoader.Start();
+CollectorService::~CollectorService() {
+  config_loader_.Stop();
+  server_.close();
+  exporter_.stop();
+  if (net_status_notifier_) {
+    net_status_notifier_->Stop();
+  }
 
-  std::unique_ptr<NetworkStatusNotifier> net_status_notifier;
+  // system_inspector_ needs to be last, since other components relay on it.
+  system_inspector_.CleanUp();
+}
 
-  std::unique_ptr<ContainerInfoInspector> container_info_inspector;
-  std::unique_ptr<NetworkStatusInspector> network_status_inspector;
-  std::unique_ptr<CollectorConfigInspector> collector_config_inspector;
+void CollectorService::RunForever() {
+  // Start monitoring services.
+  config_loader_.Start();
 
   CLOG(INFO) << "Network scrape interval set to " << config_.ScrapeInterval() << " seconds";
 
-  if (config_.grpc_channel) {
-    CLOG(INFO) << "Waiting for Sensor to become ready ...";
-    if (!WaitForGRPCServer()) {
-      CLOG(INFO) << "Interrupted while waiting for Sensor to become ready ...";
-      return;
-    }
-    CLOG(INFO) << "Sensor connectivity is successful";
-  }
-
-  if (!config_.grpc_channel || !config_.DisableNetworkFlows()) {
-    // In case if no GRPC is used, continue to setup networking infrasturcture
-    // with empty grpc_channel. NetworkConnectionInfoServiceComm will pick it
-    // up and use stdout instead.
-    std::shared_ptr<ProcessStore> process_store;
-    if (config_.IsProcessesListeningOnPortsEnabled()) {
-      process_store = std::make_shared<ProcessStore>(&system_inspector_);
-    }
-    std::shared_ptr<IConnScraper> conn_scraper = std::make_shared<ConnScraper>(config_.HostProc(), process_store);
-    conn_tracker = std::make_shared<ConnectionTracker>();
-    UnorderedSet<L4ProtoPortPair> ignored_l4proto_port_pairs(config_.IgnoredL4ProtoPortPairs());
-    conn_tracker->UpdateIgnoredL4ProtoPortPairs(std::move(ignored_l4proto_port_pairs));
-    conn_tracker->UpdateIgnoredNetworks(config_.IgnoredNetworks());
-    conn_tracker->UpdateNonAggregatedNetworks(config_.NonAggregatedNetworks());
-
-    auto network_connection_info_service_comm = std::make_shared<NetworkConnectionInfoServiceComm>(config_.Hostname(), config_.grpc_channel);
-
-    net_status_notifier = std::make_unique<NetworkStatusNotifier>(conn_scraper,
-                                                                  conn_tracker,
-                                                                  network_connection_info_service_comm,
-                                                                  config_,
-                                                                  config_.EnableConnectionStats() ? exporter.GetConnectionsTotalReporter() : 0,
-                                                                  config_.EnableConnectionStats() ? exporter.GetConnectionsRateReporter() : 0);
-    net_status_notifier->Start();
+  if (net_status_notifier_) {
+    net_status_notifier_->Start();
   }
 
-  if (!exporter.start()) {
+  if (!exporter_.start()) {
     CLOG(FATAL) << "Unable to start collector stats exporter";
   }
 
-  if (config_.IsIntrospectionEnabled()) {
-    container_info_inspector = std::make_unique<ContainerInfoInspector>(system_inspector_.GetContainerMetadataInspector());
-    server.addHandler(container_info_inspector->kBaseRoute, container_info_inspector.get());
-    network_status_inspector = std::make_unique<NetworkStatusInspector>(conn_tracker);
-    server.addHandler(network_status_inspector->kBaseRoute, network_status_inspector.get());
-    collector_config_inspector = std::make_unique<CollectorConfigInspector>(config_);
-    server.addHandler(collector_config_inspector->kBaseRoute, collector_config_inspector.get());
-  }
-
-  system_inspector_.Init(config_, conn_tracker);
   system_inspector_.Start();
 
   ControlValue cv;
@@ -135,20 +131,6 @@ void CollectorService::RunForever() {
   }
 
   CLOG(INFO) << "Shutting down collector.";
-
-  if (net_status_notifier) {
-    net_status_notifier->Stop();
-  }
-  configLoader.Stop();
-  // Shut down these first since they access the system inspector object.
-  exporter.stop();
-  server.close();
-
-  system_inspector_.CleanUp();
-}
-
-bool CollectorService::InitKernel() {
-  return system_inspector_.InitKernel(config_);
 }
 
 bool CollectorService::WaitForGRPCServer() {
@@ -157,13 +139,13 @@ bool CollectorService::WaitForGRPCServer() {
   return WaitForChannelReady(config_.grpc_channel, interrupt);
 }
 
-bool SetupKernelDriver(CollectorService& collector, const CollectorConfig& config) {
+bool CollectorService::InitKernel() {
   auto& startup_diagnostics = StartupDiagnostics::GetInstance();
-  std::string cm_name(CollectionMethodName(config.GetCollectionMethod()));
+  std::string cm_name(CollectionMethodName(config_.GetCollectionMethod()));
 
   startup_diagnostics.DriverAvailable(cm_name);
 
-  if (collector.InitKernel()) {
+  if (system_inspector_.InitKernel(config_)) {
     startup_diagnostics.DriverSuccess(cm_name);
     return true;
   }
diff --git a/collector/lib/CollectorService.h b/collector/lib/CollectorService.h
index 5cfcab2afd..8f9331f216 100644
--- a/collector/lib/CollectorService.h
+++ b/collector/lib/CollectorService.h
@@ -1,14 +1,28 @@
 #ifndef _COLLECTOR_SERVICE_H_
 #define _COLLECTOR_SERVICE_H_
 
+#include <prometheus/exposer.h>
+#include <prometheus/registry.h>
+
+#include "CivetWrapper.h"
 #include "CollectorConfig.h"
+#include "CollectorStatsExporter.h"
+#include "ConfigLoader.h"
 #include "Control.h"
+#include "NetworkStatusInspector.h"
+#include "NetworkStatusNotifier.h"
 #include "system-inspector/Service.h"
 
 namespace collector {
 
 class CollectorService {
  public:
+  CollectorService(const CollectorService&) = delete;
+  CollectorService(CollectorService&&) = delete;
+  CollectorService& operator=(const CollectorService&) = delete;
+  CollectorService& operator=(CollectorService&&) = delete;
+  ~CollectorService();
+
   CollectorService(CollectorConfig& config, std::atomic<ControlValue>* control, const std::atomic<int>* signum);
 
   void RunForever();
@@ -19,14 +33,29 @@ class CollectorService {
   bool WaitForGRPCServer();
 
   CollectorConfig& config_;
+  system_inspector::Service system_inspector_;
 
   std::atomic<ControlValue>* control_;
   const std::atomic<int>& signum_;
 
-  system_inspector::Service system_inspector_;
-};
+  // Civetweb handlers and server
+  CivetServer server_;
+  std::vector<std::unique_ptr<CivetWrapper>> civet_endpoints_;
+
+  // Prometheus
+  std::shared_ptr<prometheus::Registry> registry_;
+  prometheus::Exposer exposer_;
+  CollectorStatsExporter exporter_;
 
-bool SetupKernelDriver(CollectorService& collector, const CollectorConfig& config);
+  ConfigLoader config_loader_;
+
+  // Network monitoring
+  std::shared_ptr<ConnectionTracker> conn_tracker_;
+  std::shared_ptr<IConnScraper> conn_scraper_;
+  std::unique_ptr<NetworkStatusNotifier> net_status_notifier_;
+  std::shared_ptr<ProcessStore> process_store_;
+  std::shared_ptr<NetworkConnectionInfoServiceComm> network_connection_info_service_comm_;
+};
 
 }  // namespace collector
 
diff --git a/collector/lib/ContainerInfoInspector.cpp b/collector/lib/ContainerInfoInspector.cpp
index 25316e677c..5210dd81c8 100644
--- a/collector/lib/ContainerInfoInspector.cpp
+++ b/collector/lib/ContainerInfoInspector.cpp
@@ -5,7 +5,7 @@
 #include <string_view>
 
 namespace collector {
-const char* const ContainerInfoInspector::kBaseRoute = "/state/containers/";
+const std::string ContainerInfoInspector::kBaseRoute = "/state/containers/";
 
 bool ContainerInfoInspector::handleGet(CivetServer* server, struct mg_connection* conn) {
   const mg_request_info* req_info = mg_get_request_info(conn);
diff --git a/collector/lib/ContainerInfoInspector.h b/collector/lib/ContainerInfoInspector.h
index 43795b4639..b494e0069f 100644
--- a/collector/lib/ContainerInfoInspector.h
+++ b/collector/lib/ContainerInfoInspector.h
@@ -7,24 +7,28 @@
 #include <string>
 #include <unordered_map>
 
+#include "CivetWrapper.h"
 #include "ContainerMetadata.h"
-#include "IntrospectionEndpoint.h"
 #include "json/writer.h"
 
 namespace collector {
 
 using QueryParams = std::unordered_map<std::string, std::string>;
 
-class ContainerInfoInspector : public IntrospectionEndpoint {
+class ContainerInfoInspector : public CivetWrapper {
  public:
-  static const char* const kBaseRoute;
-
   ContainerInfoInspector(const std::shared_ptr<ContainerMetadata>& cmi) : container_metadata_inspector_(cmi) {}
 
   // implementation of CivetHandler
   bool handleGet(CivetServer* server, struct mg_connection* conn) override;
 
+  const std::string& GetBaseRoute() override {
+    return kBaseRoute;
+  }
+
  private:
+  static const std::string kBaseRoute;
+
   std::shared_ptr<ContainerMetadata> container_metadata_inspector_;
   Json::FastWriter writer_;
 };
diff --git a/collector/lib/GetStatus.cpp b/collector/lib/GetStatus.cpp
index 0d18eb39d5..bdcf56936a 100644
--- a/collector/lib/GetStatus.cpp
+++ b/collector/lib/GetStatus.cpp
@@ -6,6 +6,8 @@
 
 namespace collector {
 
+const std::string GetStatus::kBaseRoute = "/ready";
+
 bool GetStatus::handleGet(CivetServer* server, struct mg_connection* conn) {
   using namespace std;
 
diff --git a/collector/lib/GetStatus.h b/collector/lib/GetStatus.h
index 9497237414..7185929145 100644
--- a/collector/lib/GetStatus.h
+++ b/collector/lib/GetStatus.h
@@ -3,18 +3,24 @@
 
 #include <string>
 
-#include "CivetServer.h"
+#include "CivetWrapper.h"
 #include "system-inspector/SystemInspector.h"
 
 namespace collector {
 
-class GetStatus : public CivetHandler {
+class GetStatus : public CivetWrapper {
  public:
   GetStatus(std::string node_name, const system_inspector::SystemInspector* si)
       : node_name_(std::move(node_name)), system_inspector_(si) {}
-  bool handleGet(CivetServer* server, struct mg_connection* conn);
+  bool handleGet(CivetServer* server, struct mg_connection* conn) override;
+
+  const std::string& GetBaseRoute() override {
+    return kBaseRoute;
+  }
 
  private:
+  static const std::string kBaseRoute;
+
   std::string node_name_;
   const system_inspector::SystemInspector* system_inspector_;
 };
diff --git a/collector/lib/LogLevel.cpp b/collector/lib/LogLevel.cpp
index 7fb3bc98a8..7188f2ea4c 100644
--- a/collector/lib/LogLevel.cpp
+++ b/collector/lib/LogLevel.cpp
@@ -8,6 +8,8 @@
 
 namespace collector {
 
+const std::string LogLevelHandler::kBaseRoute = "/loglevel";
+
 bool LogLevelHandler::handlePost(CivetServer* server, struct mg_connection* conn) {
   using namespace std;
 
diff --git a/collector/lib/LogLevel.h b/collector/lib/LogLevel.h
index 35db732776..5f2370e709 100644
--- a/collector/lib/LogLevel.h
+++ b/collector/lib/LogLevel.h
@@ -1,13 +1,20 @@
 #ifndef _LOG_LEVEL_H_
 #define _LOG_LEVEL_H_
 
-#include "CivetServer.h"
+#include "CivetWrapper.h"
 
 namespace collector {
 
-class LogLevelHandler : public CivetHandler {
+class LogLevelHandler : public CivetWrapper {
  public:
-  bool handlePost(CivetServer* server, struct mg_connection* conn);
+  bool handlePost(CivetServer* server, struct mg_connection* conn) override;
+
+  const std::string& GetBaseRoute() override {
+    return kBaseRoute;
+  }
+
+ private:
+  static const std::string kBaseRoute;
 };
 
 }  // namespace collector
diff --git a/collector/lib/NetworkSignalHandler.cpp b/collector/lib/NetworkSignalHandler.cpp
index 07a625af19..1198657f92 100644
--- a/collector/lib/NetworkSignalHandler.cpp
+++ b/collector/lib/NetworkSignalHandler.cpp
@@ -47,6 +47,8 @@ NetworkSignalHandler::NetworkSignalHandler(sinsp* inspector, std::shared_ptr<Con
   event_extractor_->Init(inspector);
 }
 
+NetworkSignalHandler::~NetworkSignalHandler() = default;
+
 /*
  * Socket connection life-cycle scenarii:
  * - synchronous:
diff --git a/collector/lib/NetworkSignalHandler.h b/collector/lib/NetworkSignalHandler.h
index a6fbba0085..cdb3542af1 100644
--- a/collector/lib/NetworkSignalHandler.h
+++ b/collector/lib/NetworkSignalHandler.h
@@ -11,17 +11,16 @@
 // forward declarations
 class sinsp;
 class sinsp_evt;
+
 namespace collector {
 namespace system_inspector {
 class EventExtractor;
 }
-}  // namespace collector
-
-namespace collector {
 
 class NetworkSignalHandler final : public SignalHandler {
  public:
   explicit NetworkSignalHandler(sinsp* inspector, std::shared_ptr<ConnectionTracker> conn_tracker, system_inspector::Stats* stats);
+  ~NetworkSignalHandler();
 
   std::string GetName() override { return "NetworkSignalHandler"; }
   Result HandleSignal(sinsp_evt* evt) override;
diff --git a/collector/lib/NetworkStatusInspector.cpp b/collector/lib/NetworkStatusInspector.cpp
index 3b915e0903..a4304bf1bc 100644
--- a/collector/lib/NetworkStatusInspector.cpp
+++ b/collector/lib/NetworkStatusInspector.cpp
@@ -177,6 +177,10 @@ bool NetworkStatusInspector::handleGet(CivetServer* server, struct mg_connection
     return ServerError(conn, "unable to read request");
   }
 
+  if (conntracker_ == nullptr) {
+    return ServerError(conn, "not tracking network connections");
+  }
+
   std::string uri = req_info->local_uri;
   auto parameters = ParseParameters(req_info->query_string);
   if (uri == kEndpointRoute) {
diff --git a/collector/lib/NetworkStatusInspector.h b/collector/lib/NetworkStatusInspector.h
index 72e6b2c978..3e56aa4fc3 100644
--- a/collector/lib/NetworkStatusInspector.h
+++ b/collector/lib/NetworkStatusInspector.h
@@ -5,24 +5,28 @@
 
 #include <json/writer.h>
 
-#include "IntrospectionEndpoint.h"
+#include "CivetWrapper.h"
 
 namespace collector {
 
 class ConnectionTracker;
 
-class NetworkStatusInspector : public IntrospectionEndpoint {
+class NetworkStatusInspector : public CivetWrapper {
  public:
-  static const std::string kBaseRoute;
-  static const std::string kEndpointRoute;
-  static const std::string kConnectionRoute;
-
   NetworkStatusInspector(const std::shared_ptr<ConnectionTracker> conntracker);
 
   // implementation of CivetHandler
   bool handleGet(CivetServer* server, struct mg_connection* conn) override;
 
+  const std::string& GetBaseRoute() override {
+    return kBaseRoute;
+  }
+
  private:
+  static const std::string kBaseRoute;
+  static const std::string kEndpointRoute;
+  static const std::string kConnectionRoute;
+
   const std::shared_ptr<ConnectionTracker> conntracker_;
   Json::FastWriter writer_;
 
diff --git a/collector/lib/ProfilerHandler.h b/collector/lib/ProfilerHandler.h
index f1ab61318a..8a1e1a3120 100644
--- a/collector/lib/ProfilerHandler.h
+++ b/collector/lib/ProfilerHandler.h
@@ -4,7 +4,7 @@
 #include <memory>
 #include <mutex>
 
-#include "CivetServer.h"
+#include "CivetWrapper.h"
 #include "Profiler.h"
 
 namespace collector {
@@ -20,17 +20,21 @@ namespace collector {
 //   - get latest cpu profile
 // GET /profile/heap
 //   - get latest heap profile
-class ProfilerHandler : public CivetHandler {
+class ProfilerHandler : public CivetWrapper {
  public:
+  bool handleGet(CivetServer* server, struct mg_connection* conn) override;
+  bool handlePost(CivetServer* server, struct mg_connection* conn) override;
+
+  const std::string& GetBaseRoute() override {
+    return kBaseRoute;
+  }
+
+ private:
   static const std::string kCPUProfileFilename;
   static const std::string kBaseRoute;
   static const std::string kCPURoute;
   static const std::string kHeapRoute;
 
-  bool handleGet(CivetServer* server, struct mg_connection* conn);
-  bool handlePost(CivetServer* server, struct mg_connection* conn);
-
- private:
   bool ServerError(struct mg_connection* conn, const char* err);
   bool ClientError(struct mg_connection* conn, const char* err);
   bool SendStatus(struct mg_connection* conn);
diff --git a/collector/lib/system-inspector/Service.cpp b/collector/lib/system-inspector/Service.cpp
index ae8848a20d..95c0394416 100644
--- a/collector/lib/system-inspector/Service.cpp
+++ b/collector/lib/system-inspector/Service.cpp
@@ -1,6 +1,7 @@
 #include "Service.h"
 
 #include <cap-ng.h>
+#include <memory>
 #include <thread>
 
 #include <linux/ioctl.h>
@@ -31,10 +32,62 @@
 
 namespace collector::system_inspector {
 
-Service::Service() {}
-Service::~Service() {}
+Service::~Service() = default;
+
+Service::Service(const CollectorConfig& config)
+    : inspector_(std::make_unique<sinsp>(true)),
+      container_metadata_inspector_(std::make_unique<ContainerMetadata>(inspector_.get())),
+      default_formatter_(std::make_unique<sinsp_evt_formatter>(
+          inspector_.get(),
+          DEFAULT_OUTPUT_STR,
+          EventExtractor::FilterList())) {
+  // Setup the inspector.
+  // peeking into arguments has a big overhead, so we prevent it from happening
+  inspector_->set_snaplen(0);
+
+  auto log_level = (sinsp_logger::severity)logging::GetLogLevel();
+  inspector_->set_min_log_severity(log_level);
+  inspector_->disable_log_timestamps();
+  inspector_->set_log_callback(logging::InspectorLogCallback);
+
+  inspector_->set_import_users(config.ImportUsers(), false);
+  inspector_->set_thread_timeout_s(30);
+  inspector_->set_auto_threads_purging_interval_s(60);
+  inspector_->m_thread_manager->set_max_thread_table_size(config.GetSinspThreadCacheSize());
+
+  // Connection status tracking is used in NetworkSignalHandler,
+  // but only when trying to handle asynchronous connections
+  // as a special case.
+  if (config.CollectConnectionStatus()) {
+    inspector_->get_parser()->set_track_connection_status(true);
+  }
+
+  if (config.EnableRuntimeConfig()) {
+    uint64_t mask = 1 << CT_CRI |
+                    1 << CT_CRIO |
+                    1 << CT_CONTAINERD;
+
+    if (config.UseDockerCe()) {
+      mask |= 1 << CT_DOCKER;
+    }
+
+    if (config.UsePodmanCe()) {
+      mask |= 1 << CT_PODMAN;
+    }
+
+    inspector_->set_container_engine_mask(mask);
+
+    // k8s naming conventions specify that max length be 253 characters
+    // (the extra 2 are just for a nice 0xFF).
+    inspector_->set_container_labels_max_len(255);
+  } else {
+    auto engine = std::make_shared<ContainerEngine>(inspector_->m_container_manager);
+    auto* container_engines = inspector_->m_container_manager.get_container_engines();
+    container_engines->push_back(engine);
+  }
+
+  inspector_->set_filter("container.id != 'host'");
 
-void Service::Init(const CollectorConfig& config, std::shared_ptr<ConnectionTracker> conn_tracker) {
   // The self-check handlers should only operate during start up,
   // so they are added to the handler list first, so they have access
   // to self-check events before the network and process handlers have
@@ -42,19 +95,10 @@ void Service::Init(const CollectorConfig& config, std::shared_ptr<ConnectionTrac
   AddSignalHandler(std::make_unique<SelfCheckProcessHandler>(inspector_.get()));
   AddSignalHandler(std::make_unique<SelfCheckNetworkHandler>(inspector_.get()));
 
-  if (conn_tracker) {
-    auto network_signal_handler_ = std::make_unique<NetworkSignalHandler>(inspector_.get(), conn_tracker, &userspace_stats_);
-
-    network_signal_handler_->SetCollectConnectionStatus(config.CollectConnectionStatus());
-    network_signal_handler_->SetTrackSendRecv(config.TrackingSendRecv());
-
-    AddSignalHandler(std::move(network_signal_handler_));
-  }
-
   if (config.grpc_channel) {
-    signal_client_.reset(new SignalServiceClient(std::move(config.grpc_channel)));
+    signal_client_ = std::make_unique<SignalServiceClient>(config.grpc_channel);
   } else {
-    signal_client_.reset(new StdoutSignalServiceClient());
+    signal_client_ = std::make_unique<StdoutSignalServiceClient>();
   }
   AddSignalHandler(std::make_unique<ProcessSignalHandler>(inspector_.get(),
                                                           signal_client_.get(),
@@ -69,62 +113,6 @@ void Service::Init(const CollectorConfig& config, std::shared_ptr<ConnectionTrac
 }
 
 bool Service::InitKernel(const CollectorConfig& config) {
-  if (!inspector_) {
-    inspector_.reset(new sinsp(true));
-
-    // peeking into arguments has a big overhead, so we prevent it from happening
-    inspector_->set_snaplen(0);
-
-    auto log_level = (sinsp_logger::severity)logging::GetLogLevel();
-    inspector_->set_min_log_severity(log_level);
-    inspector_->disable_log_timestamps();
-    inspector_->set_log_callback(logging::InspectorLogCallback);
-
-    inspector_->set_import_users(config.ImportUsers(), false);
-    inspector_->set_thread_timeout_s(30);
-    inspector_->set_auto_threads_purging_interval_s(60);
-    inspector_->m_thread_manager->set_max_thread_table_size(config.GetSinspThreadCacheSize());
-
-    // Connection status tracking is used in NetworkSignalHandler,
-    // but only when trying to handle asynchronous connections
-    // as a special case.
-    if (config.CollectConnectionStatus()) {
-      inspector_->get_parser()->set_track_connection_status(true);
-    }
-
-    container_metadata_inspector_.reset(new ContainerMetadata(inspector_.get()));
-
-    if (config.EnableRuntimeConfig()) {
-      uint64_t mask = 1 << CT_CRI |
-                      1 << CT_CRIO |
-                      1 << CT_CONTAINERD;
-
-      if (config.UseDockerCe()) {
-        mask |= 1 << CT_DOCKER;
-      }
-
-      if (config.UsePodmanCe()) {
-        mask |= 1 << CT_PODMAN;
-      }
-
-      inspector_->set_container_engine_mask(mask);
-
-      // k8s naming conventions specify that max length be 253 characters
-      // (the extra 2 are just for a nice 0xFF).
-      inspector_->set_container_labels_max_len(255);
-    } else {
-      auto engine = std::make_shared<ContainerEngine>(inspector_->m_container_manager);
-      auto* container_engines = inspector_->m_container_manager.get_container_engines();
-      container_engines->push_back(engine);
-    }
-
-    inspector_->set_filter("container.id != 'host'");
-
-    default_formatter_.reset(new sinsp_evt_formatter(inspector_.get(),
-                                                     DEFAULT_OUTPUT_STR,
-                                                     EventExtractor::FilterList()));
-  }
-
   KernelDriverCOREEBPF driver;
   if (!driver.Setup(config, *inspector_)) {
     CLOG(ERROR) << "Failed to setup " << config.GetCollectionMethod() << " driver.";
diff --git a/collector/lib/system-inspector/Service.h b/collector/lib/system-inspector/Service.h
index 6202270f04..1648470dcf 100644
--- a/collector/lib/system-inspector/Service.h
+++ b/collector/lib/system-inspector/Service.h
@@ -5,10 +5,10 @@
 #include <bitset>
 #include <memory>
 #include <mutex>
-#include <string>
 
 #include <gtest/gtest_prod.h>
 
+#include "ConnTracker.h"
 #include "ContainerMetadata.h"
 #include "Control.h"
 #include "SignalHandler.h"
@@ -25,10 +25,13 @@ namespace collector::system_inspector {
 
 class Service : public SystemInspector {
  public:
-  Service();
-  ~Service();
+  Service(const Service&) = delete;
+  Service(Service&&) = delete;
+  Service& operator=(const Service&) = delete;
+  Service& operator=(Service&&) = delete;
+  ~Service() override;
 
-  void Init(const CollectorConfig& config, std::shared_ptr<ConnectionTracker> conn_tracker) override;
+  Service(const CollectorConfig& config);
   void Start() override;
   void Run(const std::atomic<ControlValue>& control) override;
   void CleanUp() override;
@@ -37,12 +40,17 @@ class Service : public SystemInspector {
 
   bool InitKernel(const CollectorConfig& config) override;
 
-  typedef std::weak_ptr<std::function<void(std::shared_ptr<sinsp_threadinfo>)>> ProcessInfoCallbackRef;
+  using ProcessInfoCallbackRef = std::weak_ptr<std::function<void(std::shared_ptr<sinsp_threadinfo>)>>;
 
   void GetProcessInformation(uint64_t pid, ProcessInfoCallbackRef callback);
 
   std::shared_ptr<ContainerMetadata> GetContainerMetadataInspector() { return container_metadata_inspector_; };
 
+  sinsp* GetInspector() { return inspector_.get(); }
+  Stats* GetUserspaceStats() { return &userspace_stats_; }
+
+  void AddSignalHandler(std::unique_ptr<SignalHandler> signal_handler);
+
  private:
   FRIEND_TEST(SystemInspectorServiceTest, FilterEvent);
 
@@ -62,8 +70,6 @@ class Service : public SystemInspector {
 
   bool SendExistingProcesses(SignalHandler* handler);
 
-  void AddSignalHandler(std::unique_ptr<SignalHandler> signal_handler);
-
   mutable std::mutex libsinsp_mutex_;
   std::unique_ptr<sinsp> inspector_;
   std::shared_ptr<ContainerMetadata> container_metadata_inspector_;
diff --git a/collector/lib/system-inspector/SystemInspector.h b/collector/lib/system-inspector/SystemInspector.h
index 2eea8ee714..66058c8de5 100644
--- a/collector/lib/system-inspector/SystemInspector.h
+++ b/collector/lib/system-inspector/SystemInspector.h
@@ -3,10 +3,8 @@
 
 #include <atomic>
 #include <cstdint>
-#include <string>
 
 #include "CollectorConfig.h"
-#include "ConnTracker.h"
 #include "Control.h"
 #include "ppm_events_public.h"
 
@@ -42,9 +40,13 @@ struct Stats {
 
 class SystemInspector {
  public:
+  SystemInspector() = default;
+  SystemInspector(const SystemInspector&) = default;
+  SystemInspector(SystemInspector&&) = delete;
+  SystemInspector& operator=(const SystemInspector&) = default;
+  SystemInspector& operator=(SystemInspector&&) = delete;
   virtual ~SystemInspector() = default;
 
-  virtual void Init(const CollectorConfig& config, std::shared_ptr<ConnectionTracker> conn_tracker) = 0;
   virtual bool InitKernel(const CollectorConfig& config) = 0;
   virtual void Start() = 0;
   virtual void Run(const std::atomic<ControlValue>& control) = 0;
diff --git a/integration-tests/integration_test.go b/integration-tests/integration_test.go
index 3d37267a89..56ee693570 100644
--- a/integration-tests/integration_test.go
+++ b/integration-tests/integration_test.go
@@ -541,3 +541,18 @@ func TestRuntimeConfigFile(t *testing.T) {
 func TestThreads(t *testing.T) {
 	suite.Run(t, new(suites.ThreadsTestSuite))
 }
+
+func TestPrometheus(t *testing.T) {
+	suite.Run(t, new(suites.PrometheusTestSuite))
+}
+
+func TestGetStatus(t *testing.T) {
+	suite.Run(t, &suites.HttpEndpointAvailabilityTestSuite{
+		Port:      8080,
+		Endpoints: []string{"ready"},
+	})
+}
+
+func TestLogLevelEndpoint(t *testing.T) {
+	suite.Run(t, new(suites.LogLevelTestSuite))
+}
diff --git a/integration-tests/suites/http_endpoint_availability.go b/integration-tests/suites/http_endpoint_availability.go
index 5f12baa04e..0cdfb5034a 100644
--- a/integration-tests/suites/http_endpoint_availability.go
+++ b/integration-tests/suites/http_endpoint_availability.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"net/url"
 
 	"github.com/stackrox/collector/integration-tests/pkg/collector"
 	"github.com/stackrox/collector/integration-tests/pkg/log"
@@ -22,12 +23,14 @@ func (s *HttpEndpointAvailabilityTestSuite) SetupSuite() {
 }
 
 func (s *HttpEndpointAvailabilityTestSuite) TestAvailability() {
+	collectorUrl, err := url.Parse(fmt.Sprintf("http://localhost:%d", s.Port))
+	s.Assert().NoError(err)
 	for _, endpoint := range s.Endpoints {
-		url := fmt.Sprintf("http://localhost:%d%s", s.Port, endpoint)
+		endpointUrl := collectorUrl.JoinPath(endpoint)
 
-		log.Info("url: %s", url)
+		log.Info("url: %s", endpointUrl)
 
-		resp, err := http.Get(url)
+		resp, err := http.Get(endpointUrl.String())
 		s.Require().NoError(err)
 		s.Require().NotNil(resp)
 		s.Require().Equal(resp.StatusCode, http.StatusOK, "HTTP status code")
diff --git a/integration-tests/suites/log_level_endpoint.go b/integration-tests/suites/log_level_endpoint.go
new file mode 100644
index 0000000000..93f124bfa0
--- /dev/null
+++ b/integration-tests/suites/log_level_endpoint.go
@@ -0,0 +1,79 @@
+package suites
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+	"net/http"
+
+	"github.com/stackrox/collector/integration-tests/pkg/collector"
+	"github.com/stackrox/collector/integration-tests/pkg/log"
+)
+
+const collectorUrl = "http://localhost:8080/loglevel"
+
+type LogLevelTestSuite struct {
+	IntegrationTestSuiteBase
+}
+
+func (s *LogLevelTestSuite) SetupSuite() {
+	options := &collector.StartupOptions{
+		Env: map[string]string{
+			"ROX_COLLECTOR_INTROSPECTION_ENABLE": "true",
+		},
+	}
+	s.StartCollector(false, options)
+}
+
+func (s *LogLevelTestSuite) TearDownSuite() {
+	s.StopCollector()
+}
+
+func (s *LogLevelTestSuite) TestLogLevel() {
+	err := checkLogLevel("DEBUG")
+	s.Assert().NoError(err)
+
+	level := bytes.NewBuffer([]byte("info"))
+	resp, err := http.Post(collectorUrl, "application/text", level)
+	s.Assert().NoError(err)
+	s.Assert().Equal(resp.StatusCode, 200)
+
+	defer resp.Body.Close()
+	body, err := io.ReadAll(resp.Body)
+	s.Assert().NoError(err)
+	s.Assert().True(json.Valid(body))
+
+	err = checkLogLevel("INFO")
+	s.Assert().NoError(err)
+}
+
+func checkLogLevel(level string) error {
+	valid, err := http.Post(collectorUrl, "application/text", nil)
+	if err != nil {
+		return err
+	}
+	if valid.StatusCode != 200 {
+		return log.Error("Got status code %s", valid.Status)
+	}
+
+	defer valid.Body.Close()
+	bodyV, err := io.ReadAll(valid.Body)
+	if err != nil {
+		return err
+	}
+
+	var logLevel struct {
+		Status string
+		Level  string
+	}
+
+	err = json.Unmarshal(bodyV, &logLevel)
+	if err != nil {
+		return err
+	}
+
+	if level != logLevel.Level {
+		return log.Error("Invalid log level, got=%q want=%q", logLevel.Level, level)
+	}
+	return nil
+}
diff --git a/integration-tests/suites/prometheus.go b/integration-tests/suites/prometheus.go
new file mode 100644
index 0000000000..ed51c9a12c
--- /dev/null
+++ b/integration-tests/suites/prometheus.go
@@ -0,0 +1,34 @@
+package suites
+
+import (
+	"net/http"
+
+	"github.com/prometheus/common/expfmt"
+)
+
+type PrometheusTestSuite struct {
+	IntegrationTestSuiteBase
+}
+
+func (s *PrometheusTestSuite) SetupSuite() {
+	s.StartCollector(false, nil)
+}
+
+func (s *PrometheusTestSuite) TearDownSuite() {
+	s.StopCollector()
+}
+
+// Verify prometheus metrics are being exported
+func (s *PrometheusTestSuite) TestPrometheus() {
+	response, err := http.Get("http://localhost:9090/metrics")
+	s.Require().NoError(err)
+	s.Assert().Equal(response.StatusCode, 200)
+
+	// Parse the body to check it is a valid prometheus output
+	body := response.Body
+	defer body.Close()
+	var parser expfmt.TextParser
+	m, err := parser.TextToMetricFamilies(body)
+	s.Require().NoError(err)
+	s.Assert().NotEmpty(m)
+}


Reviews:
user: login: JoukoVirtanen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T04:19:47Z
user: login: JoukoVirtanen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T04:35:27Z
user: login: Molter73
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T14:25:28Z
user: login: Molter73
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T14:26:11Z
user: login: Stringy
body: One largely ignorable nit, but otherwise LGTM! this feels so much cleaner 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-27T13:37:49Z
user: login: JoukoVirtanen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T18:53:04Z
user: login: JoukoVirtanen
body: LGTM! A couple of CI failures look like flakes. I am rerunning them.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T19:00:08Z

Review comments:
diff_hunk: @@ -0,0 +1,58 @@
+package suites
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+	"net/http"
+
+	"github.com/stackrox/collector/integration-tests/pkg/collector"
+)
+
+const collectorUrl = "http://localhost:8080/loglevel"
+
+type LogLevelTestSuite struct {
+	IntegrationTestSuiteBase
+}
+
+func (s *LogLevelTestSuite) SetupSuite() {
+	options := &collector.StartupOptions{
+		Env: map[string]string{
+			"ROX_COLLECTOR_INTROSPECTION_ENABLE": "true",
+		},
+	}
+	s.StartCollector(false, options)
+}
+
+func (s *LogLevelTestSuite) TearDownSuite() {
+	s.StopCollector()
+}
+
+func (s *LogLevelTestSuite) TestLogLevel() {
+	level := []byte("info")
+	resp, err := http.Post(collectorUrl, "application/text", bytes.NewBuffer(level))
+	s.Assert().NoError(err)
+	s.Assert().Equal(resp.StatusCode, 200)
+
+	defer resp.Body.Close()
+	body, err := io.ReadAll(resp.Body)
+	s.Assert().NoError(err)
+	s.Assert().True(json.Valid(body))
+
+	valid, err := http.Post(collectorUrl, "application/text", nil)
+	s.Assert().NoError(err)
+	s.Assert().Equal(valid.StatusCode, 200)
+
+	defer valid.Body.Close()
+	bodyV, err := io.ReadAll(valid.Body)
+	s.Assert().NoError(err)
+
+	var logLevel struct {
+		Status string
+		Level  string
+	}
+
+	err = json.Unmarshal(bodyV, &logLevel)
+	s.Assert().NoError(err)
+	s.Assert().Equal("INFO", logLevel.Level)
path: integration-tests/suites/log_level_endpoint.go
user: login: JoukoVirtanen
body: Would it be possible to test with different log levels?
created_at: 2025-03-26T04:19:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+package suites
+
+import (
+	"net/http"
+
+	"github.com/prometheus/common/expfmt"
+)
+
+type PrometheusTestSuite struct {
+	IntegrationTestSuiteBase
+}
+
+func (s *PrometheusTestSuite) SetupSuite() {
+	s.StartCollector(false, nil)
+}
+
+func (s *PrometheusTestSuite) TearDownSuite() {
+	s.StopCollector()
+}
+
+// Verify prometheus metrics are being exported
+func (s *PrometheusTestSuite) TestPrometheus() {
+	response, err := http.Get("http://localhost:9090/metrics")
+	s.Require().NoError(err)
+	s.Assert().Equal(response.StatusCode, 200)
+
+	// Parse the body to check it is a valid prometheus output
+	body := response.Body
+	defer body.Close()
+	var parser expfmt.TextParser
+	_, err = parser.TextToMetricFamilies(body)
+	s.Require().NoError(err)
path: integration-tests/suites/prometheus.go
user: login: JoukoVirtanen
body: Maybe check that some of the fields are non-zero.
created_at: 2025-03-26T04:35:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,58 @@
+package suites
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+	"net/http"
+
+	"github.com/stackrox/collector/integration-tests/pkg/collector"
+)
+
+const collectorUrl = "http://localhost:8080/loglevel"
+
+type LogLevelTestSuite struct {
+	IntegrationTestSuiteBase
+}
+
+func (s *LogLevelTestSuite) SetupSuite() {
+	options := &collector.StartupOptions{
+		Env: map[string]string{
+			"ROX_COLLECTOR_INTROSPECTION_ENABLE": "true",
+		},
+	}
+	s.StartCollector(false, options)
+}
+
+func (s *LogLevelTestSuite) TearDownSuite() {
+	s.StopCollector()
+}
+
+func (s *LogLevelTestSuite) TestLogLevel() {
+	level := []byte("info")
+	resp, err := http.Post(collectorUrl, "application/text", bytes.NewBuffer(level))
+	s.Assert().NoError(err)
+	s.Assert().Equal(resp.StatusCode, 200)
+
+	defer resp.Body.Close()
+	body, err := io.ReadAll(resp.Body)
+	s.Assert().NoError(err)
+	s.Assert().True(json.Valid(body))
+
+	valid, err := http.Post(collectorUrl, "application/text", nil)
+	s.Assert().NoError(err)
+	s.Assert().Equal(valid.StatusCode, 200)
+
+	defer valid.Body.Close()
+	bodyV, err := io.ReadAll(valid.Body)
+	s.Assert().NoError(err)
+
+	var logLevel struct {
+		Status string
+		Level  string
+	}
+
+	err = json.Unmarshal(bodyV, &logLevel)
+	s.Assert().NoError(err)
+	s.Assert().Equal("INFO", logLevel.Level)
path: integration-tests/suites/log_level_endpoint.go
user: login: Molter73
body: Well, the test starts collector in debug mode, I can add an initial check to show that the log level is changed after the call.
created_at: 2025-03-26T14:25:28Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,33 @@
+package suites
+
+import (
+	"net/http"
+
+	"github.com/prometheus/common/expfmt"
+)
+
+type PrometheusTestSuite struct {
+	IntegrationTestSuiteBase
+}
+
+func (s *PrometheusTestSuite) SetupSuite() {
+	s.StartCollector(false, nil)
+}
+
+func (s *PrometheusTestSuite) TearDownSuite() {
+	s.StopCollector()
+}
+
+// Verify prometheus metrics are being exported
+func (s *PrometheusTestSuite) TestPrometheus() {
+	response, err := http.Get("http://localhost:9090/metrics")
+	s.Require().NoError(err)
+	s.Assert().Equal(response.StatusCode, 200)
+
+	// Parse the body to check it is a valid prometheus output
+	body := response.Body
+	defer body.Close()
+	var parser expfmt.TextParser
+	_, err = parser.TextToMetricFamilies(body)
+	s.Require().NoError(err)
path: integration-tests/suites/prometheus.go
user: login: Molter73
body: The output for this is a bit weird, I'll add a check to verify the resulting map has some keys in it, think that should be enough.
created_at: 2025-03-26T14:26:10Z
author_association: COLLABORATOR
diff_hunk: @@ -1,125 +1,120 @@
 #include "CollectorService.h"
 
-#include "CollectionMethod.h"
-#include "ConfigLoader.h"
-#include "ContainerInfoInspector.h"
-
-extern "C" {
-#include <signal.h>
-}
-
 #include <memory>
 
-#include "CivetServer.h"
+#include "CollectionMethod.h"
 #include "CollectorRuntimeConfigInspector.h"
 #include "CollectorStatsExporter.h"
+#include "ConfigLoader.h"
 #include "ConnTracker.h"
-#include "Containers.h"
+#include "ContainerInfoInspector.h"
 #include "Diagnostics.h"
 #include "GRPCUtil.h"
 #include "GetStatus.h"
 #include "LogLevel.h"
+#include "NetworkSignalHandler.h"
 #include "NetworkStatusInspector.h"
 #include "NetworkStatusNotifier.h"
 #include "ProfilerHandler.h"
 #include "Utility.h"
-#include "prometheus/exposer.h"
 #include "system-inspector/Service.h"
 
-extern unsigned char g_bpf_drop_syscalls[];  // defined in libscap
-
 namespace collector {
 
+static const char* OPTIONS[] = {"listening_ports", "8080", nullptr};
+
 CollectorService::CollectorService(CollectorConfig& config, std::atomic<ControlValue>* control,
                                    const std::atomic<int>* signum)
-    : config_(config), control_(control), signum_(*signum) {
-  CLOG(INFO) << "Config: " << config;
-}
-
-void CollectorService::RunForever() {
-  // Start monitoring services.
-  // Some of these variables must remain in scope, so
-  // be cautious if decomposing to a separate function.
-  const char* options[] = {"listening_ports", "8080", 0};
-  CivetServer server(options);
-
-  std::shared_ptr<ConnectionTracker> conn_tracker;
-
-  GetStatus getStatus(config_.Hostname(), &system_inspector_);
+    : config_(config),
+      system_inspector_(config_),
+      control_(control),
+      signum_(*signum),
+      server_(OPTIONS),
+      registry_(std::make_shared<prometheus::Registry>()),
+      exposer_("9090"),
path: collector/lib/CollectorService.cpp
user: login: Stringy
body: can we make this a constant like we did with options?
created_at: 2025-03-27T10:33:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,33 @@
+package suites
+
+import (
+	"net/http"
+
+	"github.com/prometheus/common/expfmt"
+)
+
+type PrometheusTestSuite struct {
+	IntegrationTestSuiteBase
+}
+
+func (s *PrometheusTestSuite) SetupSuite() {
+	s.StartCollector(false, nil)
+}
+
+func (s *PrometheusTestSuite) TearDownSuite() {
+	s.StopCollector()
+}
+
+// Verify prometheus metrics are being exported
+func (s *PrometheusTestSuite) TestPrometheus() {
+	response, err := http.Get("http://localhost:9090/metrics")
+	s.Require().NoError(err)
+	s.Assert().Equal(response.StatusCode, 200)
+
+	// Parse the body to check it is a valid prometheus output
+	body := response.Body
+	defer body.Close()
+	var parser expfmt.TextParser
+	_, err = parser.TextToMetricFamilies(body)
+	s.Require().NoError(err)
path: integration-tests/suites/prometheus.go
user: login: JoukoVirtanen
body: This is enough.
created_at: 2025-03-30T18:53:04Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov-commenter
created_at: 2025-03-19T16:41:38Z
body: ## [Codecov](https://app.codecov.io/gh/stackrox/collector/pull/2065?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) Report
:x: Patch coverage is `0%` with `112 lines` in your changes missing coverage. Please review.
:white_check_mark: Project coverage is 27.51%. Comparing base ([`b4abdd2`](https://app.codecov.io/gh/stackrox/collector/commit/b4abdd2a8852389814548d4e896148767cb655a8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox)) to head ([`8cc2eaf`](https://app.codecov.io/gh/stackrox/collector/commit/8cc2eaf28d4dc5f06bdf0a70d83d25964fb99dd3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox)).
:warning: Report is 393 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/stackrox/collector/pull/2065?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) | Patch % | Lines |
|---|---|---|
| [collector/lib/CollectorService.cpp](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&filepath=collector%2Flib%2FCollectorService.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y29sbGVjdG9yL2xpYi9Db2xsZWN0b3JTZXJ2aWNlLmNwcA==) | 0.00% | [59 Missing :warning: ](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| [collector/lib/system-inspector/Service.cpp](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&filepath=collector%2Flib%2Fsystem-inspector%2FService.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y29sbGVjdG9yL2xpYi9zeXN0ZW0taW5zcGVjdG9yL1NlcnZpY2UuY3Bw) | 0.00% | [33 Missing :warning: ](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| [collector/lib/CivetWrapper.cpp](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&filepath=collector%2Flib%2FCivetWrapper.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y29sbGVjdG9yL2xpYi9DaXZldFdyYXBwZXIuY3Bw) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| [collector/lib/CollectorRuntimeConfigInspector.h](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&filepath=collector%2Flib%2FCollectorRuntimeConfigInspector.h&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y29sbGVjdG9yL2xpYi9Db2xsZWN0b3JSdW50aW1lQ29uZmlnSW5zcGVjdG9yLmg=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| [collector/lib/ContainerInfoInspector.h](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&filepath=collector%2Flib%2FContainerInfoInspector.h&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y29sbGVjdG9yL2xpYi9Db250YWluZXJJbmZvSW5zcGVjdG9yLmg=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| [collector/lib/GetStatus.h](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&filepath=collector%2Flib%2FGetStatus.h&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y29sbGVjdG9yL2xpYi9HZXRTdGF0dXMuaA==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| [collector/lib/LogLevel.h](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&filepath=collector%2Flib%2FLogLevel.h&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y29sbGVjdG9yL2xpYi9Mb2dMZXZlbC5o) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| [collector/lib/NetworkStatusInspector.cpp](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&filepath=collector%2Flib%2FNetworkStatusInspector.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y29sbGVjdG9yL2xpYi9OZXR3b3JrU3RhdHVzSW5zcGVjdG9yLmNwcA==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| [collector/lib/NetworkStatusInspector.h](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&filepath=collector%2Flib%2FNetworkStatusInspector.h&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y29sbGVjdG9yL2xpYi9OZXR3b3JrU3RhdHVzSW5zcGVjdG9yLmg=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| [collector/lib/ProfilerHandler.h](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&filepath=collector%2Flib%2FProfilerHandler.h&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y29sbGVjdG9yL2xpYi9Qcm9maWxlckhhbmRsZXIuaA==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| ... and [3 more](https://app.codecov.io/gh/stackrox/collector/pull/2065?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) | |

<details><summary>Additional details and impacted files</summary>



```diff
@@            Coverage Diff             @@
##           master    #2065      +/-   ##
==========================================
- Coverage   27.53%   27.51%   -0.03%     
==========================================
  Files          89       93       +4     
  Lines        5723     5728       +5     
  Branches     2546     2534      -12     
==========================================
  Hits         1576     1576              
- Misses       3480     3485       +5     
  Partials      667      667              
```

| [Flag](https://app.codecov.io/gh/stackrox/collector/pull/2065/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) | Coverage Δ | |
|---|---|---|
| [collector-unit-tests](https://app.codecov.io/gh/stackrox/collector/pull/2065/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) | `27.51% <0.00%> (-0.03%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#carryforward-flags-in-the-pull-request-comment) to find out more.
</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/stackrox/collector/pull/2065?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox).
author_association: NONE
user: login: Molter73
created_at: 2025-03-26T15:20:25Z
body: @sourcery-ai review
author_association: COLLABORATOR