Keep a separate logfile for each run with Abseil logging, as google-glog did

This commit is contained in:
Carles Fernandez
2026-09-27 18:26:57 +02:00
parent 20a0a69780
commit 044aee9bc2
13 changed files with 638 additions and 376 deletions
+4
View File
@@ -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/
+24
View File
@@ -206,3 +206,27 @@ set_property(TARGET core_libs
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<BUILD_INTERFACE:${GNSSSDR_SOURCE_DIR}/src/core/interfaces>
)
# 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 $<$<BOOL:${NEED_BOOST_SYSTEM}>:Boost::system>)
endif()
endif()
+227
View File
@@ -0,0 +1,227 @@
/*!
* \file gnss_sdr_log_sink.cc
* \brief Buffered, per-run file logging for Abseil.
* \author Carles Fernandez-Prades, 2026 <carles.fernandez@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_log_sink.h"
#include "gnss_sdr_filesystem.h"
#include <absl/base/log_severity.h>
#include <absl/log/log_entry.h>
#include <absl/log/log_sink_registry.h>
#include <absl/time/clock.h>
#include <absl/time/time.h>
#include <cerrno>
#include <fcntl.h>
#include <stdexcept>
#include <system_error>
#include <utility>
#if defined(_WIN32)
#include <io.h>
#include <process.h>
#include <sys/stat.h>
#else
#include <unistd.h>
#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<std::mutex> 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<std::mutex> 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<GnssSdrLogSink>(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();
}
}
+90
View File
@@ -0,0 +1,90 @@
/*!
* \file gnss_sdr_log_sink.h
* \brief Buffered, per-run file logging for Abseil.
* \author Carles Fernandez-Prades, 2026 <carles.fernandez@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
*
* -----------------------------------------------------------------------------
*/
#ifndef GNSS_SDR_LOG_SINK_H
#define GNSS_SDR_LOG_SINK_H
#include <absl/log/log_sink.h>
#include <atomic>
#include <cstdio>
#include <memory>
#include <mutex>
#include <string>
/** \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<std::FILE, FileCloser> 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<GnssSdrLogSink> d_sink;
};
/** \} */
/** \} */
#endif // GNSS_SDR_LOG_SINK_H
+1 -1
View File
@@ -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 "$<LINK_LIBRARY:WHOLE_ARCHIVE,absl::log_flags>")
endif()
+3 -80
View File
@@ -37,10 +37,8 @@
#include <boost/thread/exceptions.hpp> // for thread_resource_error
#include <chrono> // for time_point
#include <exception> // for exception
#include <fstream> // for ofstream
#include <iostream> // for operator<<
#include <memory> // for unique_ptr
#include <mutex> // for lock_guard, mutex
#include <ostream> // for std::flush
#include <string> // for string
@@ -54,6 +52,7 @@ using namespace google;
}
#endif
#else
#include "gnss_sdr_log_sink.h"
#include <absl/flags/flag.h>
#include <absl/flags/parse.h>
#include <absl/flags/usage.h>
@@ -62,84 +61,7 @@ using namespace google;
#include <absl/log/globals.h>
#include <absl/log/initialize.h>
#include <absl/log/log.h>
#include <absl/log/log_sink.h>
#include <absl/log/log_sink_registry.h>
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<std::mutex> 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<GnssSdrLogSink> 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
{
+11 -10
View File
@@ -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 "$<LINK_LIBRARY:WHOLE_ARCHIVE,absl::log_flags>")
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 "$<LINK_LIBRARY:WHOLE_ARCHIVE,absl::log_flags>")
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=$<$<CONFIG:Debug>:Debug>$<$<CONFIG:Release>:Release>$<$<CONFIG:RelWithDebInfo>:RelWithDebInfo>$<$<CONFIG:MinSizeRel>:MinSizeRel>$<$<CONFIG:NoOptWithASM>:Debug>$<$<CONFIG:Coverage>:Debug>$<$<CONFIG:O2WithASM>:RelWithDebInfo>$<$<CONFIG:O3WithASM>:RelWithDebInfo>$<$<CONFIG:ASAN>: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 "$<LINK_LIBRARY:WHOLE_ARCHIVE,absl::log_flags>")
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 "$<LINK_LIBRARY:WHOLE_ARCHIVE,absl::log_flags>")
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 "$<LINK_LIBRARY:WHOLE_ARCHIVE,absl::log_flags>")
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 "$<LINK_LIBRARY:WHOLE_ARCHIVE,absl::log_flags>")
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 "$<LINK_LIBRARY:WHOLE_ARCHIVE,absl::log_flags>")
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 "$<LINK_LIBRARY:WHOLE_ARCHIVE,absl::log_flags>")
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 "$<LINK_LIBRARY:WHOLE_ARCHIVE,absl::log_flags>")
endif()
+4 -79
View File
@@ -21,10 +21,8 @@
#include "gnss_sdr_flags.h"
#include "gps_acq_assist.h"
#include <gtest/gtest.h>
#include <fstream>
#include <iostream>
#include <memory>
#include <mutex>
#include <ostream>
#include <string>
@@ -39,86 +37,12 @@ using namespace google;
DECLARE_string(log_dir);
#endif
#else
#include "gnss_sdr_log_sink.h"
#include <absl/flags/flag.h>
#include <absl/flags/parse.h>
#include <absl/log/flags.h>
#include <absl/log/initialize.h>
#include <absl/log/log.h>
#include <absl/log/log_sink.h>
#include <absl/log/log_sink_registry.h>
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<std::mutex> 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<TestLogSink> log_sink;
bool registered = false;
};
#endif
@@ -140,7 +64,7 @@ try
{
} // catch the "testing::internal::<unnamed>::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::<unnamed>::ClassUniqueToAlwaysTrue" from gtest
log_sink.Register();
absl::InitializeLog();
log_sink.Register(absl::GetFlag(FLAGS_log_dir), "test");
#endif
int res = 0;
try
+4 -46
View File
@@ -47,7 +47,6 @@
#include <fstream>
#include <iomanip>
#include <memory>
#include <mutex>
#include <numeric>
#include <ostream>
#include <string>
@@ -62,42 +61,17 @@ using namespace google;
}
#endif
#else
#include "gnss_sdr_log_sink.h"
#include <absl/flags/parse.h>
#include <absl/log/globals.h>
#include <absl/log/initialize.h>
#include <absl/log/log.h>
#include <absl/log/log_sink.h>
#include <absl/log/log_sink_registry.h>
#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<std::mutex> 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<PositionTestLogSink> 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;
}
+4 -77
View File
@@ -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<std::mutex> 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<TestingLogSink> 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
{
@@ -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 <gtest/gtest.h>
#include <absl/log/log.h>
#include <absl/log/log_sink_registry.h>
#include <absl/time/clock.h>
#include <absl/time/time.h>
#include <atomic>
#include <fstream>
#include <iterator>
#include <regex>
#include <set>
#include <string>
#include <thread>
#include <vector>
#if !defined(_WIN32)
#include <sys/stat.h>
#include <unistd.h>
#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<char>(file), std::istreambuf_iterator<char>());
}
// 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<std::string> 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<bool> done{false};
std::thread flusher([&sink, &done]() {
while (!done.load())
{
sink.Flush();
std::this_thread::yield();
}
});
std::vector<std::thread> 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<std::string> 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
+1 -1
View File
@@ -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)
+5 -82
View File
@@ -63,7 +63,6 @@
#include <iostream>
#include <map>
#include <memory>
#include <mutex>
#include <stdexcept> // for logic_error
#include <string>
#include <thread>
@@ -74,6 +73,7 @@
#include <gflags/gflags.h>
#include <glog/logging.h>
#else
#include "gnss_sdr_log_sink.h"
#include <absl/flags/flag.h>
#include <absl/flags/parse.h>
#include <absl/flags/usage.h>
@@ -81,10 +81,6 @@
#include <absl/log/globals.h>
#include <absl/log/initialize.h>
#include <absl/log/log.h>
#include <absl/log/log_sink.h>
#include <absl/log/log_sink_registry.h>
#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<std::mutex> 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<FrontEndCalLogSink> 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';