diff --git a/guides/cpp_style.md b/guides/cpp_style.md index d211431..bba5877 100644 --- a/guides/cpp_style.md +++ b/guides/cpp_style.md @@ -52,3 +52,13 @@ - Enum values use `CamelCase`. - Mark non-inheritable classes with `final`. - Use `override` for overridden virtual methods. + +## Semantic values + +- Avoid magic numbers: do not leave unnamed numbers such as `0xD0` when the meaning is not obvious. +- Avoid magic literals: apply the same rule to strings, bytes, characters, regexes, paths, and flags. +- Use intention-revealing names: the name should explain the value role, such as `invalid_utf8_leading_byte` instead of `byte`. +- Make invalid and edge cases explicit: test data for errors should state which error it validates. +- Prefer named constants for semantic values: if a value has domain meaning, give it a name. +- Separate data meaning from representation: `0xD0` is representation; truncated UTF-8 leading byte is meaning. +- Follow the Principle of Least Astonishment: readers should not have to guess why a specific byte or literal was chosen. diff --git a/include/logit_cpp/logit/loggers/ILogReader.hpp b/include/logit_cpp/logit/loggers/ILogReader.hpp index 406837c..959b84e 100644 --- a/include/logit_cpp/logit/loggers/ILogReader.hpp +++ b/include/logit_cpp/logit/loggers/ILogReader.hpp @@ -10,6 +10,9 @@ #include #include #include +#if __cplusplus >= 201703L +#include +#endif namespace logit { @@ -34,6 +37,28 @@ namespace logit { Descending ///< Newest first. }; +#if __cplusplus >= 201703L + /// \enum LogReadError + /// \brief Result status for log read APIs that preserve failure details. + enum class LogReadError { + None, + NotFound, + StorageError, + DecodeError, + UnsupportedVersion, + DecompressionError + }; + + /// \struct LogReadResult + /// \brief Value-or-error result returned by detailed log read APIs. + template + struct LogReadResult { + std::optional value; ///< Present when the read succeeded with a value. + LogReadError error = LogReadError::None; ///< Error status, or None on success. + std::string message; ///< Optional diagnostic message for failed reads. + }; +#endif + /// \class ILogReader /// \brief Optional interface for backends that expose stored records. /// @@ -54,6 +79,18 @@ namespace logit { int64_t to_ms, std::size_t limit = 0) const = 0; +#if __cplusplus >= 201703L + /// \brief Reads records and preserves backend-specific read errors. + virtual LogReadResult> read_range_result( + int64_t from_ms, + int64_t to_ms, + std::size_t limit = 0) const { + LogReadResult> result; + result.value = read_range(from_ms, to_ms, limit); + return result; + } +#endif + /// \brief Reads the most recent records. /// \param limit Maximum number of records (0 = unlimited). /// \param period_ms Time window in milliseconds from now backward (0 = unlimited). @@ -63,6 +100,18 @@ namespace logit { std::size_t limit, int64_t period_ms = 0, LogReadOrder order = LogReadOrder::Ascending) const = 0; + +#if __cplusplus >= 201703L + /// \brief Reads recent records and preserves backend-specific read errors. + virtual LogReadResult> read_recent_result( + std::size_t limit, + int64_t period_ms = 0, + LogReadOrder order = LogReadOrder::Ascending) const { + LogReadResult> result; + result.value = read_recent(limit, period_ms, order); + return result; + } +#endif }; } // namespace logit diff --git a/include/logit_cpp/logit/loggers/MdbxLogger.hpp b/include/logit_cpp/logit/loggers/MdbxLogger.hpp index 9d76c49..921f8b9 100644 --- a/include/logit_cpp/logit/loggers/MdbxLogger.hpp +++ b/include/logit_cpp/logit/loggers/MdbxLogger.hpp @@ -25,6 +25,21 @@ namespace logit { Zstd = 2 ///< Store zstd-compressed payload bytes. }; + namespace detail { + class MdbxReadException : public std::runtime_error { + public: + MdbxReadException(LogReadError error, const std::string& message) + : std::runtime_error(message), m_error(error) {} + + LogReadError error() const noexcept { + return m_error; + } + + private: + LogReadError m_error; + }; + } + /// \class MdbxLogger /// \brief Stores formatted logs in MDBX tables with optional async batching. class MdbxLogger final : public ILogger, public ILogReader, public ILogSubscriber { @@ -197,9 +212,19 @@ namespace logit { int64_t from_ms, int64_t to_ms, std::size_t limit = 0) const override { - std::vector out; + auto result = read_range_result(from_ms, to_ms, limit); + return result.value ? *result.value : std::vector(); + } + + /// \brief Reads records and preserves storage/decode errors. + LogReadResult> read_range_result( + int64_t from_ms, + int64_t to_ms, + std::size_t limit = 0) const override { + LogReadResult> result; + result.value.emplace(); if (to_ms <= from_ms) { - return out; + return result; } try { @@ -210,15 +235,29 @@ namespace logit { std::lock_guard db_lock(m_db_mutex); m_records->for_each_range(from_key, to_key, - [&out, limit](const std::string&, const Record& record) -> bool { - out.push_back(to_view(record)); - return limit == 0 || out.size() < limit; + [&result, limit](const std::string&, const Record& record) -> bool { + result.value->push_back(to_view(record)); + return limit == 0 || result.value->size() < limit; }); + } catch (const detail::MdbxReadException& e) { + result.value.reset(); + result.error = e.error(); + result.message = e.what(); + } catch (const mdbxc::MdbxException& e) { + result.value.reset(); + result.error = LogReadError::StorageError; + result.message = e.what(); + } catch (const std::exception& e) { + result.value.reset(); + result.error = LogReadError::DecodeError; + result.message = e.what(); } catch (...) { - out.clear(); + result.value.reset(); + result.error = LogReadError::DecodeError; + result.message = "MdbxLogger: unknown read_range error"; } - return out; + return result; } /// \brief Reads the most recent records. @@ -230,82 +269,147 @@ namespace logit { std::size_t limit, int64_t period_ms = 0, LogReadOrder order = LogReadOrder::Ascending) const override { + auto result = read_recent_result(limit, period_ms, order); + return result.value ? *result.value : std::vector(); + } + + /// \brief Reads the most recent records and preserves storage/decode errors. + LogReadResult> read_recent_result( + std::size_t limit, + int64_t period_ms = 0, + LogReadOrder order = LogReadOrder::Ascending) const override { const int64_t now_ms = LOGIT_CURRENT_TIMESTAMP_MS(); const int64_t from_ms = (period_ms > 0) ? (now_ms - period_ms) : 0; - auto records = read_range(from_ms, now_ms + 1, 0); - if (limit > 0 && records.size() > limit) { - records.erase( - records.begin(), - records.begin() + static_cast(records.size() - limit)); + auto result = read_range_result(from_ms, now_ms + 1, 0); + if (!result.value) { + return result; + } + if (limit > 0 && result.value->size() > limit) { + result.value->erase( + result.value->begin(), + result.value->begin() + static_cast(result.value->size() - limit)); } - if (order == LogReadOrder::Descending && !records.empty()) { - std::reverse(records.begin(), records.end()); + if (order == LogReadOrder::Descending && !result.value->empty()) { + std::reverse(result.value->begin(), result.value->end()); } - return records; + return result; } /// \brief Reads a payload by id. std::optional read_payload(uint64_t payload_id) const { + return read_payload_result(payload_id).value; + } + + /// \brief Reads a payload by id and preserves storage/decode errors. + LogReadResult read_payload_result(uint64_t payload_id) const { + LogReadResult result; if (payload_id == 0) { - return std::nullopt; + result.error = LogReadError::NotFound; + result.message = "MdbxLogger: payload id is zero"; + return result; } try { std::lock_guard db_lock(m_db_mutex); #if __cplusplus >= 201703L auto value = m_payloads->find(payload_id); - if (!value) return std::nullopt; - return to_view(*value); + if (!value) return make_not_found_result("MdbxLogger: payload not found"); + result.value = to_view(*value); #else std::pair value = m_payloads->find(payload_id); - if (!value.first) return std::nullopt; - return to_view(value.second); + if (!value.first) return make_not_found_result("MdbxLogger: payload not found"); + result.value = to_view(value.second); #endif + } catch (const detail::MdbxReadException& e) { + result.error = e.error(); + result.message = e.what(); + } catch (const mdbxc::MdbxException& e) { + result.error = LogReadError::StorageError; + result.message = e.what(); + } catch (const std::exception& e) { + result.error = LogReadError::DecodeError; + result.message = e.what(); } catch (...) { - return std::nullopt; + result.error = LogReadError::DecodeError; + result.message = "MdbxLogger: unknown payload read error"; } + return result; } /// \brief Reads and decompresses payload data by id. /// \return Original (decompressed) payload string, or std::nullopt if not found or decompression fails. std::optional read_payload_data(uint64_t payload_id) const { - auto view = read_payload(payload_id); - if (!view) { - return std::nullopt; + return read_payload_data_result(payload_id).value; + } + + /// \brief Reads and decompresses payload data while preserving failure details. + LogReadResult read_payload_data_result(uint64_t payload_id) const { + LogReadResult result; + auto view = read_payload_result(payload_id); + if (!view.value) { + result.error = view.error; + result.message = view.message; + return result; } - if (view->compression == MdbxPayloadCompression::None) { - return view->data; + if (view.value->compression == MdbxPayloadCompression::None) { + result.value = view.value->data; + return result; } std::string output; bool ok = false; - if (view->compression == MdbxPayloadCompression::Gzip) { - ok = detail::decompress_string_gzip(view->data, output); - } else if (view->compression == MdbxPayloadCompression::Zstd) { - ok = detail::decompress_string_zstd(view->data, output); + if (view.value->compression == MdbxPayloadCompression::Gzip) { + ok = detail::decompress_string_gzip(view.value->data, output); + } else if (view.value->compression == MdbxPayloadCompression::Zstd) { + ok = detail::decompress_string_zstd(view.value->data, output); } - return ok ? std::optional(std::move(output)) : std::nullopt; + if (ok) { + result.value = std::move(output); + } else { + result.error = LogReadError::DecompressionError; + result.message = "MdbxLogger: failed to decompress payload data"; + } + return result; } /// \brief Reads session metadata by id. std::optional read_session(uint64_t session_id) const { + return read_session_result(session_id).value; + } + + /// \brief Reads session metadata by id and preserves storage/decode errors. + LogReadResult read_session_result(uint64_t session_id) const { + LogReadResult result; if (session_id == 0) { - return std::nullopt; + result.error = LogReadError::NotFound; + result.message = "MdbxLogger: session id is zero"; + return result; } try { std::lock_guard db_lock(m_db_mutex); #if __cplusplus >= 201703L auto value = m_sessions->find(session_id); - if (!value) return std::nullopt; - return to_view(*value); + if (!value) return make_not_found_result("MdbxLogger: session not found"); + result.value = to_view(*value); #else std::pair value = m_sessions->find(session_id); - if (!value.first) return std::nullopt; - return to_view(value.second); + if (!value.first) return make_not_found_result("MdbxLogger: session not found"); + result.value = to_view(value.second); #endif + } catch (const detail::MdbxReadException& e) { + result.error = e.error(); + result.message = e.what(); + } catch (const mdbxc::MdbxException& e) { + result.error = LogReadError::StorageError; + result.message = e.what(); + } catch (const std::exception& e) { + result.error = LogReadError::DecodeError; + result.message = e.what(); } catch (...) { - return std::nullopt; + result.error = LogReadError::DecodeError; + result.message = "MdbxLogger: unknown session read error"; } + return result; } std::string get_string_param(const LoggerParam& param) const override { @@ -455,6 +559,14 @@ namespace logit { std::unordered_map m_callbacks; std::atomic m_next_callback_id{1}; + template + static LogReadResult make_not_found_result(const std::string& message) { + LogReadResult result; + result.error = LogReadError::NotFound; + result.message = message; + return result; + } + void notify_callbacks(const std::vector& views) const { std::vector callbacks_copy; { @@ -528,7 +640,9 @@ namespace logit { detail::MdbxByteReader in(data, size); const uint32_t version = in.read_u32(); if (version != 1) { - throw std::runtime_error("MdbxLogger: unsupported session value version"); + throw detail::MdbxReadException( + LogReadError::UnsupportedVersion, + "MdbxLogger: unsupported session value version"); } Session s; s.app_name = in.read_string(); @@ -559,7 +673,9 @@ namespace logit { detail::MdbxByteReader in(data, size); const uint32_t version = in.read_u32(); if (version != 1) { - throw std::runtime_error("MdbxLogger: unsupported record value version"); + throw detail::MdbxReadException( + LogReadError::UnsupportedVersion, + "MdbxLogger: unsupported record value version"); } Record r; r.session_id = in.read_u64(); @@ -588,13 +704,17 @@ namespace logit { detail::MdbxByteReader in(data, size); const uint32_t version = in.read_u32(); if (version != 1) { - throw std::runtime_error("MdbxLogger: unsupported payload value version"); + throw detail::MdbxReadException( + LogReadError::UnsupportedVersion, + "MdbxLogger: unsupported payload value version"); } Payload p; p.payload_id = in.read_u64(); const uint8_t compression = in.read_u8(); if (compression > static_cast(MdbxPayloadCompression::Zstd)) { - throw std::runtime_error("MdbxLogger: unsupported payload compression"); + throw detail::MdbxReadException( + LogReadError::DecodeError, + "MdbxLogger: unsupported payload compression"); } p.compression = static_cast(compression); p.data = in.read_string(); diff --git a/include/logit_cpp/logit/loggers/MemoryLogger.hpp b/include/logit_cpp/logit/loggers/MemoryLogger.hpp index ef83830..120f206 100644 --- a/include/logit_cpp/logit/loggers/MemoryLogger.hpp +++ b/include/logit_cpp/logit/loggers/MemoryLogger.hpp @@ -148,6 +148,10 @@ namespace logit { int64_t from_ms, int64_t to_ms, std::size_t limit = 0) const override { +#if __cplusplus >= 201703L + auto result = read_range_result(from_ms, to_ms, limit); + return result.value ? *result.value : std::vector(); +#else std::vector out; if (to_ms <= from_ms) { return out; @@ -165,13 +169,45 @@ namespace logit { } } return out; +#endif } +#if __cplusplus >= 201703L + /// \brief Reads records and reports MemoryLogger read status. + LogReadResult> read_range_result( + int64_t from_ms, + int64_t to_ms, + std::size_t limit = 0) const override { + LogReadResult> result; + result.value.emplace(); + if (to_ms <= from_ms) { + return result; + } + + std::lock_guard lock(m_mutex); + m_evict_expired_locked(LOGIT_CURRENT_TIMESTAMP_MS()); + + for (const auto& entry : m_entries) { + if (entry.timestamp_ms >= from_ms && entry.timestamp_ms < to_ms) { + result.value->push_back(to_view(entry)); + if (limit > 0 && result.value->size() >= limit) { + break; + } + } + } + return result; + } +#endif + /// \brief Reads the most recent records. std::vector read_recent( std::size_t limit, int64_t period_ms = 0, LogReadOrder order = LogReadOrder::Ascending) const override { +#if __cplusplus >= 201703L + auto result = read_recent_result(limit, period_ms, order); + return result.value ? *result.value : std::vector(); +#else std::vector out; std::lock_guard lock(m_mutex); m_evict_expired_locked(LOGIT_CURRENT_TIMESTAMP_MS()); @@ -192,7 +228,38 @@ namespace logit { std::reverse(out.begin(), out.end()); } return out; +#endif + } + +#if __cplusplus >= 201703L + /// \brief Reads recent records and reports MemoryLogger read status. + LogReadResult> read_recent_result( + std::size_t limit, + int64_t period_ms = 0, + LogReadOrder order = LogReadOrder::Ascending) const override { + LogReadResult> result; + result.value.emplace(); + std::lock_guard lock(m_mutex); + m_evict_expired_locked(LOGIT_CURRENT_TIMESTAMP_MS()); + + const int64_t now_ms = LOGIT_CURRENT_TIMESTAMP_MS(); + const int64_t from_ms = (period_ms > 0) ? (now_ms - period_ms) : 0; + + for (auto it = m_entries.rbegin(); it != m_entries.rend(); ++it) { + if (period_ms <= 0 || it->timestamp_ms >= from_ms) { + result.value->push_back(to_view(*it)); + if (limit > 0 && result.value->size() >= limit) { + break; + } + } + } + + if (order == LogReadOrder::Ascending) { + std::reverse(result.value->begin(), result.value->end()); + } + return result; } +#endif private: static LogRecordView to_view(const BufferedLogEntry& e) { diff --git a/tests/mdbx_logger_test.cpp b/tests/mdbx_logger_test.cpp index 14be777..96c6cad 100644 --- a/tests/mdbx_logger_test.cpp +++ b/tests/mdbx_logger_test.cpp @@ -79,6 +79,71 @@ logit::LogRecord make_record(logit::LogLevel level, int64_t timestamp_ms, int li false); } +mdbxc::Config make_raw_db_config(const std::string& path) { + mdbxc::Config config; + config.pathname = path; + config.max_dbs = 4; + config.no_subdir = true; + config.sync_durable = true; + return config; +} + +std::string bytes_to_string(const std::vector& bytes) { + if (bytes.empty()) { + return std::string(); + } + return std::string(reinterpret_cast(&bytes[0]), bytes.size()); +} + +std::string make_unsupported_version_value() { + const uint32_t unsupported_schema_version = 2; + logit::detail::MdbxByteWriter out; + out.write_u32(unsupported_schema_version); + return bytes_to_string(out.bytes()); +} + +std::string make_payload_value( + uint64_t payload_id, + logit::MdbxPayloadCompression compression, + const std::string& data) { + const uint32_t current_schema_version = 1; + logit::detail::MdbxByteWriter out; + out.write_u32(current_schema_version); + out.write_u64(payload_id); + out.write_u8(static_cast(compression)); + out.write_string(data); + return bytes_to_string(out.bytes()); +} + +std::string make_payload_with_unknown_compression(uint64_t payload_id) { + const uint32_t current_schema_version = 1; + const uint8_t unknown_payload_compression = 255; + logit::detail::MdbxByteWriter out; + out.write_u32(current_schema_version); + out.write_u64(payload_id); + out.write_u8(unknown_payload_compression); + out.write_string("payload"); + return bytes_to_string(out.bytes()); +} + +void write_raw_payload_value(const std::string& path, uint64_t payload_id, const std::string& value) { + auto connection = mdbxc::Connection::create(make_raw_db_config(path)); + mdbxc::KeyValueTable payloads(connection, "log_payloads"); + payloads.insert_or_assign(payload_id, value); +} + +void write_raw_session_value(const std::string& path, uint64_t session_id, const std::string& value) { + auto connection = mdbxc::Connection::create(make_raw_db_config(path)); + mdbxc::KeyValueTable sessions(connection, "log_sessions"); + sessions.insert_or_assign(session_id, value); +} + +void write_raw_record_value(const std::string& path, const std::string& key, const std::string& value) { + auto connection = mdbxc::Connection::create(make_raw_db_config(path)); + mdbxc::KeyValueTable records(connection, "log_records_by_time"); + records.insert_or_assign(key, value); +} + void test_sync_range_session_and_sequences() { const std::string path = make_db_path("sync"); cleanup_db(path); @@ -312,6 +377,105 @@ void test_init_error_callback_and_rethrow() { cleanup_path_tree(path); } +void test_read_result_not_found() { + const std::string path = make_db_path("read_not_found"); + cleanup_db(path); + + { + logit::MdbxLogger::Config config; + config.path = path; + config.async = false; + + logit::MdbxLogger logger(config); + const uint64_t missing_payload_id = 404; + const uint64_t missing_session_id = 405; + + auto payload = logger.read_payload_result(missing_payload_id); + assert(!payload.value); + assert(payload.error == logit::LogReadError::NotFound); + assert(!payload.message.empty()); + assert(!logger.read_payload(missing_payload_id)); + + auto payload_data = logger.read_payload_data_result(missing_payload_id); + assert(!payload_data.value); + assert(payload_data.error == logit::LogReadError::NotFound); + assert(!logger.read_payload_data(missing_payload_id)); + + auto session = logger.read_session_result(missing_session_id); + assert(!session.value); + assert(session.error == logit::LogReadError::NotFound); + assert(!session.message.empty()); + assert(!logger.read_session(missing_session_id)); + + logger.shutdown(); + } + + cleanup_db(path); +} + +void test_read_result_decode_errors() { + const std::string path = make_db_path("read_decode"); + cleanup_db(path); + + const uint64_t unsupported_version_payload_id = 501; + const uint64_t unknown_compression_payload_id = 502; + const uint64_t bad_gzip_payload_id = 503; + const uint64_t unsupported_version_session_id = 504; + const int64_t corrupt_record_timestamp_ms = 6200; + const std::string corrupt_record_key = + logit::detail::make_mdbx_record_key(corrupt_record_timestamp_ms, 0); + + write_raw_payload_value(path, unsupported_version_payload_id, make_unsupported_version_value()); + write_raw_payload_value(path, unknown_compression_payload_id, + make_payload_with_unknown_compression(unknown_compression_payload_id)); + write_raw_payload_value(path, bad_gzip_payload_id, + make_payload_value(bad_gzip_payload_id, logit::MdbxPayloadCompression::Gzip, "not-gzip-data")); + write_raw_session_value(path, unsupported_version_session_id, make_unsupported_version_value()); + write_raw_record_value(path, corrupt_record_key, make_unsupported_version_value()); + + { + logit::MdbxLogger::Config config; + config.path = path; + config.async = false; + + logit::MdbxLogger logger(config); + + auto unsupported_payload = logger.read_payload_result(unsupported_version_payload_id); + assert(!unsupported_payload.value); + assert(unsupported_payload.error == logit::LogReadError::UnsupportedVersion); + assert(!logger.read_payload(unsupported_version_payload_id)); + + auto unknown_compression_payload = logger.read_payload_result(unknown_compression_payload_id); + assert(!unknown_compression_payload.value); + assert(unknown_compression_payload.error == logit::LogReadError::DecodeError); + + auto bad_gzip_payload = logger.read_payload_data_result(bad_gzip_payload_id); + assert(!bad_gzip_payload.value); + assert(bad_gzip_payload.error == logit::LogReadError::DecompressionError); + assert(!logger.read_payload_data(bad_gzip_payload_id)); + + auto unsupported_session = logger.read_session_result(unsupported_version_session_id); + assert(!unsupported_session.value); + assert(unsupported_session.error == logit::LogReadError::UnsupportedVersion); + assert(!logger.read_session(unsupported_version_session_id)); + + auto corrupt_range = logger.read_range_result( + corrupt_record_timestamp_ms, + corrupt_record_timestamp_ms + 1); + assert(!corrupt_range.value); + assert(corrupt_range.error == logit::LogReadError::UnsupportedVersion); + assert(logger.read_range(corrupt_record_timestamp_ms, corrupt_record_timestamp_ms + 1).empty()); + + auto corrupt_recent = logger.read_recent_result(10, LOGIT_CURRENT_TIMESTAMP_MS()); + assert(!corrupt_recent.value); + assert(corrupt_recent.error == logit::LogReadError::UnsupportedVersion); + + logger.shutdown(); + } + + cleanup_db(path); +} + void test_read_range_empty_and_limits() { const std::string path = make_db_path("range"); cleanup_db(path); @@ -503,6 +667,8 @@ int main() { test_on_error_callback(); test_nested_parent_directory_created(); test_init_error_callback_and_rethrow(); + test_read_result_not_found(); + test_read_result_decode_errors(); test_read_range_empty_and_limits(); test_read_recent(); test_callback_sync();