diff --git a/src/agent/CMakeLists.txt b/src/agent/CMakeLists.txt index 9cb10e337..1014ed240 100644 --- a/src/agent/CMakeLists.txt +++ b/src/agent/CMakeLists.txt @@ -18,7 +18,7 @@ if(CMAKE_HOST_WIN32 AND NOT DEFINED VCPKG_TARGET_TRIPLET) set(VCPKG_TARGET_TRIPLET "x64-windows-static" CACHE STRING "vcpkg triplet for the self-contained agent build") endif() -project(HsmAgent VERSION 0.5.25 LANGUAGES CXX) +project(HsmAgent VERSION 0.5.26 LANGUAGES CXX) if(MSVC) # Static CRT (/MT) to match the static triplet — the exe must run on a clean machine with no VC++ diff --git a/src/agent/include/agent/file_logger.hpp b/src/agent/include/agent/file_logger.hpp index 341596875..5e0778485 100644 --- a/src/agent/include/agent/file_logger.hpp +++ b/src/agent/include/agent/file_logger.hpp @@ -17,7 +17,11 @@ namespace hsm::agent class FileLogger { public: - explicit FileLogger(std::wstring path, std::size_t max_bytes = 5u * 1024u * 1024u); + /// Messages below `min_level` are dropped (default Info — Debug stays out of the file unless + /// explicitly lowered; parity with the collector's built-in file logger). + explicit FileLogger(std::wstring path, + hsm::collector::LogLevel min_level = hsm::collector::LogLevel::Info, + std::size_t max_bytes = 5u * 1024u * 1024u); FileLogger(const FileLogger&) = delete; FileLogger& operator=(const FileLogger&) = delete; @@ -30,6 +34,7 @@ namespace hsm::agent std::mutex mutex_; std::wstring path_; std::size_t max_bytes_; + hsm::collector::LogLevel min_level_; std::ofstream out_; std::size_t written_ = 0; }; diff --git a/src/agent/src/logging/file_logger.cpp b/src/agent/src/logging/file_logger.cpp index 9cfa2833a..e4d284442 100644 --- a/src/agent/src/logging/file_logger.cpp +++ b/src/agent/src/logging/file_logger.cpp @@ -38,8 +38,8 @@ namespace hsm::agent } } // namespace - FileLogger::FileLogger(std::wstring path, std::size_t max_bytes) - : path_(std::move(path)), max_bytes_(max_bytes) + FileLogger::FileLogger(std::wstring path, hsm::collector::LogLevel min_level, std::size_t max_bytes) + : path_(std::move(path)), max_bytes_(max_bytes), min_level_(min_level) { out_.open(path_, std::ios::app | std::ios::binary); if (out_) @@ -65,6 +65,8 @@ namespace hsm::agent void FileLogger::Write(hsm::collector::LogLevel level, const std::string& message) { + if (level < min_level_) + return; std::lock_guard lock(mutex_); if (!out_) return; diff --git a/src/native/collector/CMakeLists.txt b/src/native/collector/CMakeLists.txt index 5d6a5a987..0ca284776 100644 --- a/src/native/collector/CMakeLists.txt +++ b/src/native/collector/CMakeLists.txt @@ -269,6 +269,9 @@ if(HSM_COLLECTOR_HTTP) add_test(NAME native_http_registers_sensors_on_start COMMAND hsm_collector_tests native_http_registers_sensors_on_start ) + add_test(NAME native_http_registration_failure_logs_status + COMMAND hsm_collector_tests native_http_registration_failure_logs_status + ) endif() add_test(NAME native_invalid_argument_clears_out_params diff --git a/src/native/collector/src/hsm_collector.cpp b/src/native/collector/src/hsm_collector.cpp index 78d74a8d5..c568744bf 100644 --- a/src/native/collector/src/hsm_collector.cpp +++ b/src/native/collector/src/hsm_collector.cpp @@ -2341,7 +2341,10 @@ namespace // "Type":0 Command discriminator the server's CommandRequestBaseDeserializationConverter // keys on. Best-effort: a failure is logged and Start proceeds (values would fail too if the // server is unreachable; the value queue's durable retry handles a transient outage). - void PostRegistrationsWire(const std::vector>& sensors) + // `runtime` = a sensor registered lazily AFTER Start (top-CPU process, network interface, + // service-status, TCP-rate) rather than the connect-time batch. It changes the log label + // (no "on connect") and level (Debug, so per-sample registrations do not spam the log). + void PostRegistrationsWire(const std::vector>& sensors, bool runtime = false) { if (sensors.empty()) return; @@ -2362,12 +2365,22 @@ namespace }; const auto response = http_transport_->Post(endpoints_.CommandsList(), body, headers); + // Name a single sensor by path; the connect batch by count. + const std::string what = sensors.size() == 1 ? "sensor " + sensors.front()->Path() + : std::to_string(sensors.size()) + " sensor(s)"; if (!response.IsSuccess()) - LogError("Failed to register " + std::to_string(sensors.size()) + - " sensor(s) on Start: " + (response.error.empty() ? "non-2xx response" : response.error)); + { + // HTTP status for parity with the value send-fail log; fall back to the transport + // error text (e.g. "Could not connect to server") when no HTTP response arrived. + const std::string reason = response.status_code > 0 + ? "HTTP " + std::to_string(response.status_code) + : (response.error.empty() ? "no response" : response.error); + LogError("Failed to register " + what + (runtime ? "" : " on connect") + ": " + reason); + } + else if (runtime) + LogMessage(HSM_LOG_LEVEL_DEBUG, "Registered " + what + "."); else - LogMessage( - HSM_LOG_LEVEL_INFO, "Registered " + std::to_string(sensors.size()) + " sensor(s) on connect."); + LogMessage(HSM_LOG_LEVEL_INFO, "Registered " + what + " on connect."); } #endif @@ -3843,7 +3856,7 @@ namespace return; #if defined(HSM_COLLECTOR_HTTP) if (send_wire_) - PostRegistrationsWire({ tcp_fail_rate_sensor_ }); + PostRegistrationsWire({ tcp_fail_rate_sensor_ }, /*runtime=*/true); #endif { @@ -4014,7 +4027,7 @@ namespace it = cache.emplace(iface, sensor).first; #if defined(HSM_COLLECTOR_HTTP) if (send_wire_) - PostRegistrationsWire({ sensor }); + PostRegistrationsWire({ sensor }, /*runtime=*/true); #endif } @@ -4105,7 +4118,7 @@ namespace return; #if defined(HSM_COLLECTOR_HTTP) if (send_wire_) - PostRegistrationsWire({ service_status_sensor_ }); + PostRegistrationsWire({ service_status_sensor_ }, /*runtime=*/true); #endif { @@ -4353,7 +4366,7 @@ namespace // so the server gets the description. HttpTransport::Post is // thread-safe (per-call libcurl easy handle). if (send_wire_) - PostRegistrationsWire({ std::move(sensor) }); + PostRegistrationsWire({ std::move(sensor) }, /*runtime=*/true); #endif } else @@ -4500,11 +4513,13 @@ namespace dropped = DispatchQueuedLocked(lock, /*clear_remainder_on_failure=*/true); } - // Error routing — shutdown discard: a bounded stop that drops pending values on a - // dead/failing transport reports it (logged outside the queue lock so a slow log - // sink cannot stall shutdown). Deduplicated like any other error. + // Debug, not Error: dropping buffered values on a bounded graceful stop is expected and + // contracted (a stop must not block on a dead/failing transport). Logging it as Error + // spammed the log and the Windows Event Log on every routine restart; keep it only as a + // Debug breadcrumb (logged outside the queue lock so a slow sink cannot stall shutdown). if (dropped > 0) - LogError("Collector stop dropped " + std::to_string(dropped) + " pending value(s): transport unavailable."); + LogMessage(HSM_LOG_LEVEL_DEBUG, + "Collector stop dropped " + std::to_string(dropped) + " pending value(s): transport unavailable."); } // Pops and sends batches of up to max_values_in_package_ until the queue is empty or a diff --git a/src/native/collector/tests/hsm_collector_tests.cpp b/src/native/collector/tests/hsm_collector_tests.cpp index e5630b063..a81b9a7ca 100644 --- a/src/native/collector/tests/hsm_collector_tests.cpp +++ b/src/native/collector/tests/hsm_collector_tests.cpp @@ -3786,6 +3786,51 @@ namespace Require(hsm_collector_stop(collector.value) == HSM_RESULT_OK, "collector stop failed"); } + + // A registration POST that gets a non-2xx response must log the HTTP status for diagnosis (parity + // with the value send-fail log), and the Start batch keeps the "on connect" label — never the + // stale "on Start". + void NativeHttpRegistrationFailureLogsStatus() + { + hsm::test::HttpCaptureServer server(503); + + std::vector> logs; + + hsm_collector_options_t options{}; + options.access_key = "reg-key"; + options.server_address = "http://127.0.0.1"; + options.port = server.Port(); + options.client_name = "reg-client"; + options.allow_plaintext_transport = true; + options.package_collect_period_ms = 20; + + CollectorHandle collector = CreateCollector(options); + hsm_collector_set_logger( + collector.value, + [](hsm_log_level_t level, const char* message, void* user_data) { + static_cast>*>(user_data)->emplace_back(level, message); + }, + &logs); + hsm_collector_test_install_http_sender(collector.value); + SensorHandle sensor = CreateIntSensor(collector.value, "reg/int"); + + // Registration is POSTed synchronously during Start, so the failure is logged by the time + // start() returns. + Require(hsm_collector_start(collector.value) == HSM_RESULT_OK, "collector start failed"); + Require(hsm_collector_stop(collector.value) == HSM_RESULT_OK, "collector stop failed"); + + const auto error_has = [&](const std::string& needle) { + for (const auto& entry : logs) + if (entry.first == HSM_LOG_LEVEL_ERROR && entry.second.find(needle) != std::string::npos) + return true; + return false; + }; + Require(error_has("Failed to register"), "a failed registration must log an error"); + Require(error_has("HTTP 503"), "the registration failure must carry the HTTP status code"); + Require(error_has("on connect"), "the Start batch failure keeps the 'on connect' label"); + for (const auto& entry : logs) + Require(entry.second.find("on Start") == std::string::npos, "the stale 'on Start' label must be gone"); + } #endif void NativeWireTimeSpanAndVersionMatchNet() @@ -5032,6 +5077,7 @@ namespace { "native_http_transport_posts_to_capture_server", [](const std::string&) { NativeHttpTransportPostsToCaptureServer(); } }, { "native_http_live_send_posts_to_capture_server", [](const std::string&) { NativeHttpLiveSendPostsToCaptureServer(); } }, { "native_http_registers_sensors_on_start", [](const std::string&) { NativeHttpRegistersSensorsOnStart(); } }, + { "native_http_registration_failure_logs_status", [](const std::string&) { NativeHttpRegistrationFailureLogsStatus(); } }, #endif { "native_http_endpoint_routing_matches_net", [](const std::string&) { NativeHttpEndpointRoutingMatchesNet(); } }, { "native_http_retry_policy_matches_net", [](const std::string&) { NativeHttpRetryPolicyMatchesNet(); } },