From 044aee9bc28c0158fce583ce34484a298d1b05d7 Mon Sep 17 00:00:00 2001 From: Carles Fernandez Date: Sun, 27 Sep 2026 18:26:57 +0200 Subject: [PATCH] Keep a separate logfile for each run with Abseil logging, as google-glog did --- docs/CHANGELOG.md | 4 + src/core/libs/CMakeLists.txt | 24 ++ src/core/libs/gnss_sdr_log_sink.cc | 227 +++++++++++++++ src/core/libs/gnss_sdr_log_sink.h | 90 ++++++ src/main/CMakeLists.txt | 2 +- src/main/main.cc | 83 +----- tests/CMakeLists.txt | 21 +- tests/single_test_main.cc | 83 +----- tests/system-tests/position_test.cc | 50 +--- tests/test_main.cc | 81 +----- .../control-plane/gnss_sdr_log_sink_test.cc | 260 ++++++++++++++++++ utils/front-end-cal/CMakeLists.txt | 2 +- utils/front-end-cal/main.cc | 87 +----- 13 files changed, 638 insertions(+), 376 deletions(-) create mode 100644 src/core/libs/gnss_sdr_log_sink.cc create mode 100644 src/core/libs/gnss_sdr_log_sink.h create mode 100644 tests/unit-tests/control-plane/gnss_sdr_log_sink_test.cc diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1eb27f135..eb63e74f9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -531,6 +531,10 @@ All notable changes to GNSS-SDR will be documented in this file. - The PVT Monitor now reports per-signal details for satellites used in the position solution, including PRN, constellation, signal, azimuth, elevation, and whether multiple signals were combined. Contributed by @joebre. +- Abseil logging now creates a unique timestamp/PID logfile for each run, + preserving previous logs across the receiver, calibration tool, and test + runners. On POSIX systems, an atomically updated relative symlink points to + the latest logfile. See the definitions of concepts and metrics at https://gnss-sdr.org/design-forces/ diff --git a/src/core/libs/CMakeLists.txt b/src/core/libs/CMakeLists.txt index 75dfa4f0c..d1460eb97 100644 --- a/src/core/libs/CMakeLists.txt +++ b/src/core/libs/CMakeLists.txt @@ -206,3 +206,27 @@ set_property(TARGET core_libs $ $ ) + +# Keep the file sink independent of receiver and signal-processing dependencies. +# Older toolchains using glog do not build this C++17-only target. +if(NOT ENABLE_GLOG_AND_GFLAGS) + add_library(gnss_sdr_log_sink STATIC gnss_sdr_log_sink.cc gnss_sdr_log_sink.h) + target_compile_features(gnss_sdr_log_sink PUBLIC cxx_std_17) + target_include_directories(gnss_sdr_log_sink + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE ${GNSSSDR_SOURCE_DIR}/src/algorithms/libs + ) + target_link_libraries(gnss_sdr_log_sink + PUBLIC absl::log_sink + PRIVATE absl::log_sink_registry absl::time Threads::Threads + ) + if(FILESYSTEM_FOUND) + target_link_libraries(gnss_sdr_log_sink PRIVATE std::filesystem) + target_compile_definitions(gnss_sdr_log_sink PRIVATE HAS_STD_FILESYSTEM=1) + if(find_experimental) + target_compile_definitions(gnss_sdr_log_sink PRIVATE HAS_STD_FILESYSTEM_EXPERIMENTAL=1) + endif() + else() + target_link_libraries(gnss_sdr_log_sink PRIVATE Boost::filesystem $<$:Boost::system>) + endif() +endif() diff --git a/src/core/libs/gnss_sdr_log_sink.cc b/src/core/libs/gnss_sdr_log_sink.cc new file mode 100644 index 000000000..ad6016594 --- /dev/null +++ b/src/core/libs/gnss_sdr_log_sink.cc @@ -0,0 +1,227 @@ +/*! + * \file gnss_sdr_log_sink.cc + * \brief Buffered, per-run file logging for Abseil. + * \author Carles Fernandez-Prades, 2026 + * + * ----------------------------------------------------------------------------- + * + * GNSS-SDR is a Global Navigation Satellite System software-defined receiver. + * This file is part of GNSS-SDR. + * + * Copyright (C) 2010-2026 (see AUTHORS file for a list of contributors) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * ----------------------------------------------------------------------------- + */ + +#include "gnss_sdr_log_sink.h" +#include "gnss_sdr_filesystem.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(_WIN32) +#include +#include +#include +#else +#include +#endif + +GnssSdrLogSink::GnssSdrLogSink(const std::string& directory, const std::string& basename) +{ + if (basename.empty() || basename == "." || basename == ".." || basename.find_first_of("/\\:") != std::string::npos) + { + throw std::invalid_argument("Invalid log basename: " + basename); + } + const fs::path log_directory = fs::absolute(directory.empty() ? fs::temp_directory_path() : fs::path(directory)); + fs::create_directories(log_directory); +#if defined(_WIN32) + const auto pid = _getpid(); +#else + const auto pid = getpid(); +#endif + const std::string stem = basename + "." + absl::FormatTime("%Y%m%d-%H%M%S", absl::Now(), absl::LocalTimeZone()) + "." + std::to_string(pid); + for (unsigned int attempt = 0;; ++attempt) + { + const std::string suffix = attempt == 0 ? "" : "." + std::to_string(attempt); + const fs::path path = log_directory / (stem + suffix + ".log"); + d_filename = path.string(); + // C++17 ofstream cannot create exclusively. Keep the descriptor open + // through fdopen so there is no close/reopen race or truncation. +#if defined(_WIN32) + const int fd = _wopen(path.c_str(), _O_WRONLY | _O_CREAT | _O_EXCL | _O_BINARY | _O_NOINHERIT, _S_IREAD | _S_IWRITE); +#else + // Same default mode as glog's --logfile_mode; the process umask still applies. + const int fd = open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0664); +#endif + if (fd < 0) + { + const int error = errno; + if (error == EEXIST) + { + continue; + } + throw std::system_error(error, std::generic_category(), "Cannot create logfile " + d_filename); + } +#if defined(_WIN32) + d_logfile.reset(_fdopen(fd, "wb")); +#else + d_logfile.reset(fdopen(fd, "wb")); +#endif + if (!d_logfile) + { + const int error = errno; +#if defined(_WIN32) + _close(fd); +#else + close(fd); +#endif + throw std::system_error(error, std::generic_category(), "Cannot open logfile stream " + d_filename); + } + break; + } + try + { + UpdateLatestLink(basename); + } + catch (const std::exception& ex) + { + std::fprintf(stderr, "Cannot update latest logfile link for %s: %s\n", d_filename.c_str(), ex.what()); + } +} + + +GnssSdrLogSink::~GnssSdrLogSink() +{ + Flush(); + if (std::fclose(d_logfile.release()) != 0) + { + ReportWriteError(); + } +} + + +void GnssSdrLogSink::UpdateLatestLink(const std::string& basename) const +{ +#if defined(_WIN32) + // Creating symlinks can require privileges on Windows. The per-run file + // remains available without a stable link. + (void)basename; +#else + const fs::path path(d_filename); + const fs::path latest = path.parent_path() / (basename + ".log"); + // Preserve a regular logfile left by versions predating this sink. A hard + // link keeps its contents available even when the stable name is replaced. + if (fs::is_regular_file(fs::symlink_status(latest))) + { + fs::create_hard_link(latest, fs::path(d_filename + ".previous")); + } + fs::path temporary; + for (unsigned int attempt = 0;; ++attempt) + { + temporary = fs::path(d_filename + ".link." + std::to_string(attempt)); + errorlib::error_code error; + fs::create_symlink(path.filename(), temporary, error); + if (!error) + { + break; + } + if (error != errorlib::errc::file_exists) + { + throw fs::filesystem_error("Cannot create latest logfile symlink", temporary, error); + } + } + errorlib::error_code error; + fs::rename(temporary, latest, error); + if (error) + { + errorlib::error_code ignored; + fs::remove(temporary, ignored); + throw fs::filesystem_error("Cannot replace latest logfile symlink", latest, error); + } +#endif +} + + +void GnssSdrLogSink::Send(const absl::LogEntry& entry) noexcept +{ + try + { + std::lock_guard lock(d_mutex); + const auto text = entry.text_message_with_prefix_and_newline(); + if (std::fwrite(text.data(), 1, text.size(), d_logfile.get()) != text.size()) + { + ReportWriteError(); + } + if (entry.log_severity() >= absl::LogSeverity::kError && std::fflush(d_logfile.get()) != 0) + { + ReportWriteError(); + } + } + catch (...) + { + ReportWriteError(); + } +} + + +void GnssSdrLogSink::Flush() noexcept +{ + try + { + std::lock_guard lock(d_mutex); + if (std::fflush(d_logfile.get()) != 0) + { + ReportWriteError(); + } + } + catch (...) + { + ReportWriteError(); + } +} + + +void GnssSdrLogSink::ReportWriteError() noexcept +{ + if (!d_error_reported.test_and_set()) + { + std::fprintf(stderr, "Failed to write or flush logfile %s\n", d_filename.c_str()); + } +} + + +GnssSdrLogSinkGuard::~GnssSdrLogSinkGuard() +{ + Shutdown(); +} + + +void GnssSdrLogSinkGuard::Register(const std::string& directory, const std::string& basename) +{ + if (d_sink) + { + throw std::logic_error("Log sink is already registered"); + } + auto sink = std::make_unique(directory, basename); + absl::AddLogSink(sink.get()); + d_sink = std::move(sink); +} + + +void GnssSdrLogSinkGuard::Shutdown() noexcept +{ + if (d_sink) + { + // Stop callbacks before the destructor flushes and closes the file. + absl::RemoveLogSink(d_sink.get()); + d_sink.reset(); + } +} diff --git a/src/core/libs/gnss_sdr_log_sink.h b/src/core/libs/gnss_sdr_log_sink.h new file mode 100644 index 000000000..48150e49d --- /dev/null +++ b/src/core/libs/gnss_sdr_log_sink.h @@ -0,0 +1,90 @@ +/*! + * \file gnss_sdr_log_sink.h + * \brief Buffered, per-run file logging for Abseil. + * \author Carles Fernandez-Prades, 2026 + * + * ----------------------------------------------------------------------------- + * + * GNSS-SDR is a Global Navigation Satellite System software-defined receiver. + * This file is part of GNSS-SDR. + * + * Copyright (C) 2010-2026 (see AUTHORS file for a list of contributors) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * ----------------------------------------------------------------------------- + */ + + +#ifndef GNSS_SDR_LOG_SINK_H +#define GNSS_SDR_LOG_SINK_H + +#include +#include +#include +#include +#include +#include + +/** \addtogroup Core + * \{ */ +/** \addtogroup Core_Receiver_Library + * Utilities for the core GNSS receiver. + * \{ */ + + +/** A sink must be unregistered, and direct callers stopped, before destruction. + * Directory/file creation errors throw; symlink and runtime I/O errors are + * reported to stderr. An empty directory selects the system temporary directory. + */ +class GnssSdrLogSink : public absl::LogSink +{ +public: + explicit GnssSdrLogSink(const std::string& directory, const std::string& basename = "gnss-sdr"); + ~GnssSdrLogSink() override; + GnssSdrLogSink(const GnssSdrLogSink&) = delete; + GnssSdrLogSink& operator=(const GnssSdrLogSink&) = delete; + GnssSdrLogSink(GnssSdrLogSink&&) = delete; + GnssSdrLogSink& operator=(GnssSdrLogSink&&) = delete; + + void Send(const absl::LogEntry& entry) noexcept override; + void Flush() noexcept override; + const std::string& filename() const noexcept { return d_filename; } + +private: + struct FileCloser + { + void operator()(std::FILE* file) const noexcept { std::fclose(file); } + }; + + void UpdateLatestLink(const std::string& basename) const; + void ReportWriteError() noexcept; + + std::string d_filename; + std::unique_ptr d_logfile; + std::mutex d_mutex; + std::atomic_flag d_error_reported = ATOMIC_FLAG_INIT; +}; + +/** Owns a registered sink. Initialize Abseil once before calling Register(). + * Register/Shutdown belong to the owning thread; Send/Flush may run concurrently. + */ +class GnssSdrLogSinkGuard +{ +public: + GnssSdrLogSinkGuard() = default; + ~GnssSdrLogSinkGuard(); + GnssSdrLogSinkGuard(const GnssSdrLogSinkGuard&) = delete; + GnssSdrLogSinkGuard& operator=(const GnssSdrLogSinkGuard&) = delete; + GnssSdrLogSinkGuard(GnssSdrLogSinkGuard&&) = delete; + GnssSdrLogSinkGuard& operator=(GnssSdrLogSinkGuard&&) = delete; + + void Register(const std::string& directory, const std::string& basename = "gnss-sdr"); + void Shutdown() noexcept; + +private: + std::unique_ptr d_sink; +}; + +/** \} */ +/** \} */ +#endif // GNSS_SDR_LOG_SINK_H diff --git a/src/main/CMakeLists.txt b/src/main/CMakeLists.txt index 022def69d..b1ea054ec 100644 --- a/src/main/CMakeLists.txt +++ b/src/main/CMakeLists.txt @@ -26,7 +26,7 @@ if(ENABLE_GLOG_AND_GFLAGS) target_link_libraries(gnss-sdr PRIVATE Gflags::gflags Glog::glog) target_compile_definitions(gnss-sdr PUBLIC -DUSE_GLOG_AND_GFLAGS=1) else() - target_link_libraries(gnss-sdr PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize absl::log_sink absl::log_sink_registry) + target_link_libraries(gnss-sdr PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize gnss_sdr_log_sink) target_link_libraries(gnss-sdr INTERFACE "$") endif() diff --git a/src/main/main.cc b/src/main/main.cc index 9b949b1d9..f18687009 100644 --- a/src/main/main.cc +++ b/src/main/main.cc @@ -37,10 +37,8 @@ #include // for thread_resource_error #include // for time_point #include // for exception -#include // for ofstream #include // for operator<< #include // for unique_ptr -#include // for lock_guard, mutex #include // for std::flush #include // for string @@ -54,6 +52,7 @@ using namespace google; } #endif #else +#include "gnss_sdr_log_sink.h" #include #include #include @@ -62,84 +61,7 @@ using namespace google; #include #include #include -#include -#include std::string GnssSdrVersionString() { return std::string("gnss-sdr version ") + std::string(GNSS_SDR_VERSION) + "\n"; } -class GnssSdrLogSink : public absl::LogSink -{ -public: - GnssSdrLogSink() - { - if (!absl::GetFlag(FLAGS_log_dir).empty()) - { - filename = absl::GetFlag(FLAGS_log_dir) + "/gnss-sdr.log"; - } - else - { - filename = GetTempDir() + "/gnss-sdr.log"; - } - logfile.open(filename); - } - void Send(const absl::LogEntry& entry) override - { - std::lock_guard lock(logfile_mutex); - logfile << entry.text_message_with_prefix_and_newline() << std::flush; - } - -private: - std::mutex logfile_mutex; - std::ofstream logfile; - std::string filename; -}; - -class GnssSdrLogSinkGuard -{ -public: - GnssSdrLogSinkGuard() = default; - GnssSdrLogSinkGuard(const GnssSdrLogSinkGuard&) = delete; - GnssSdrLogSinkGuard& operator=(const GnssSdrLogSinkGuard&) = delete; - GnssSdrLogSinkGuard(GnssSdrLogSinkGuard&&) = delete; - GnssSdrLogSinkGuard& operator=(GnssSdrLogSinkGuard&&) = delete; - ~GnssSdrLogSinkGuard() noexcept - { - Shutdown(); - } - - void Register() - { - log_sink.reset(new GnssSdrLogSink); - absl::AddLogSink(log_sink.get()); - registered = true; - absl::InitializeLog(); - } - - void Shutdown() noexcept - { - if (registered) - { - try - { - absl::FlushLogSinks(); - } - catch (...) - { - } - try - { - absl::RemoveLogSink(log_sink.get()); - } - catch (...) - { - } - registered = false; - } - log_sink.reset(); - } - -private: - std::unique_ptr log_sink; - bool registered = false; -}; #endif #if CUDA_GPU_ACCEL @@ -216,7 +138,8 @@ try google::InitGoogleLogging(argv[0]); if (FLAGS_log_dir.empty()) #else - log_sink.Register(); + absl::InitializeLog(); + log_sink.Register(absl::GetFlag(FLAGS_log_dir), "gnss-sdr"); if (absl::GetFlag(FLAGS_log_dir).empty()) #endif { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0ea8ac2dc..c7ddd32d3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -740,7 +740,8 @@ if(ENABLE_UNIT_TESTING) target_link_libraries(run_tests PRIVATE Gflags::gflags Glog::glog) target_compile_definitions(run_tests PRIVATE -DUSE_GLOG_AND_GFLAGS=1) else() - target_link_libraries(run_tests PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize) + target_link_libraries(run_tests PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize gnss_sdr_log_sink) + target_sources(run_tests PRIVATE unit-tests/control-plane/gnss_sdr_log_sink_test.cc) target_link_libraries(run_tests INTERFACE "$") endif() target_include_directories(run_tests @@ -889,7 +890,7 @@ if(ENABLE_FPGA) target_link_libraries(gps_l1_ca_dll_pll_tracking_test_fpga PRIVATE Gflags::gflags Glog::glog) target_compile_definitions(gps_l1_ca_dll_pll_tracking_test_fpga PRIVATE -DUSE_GLOG_AND_GFLAGS=1) else() - target_link_libraries(gps_l1_ca_dll_pll_tracking_test_fpga PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize) + target_link_libraries(gps_l1_ca_dll_pll_tracking_test_fpga PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize gnss_sdr_log_sink) target_link_libraries(gps_l1_ca_dll_pll_tracking_test_fpga INTERFACE "$") endif() target_include_directories(gps_l1_ca_dll_pll_tracking_test_fpga @@ -1016,7 +1017,7 @@ if(ENABLE_SYSTEM_TESTING) if(ENABLE_GLOG_AND_GFLAGS) set(OPT_LIBS_ ${OPT_LIBS_} Gflags::gflags Glog::glog) else() - set(OPT_LIBS_ ${OPT_LIBS_} absl::flags_parse absl::log absl::log_initialize absl::log_sink absl::log_sink_registry) + set(OPT_LIBS_ ${OPT_LIBS_} absl::flags_parse absl::log absl::log_initialize gnss_sdr_log_sink) endif() add_system_test(position_test CMAKE_ARGS -DCMAKE_BUILD_TYPE=$<$:Debug>$<$:Release>$<$:RelWithDebInfo>$<$:MinSizeRel>$<$:Debug>$<$:Debug>$<$:RelWithDebInfo>$<$:RelWithDebInfo>$<$:Debug> @@ -1092,7 +1093,7 @@ if(NOT ENABLE_PACKAGING AND NOT ENABLE_FPGA) target_link_libraries(flowgraph_test PRIVATE Gflags::gflags Glog::glog) target_compile_definitions(flowgraph_test PRIVATE -DUSE_GLOG_AND_GFLAGS=1) else() - target_link_libraries(flowgraph_test PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize) + target_link_libraries(flowgraph_test PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize gnss_sdr_log_sink) target_link_libraries(flowgraph_test INTERFACE "$") endif() @@ -1151,7 +1152,7 @@ if(NOT ENABLE_PACKAGING AND NOT ENABLE_FPGA) target_link_libraries(gnss_block_test PRIVATE Gflags::gflags Glog::glog) target_compile_definitions(gnss_block_test PRIVATE -DUSE_GLOG_AND_GFLAGS=1) else() - target_link_libraries(gnss_block_test PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize) + target_link_libraries(gnss_block_test PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize gnss_sdr_log_sink) target_link_libraries(gnss_block_test INTERFACE "$") endif() @@ -1212,7 +1213,7 @@ if(NOT ENABLE_PACKAGING AND NOT ENABLE_FPGA) target_link_libraries(gnuradio_block_test PRIVATE Gflags::gflags Glog::glog) target_compile_definitions(gnuradio_block_test PRIVATE -DUSE_GLOG_AND_GFLAGS=1) else() - target_link_libraries(gnuradio_block_test PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize) + target_link_libraries(gnuradio_block_test PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize gnss_sdr_log_sink) target_link_libraries(gnuradio_block_test INTERFACE "$") endif() @@ -1249,7 +1250,7 @@ if(ENABLE_GLOG_AND_GFLAGS) target_link_libraries(matio_test PRIVATE Gflags::gflags Glog::glog) target_compile_definitions(matio_test PRIVATE -DUSE_GLOG_AND_GFLAGS=1) else() - target_link_libraries(matio_test PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize) + target_link_libraries(matio_test PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize gnss_sdr_log_sink) target_link_libraries(matio_test INTERFACE "$") endif() @@ -1302,7 +1303,7 @@ if(NOT ENABLE_PACKAGING AND NOT ENABLE_FPGA) target_link_libraries(acq_test PRIVATE Gflags::gflags Glog::glog) target_compile_definitions(acq_test PRIVATE -DUSE_GLOG_AND_GFLAGS=1) else() - target_link_libraries(acq_test PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize) + target_link_libraries(acq_test PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize gnss_sdr_log_sink) target_link_libraries(acq_test INTERFACE "$") endif() target_include_directories(acq_test @@ -1394,7 +1395,7 @@ if(NOT ENABLE_PACKAGING AND NOT ENABLE_FPGA) target_link_libraries(trk_test PRIVATE Gflags::gflags Glog::glog) target_compile_definitions(trk_test PRIVATE -DUSE_GLOG_AND_GFLAGS=1) else() - target_link_libraries(trk_test PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize) + target_link_libraries(trk_test PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize gnss_sdr_log_sink) target_link_libraries(trk_test INTERFACE "$") endif() if(USE_GENERIC_LAMBDAS) @@ -1459,7 +1460,7 @@ if(NOT ENABLE_PACKAGING AND NOT ENABLE_FPGA) target_link_libraries(control_thread_test PRIVATE Gflags::gflags Glog::glog) target_compile_definitions(control_thread_test PRIVATE -DUSE_GLOG_AND_GFLAGS=1) else() - target_link_libraries(control_thread_test PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize) + target_link_libraries(control_thread_test PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize gnss_sdr_log_sink) target_link_libraries(control_thread_test INTERFACE "$") endif() diff --git a/tests/single_test_main.cc b/tests/single_test_main.cc index 9537a1f09..d890834ae 100644 --- a/tests/single_test_main.cc +++ b/tests/single_test_main.cc @@ -21,10 +21,8 @@ #include "gnss_sdr_flags.h" #include "gps_acq_assist.h" #include -#include #include #include -#include #include #include @@ -39,86 +37,12 @@ using namespace google; DECLARE_string(log_dir); #endif #else +#include "gnss_sdr_log_sink.h" #include #include #include #include #include -#include -#include -class TestLogSink : public absl::LogSink -{ -public: - TestLogSink() - { - if (!absl::GetFlag(FLAGS_log_dir).empty()) - { - logfile.open(absl::GetFlag(FLAGS_log_dir) + "/test.log"); - } - else - { - logfile.open(GetTempDir() + "/test.log"); - } - } - void Send(const absl::LogEntry& entry) override - { - std::lock_guard lock(logfile_mutex); - logfile << entry.text_message_with_prefix_and_newline() << std::flush; - } - -private: - std::mutex logfile_mutex; - std::ofstream logfile; -}; - -class TestLogSinkGuard -{ -public: - TestLogSinkGuard() = default; - TestLogSinkGuard(const TestLogSinkGuard&) = delete; - TestLogSinkGuard& operator=(const TestLogSinkGuard&) = delete; - TestLogSinkGuard(TestLogSinkGuard&&) = delete; - TestLogSinkGuard& operator=(TestLogSinkGuard&&) = delete; - ~TestLogSinkGuard() noexcept - { - Shutdown(); - } - - void Register() - { - log_sink.reset(new TestLogSink); - absl::AddLogSink(log_sink.get()); - registered = true; - absl::InitializeLog(); - } - - void Shutdown() noexcept - { - if (registered) - { - try - { - absl::FlushLogSinks(); - } - catch (...) - { - } - try - { - absl::RemoveLogSink(log_sink.get()); - } - catch (...) - { - } - registered = false; - } - log_sink.reset(); - } - -private: - std::unique_ptr log_sink; - bool registered = false; -}; #endif @@ -140,7 +64,7 @@ try { } // catch the "testing::internal::::ClassUniqueToAlwaysTrue" from gtest #else - TestLogSinkGuard log_sink; + GnssSdrLogSinkGuard log_sink; absl::ParseCommandLine(argc, argv); try { @@ -149,7 +73,8 @@ try catch (...) { } // catch the "testing::internal::::ClassUniqueToAlwaysTrue" from gtest - log_sink.Register(); + absl::InitializeLog(); + log_sink.Register(absl::GetFlag(FLAGS_log_dir), "test"); #endif int res = 0; try diff --git a/tests/system-tests/position_test.cc b/tests/system-tests/position_test.cc index df90734ad..8a131e8fb 100644 --- a/tests/system-tests/position_test.cc +++ b/tests/system-tests/position_test.cc @@ -47,7 +47,6 @@ #include #include #include -#include #include #include #include @@ -62,42 +61,17 @@ using namespace google; } #endif #else +#include "gnss_sdr_log_sink.h" #include #include #include #include -#include -#include #endif #if USE_GLOG_AND_GFLAGS DEFINE_int32(num_channels, 11, "Number of channels"); #else ABSL_FLAG(int32_t, num_channels, 11, "Number of channels"); -class PositionTestLogSink : public absl::LogSink -{ -public: - PositionTestLogSink() - { - if (!absl::GetFlag(FLAGS_log_dir).empty()) - { - logfile.open(absl::GetFlag(FLAGS_log_dir) + "/position_test.log"); - } - else - { - logfile.open(GetTempDir() + "/position_test.log"); - } - } - void Send(const absl::LogEntry& entry) override - { - std::lock_guard lock(logfile_mutex); - logfile << entry.text_message_with_prefix_and_newline() << std::flush; - } - -private: - std::mutex logfile_mutex; - std::ofstream logfile; -}; #endif // For GPS NAVIGATION (L1) @@ -1249,8 +1223,7 @@ int main(int argc, char** argv) #if USE_GLOG_AND_GFLAGS bool command_line_flags_initialized = false; #else - std::unique_ptr log_sink; - bool log_sink_registered = false; + GnssSdrLogSinkGuard log_sink; #endif try @@ -1270,10 +1243,8 @@ int main(int argc, char** argv) google::InitGoogleLogging(argv[0]); #else absl::ParseCommandLine(argc, argv); - log_sink.reset(new PositionTestLogSink); - absl::AddLogSink(log_sink.get()); - log_sink_registered = true; absl::InitializeLog(); + log_sink.Register(absl::GetFlag(FLAGS_log_dir), "position_test"); #endif // Run the Tests @@ -1311,20 +1282,7 @@ int main(int argc, char** argv) } } #else - if (log_sink_registered) - { - try - { - absl::FlushLogSinks(); - absl::RemoveLogSink(log_sink.get()); - log_sink_registered = false; - log_sink.reset(); - } - catch (...) - { - std::cerr << "Unexpected exception while shutting down logging.\n"; - } - } + log_sink.Shutdown(); #endif return res; } diff --git a/tests/test_main.cc b/tests/test_main.cc index e030c531e..4aeaeeedd 100644 --- a/tests/test_main.cc +++ b/tests/test_main.cc @@ -19,81 +19,7 @@ #include "unit_tests_common.h" #if !USE_GLOG_AND_GFLAGS -class TestingLogSink : public absl::LogSink -{ -public: - TestingLogSink() - { - if (!absl::GetFlag(FLAGS_log_dir).empty()) - { - filename = std::string(absl::GetFlag(FLAGS_log_dir) + "/run_tests.log"); - } - else - { - filename = std::string(GetTempDir() + "/run_tests.log"); - } - logfile.open(filename); - } - void Send(const absl::LogEntry& entry) override - { - std::lock_guard lock(logfile_mutex); - logfile << entry.text_message_with_prefix_and_newline() << std::flush; - } - -private: - std::mutex logfile_mutex; - std::ofstream logfile; - std::string filename; -}; - -class TestingLogSinkGuard -{ -public: - TestingLogSinkGuard() = default; - TestingLogSinkGuard(const TestingLogSinkGuard&) = delete; - TestingLogSinkGuard& operator=(const TestingLogSinkGuard&) = delete; - TestingLogSinkGuard(TestingLogSinkGuard&&) = delete; - TestingLogSinkGuard& operator=(TestingLogSinkGuard&&) = delete; - ~TestingLogSinkGuard() noexcept - { - Shutdown(); - } - - void Register() - { - log_sink.reset(new TestingLogSink); - absl::AddLogSink(log_sink.get()); - registered = true; - absl::InitializeLog(); - } - - void Shutdown() noexcept - { - if (registered) - { - try - { - absl::FlushLogSinks(); - } - catch (...) - { - } - try - { - absl::RemoveLogSink(log_sink.get()); - } - catch (...) - { - } - registered = false; - } - log_sink.reset(); - } - -private: - std::unique_ptr log_sink; - bool registered = false; -}; +#include "gnss_sdr_log_sink.h" #endif // For GPS NAVIGATION (L1) @@ -116,9 +42,10 @@ try gflags::ParseCommandLineFlags(&argc, &argv, true); google::InitGoogleLogging(argv[0]); #else - TestingLogSinkGuard log_sink; + GnssSdrLogSinkGuard log_sink; absl::ParseCommandLine(argc, argv); - log_sink.Register(); + absl::InitializeLog(); + log_sink.Register(absl::GetFlag(FLAGS_log_dir), "run_tests"); #endif try { diff --git a/tests/unit-tests/control-plane/gnss_sdr_log_sink_test.cc b/tests/unit-tests/control-plane/gnss_sdr_log_sink_test.cc new file mode 100644 index 000000000..7780620fe --- /dev/null +++ b/tests/unit-tests/control-plane/gnss_sdr_log_sink_test.cc @@ -0,0 +1,260 @@ +/*! + * \file gnss_sdr_log_sink_test.cc + * \brief Tests for per-run Abseil file logging. + * \author Carles Fernandez-Prades, 2026. cfernandez(at)cttc.es + * + * ----------------------------------------------------------------------------- + * + * GNSS-SDR is a Global Navigation Satellite System software-defined receiver. + * This file is part of GNSS-SDR. + * + * Copyright (C) 2010-2026 (see AUTHORS file for a list of contributors) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * ----------------------------------------------------------------------------- + */ + +#include "gnss_sdr_filesystem.h" +#include "gnss_sdr_log_sink.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if !defined(_WIN32) +#include +#include +#endif + +class GnssSdrLogSinkTest : public ::testing::Test +{ +protected: + void SetUp() override + { + do + { + directory = fs::temp_directory_path() / ("gnss-sdr-log-sink-test-" + std::to_string(absl::ToUnixNanos(absl::Now()))); + } + while (!fs::create_directory(directory)); + } + + void TearDown() override + { + errorlib::error_code error; + fs::remove_all(directory, error); + } + + static std::string Read(const fs::path& path) + { + std::ifstream file(path.string(), std::ios::binary); + return std::string(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + + // Compare the exact bytes supplied by Abseil, without assuming a timezone + // or reimplementing its prefix formatting in the test. + class ForwardingSink : public absl::LogSink + { + public: + explicit ForwardingSink(GnssSdrLogSink& sink) : sink(sink) {} + void Send(const absl::LogEntry& entry) override + { + expected.append(entry.text_message_with_prefix_and_newline().data(), entry.text_message_with_prefix_and_newline().size()); + sink.Send(entry); + } + GnssSdrLogSink& sink; + std::string expected; + }; + + fs::path directory; +}; + + +TEST_F(GnssSdrLogSinkTest, CreatesDirectoriesAndPreservesEveryBasename) +{ + const auto nested = directory / "nested" / "logs"; + for (const auto* basename : {"gnss-sdr", "run_tests", "test", "front_end_cal", "position_test"}) + { + GnssSdrLogSink sink(nested.string(), basename); + const fs::path logfile(sink.filename()); + EXPECT_TRUE(fs::is_regular_file(logfile)); + EXPECT_EQ(logfile.parent_path(), nested); + EXPECT_TRUE(std::regex_match(logfile.filename().string(), std::regex(std::string(basename) + R"(\.[0-9]{8}-[0-9]{6}\.[0-9]+\.log)"))); +#if !defined(_WIN32) + EXPECT_EQ(fs::read_symlink(nested / (std::string(basename) + ".log")), logfile.filename()); +#endif + } +} + + +TEST_F(GnssSdrLogSinkTest, RepeatedCreationPreservesFilesAndUpdatesRelativeLink) +{ + std::set filenames; + for (int run = 0; run < 20; ++run) + { + GnssSdrLogSink sink(directory.string()); + EXPECT_TRUE(filenames.insert(sink.filename()).second); + LOG(INFO).NoPrefix().ToSinkOnly(&sink) << "run " << run; +#if !defined(_WIN32) + EXPECT_EQ(fs::read_symlink(directory / "gnss-sdr.log"), fs::path(sink.filename()).filename()); +#endif + } + for (const auto& filename : filenames) + { + EXPECT_EQ(Read(filename).substr(0, 4), "run "); + } +} + + +TEST_F(GnssSdrLogSinkTest, PreservesAbseilFormattingAndFlushesErrors) +{ + GnssSdrLogSink sink(directory.string()); + ForwardingSink capture(sink); + LOG(INFO).AtLocation("log_sink_test.cc", 42).ToSinkOnly(&capture) << "buffered message"; + sink.Flush(); + EXPECT_EQ(Read(sink.filename()), capture.expected); + EXPECT_NE(capture.expected.find("log_sink_test.cc:42] buffered message\n"), std::string::npos); + LOG(ERROR).NoPrefix().ToSinkOnly(&capture) << "flush immediately"; + EXPECT_EQ(Read(sink.filename()), capture.expected); +} + + +TEST_F(GnssSdrLogSinkTest, DestructorFlushesBufferedMessages) +{ + std::string filename; + { + GnssSdrLogSink sink(directory.string()); + filename = sink.filename(); + LOG(INFO).NoPrefix().ToSinkOnly(&sink) << "flushed on destruction"; + } + EXPECT_EQ(Read(filename), "flushed on destruction\n"); +} + + +TEST_F(GnssSdrLogSinkTest, ConcurrentSendAndFlushPreserveWholeMessages) +{ + GnssSdrLogSink sink(directory.string()); + std::atomic done{false}; + std::thread flusher([&sink, &done]() { + while (!done.load()) + { + sink.Flush(); + std::this_thread::yield(); + } + }); + std::vector writers; + writers.reserve(8); + for (int writer = 0; writer < 8; ++writer) + { + writers.emplace_back([&sink, writer]() { + for (int message = 0; message < 250; ++message) + { + LOG(INFO).NoPrefix().ToSinkOnly(&sink) << writer << ":" << message; + } + }); + } + for (auto& writer : writers) + { + writer.join(); + } + done.store(true); + flusher.join(); + sink.Flush(); + std::ifstream file(sink.filename()); + std::set messages; + std::string line; + while (std::getline(file, line)) + { + EXPECT_TRUE(messages.insert(line).second); + } + EXPECT_EQ(messages.size(), 2000U); + for (int writer = 0; writer < 8; ++writer) + { + for (int message = 0; message < 250; ++message) + { + EXPECT_EQ(messages.count(std::to_string(writer) + ":" + std::to_string(message)), 1U); + } + } +} + + +TEST_F(GnssSdrLogSinkTest, RejectsInvalidDirectoryAndBasename) +{ + const auto file = directory / "file"; + std::ofstream(file.string()) << "keep me"; + EXPECT_THROW(GnssSdrLogSink sink(file.string()), std::exception); + EXPECT_THROW(GnssSdrLogSink sink((file / "logs").string()), std::exception); + EXPECT_THROW(GnssSdrLogSink sink(directory.string(), "../escape"), std::invalid_argument); + EXPECT_EQ(Read(file), "keep me"); +} + + +TEST_F(GnssSdrLogSinkTest, GuardRegistersFlushesAndUnregisters) +{ + GnssSdrLogSinkGuard guard; + guard.Register(directory.string()); + EXPECT_THROW(guard.Register(directory.string()), std::logic_error); + LOG(INFO) << "registered message"; + absl::FlushLogSinks(); + fs::path logfile; + for (const auto& entry : fs::directory_iterator(directory)) + { + if (!fs::is_symlink(entry.path()) && entry.path().extension() == ".log") + { + logfile = entry.path(); + } + } + ASSERT_FALSE(logfile.empty()); + EXPECT_NE(Read(logfile).find("registered message"), std::string::npos); + guard.Shutdown(); + guard.Shutdown(); + const auto contents = Read(logfile); + LOG(INFO) << "after unregister"; + EXPECT_EQ(Read(logfile), contents); +} + + +#if !defined(_WIN32) +TEST_F(GnssSdrLogSinkTest, PreservesLegacyStableLogfile) +{ + std::ofstream((directory / "gnss-sdr.log").string()) << "legacy log"; + GnssSdrLogSink sink(directory.string()); + EXPECT_EQ(Read(sink.filename() + ".previous"), "legacy log"); + EXPECT_EQ(fs::read_symlink(directory / "gnss-sdr.log"), fs::path(sink.filename()).filename()); +} + + +TEST_F(GnssSdrLogSinkTest, SymlinkFailureLeavesLoggingUsableAndCleansTemporaryLink) +{ + fs::create_directory(directory / "gnss-sdr.log"); + GnssSdrLogSink sink(directory.string()); + LOG(ERROR).NoPrefix().ToSinkOnly(&sink) << "still logging"; + EXPECT_EQ(Read(sink.filename()), "still logging\n"); + EXPECT_TRUE(fs::is_directory(directory / "gnss-sdr.log")); + for (const auto& entry : fs::directory_iterator(directory)) + { + EXPECT_EQ(entry.path().filename().string().find(".link."), std::string::npos); + } +} + + +TEST_F(GnssSdrLogSinkTest, RejectsUnwritableDirectory) +{ + if (geteuid() == 0) + { + GTEST_SKIP() << "Root can write despite directory permissions"; + } + const auto readonly = directory / "readonly"; + fs::create_directory(readonly); + ASSERT_EQ(chmod(readonly.c_str(), 0500), 0); + EXPECT_THROW(GnssSdrLogSink sink(readonly.string()), std::exception); + EXPECT_EQ(chmod(readonly.c_str(), 0700), 0); +} +#endif diff --git a/utils/front-end-cal/CMakeLists.txt b/utils/front-end-cal/CMakeLists.txt index 4d8f5907e..64df797be 100644 --- a/utils/front-end-cal/CMakeLists.txt +++ b/utils/front-end-cal/CMakeLists.txt @@ -70,7 +70,7 @@ if(ENABLE_GLOG_AND_GFLAGS) target_link_libraries(front-end-cal PRIVATE Glog::glog) target_compile_definitions(front-end-cal PRIVATE -DUSE_GLOG_AND_GFLAGS=1) else() - target_link_libraries(front-end-cal PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize absl::log_sink absl::log_sink_registry) + target_link_libraries(front-end-cal PRIVATE absl::flags absl::flags_parse absl::log absl::log_initialize gnss_sdr_log_sink) endif() if(GNURADIO_USES_STD_POINTERS) diff --git a/utils/front-end-cal/main.cc b/utils/front-end-cal/main.cc index 6ff908fe1..5f671f32e 100644 --- a/utils/front-end-cal/main.cc +++ b/utils/front-end-cal/main.cc @@ -63,7 +63,6 @@ #include #include #include -#include #include // for logic_error #include #include @@ -74,6 +73,7 @@ #include #include #else +#include "gnss_sdr_log_sink.h" #include #include #include @@ -81,10 +81,6 @@ #include #include #include -#include -#include - - #endif #if HAS_GENERIC_LAMBDA @@ -107,80 +103,6 @@ using namespace google; #endif DECLARE_string(log_dir); #else -class FrontEndCalLogSink : public absl::LogSink -{ -public: - FrontEndCalLogSink() - { - if (!absl::GetFlag(FLAGS_log_dir).empty()) - { - logfile.open(absl::GetFlag(FLAGS_log_dir) + "/front_end_cal.log"); - } - else - { - logfile.open(GetTempDir() + "/front_end_cal.log"); - } - } - void Send(const absl::LogEntry& entry) override - { - std::lock_guard lock(logfile_mutex); - logfile << entry.text_message_with_prefix_and_newline() << std::flush; - } - -private: - std::mutex logfile_mutex; - std::ofstream logfile; -}; - -class FrontEndCalLogSinkGuard -{ -public: - FrontEndCalLogSinkGuard() = default; - FrontEndCalLogSinkGuard(const FrontEndCalLogSinkGuard&) = delete; - FrontEndCalLogSinkGuard& operator=(const FrontEndCalLogSinkGuard&) = delete; - FrontEndCalLogSinkGuard(FrontEndCalLogSinkGuard&&) = delete; - FrontEndCalLogSinkGuard& operator=(FrontEndCalLogSinkGuard&&) = delete; - ~FrontEndCalLogSinkGuard() noexcept - { - Shutdown(); - } - - void Register() - { - log_sink.reset(new FrontEndCalLogSink); - absl::AddLogSink(log_sink.get()); - registered = true; - absl::InitializeLog(); - } - - void Shutdown() noexcept - { - if (registered) - { - try - { - absl::FlushLogSinks(); - } - catch (...) - { - } - try - { - absl::RemoveLogSink(log_sink.get()); - } - catch (...) - { - } - registered = false; - } - log_sink.reset(); - } - -private: - std::unique_ptr log_sink; - bool registered = false; -}; - std::string FrontEndCalVersionString() { return std::string(FRONT_END_CAL_VERSION) + "\n"; } #endif @@ -369,7 +291,7 @@ try { #if USE_GLOG_AND_GFLAGS #else - FrontEndCalLogSinkGuard log_sink; + GnssSdrLogSinkGuard log_sink; #endif try @@ -404,12 +326,13 @@ try google::InitGoogleLogging(argv[0]); if (FLAGS_log_dir.empty()) #else - log_sink.Register(); + absl::InitializeLog(); + log_sink.Register(absl::GetFlag(FLAGS_log_dir), "front_end_cal"); if (absl::GetFlag(FLAGS_log_dir).empty()) #endif { std::cout << "Logging will be done at " - << "/tmp" + << fs::temp_directory_path() << '\n' << "Use front-end-cal --log_dir=/path/to/log to change that." << '\n';