diff --git a/include/logit_cpp/logit/Logger.hpp b/include/logit_cpp/logit/Logger.hpp index cb5298f..af2ce38 100644 --- a/include/logit_cpp/logit/Logger.hpp +++ b/include/logit_cpp/logit/Logger.hpp @@ -399,6 +399,89 @@ namespace logit { } } + /// \brief Clears logger-owned records for a specific logger. + /// \param logger_index Index of logger. + /// \param options Data categories to clear. + /// \return Cleanup result for the selected logger. + LogClearResult clear_logger(int logger_index, const LogClearOptions& options = LogClearOptions()) { + LogClearResult result; + if (m_shutdown.load(std::memory_order_acquire)) { + result.ok = false; + result.status = LogClearStatus::Failed; + result.message = "logger system is shut down"; + return result; + } + + auto strategy = get_strategy_snapshot(logger_index); + if (!strategy) { + result.ok = false; + result.status = LogClearStatus::Failed; + result.message = "logger index not found"; + return result; + } + + std::lock_guard exec_lock(strategy->exec_mx); + if (m_shutdown.load(std::memory_order_acquire)) { + result.ok = false; + result.status = LogClearStatus::Failed; + result.message = "logger system is shut down"; + return result; + } + return strategy->logger->clear_logs(options); + } + + /// \brief Clears logger-owned records for all registered loggers. + /// \param options Data categories to clear. + /// \return Aggregated cleanup result. Unsupported loggers are skipped. + LogClearResult clear_all_loggers(const LogClearOptions& options = LogClearOptions()) { + LogClearResult result; + if (m_shutdown.load(std::memory_order_acquire)) { + result.ok = false; + result.status = LogClearStatus::Failed; + result.message = "logger system is shut down"; + return result; + } + + const auto snapshot = get_all_strategy_snapshots(); + std::size_t supported = 0; + std::size_t failed = 0; + std::size_t unsupported = 0; + for (const auto& strategy : snapshot) { + if (!strategy) continue; + std::lock_guard exec_lock(strategy->exec_mx); + if (m_shutdown.load(std::memory_order_acquire)) { + result.ok = false; + result.status = LogClearStatus::Failed; + result.message = "logger system is shut down"; + return result; + } + const LogClearResult one = strategy->logger->clear_logs(options); + if (one.status == LogClearStatus::Cleared && one.ok) { + ++supported; + result.cleared_records += one.cleared_records; + } else if (one.status == LogClearStatus::Unsupported) { + ++unsupported; + } else { + ++failed; + if (result.message.empty()) { + result.message = one.message; + } + } + } + + result.ok = supported > 0 && failed == 0; + if (result.ok) { + result.status = LogClearStatus::Cleared; + result.message = unsupported > 0 ? "cleared supported loggers; skipped unsupported loggers" : "cleared"; + } else { + result.status = unsupported > 0 && failed == 0 ? LogClearStatus::Unsupported : LogClearStatus::Failed; + if (result.message.empty()) { + result.message = unsupported > 0 ? "no logger supports clearing" : "no loggers to clear"; + } + } + return result; + } + /// \brief Returns the number of registered logger strategies. std::size_t logger_count() const { LoggerReadLock lock(m_loggers_mx); diff --git a/include/logit_cpp/logit/log_macros.hpp b/include/logit_cpp/logit/log_macros.hpp index 4080976..a1927a4 100644 --- a/include/logit_cpp/logit/log_macros.hpp +++ b/include/logit_cpp/logit/log_macros.hpp @@ -2812,6 +2812,30 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT #define LOGIT_READ_LOG_FILES(logger_index, paths) \ logit::Logger::get_instance().read_log_files(logger_index, paths) +/// \brief Clears logger-owned records for a specific logger. +/// \param logger_index Index of logger. +/// \return Cleanup result for the selected logger. +#define LOGIT_CLEAR_LOGGER(logger_index) \ + logit::Logger::get_instance().clear_logger(logger_index) + +/// \brief Clears logger-owned records for a specific logger with options. +/// \param logger_index Index of logger. +/// \param options LogClearOptions value. +/// \return Cleanup result for the selected logger. +#define LOGIT_CLEAR_LOGGER_EX(logger_index, options) \ + logit::Logger::get_instance().clear_logger(logger_index, options) + +/// \brief Clears logger-owned records for all registered loggers. +/// \return Aggregated cleanup result. +#define LOGIT_CLEAR_ALL_LOGGERS() \ + logit::Logger::get_instance().clear_all_loggers() + +/// \brief Clears logger-owned records for all registered loggers with options. +/// \param options LogClearOptions value. +/// \return Aggregated cleanup result. +#define LOGIT_CLEAR_ALL_LOGGERS_EX(options) \ + logit::Logger::get_instance().clear_all_loggers(options) + /// \brief Enables or disables a logger. /// \param logger_index Index of logger. /// \param enabled True to enable, false to disable. diff --git a/include/logit_cpp/logit/loggers/FileLogger.hpp b/include/logit_cpp/logit/loggers/FileLogger.hpp index 86eeda5..6fa315e 100644 --- a/include/logit_cpp/logit/loggers/FileLogger.hpp +++ b/include/logit_cpp/logit/loggers/FileLogger.hpp @@ -465,6 +465,51 @@ namespace logit { return results; } + /// \brief Clears managed log files and reopens the current log file. + LogClearResult clear_logs(const LogClearOptions& options = LogClearOptions()) override { + (void)options; + wait(); + + std::lock_guard lifecycle_lock(m_lifecycle_mutex); + LogClearResult result; + if (m_shutdown.load(std::memory_order_acquire)) { + result.ok = false; + result.status = LogClearStatus::Failed; + result.message = "FileLogger is shut down"; + return result; + } + + try { + std::lock_guard lock(m_mutex); + const std::vector files = list_log_files(); + if (m_file.is_open()) { + m_file.flush(); + m_file.close(); + } + for (size_t i = 0; i < files.size(); ++i) { + if (remove_file_path(files[i].path)) { + ++result.cleared_records; + } + } + m_current_file_size = 0; + m_last_log_ts.store(0, std::memory_order_release); + m_last_log_mono_ts.store(0, std::memory_order_release); + open_log_file(get_current_utc_date_ts()); + result.ok = true; + result.status = LogClearStatus::Cleared; + result.message = "cleared"; + } catch (const std::exception& e) { + result.ok = false; + result.status = LogClearStatus::Failed; + result.message = std::string("FileLogger clear error: ") + e.what(); + } catch (...) { + result.ok = false; + result.status = LogClearStatus::Failed; + result.message = "FileLogger clear error"; + } + return result; + } + /// \brief Waits for all asynchronous tasks to complete. void wait() override { std::lock_guard lifecycle_lock(m_lifecycle_mutex); @@ -660,6 +705,14 @@ namespace logit { return !in.bad(); } + bool remove_file_path(const std::string& file_path) const { +# if defined(_WIN32) + return std::remove(utf8_to_ansi(file_path).c_str()) == 0; +# else + return std::remove(file_path.c_str()) == 0; +# endif + } + void sort_log_files_newest_first(std::vector& files) const { std::sort(files.begin(), files.end(), [this](const LogFileInfo& lhs, const LogFileInfo& rhs) { if (lhs.day_start_ms != rhs.day_start_ms) { diff --git a/include/logit_cpp/logit/loggers/ILogger.hpp b/include/logit_cpp/logit/loggers/ILogger.hpp index b7d1ca4..7321e59 100644 --- a/include/logit_cpp/logit/loggers/ILogger.hpp +++ b/include/logit_cpp/logit/loggers/ILogger.hpp @@ -8,11 +8,37 @@ /// \ingroup LogBackends Logging Backends /// \{ +#include #include #include namespace logit { + /// \struct LogClearOptions + /// \brief Selects which data categories a logger cleanup should remove. + struct LogClearOptions { + bool include_persistent_records = true; ///< Clear primary stored log records. + bool include_payloads = true; ///< Clear associated payload/blob data. + bool include_sessions = false; ///< Clear session metadata when supported. + }; + + /// \enum LogClearStatus + /// \brief Backend cleanup status. + enum class LogClearStatus { + Cleared, + Unsupported, + Failed + }; + + /// \struct LogClearResult + /// \brief Result returned by logger cleanup operations. + struct LogClearResult { + bool ok = false; ///< True when the backend completed cleanup. + LogClearStatus status = LogClearStatus::Failed; ///< Detailed cleanup status. + std::size_t cleared_records = 0; ///< Number of primary records/files cleared when available. + std::string message; ///< Optional diagnostic or unsupported reason. + }; + /// \brief Optional buffered snapshot access returns empty results by default. /// Custom loggers may override these methods when they keep recent history. @@ -108,6 +134,18 @@ namespace logit { return results; } + /// \brief Clears logger-owned buffered or persisted records when supported. + /// \param options Data categories to clear. + /// \return Cleanup result. Default reports unsupported. + virtual LogClearResult clear_logs(const LogClearOptions& options = LogClearOptions()) { + (void)options; + LogClearResult result; + result.ok = false; + result.status = LogClearStatus::Unsupported; + result.message = "unsupported"; + return result; + } + /// \brief Waits for all asynchronous logging operations to complete. /// /// This pure virtual function must be implemented by derived logger classes. diff --git a/include/logit_cpp/logit/loggers/MdbxLogger.hpp b/include/logit_cpp/logit/loggers/MdbxLogger.hpp index 921f8b9..0e83e4e 100644 --- a/include/logit_cpp/logit/loggers/MdbxLogger.hpp +++ b/include/logit_cpp/logit/loggers/MdbxLogger.hpp @@ -460,6 +460,57 @@ namespace logit { return static_cast(m_log_level.load(std::memory_order_acquire)); } + LogClearResult clear_logs(const LogClearOptions& options = LogClearOptions()) override { + wait(); + + LogClearResult result; + try { + { + std::lock_guard db_lock(m_db_mutex); + auto txn = m_connection->transaction(mdbxc::TransactionMode::WRITABLE); + if (options.include_persistent_records) { + result.cleared_records = m_records->count(txn); + m_records->clear(txn); + } + if (options.include_payloads) { + m_payloads->clear(txn); + } + if (options.include_sessions) { + m_sessions->clear(txn); + Session session; + session.app_name = m_config.app_name; + session.start_time_ms = LOGIT_CURRENT_TIMESTAMP_MS(); + session.end_time_ms = 0; + session.process_id = detail::current_process_id(); + session.schema_version = 1; + m_session_id = make_unique_id(); + m_sessions->insert_or_assign(m_session_id, session, txn); + } + txn.commit(); + } + + { + std::lock_guard lock(m_mutex); + m_next_sequence_by_timestamp.clear(); + } + m_last_log_ts.store(0, std::memory_order_release); + m_last_log_mono_ts.store(0, std::memory_order_release); + + result.ok = true; + result.status = LogClearStatus::Cleared; + result.message = "cleared"; + } catch (const std::exception& e) { + result.ok = false; + result.status = LogClearStatus::Failed; + result.message = std::string("MdbxLogger clear error: ") + e.what(); + } catch (...) { + result.ok = false; + result.status = LogClearStatus::Failed; + result.message = "MdbxLogger clear error"; + } + return result; + } + uint64_t dropped_count() const { return m_dropped.load(std::memory_order_acquire); } diff --git a/include/logit_cpp/logit/loggers/MemoryLogger.hpp b/include/logit_cpp/logit/loggers/MemoryLogger.hpp index 120f206..7cb4921 100644 --- a/include/logit_cpp/logit/loggers/MemoryLogger.hpp +++ b/include/logit_cpp/logit/loggers/MemoryLogger.hpp @@ -143,6 +143,22 @@ namespace logit { /// \brief Memory logger is synchronous, so no flush step is needed. void wait() override {} + /// \brief Clears buffered entries. + LogClearResult clear_logs(const LogClearOptions& options = LogClearOptions()) override { + (void)options; + std::lock_guard lock(m_mutex); + LogClearResult result; + result.ok = true; + result.status = LogClearStatus::Cleared; + result.cleared_records = m_entries.size(); + result.message = "cleared"; + m_entries.clear(); + m_total_bytes = 0; + m_last_log_ts.store(0, std::memory_order_relaxed); + m_last_log_mono_ts.store(0, std::memory_order_relaxed); + return result; + } + /// \brief Reads records in `[from_ms, to_ms)` ordered by timestamp. std::vector read_range( int64_t from_ms, diff --git a/include/logit_cpp/logit/loggers/UniqueFileLogger.hpp b/include/logit_cpp/logit/loggers/UniqueFileLogger.hpp index 22d72f6..4c794ed 100644 --- a/include/logit_cpp/logit/loggers/UniqueFileLogger.hpp +++ b/include/logit_cpp/logit/loggers/UniqueFileLogger.hpp @@ -383,6 +383,49 @@ namespace logit { } } + /// \brief Clears managed unique log files. + LogClearResult clear_logs(const LogClearOptions& options = LogClearOptions()) override { + (void)options; + wait(); + + std::lock_guard lifecycle_lock(m_lifecycle_mutex); + LogClearResult result; + if (m_shutdown.load(std::memory_order_acquire)) { + result.ok = false; + result.status = LogClearStatus::Failed; + result.message = "UniqueFileLogger is shut down"; + return result; + } + + try { + std::lock_guard lock(m_mutex); + const std::vector files = list_log_files(); + for (size_t i = 0; i < files.size(); ++i) { + if (remove_file_path(files[i].path)) { + ++result.cleared_records; + } + } + { + std::lock_guard info_lock(m_thread_log_info_mutex); + m_thread_log_info.clear(); + } + m_last_log_ts.store(0, std::memory_order_release); + m_last_log_mono_ts.store(0, std::memory_order_release); + result.ok = true; + result.status = LogClearStatus::Cleared; + result.message = "cleared"; + } catch (const std::exception& e) { + result.ok = false; + result.status = LogClearStatus::Failed; + result.message = std::string("UniqueFileLogger clear error: ") + e.what(); + } catch (...) { + result.ok = false; + result.status = LogClearStatus::Failed; + result.message = "UniqueFileLogger clear error"; + } + return result; + } + /// \brief Stops logger-owned asynchronous resources after draining pending writes. void shutdown() override { { @@ -578,6 +621,14 @@ namespace logit { return !in.bad(); } + bool remove_file_path(const std::string& file_path) const { +# if defined(_WIN32) + return std::remove(utf8_to_ansi(file_path).c_str()) == 0; +# else + return std::remove(file_path.c_str()) == 0; +# endif + } + bool is_direct_child_path(const std::string& file_path, const std::string& directory_path) const { # if __cplusplus >= 201703L # if defined(_WIN32) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 073947a..ec19675 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -45,6 +45,7 @@ else() log_filters_tags_test.cpp logger_shutdown_race_test.cpp logger_snapshot_read_path_test.cpp + logger_clear_api_test.cpp memory_logger_backend_test.cpp memory_logger_concurrency_test.cpp memory_logger_integration_test.cpp diff --git a/tests/logger_clear_api_test.cpp b/tests/logger_clear_api_test.cpp new file mode 100644 index 0000000..ad5390f --- /dev/null +++ b/tests/logger_clear_api_test.cpp @@ -0,0 +1,210 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::string make_unique_directory_name(const std::string& prefix) { + const long long stamp = static_cast( + std::chrono::steady_clock::now().time_since_epoch().count()); + return prefix + "_" + std::to_string(stamp); +} + +logit::LogRecord make_record(logit::LogLevel level, int64_t timestamp_ms, int line) { + return logit::LogRecord( + level, + timestamp_ms, + "logger_clear_api_test.cpp", + line, + "make_record", + "message", + "", + -1, + false, + false, + false); +} + +void test_memory_logger_clear_direct() { + logit::MemoryLogger logger(logit::MemoryLogger::Config{0, 0, 0}); + logger.log(make_record(logit::LogLevel::LOG_LVL_INFO, 100, 10), "first"); + logger.log(make_record(logit::LogLevel::LOG_LVL_WARN, 101, 11), "second"); + assert(logger.get_buffered_entries().size() == 2); + + logit::LogClearResult result = logger.clear_logs(); + assert(result.ok); + assert(result.status == logit::LogClearStatus::Cleared); + assert(result.cleared_records == 2); + assert(logger.get_buffered_entries().empty()); + assert(logger.get_int_param(logit::LoggerParam::LastLogTimestamp) == 0); + + logger.log(make_record(logit::LogLevel::LOG_LVL_ERROR, 102, 12), "after-clear"); + const auto entries = logger.get_buffered_entries(); + assert(entries.size() == 1); + assert(entries[0].message == "after-clear"); +} + +void test_logger_clear_specific_and_all() { + logit::Logger& logger = logit::Logger::get_instance(); + const int memory_index = static_cast(logger.logger_count()); + logger.add_logger( + std::unique_ptr(new logit::MemoryLogger(logit::MemoryLogger::Config{0, 0, 0})), + std::unique_ptr(new logit::SimpleLogFormatter("%v"))); + const int console_index = static_cast(logger.logger_count()); + logger.add_logger( + std::unique_ptr(new logit::ConsoleLogger()), + std::unique_ptr(new logit::SimpleLogFormatter("%v"))); + const int second_memory_index = static_cast(logger.logger_count()); + logger.add_logger( + std::unique_ptr(new logit::MemoryLogger(logit::MemoryLogger::Config{0, 0, 0})), + std::unique_ptr(new logit::SimpleLogFormatter("%v"))); + + LOGIT_RAW_TO(memory_index, "memory-one"); + LOGIT_RAW_TO(second_memory_index, "memory-two"); + assert(LOGIT_GET_BUFFERED_STRINGS(memory_index).size() == 1); + assert(LOGIT_GET_BUFFERED_STRINGS(second_memory_index).size() == 1); + + logit::LogClearResult one = LOGIT_CLEAR_LOGGER(memory_index); + assert(one.ok); + assert(one.status == logit::LogClearStatus::Cleared); + assert(one.cleared_records == 1); + assert(LOGIT_GET_BUFFERED_STRINGS(memory_index).empty()); + assert(LOGIT_GET_BUFFERED_STRINGS(second_memory_index).size() == 1); + + logit::LogClearResult unsupported = LOGIT_CLEAR_LOGGER(console_index); + assert(!unsupported.ok); + assert(unsupported.status == logit::LogClearStatus::Unsupported); + + logit::LogClearResult all = LOGIT_CLEAR_ALL_LOGGERS(); + assert(all.ok); + assert(all.status == logit::LogClearStatus::Cleared); + assert(all.cleared_records >= 1); + assert(LOGIT_GET_BUFFERED_STRINGS(second_memory_index).empty()); + + LOGIT_RAW_TO(memory_index, "after-clear"); + assert(LOGIT_GET_BUFFERED_STRINGS(memory_index).size() == 1); +} + +void test_file_loggers_clear_and_continue() { + logit::FileLogger::Config file_config; + file_config.directory = make_unique_directory_name("clear_file_logs"); + file_config.async = false; + logit::FileLogger file_logger(file_config); + file_logger.log(make_record(logit::LogLevel::LOG_LVL_INFO, LOGIT_CURRENT_TIMESTAMP_MS(), 20), "file-before-clear"); + assert(!file_logger.list_log_files().empty()); + + logit::LogClearResult file_clear = file_logger.clear_logs(); + assert(file_clear.ok); + assert(file_clear.status == logit::LogClearStatus::Cleared); + assert(file_clear.cleared_records >= 1); + std::vector file_logs = file_logger.list_log_files(); + assert(file_logs.size() == 1); + logit::LogFileReadResult empty_current = file_logger.read_log_file(file_logs[0].path); + assert(empty_current.ok); + assert(empty_current.content.empty()); + + file_logger.log(make_record(logit::LogLevel::LOG_LVL_INFO, LOGIT_CURRENT_TIMESTAMP_MS(), 21), "file-after-clear"); + file_logs = file_logger.list_log_files(); + assert(!file_logs.empty()); + logit::LogFileReadResult after_file = file_logger.read_log_file(file_logs[0].path); + assert(after_file.ok); + assert(after_file.content.find("file-after-clear") != std::string::npos); + + logit::UniqueFileLogger::Config unique_config; + unique_config.directory = make_unique_directory_name("clear_unique_logs"); + unique_config.async = false; + logit::UniqueFileLogger unique_logger(unique_config); + unique_logger.log(make_record(logit::LogLevel::LOG_LVL_INFO, LOGIT_CURRENT_TIMESTAMP_MS(), 30), "unique-before-clear"); + assert(!unique_logger.list_log_files().empty()); + + logit::LogClearResult unique_clear = unique_logger.clear_logs(); + assert(unique_clear.ok); + assert(unique_clear.status == logit::LogClearStatus::Cleared); + assert(unique_clear.cleared_records >= 1); + assert(unique_logger.list_log_files().empty()); + + unique_logger.log(make_record(logit::LogLevel::LOG_LVL_INFO, LOGIT_CURRENT_TIMESTAMP_MS(), 31), "unique-after-clear"); + assert(unique_logger.list_log_files().size() == 1); +} + +class BlockingClearLogger final : public logit::ILogger { +public: + void log(const logit::LogRecord&, const std::string&) override { + entered.store(true, std::memory_order_release); + while (!allow_exit.load(std::memory_order_acquire)) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + + std::string get_string_param(const logit::LoggerParam&) const override { return std::string(); } + int64_t get_int_param(const logit::LoggerParam&) const override { return 0; } + double get_float_param(const logit::LoggerParam&) const override { return 0.0; } + void set_log_level(logit::LogLevel level) override { m_level.store(static_cast(level), std::memory_order_relaxed); } + logit::LogLevel get_log_level() const override { return static_cast(m_level.load(std::memory_order_relaxed)); } + void wait() override {} + + logit::LogClearResult clear_logs(const logit::LogClearOptions& options = logit::LogClearOptions()) override { + (void)options; + logit::LogClearResult result; + result.ok = true; + result.status = logit::LogClearStatus::Cleared; + result.message = "cleared"; + return result; + } + + std::atomic entered = ATOMIC_VAR_INIT(false); + std::atomic allow_exit = ATOMIC_VAR_INIT(false); + +private: + std::atomic m_level = ATOMIC_VAR_INIT(static_cast(logit::LogLevel::LOG_LVL_TRACE)); +}; + +void test_clear_uses_strategy_snapshot() { + logit::Logger& logger = logit::Logger::get_instance(); + BlockingClearLogger* blocking = new BlockingClearLogger(); + const int blocking_index = static_cast(logger.logger_count()); + logger.add_logger( + std::unique_ptr(blocking), + std::unique_ptr(new logit::SimpleLogFormatter("%v"))); + const int memory_index = static_cast(logger.logger_count()); + logger.add_logger( + std::unique_ptr(new logit::MemoryLogger(logit::MemoryLogger::Config{0, 0, 0})), + std::unique_ptr(new logit::SimpleLogFormatter("%v"))); + + std::thread writer([blocking_index]() { + LOGIT_RAW_TO(blocking_index, "held-by-blocking-logger"); + }); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (!blocking->entered.load(std::memory_order_acquire)) { + if (std::chrono::steady_clock::now() >= deadline) { + blocking->allow_exit.store(true, std::memory_order_release); + writer.join(); + assert(false); + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + auto clear_future = std::async(std::launch::async, [memory_index]() { + return LOGIT_CLEAR_LOGGER(memory_index); + }); + assert(clear_future.wait_for(std::chrono::milliseconds(100)) == std::future_status::ready); + assert(clear_future.get().ok); + + blocking->allow_exit.store(true, std::memory_order_release); + writer.join(); +} + +} // namespace + +int main() { + test_memory_logger_clear_direct(); + test_logger_clear_specific_and_all(); + test_file_loggers_clear_and_continue(); + test_clear_uses_strategy_snapshot(); + return 0; +} diff --git a/tests/mdbx_logger_test.cpp b/tests/mdbx_logger_test.cpp index 96c6cad..85072b6 100644 --- a/tests/mdbx_logger_test.cpp +++ b/tests/mdbx_logger_test.cpp @@ -476,6 +476,85 @@ void test_read_result_decode_errors() { cleanup_db(path); } +void test_clear_logs_keeps_session_and_accepts_new_records() { + const std::string path = make_db_path("clear_keep_session"); + cleanup_db(path); + + { + logit::MdbxLogger::Config config; + config.path = path; + config.async = false; + config.large_payload_threshold = 4; + config.payload_preview_size = 3; + config.store_large_payloads_separately = true; + + logit::MdbxLogger logger(config); + const uint64_t session_id = logger.session_id(); + const std::string large_payload = "large-payload-before-clear"; + logger.log(make_record(logit::LogLevel::LOG_LVL_INFO, 6300, 55), large_payload); + + auto before = logger.read_range(6300, 6301); + assert(before.size() == 1); + assert(before[0].payload_id != 0); + const uint64_t payload_id = before[0].payload_id; + assert(logger.read_payload(payload_id)); + + logit::LogClearResult result = logger.clear_logs(); + assert(result.ok); + assert(result.status == logit::LogClearStatus::Cleared); + assert(result.cleared_records == 1); + assert(logger.read_range(6300, 6301).empty()); + assert(!logger.read_payload(payload_id)); + + auto session = logger.read_session(session_id); + assert(session); + assert(session->end_time_ms == 0); + + logger.log(make_record(logit::LogLevel::LOG_LVL_WARN, 6301, 56), "after-clear"); + auto after = logger.read_range(6301, 6302); + assert(after.size() == 1); + assert(after[0].message == "after-clear"); + + logger.shutdown(); + } + + cleanup_db(path); +} + +void test_clear_logs_can_remove_sessions() { + const std::string path = make_db_path("clear_sessions"); + cleanup_db(path); + + { + logit::MdbxLogger::Config config; + config.path = path; + config.async = false; + + logit::MdbxLogger logger(config); + const uint64_t session_id = logger.session_id(); + logger.log(make_record(logit::LogLevel::LOG_LVL_INFO, 6400, 57), "before-clear"); + + logit::LogClearOptions options; + options.include_sessions = true; + logit::LogClearResult result = logger.clear_logs(options); + assert(result.ok); + assert(result.status == logit::LogClearStatus::Cleared); + assert(result.cleared_records == 1); + assert(!logger.read_session(session_id)); + const uint64_t new_session_id = logger.session_id(); + assert(new_session_id != session_id); + assert(logger.read_session(new_session_id)); + + logger.log(make_record(logit::LogLevel::LOG_LVL_INFO, 6401, 58), "after-session-clear"); + auto records = logger.read_range(6401, 6402); + assert(records.size() == 1); + assert(records[0].session_id == new_session_id); + logger.shutdown(); + } + + cleanup_db(path); +} + void test_read_range_empty_and_limits() { const std::string path = make_db_path("range"); cleanup_db(path); @@ -669,6 +748,8 @@ int main() { test_init_error_callback_and_rethrow(); test_read_result_not_found(); test_read_result_decode_errors(); + test_clear_logs_keeps_session_and_accepts_new_records(); + test_clear_logs_can_remove_sessions(); test_read_range_empty_and_limits(); test_read_recent(); test_callback_sync();