diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt index 334b84706..51a5541bd 100644 --- a/bindings/c/CMakeLists.txt +++ b/bindings/c/CMakeLists.txt @@ -39,6 +39,7 @@ set(SVS_C_API_SOURCES src/index_vamana.hpp src/index_builder.hpp src/leanvec_training_data.hpp + src/logger.hpp src/storage.hpp src/stream.hpp src/threadpool.hpp diff --git a/bindings/c/include/svs/c/svs_c.h b/bindings/c/include/svs/c/svs_c.h index f54c70868..6ed11d189 100644 --- a/bindings/c/include/svs/c/svs_c.h +++ b/bindings/c/include/svs/c/svs_c.h @@ -58,6 +58,26 @@ enum svs_error_code { SVS_ERROR_UNKNOWN = 1000 }; +/// @brief Severity of a log message. +enum svs_log_level { + SVS_LOG_LEVEL_TRACE = 0, + SVS_LOG_LEVEL_DEBUG = 1, + SVS_LOG_LEVEL_INFO = 2, + SVS_LOG_LEVEL_WARN = 3, + SVS_LOG_LEVEL_ERROR = 4, + SVS_LOG_LEVEL_CRITICAL = 5, + SVS_LOG_LEVEL_OFF = 6 +}; + +/// @brief Output of a logger created with svs_logger_create(). +enum svs_logging_kind { + SVS_LOGGING_KIND_NONE = 0, + SVS_LOGGING_KIND_STDOUT = 1, + SVS_LOGGING_KIND_STDERR = 2, + SVS_LOGGING_KIND_FILE_APPEND = 3, + SVS_LOGGING_KIND_FILE_TRUNCATE = 4 +}; + typedef struct svs_error_desc* svs_error_h; /// @brief Distance metric used to compare vectors. @@ -124,6 +144,45 @@ enum svs_threadpool_kind { SVS_THREADPOOL_KIND_CUSTOM = 3 }; +/// @brief Operations table for a custom logging interface +/// @remarks The user must ensure that the log function is thread-safe (it may be called +/// from SVS worker threads and must not throw or longjmp) and that the operations table +/// and @p self remain valid while any logger, index or default logger using them exists. +/// @var svs_logging_interface_ops::version +/// Version of the logging interface. +/// @var svs_logging_interface_ops::struct_size +/// Size of the structure, used for versioning and compatibility checks. +/// @var svs_logging_interface_ops::log +/// Function pointer to log a message. +/// @param self Pointer to the logging interface instance. +/// @param level Logging level of the message. +/// @param message Null-terminated bare message text, valid only for the duration of the +/// call. +struct svs_logging_interface_ops { + uint32_t version; + size_t struct_size; + void (*log)(void* self, enum svs_log_level level, const char* message); +}; + +/// @brief Macro to create a user-defined logging interface operations structure +/// @param log_func Function pointer to log a message +#define SVS_INIT_LOGGING_OPS(log_func) \ + { \ + .version = SVS_C_API_VERSION, \ + .struct_size = sizeof(struct svs_logging_interface_ops), .log = &log_func \ + } + +/// @brief Structure representing a custom logging interface +/// @var svs_logging_interface::ops +/// Function pointers for the logging operations. +/// @var svs_logging_interface::self +/// Pointer to the user-defined logger instance. This pointer is passed to the +/// function pointers in @p ops when they are called. +struct svs_logging_interface { + const struct svs_logging_interface_ops* ops; + void* self; +}; + /// @brief Operations table for a custom thread pool interface /// @remarks The user must ensure that the thread pool implementation is thread-safe and /// that the provided function pointers remain valid for the lifetime of the thread pool @@ -589,9 +648,12 @@ typedef struct svs_algorithm* svs_algorithm_h; typedef struct svs_storage* svs_storage_h; typedef struct svs_search_params* svs_search_params_h; typedef struct svs_leanvec_training_data* svs_leanvec_training_data_h; +typedef struct svs_logger* svs_logger_h; // Fully defined types; "_t" suffix indicates a fully defined struct typedef enum svs_error_code svs_error_code_t; +typedef enum svs_logging_kind svs_logging_kind_t; +typedef enum svs_log_level svs_log_level_t; typedef enum svs_distance_metric svs_distance_metric_t; typedef enum svs_algorithm_type svs_algorithm_type_t; typedef enum svs_data_type svs_data_type_t; @@ -600,6 +662,9 @@ typedef enum svs_threadpool_kind svs_threadpool_kind_t; typedef enum svs_allocator_kind svs_allocator_kind_t; typedef enum svs_sync_kind svs_sync_kind_t; +typedef struct svs_logging_interface_ops svs_logging_ops_t; +typedef struct svs_logging_interface svs_logging_t; +typedef struct svs_logging_interface* svs_logging_i; typedef struct svs_threadpool_interface_ops svs_threadpool_ops_t; typedef struct svs_threadpool_interface svs_threadpool_t; typedef struct svs_threadpool_interface* svs_threadpool_i; @@ -664,6 +729,86 @@ SVS_API const char* svs_error_get_message(svs_error_h err); /// @param err The error handle to free SVS_API void svs_error_free(svs_error_h err); +/// @brief Create a logger writing to a built-in output +/// @param kind The output kind of the logger +/// @param path The file to write to for the SVS_LOGGING_KIND_FILE_* kinds; ignored +/// otherwise +/// @param out_err An optional error handle to capture errors +/// @return A handle to the created logger or NULL if creation failed +/// @remarks The output of a logger is fixed at creation; a new logger has level +/// SVS_LOG_LEVEL_WARN and pattern "%v". +SVS_API svs_logger_h svs_logger_create( + svs_logging_kind_t kind, const char* path, svs_error_h out_err /*=NULL*/ +); + +/// @brief Create a logger forwarding every message to a user callback +/// @param user_logger The custom logging interface, initialized with SVS_INIT_LOGGING_OPS +/// @param out_err An optional error handle to capture errors +/// @return A handle to the created logger or NULL if creation failed +/// @remarks The callback may be called from SVS worker threads and must be thread-safe and +/// must not throw or longjmp. It receives the bare message; the logger pattern does not +/// apply to it. +SVS_API svs_logger_h +svs_logger_create_custom(svs_logging_i user_logger, svs_error_h out_err /*=NULL*/); + +/// @brief Free a logger handle +/// @param logger The logger handle to free +/// @remarks Indexes and the SVS default logger using this logger keep it alive after the +/// handle is freed. +SVS_API void svs_logger_free(svs_logger_h logger); + +/// @brief Set the logging level for a logger +/// @param logger The logger handle +/// @param level The logging level to set +/// @param out_err An optional error handle to capture errors +/// @return true on success, false on failure +/// @remarks Unknown level values fail with SVS_ERROR_INVALID_ARGUMENT. +SVS_API bool svs_logger_set_level( + svs_logger_h logger, svs_log_level_t level, svs_error_h out_err /*=NULL*/ +); + +/// @brief Get the logging level for a logger +/// @param logger The logger handle +/// @param out_level Pointer to store the retrieved logging level +/// @param out_err An optional error handle to capture errors +/// @return true on success, false on failure +SVS_API bool svs_logger_get_level( + svs_logger_h logger, svs_log_level_t* out_level, svs_error_h out_err /*=NULL*/ +); + +/// @brief Set format pattern for a logger +/// @param logger The logger handle +/// @param pattern The format pattern to set, in spdlog syntax (e.g. "[index A] %v") +/// Pattern syntax: https://github.com/gabime/spdlog/wiki/Custom-formatting +/// @param out_err An optional error handle to capture errors +/// @return true on success, false on failure +/// @remarks The pattern applies to the stdout, stderr and file outputs; custom callbacks +/// receive the bare message. +SVS_API bool svs_logger_set_pattern( + svs_logger_h logger, const char* pattern, svs_error_h out_err /*=NULL*/ +); + +/// @brief Get the format pattern for a logger +/// @param logger The logger handle +/// @param out_pattern Pointer to store the retrieved format pattern (default "%v") +/// @param out_err An optional error handle to capture errors +/// @return true on success, false on failure +/// @remarks The returned pointer is valid until the next svs_logger_set_pattern() call on +/// the handle or until svs_logger_free(). +SVS_API bool svs_logger_get_pattern( + svs_logger_h logger, const char** out_pattern, svs_error_h out_err /*=NULL*/ +); + +/// @brief Set default logger for SVS library +/// @param logger The logger handle to set as default; NULL restores the SVS built-in +/// default +/// @param out_err An optional error handle to capture errors +/// @return true on success, false on failure +/// @remarks Indexes keep the default logger current when they are built or loaded. +SVS_API bool svs_set_default_logger( + svs_logger_h logger, svs_error_h out_err /*=NULL*/ +); + /// @brief Create a Vamana algorithm configuration /// @param graph_degree The graph degree parameter /// @param build_window_size The build window size parameter @@ -896,6 +1041,18 @@ SVS_API svs_index_builder_h svs_index_builder_create( /// @param builder The index builder handle to free SVS_API void svs_index_builder_free(svs_index_builder_h builder); +/// @brief Set the logger for the index builder +/// @param builder The index builder handle +/// @param logger The logger handle for indexes built, loaded or converted with this +/// builder; NULL uses the global default logger +/// @param out_err An optional error handle to capture errors +/// @return true on success, false on failure +/// @remarks Indexes keep the logger they were created with; the handle may be freed after +/// this call. Stream loads ignore it and use the global default logger. +SVS_API bool svs_index_builder_set_logger( + svs_index_builder_h builder, svs_logger_h logger, svs_error_h out_err /*=NULL*/ +); + /// @brief Set the storage configuration for the index builder /// @param builder The index builder handle /// @param storage The storage configuration handle diff --git a/bindings/c/src/dispatcher_dynamic_vamana.cpp b/bindings/c/src/dispatcher_dynamic_vamana.cpp index 96a706f27..9686ebc29 100644 --- a/bindings/c/src/dispatcher_dynamic_vamana.cpp +++ b/bindings/c/src/dispatcher_dynamic_vamana.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -48,7 +49,8 @@ svs::DynamicVamana build_dynamic_vamana_index( Distance D, svs::threads::ThreadPoolHandle pool, const AllocatorBuilder& allocator_builder, - const svs::data::BlockingParameters& block_params + const svs::data::BlockingParameters& block_params, + svs::logging::logger_ptr logger ) { using allocator_type = typename DataBuilder::allocator_type; using value_type = typename allocator_type::value_type; @@ -65,7 +67,8 @@ svs::DynamicVamana build_dynamic_vamana_index( std::move(src_data.second), std::move(D), std::move(pool), - graph_allocator + graph_allocator, + std::move(logger) ); } @@ -77,7 +80,8 @@ svs::DynamicVamana load_dynamic_vamana_index( Distance D, svs::threads::ThreadPoolHandle pool, const AllocatorBuilder& allocator_builder, - const svs::data::BlockingParameters& block_params + const svs::data::BlockingParameters& block_params, + svs::logging::logger_ptr logger ) { using allocator_type = typename DataLoader::allocator_type; using value_type = typename allocator_type::value_type; @@ -93,7 +97,9 @@ svs::DynamicVamana load_dynamic_vamana_index( svs::GraphLoader{directory / "graph", graph_allocator}, std::move(data), std::move(D), - std::move(pool) + std::move(pool), + /*debug_load_from_static=*/false, + std::move(logger) ); } @@ -105,7 +111,8 @@ svs::DynamicVamana load_stream_dynamic_vamana_index( Distance distance, svs::threads::ThreadPoolHandle pool, const AllocatorBuilder& allocator_builder, - const svs::data::BlockingParameters& block_params + const svs::data::BlockingParameters& block_params, + svs::logging::logger_ptr SVS_UNUSED(logger) ) { using allocator_type = typename DataLoader::allocator_type; using value_type = typename allocator_type::value_type; @@ -164,7 +171,8 @@ using BuildDynamicIndexDispatcher = svs::lib::Dispatcher< svs::DistanceType, svs::threads::ThreadPoolHandle, const AllocatorBuilder&, - const svs::data::BlockingParameters&>; + const svs::data::BlockingParameters&, + svs::logging::logger_ptr>; const BuildDynamicIndexDispatcher& build_dynamic_vamana_index_dispatcher() { static BuildDynamicIndexDispatcher dispatcher = [] { @@ -184,7 +192,8 @@ using CopyDynamicIndexDispatcher = svs::lib::Dispatcher< svs::DistanceType, svs::threads::ThreadPoolHandle, const AllocatorBuilder&, - const svs::data::BlockingParameters&>; + const svs::data::BlockingParameters&, + svs::logging::logger_ptr>; template svs::DynamicVamana copy_dynamic_vamana_index( @@ -195,7 +204,8 @@ svs::DynamicVamana copy_dynamic_vamana_index( Distance distance, svs::threads::ThreadPoolHandle pool, const AllocatorBuilder& allocator_builder, - const svs::data::BlockingParameters& block_params + const svs::data::BlockingParameters& block_params, + svs::logging::logger_ptr logger ) { auto config = src_index.parameters(); @@ -255,7 +265,9 @@ svs::DynamicVamana copy_dynamic_vamana_index( std::move(graph), std::move(data), distance, - std::move(pool) + std::move(pool), + /*debug_load_from_static=*/false, + std::move(logger) ); } @@ -324,7 +336,8 @@ svs::DynamicVamana dispatch_dynamic_vamana_index_build( svs::DistanceType distance_type, svs::threads::ThreadPoolHandle pool, const AllocatorBuilder& allocator_builder, - const svs::data::BlockingParameters& block_params + const svs::data::BlockingParameters& block_params, + svs::logging::logger_ptr logger ) { return build_dynamic_vamana_index_dispatcher().invoke( build_params, @@ -333,7 +346,8 @@ svs::DynamicVamana dispatch_dynamic_vamana_index_build( distance_type, std::move(pool), allocator_builder, - block_params + block_params, + std::move(logger) ); } @@ -344,7 +358,8 @@ svs::DynamicVamana dispatch_dynamic_vamana_index_load( svs::DistanceType distance_type, svs::threads::ThreadPoolHandle pool, const AllocatorBuilder& allocator_builder, - const svs::data::BlockingParameters& block_params + const svs::data::BlockingParameters& block_params, + svs::logging::logger_ptr logger ) { return build_dynamic_vamana_index_dispatcher().invoke( build_params, @@ -353,7 +368,8 @@ svs::DynamicVamana dispatch_dynamic_vamana_index_load( distance_type, std::move(pool), allocator_builder, - block_params + block_params, + std::move(logger) ); } @@ -373,7 +389,8 @@ svs::DynamicVamana dispatch_dynamic_vamana_index_load_stream( distance_type, std::move(pool), allocator_builder, - block_params + block_params, + nullptr ); } @@ -385,7 +402,8 @@ svs::DynamicVamana dispatch_dynamic_vamana_index_copy( svs::DistanceType distance_type, svs::threads::ThreadPoolHandle pool, const AllocatorBuilder& allocator_builder, - const svs::data::BlockingParameters& block_params + const svs::data::BlockingParameters& block_params, + svs::logging::logger_ptr logger ) { return copy_dynamic_index_dispatcher().invoke( build_params, @@ -395,7 +413,8 @@ svs::DynamicVamana dispatch_dynamic_vamana_index_copy( distance_type, std::move(pool), allocator_builder, - block_params + block_params, + std::move(logger) ); } diff --git a/bindings/c/src/dispatcher_dynamic_vamana.hpp b/bindings/c/src/dispatcher_dynamic_vamana.hpp index 0c7f8d388..ca3a382ff 100644 --- a/bindings/c/src/dispatcher_dynamic_vamana.hpp +++ b/bindings/c/src/dispatcher_dynamic_vamana.hpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -40,7 +41,8 @@ svs::DynamicVamana dispatch_dynamic_vamana_index_build( svs::DistanceType distance_type, svs::threads::ThreadPoolHandle pool, const AllocatorBuilder& allocator_builder, - const svs::data::BlockingParameters& block_params + const svs::data::BlockingParameters& block_params, + svs::logging::logger_ptr logger ); svs::DynamicVamana dispatch_dynamic_vamana_index_load( @@ -50,7 +52,8 @@ svs::DynamicVamana dispatch_dynamic_vamana_index_load( svs::DistanceType distance_type, svs::threads::ThreadPoolHandle pool, const AllocatorBuilder& allocator_builder, - const svs::data::BlockingParameters& block_params + const svs::data::BlockingParameters& block_params, + svs::logging::logger_ptr logger ); svs::DynamicVamana dispatch_dynamic_vamana_index_load_stream( @@ -71,7 +74,8 @@ svs::DynamicVamana dispatch_dynamic_vamana_index_copy( svs::DistanceType distance_type, svs::threads::ThreadPoolHandle pool, const AllocatorBuilder& allocator_builder, - const svs::data::BlockingParameters& block_params + const svs::data::BlockingParameters& block_params, + svs::logging::logger_ptr logger ); svs::index::vamana::MemoryBreakdown dispatch_dynamic_vamana_memory_estimate( diff --git a/bindings/c/src/dispatcher_vamana.cpp b/bindings/c/src/dispatcher_vamana.cpp index e2414d95e..09c6b3b8f 100644 --- a/bindings/c/src/dispatcher_vamana.cpp +++ b/bindings/c/src/dispatcher_vamana.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -46,7 +47,8 @@ svs::Vamana build_vamana_index( DataBuilder builder, Distance distance, svs::threads::ThreadPoolHandle pool, - const AllocatorBuilder& allocator_builder + const AllocatorBuilder& allocator_builder, + svs::logging::logger_ptr logger ) { using value_type = typename DataBuilder::allocator_type::value_type; auto data = @@ -56,7 +58,8 @@ svs::Vamana build_vamana_index( std::move(data), distance, std::move(pool), - allocator_builder.build_for_graph() + allocator_builder.build_for_graph(), + std::move(logger) ); } @@ -67,7 +70,8 @@ svs::Vamana load_vamana_index( DataLoader loader, Distance distance, svs::threads::ThreadPoolHandle pool, - const AllocatorBuilder& allocator_builder + const AllocatorBuilder& allocator_builder, + svs::logging::logger_ptr logger ) { using value_type = typename DataLoader::allocator_type::value_type; auto data = loader.load(directory / "data", allocator_builder.build()); @@ -77,7 +81,8 @@ svs::Vamana load_vamana_index( directory / "graph", allocator_builder.build_for_graph()}, std::move(data), distance, - std::move(pool) + std::move(pool), + std::move(logger) ); } @@ -88,7 +93,8 @@ svs::Vamana load_stream_vamana_index( DataLoader SVS_UNUSED(loader), Distance distance, svs::threads::ThreadPoolHandle pool, - const AllocatorBuilder& allocator_builder + const AllocatorBuilder& allocator_builder, + svs::logging::logger_ptr SVS_UNUSED(logger) ) { using value_type = typename DataLoader::allocator_type::value_type; using data_type = typename DataLoader::data_type; @@ -143,7 +149,8 @@ using BuildIndexDispatcher = svs::lib::Dispatcher< const Storage*, svs::DistanceType, svs::threads::ThreadPoolHandle, - const AllocatorBuilder&>; + const AllocatorBuilder&, + svs::logging::logger_ptr>; const BuildIndexDispatcher& build_vamana_index_dispatcher() { static BuildIndexDispatcher dispatcher = [] { @@ -162,7 +169,8 @@ using CopyIndexDispatcher = svs::lib::Dispatcher< const Storage*, // dst svs::DistanceType, svs::threads::ThreadPoolHandle, - const AllocatorBuilder&>; + const AllocatorBuilder&, + svs::logging::logger_ptr>; template svs::Vamana copy_vamana_index( @@ -172,7 +180,8 @@ svs::Vamana copy_vamana_index( DstDataBuilder dst_builder, Distance distance, svs::threads::ThreadPoolHandle pool, - const AllocatorBuilder& allocator_builder + const AllocatorBuilder& allocator_builder, + svs::logging::logger_ptr logger ) { auto config = src_index.parameters(); @@ -216,7 +225,12 @@ svs::Vamana copy_vamana_index( auto data = dst_builder.build(src_data, pool, allocator_builder.build()); return svs::Vamana::assemble( - config, std::move(graph), std::move(data), distance, std::move(pool) + config, + std::move(graph), + std::move(data), + distance, + std::move(pool), + std::move(logger) ); } @@ -284,7 +298,8 @@ svs::Vamana dispatch_vamana_index_build( const Storage* storage, svs::DistanceType distance_type, svs::threads::ThreadPoolHandle pool, - const AllocatorBuilder& allocator_builder + const AllocatorBuilder& allocator_builder, + svs::logging::logger_ptr logger ) { return build_vamana_index_dispatcher().invoke( build_params, @@ -292,7 +307,8 @@ svs::Vamana dispatch_vamana_index_build( storage, distance_type, std::move(pool), - allocator_builder + allocator_builder, + std::move(logger) ); } @@ -302,7 +318,8 @@ svs::Vamana dispatch_vamana_index_load( const Storage* storage, svs::DistanceType distance_type, svs::threads::ThreadPoolHandle pool, - const AllocatorBuilder& allocator_builder + const AllocatorBuilder& allocator_builder, + svs::logging::logger_ptr logger ) { return build_vamana_index_dispatcher().invoke( build_params, @@ -310,7 +327,8 @@ svs::Vamana dispatch_vamana_index_load( storage, distance_type, std::move(pool), - allocator_builder + allocator_builder, + std::move(logger) ); } @@ -328,7 +346,8 @@ svs::Vamana dispatch_vamana_index_load_stream( storage, distance_type, std::move(pool), - allocator_builder + allocator_builder, + nullptr ); } @@ -339,7 +358,8 @@ svs::Vamana dispatch_vamana_index_copy( const Storage* dst_storage, svs::DistanceType distance_type, svs::threads::ThreadPoolHandle pool, - const AllocatorBuilder& allocator_builder + const AllocatorBuilder& allocator_builder, + svs::logging::logger_ptr logger ) { return copy_vamana_index_dispatcher().invoke( build_params, @@ -348,7 +368,8 @@ svs::Vamana dispatch_vamana_index_copy( dst_storage, distance_type, std::move(pool), - allocator_builder + allocator_builder, + std::move(logger) ); } diff --git a/bindings/c/src/dispatcher_vamana.hpp b/bindings/c/src/dispatcher_vamana.hpp index d3f497ecb..8c6416f5a 100644 --- a/bindings/c/src/dispatcher_vamana.hpp +++ b/bindings/c/src/dispatcher_vamana.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -37,7 +38,8 @@ svs::Vamana dispatch_vamana_index_build( const Storage* storage, svs::DistanceType distance_type, svs::threads::ThreadPoolHandle pool, - const AllocatorBuilder& allocator_builder + const AllocatorBuilder& allocator_builder, + svs::logging::logger_ptr logger ); svs::Vamana dispatch_vamana_index_load( @@ -46,7 +48,8 @@ svs::Vamana dispatch_vamana_index_load( const Storage* storage, svs::DistanceType distance_type, svs::threads::ThreadPoolHandle pool, - const AllocatorBuilder& allocator_builder + const AllocatorBuilder& allocator_builder, + svs::logging::logger_ptr logger ); svs::Vamana dispatch_vamana_index_load_stream( @@ -65,7 +68,8 @@ svs::Vamana dispatch_vamana_index_copy( const Storage* dst_storage, svs::DistanceType distance_type, svs::threads::ThreadPoolHandle pool, - const AllocatorBuilder& allocator_builder + const AllocatorBuilder& allocator_builder, + svs::logging::logger_ptr logger ); svs::index::vamana::MemoryBreakdown dispatch_vamana_memory_estimate( diff --git a/bindings/c/src/index_builder.cpp b/bindings/c/src/index_builder.cpp index ee5fe90fc..14db878d6 100644 --- a/bindings/c/src/index_builder.cpp +++ b/bindings/c/src/index_builder.cpp @@ -58,7 +58,8 @@ std::shared_ptr IndexBuilder::build(const svs::data::ConstSimpleDataView< storage.get(), to_distance_type(distance_metric), pool_builder.build(), - allocator_builder + allocator_builder, + get_logger() ) ); @@ -79,7 +80,8 @@ std::shared_ptr IndexBuilder::load(const std::filesystem::path& directory storage.get(), to_distance_type(distance_metric), pool_builder.build(), - allocator_builder + allocator_builder, + get_logger() ) ); @@ -167,7 +169,8 @@ std::shared_ptr IndexBuilder::copy(const std::shared_ptr& src_inde storage.get(), to_distance_type(distance_metric), pool_builder.build(), - allocator_builder + allocator_builder, + get_logger() ) ); @@ -215,7 +218,8 @@ std::shared_ptr IndexBuilder::copy_dynamic( to_distance_type(distance_metric), pool_builder.build(), allocator_builder, - block_params + block_params, + get_logger() ), sync_kind ); @@ -243,7 +247,8 @@ std::shared_ptr IndexBuilder::build_dynamic( to_distance_type(distance_metric), pool_builder.build(), allocator_builder, - block_params + block_params, + get_logger() ), sync_kind ); @@ -271,7 +276,8 @@ std::shared_ptr IndexBuilder::load_dynamic( to_distance_type(distance_metric), pool_builder.build(), allocator_builder, - block_params + block_params, + get_logger() ), sync_kind ); diff --git a/bindings/c/src/index_builder.hpp b/bindings/c/src/index_builder.hpp index 769a19d18..82eca1066 100644 --- a/bindings/c/src/index_builder.hpp +++ b/bindings/c/src/index_builder.hpp @@ -26,6 +26,7 @@ #include #include +#include #include #include @@ -46,6 +47,9 @@ struct IndexBuilder { std::unique_ptr storage; ThreadPoolBuilder pool_builder; AllocatorBuilder allocator_builder; + // Logger for indexes built or loaded by this builder. Empty means: use the SVS global + // default logger current at build/load time. + svs::logging::logger_ptr logger; IndexBuilder( svs_distance_metric_t distance_metric, @@ -65,7 +69,8 @@ struct IndexBuilder { , algorithm(other.algorithm->clone()) , storage(other.storage->clone()) , pool_builder(other.pool_builder) - , allocator_builder(other.allocator_builder) {} + , allocator_builder(other.allocator_builder) + , logger(other.logger) {} IndexBuilder& operator=(const IndexBuilder& other) { if (this != &other) { @@ -75,6 +80,7 @@ struct IndexBuilder { storage = other.storage->clone(); pool_builder = other.pool_builder; allocator_builder = other.allocator_builder; + logger = other.logger; } return *this; } @@ -95,6 +101,13 @@ struct IndexBuilder { std::swap(this->allocator_builder, allocator_builder); } + void set_logger(svs::logging::logger_ptr logger) { this->logger = std::move(logger); } + + // The logger to pass to SVS: the builder's logger if set, else the current global. + svs::logging::logger_ptr get_logger() const { + return logger ? logger : svs::logging::get(); + } + std::shared_ptr build(const svs::data::ConstSimpleDataView& data); std::shared_ptr load(const std::filesystem::path& directory); diff --git a/bindings/c/src/logger.hpp b/bindings/c/src/logger.hpp new file mode 100644 index 000000000..3de55081b --- /dev/null +++ b/bindings/c/src/logger.hpp @@ -0,0 +1,140 @@ +/* + * Copyright 2026 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "svs/c/svs_c.h" + +#include "error.hpp" + +#include + +#include "spdlog/sinks/callback_sink.h" + +#include +#include +#include +#include + +namespace svs { +namespace c_runtime { + +// spdlog level values are passed to the user callback by a plain cast. +static_assert(static_cast(SVS_LOG_LEVEL_TRACE) == SPDLOG_LEVEL_TRACE); +static_assert(static_cast(SVS_LOG_LEVEL_DEBUG) == SPDLOG_LEVEL_DEBUG); +static_assert(static_cast(SVS_LOG_LEVEL_INFO) == SPDLOG_LEVEL_INFO); +static_assert(static_cast(SVS_LOG_LEVEL_WARN) == SPDLOG_LEVEL_WARN); +static_assert(static_cast(SVS_LOG_LEVEL_ERROR) == SPDLOG_LEVEL_ERROR); +static_assert(static_cast(SVS_LOG_LEVEL_CRITICAL) == SPDLOG_LEVEL_CRITICAL); +static_assert(static_cast(SVS_LOG_LEVEL_OFF) == SPDLOG_LEVEL_OFF); + +/// Name given to every spdlog logger created by the C API. +inline constexpr const char* logger_name = "svs_c"; + +/// Default pattern of every logger handle: the bare message. +inline constexpr const char* default_log_pattern = "%v"; + +/// Checks the version, struct size and NULL pointers of a user-provided custom logger. +inline void validate_custom_logger(const svs_logging_i user_logger) { + if (user_logger == nullptr) { + throw std::invalid_argument("Custom logger pointer cannot be null."); + } + if (user_logger->ops == nullptr) { + throw std::invalid_argument("Custom logger interface is not initialized."); + } + if (user_logger->ops->version > svs_get_version()) { + throw std::invalid_argument("Custom logger interface version is not supported."); + } + if (user_logger->ops->struct_size < sizeof(svs_logging_ops_t)) { + throw std::invalid_argument("Incompatible custom logger interface struct size."); + } + if (user_logger->ops->log == nullptr) { + throw std::invalid_argument("Custom logger interface has null log function."); + } +} + +/// Maps a C API log level to the SVS logging level. Throws on unknown values. +inline svs::logging::Level to_logging_level(svs_log_level_t level) { + switch (level) { + case SVS_LOG_LEVEL_TRACE: + return svs::logging::Level::Trace; + case SVS_LOG_LEVEL_DEBUG: + return svs::logging::Level::Debug; + case SVS_LOG_LEVEL_INFO: + return svs::logging::Level::Info; + case SVS_LOG_LEVEL_WARN: + return svs::logging::Level::Warn; + case SVS_LOG_LEVEL_ERROR: + return svs::logging::Level::Error; + case SVS_LOG_LEVEL_CRITICAL: + return svs::logging::Level::Critical; + case SVS_LOG_LEVEL_OFF: + return svs::logging::Level::Off; + default: + throw std::invalid_argument("Invalid log level"); + } +} + +/// Creates the spdlog sink for a built-in output kind. +inline svs::logging::sink_ptr make_sink(svs_logging_kind_t kind, const char* path) { + switch (kind) { + case SVS_LOGGING_KIND_NONE: + return svs::logging::null_sink(); + case SVS_LOGGING_KIND_STDOUT: + return svs::logging::stdout_sink(); + case SVS_LOGGING_KIND_STDERR: + return svs::logging::stderr_sink(); + case SVS_LOGGING_KIND_FILE_APPEND: + case SVS_LOGGING_KIND_FILE_TRUNCATE: + if (path == nullptr || *path == '\0') { + throw std::invalid_argument( + "File path must be provided for file logging kind" + ); + } + return svs::logging::file_sink(path, kind == SVS_LOGGING_KIND_FILE_TRUNCATE); + default: + throw std::invalid_argument("Invalid logging kind"); + } +} + +/// Creates the spdlog sink that forwards every message to a user callback. +inline svs::logging::sink_ptr make_custom_sink(const svs_logging_i user_logger) { + validate_custom_logger(user_logger); + auto log = user_logger->ops->log; + auto self = user_logger->self; + return std::make_shared( + [log, self](const spdlog::details::log_msg& msg) { + std::string text(msg.payload.data(), msg.payload.size()); + log(self, static_cast(msg.level), text.c_str()); + } + ); +} + +} // namespace c_runtime +} // namespace svs + +/// The logger handle of the C API (svs_logger_h). +struct svs_logger { + svs::logging::logger_ptr impl; + std::string pattern = svs::c_runtime::default_log_pattern; + + explicit svs_logger(svs::logging::sink_ptr sink) + : impl{std::make_shared( + svs::c_runtime::logger_name, std::move(sink) + )} { + impl->set_level(spdlog::level::warn); + impl->set_pattern(pattern); + } +}; diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp index 7fc2e58fc..184822103 100644 --- a/bindings/c/src/svs_c.cpp +++ b/bindings/c/src/svs_c.cpp @@ -22,6 +22,7 @@ #include "index.hpp" #include "index_builder.hpp" #include "leanvec_training_data.hpp" +#include "logger.hpp" #include "storage.hpp" #include "stream.hpp" #include "threadpool.hpp" @@ -33,6 +34,7 @@ #include #include #include +#include #include #include @@ -70,6 +72,102 @@ extern "C" uint32_t svs_get_version() { return SVS_C_API_VERSION; } extern "C" const char* svs_get_version_string() { return SVS_C_API_VERSION_STRING; } +extern "C" svs_logger_h svs_logger_create( + svs_logging_kind_t kind, const char* path, svs_error_h out_err /*=NULL*/ +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { return new svs_logger{make_sink(kind, path)}; }, out_err + ); +} + +extern "C" svs_logger_h +svs_logger_create_custom(svs_logging_i user_logger, svs_error_h out_err /*=NULL*/) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { return new svs_logger{make_custom_sink(user_logger)}; }, out_err + ); +} + +extern "C" void svs_logger_free(svs_logger_h logger) { delete logger; } + +extern "C" bool svs_logger_set_level( + svs_logger_h logger, svs_log_level_t level, svs_error_h out_err /*=NULL*/ +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { + INVALID_ARGUMENT_IF(logger == nullptr, "Logger must not be null"); + svs::logging::set_level(logger->impl, to_logging_level(level)); + return true; + }, + out_err + ); +} + +extern "C" bool svs_logger_get_level( + svs_logger_h logger, svs_log_level_t* out_level, svs_error_h out_err /*=NULL*/ +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { + INVALID_ARGUMENT_IF(logger == nullptr, "Logger must not be null"); + EXPECT_ARG_NOT_NULL(out_level); + *out_level = static_cast(logger->impl->level()); + return true; + }, + out_err + ); +} + +extern "C" bool svs_logger_set_pattern( + svs_logger_h logger, const char* pattern, svs_error_h out_err /*=NULL*/ +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { + INVALID_ARGUMENT_IF(logger == nullptr, "Logger must not be null"); + EXPECT_ARG_NOT_NULL(pattern); + auto new_pattern = std::string(pattern); + INVALID_ARGUMENT_IF(new_pattern.empty(), "Pattern should not be empty"); + logger->impl->set_pattern(new_pattern); + logger->pattern = std::move(new_pattern); + return true; + }, + out_err + ); +} + +extern "C" bool svs_logger_get_pattern( + svs_logger_h logger, const char** out_pattern, svs_error_h out_err /*=NULL*/ +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { + INVALID_ARGUMENT_IF(logger == nullptr, "Logger must not be null"); + EXPECT_ARG_NOT_NULL(out_pattern); + *out_pattern = logger->pattern.c_str(); + return true; + }, + out_err + ); +} + +extern "C" bool svs_set_default_logger(svs_logger_h logger, svs_error_h out_err /*=NULL*/) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { + if (logger == nullptr) { + svs::logging::reset_to_default(); + } else { + svs::logging::set(logger->impl); + } + return true; + }, + out_err + ); +} + extern "C" svs_algorithm_h svs_algorithm_create_vamana( size_t graph_degree, size_t build_window_size, @@ -508,6 +606,23 @@ extern "C" bool svs_index_builder_set_storage( ); } +extern "C" bool svs_index_builder_set_logger( + svs_index_builder_h builder, svs_logger_h logger, svs_error_h out_err /*=NULL*/ +) { + using namespace svs::c_runtime; + return wrap_exceptions( + [&]() { + EXPECT_ARG_NOT_NULL(builder); + // NULL clears the builder logger: indexes then use the global default logger. + // Otherwise share the handle's spdlog logger, so the handle may be freed. + builder->impl->set_logger(logger == nullptr ? nullptr : logger->impl); + return true; + }, + out_err, + false + ); +} + extern "C" bool svs_index_builder_set_threadpool( svs_index_builder_h builder, svs_threadpool_kind_t kind, diff --git a/bindings/c/tests/CMakeLists.txt b/bindings/c/tests/CMakeLists.txt index 703804d01..48a527e7c 100644 --- a/bindings/c/tests/CMakeLists.txt +++ b/bindings/c/tests/CMakeLists.txt @@ -54,6 +54,7 @@ set(C_API_TEST_SOURCES c_api_index.cpp c_api_index_convert.cpp c_api_dynamic_index.cpp + c_api_logging.cpp c_api_dynamic_index_sync.cpp c_api_stream.cpp ) diff --git a/bindings/c/tests/README.md b/bindings/c/tests/README.md index d2a548d47..c351a7527 100644 --- a/bindings/c/tests/README.md +++ b/bindings/c/tests/README.md @@ -29,6 +29,7 @@ The tests are organized into separate files by functionality: - **c_api_index_builder.cpp**: Tests for index builder creation and configuration - **c_api_index.cpp**: Tests for index building, searching, and basic operations - **c_api_dynamic_index.cpp**: Tests for dynamic index operations (add, delete, consolidate, compact) +- **c_api_logging.cpp**: Tests for logger handles (output kinds, custom callback, level, pattern, default logger) - **c_api_dynamic_index_sync.cpp**: Tests for dynamic index parameters (`_ex` functions) and internal synchronization under concurrent readers/writers - **c_api_stream.cpp**: Tests for stream-based save and load operations, error handling, and round-trip validation @@ -73,6 +74,9 @@ cmake -DSVS_BUILD_C_API_TESTS=OFF .. # Run dynamic index tests ./svs_c_api_test "[c_api][dynamic]" +# Run logging tests +./svs_c_api_test "[c_api][logging]" + # Run dynamic index synchronization tests ./svs_c_api_test "[c_api][dynamic][sync]" ``` @@ -139,6 +143,12 @@ The tests cover the following aspects of the C API: - Vector reconstruction - Thread count management +### Logging + +- Logger creation for each output kind and custom callbacks; invalid arguments rejected +- Level and pattern getters/setters +- Default logger set/reset + ### Dynamic Index Operations - Dynamic index building with/without explicit IDs diff --git a/bindings/c/tests/c_api_logging.cpp b/bindings/c/tests/c_api_logging.cpp new file mode 100644 index 000000000..ebf901364 --- /dev/null +++ b/bindings/c/tests/c_api_logging.cpp @@ -0,0 +1,1117 @@ +/* + * Copyright 2026 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// C API +#include "svs/c/svs_c.h" + +// catch2 +#include "catch2/catch_test_macros.hpp" + +// Test utilities +#include "c_api_test_utils.h" + +// Standard library +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// Records every message received by the custom logger callback. +struct LogRecorder { + std::mutex mutex; + std::vector> messages; + + bool contains(svs_log_level_t level, const std::string& text) { + std::lock_guard lock{mutex}; + return std::any_of(messages.begin(), messages.end(), [&](const auto& m) { + return m.first == level && m.second.find(text) != std::string::npos; + }); + } + + bool any_below(svs_log_level_t level) { + std::lock_guard lock{mutex}; + return std::any_of(messages.begin(), messages.end(), [&](const auto& m) { + return m.first < level; + }); + } +}; + +void record_log(void* self, enum svs_log_level level, const char* message) { + auto* recorder = static_cast(self); + std::lock_guard lock{recorder->mutex}; + recorder->messages.emplace_back(level, message); +} + +void noop_log(void* /*self*/, enum svs_log_level /*level*/, const char* /*message*/) {} + +// Restores the SVS default logger so later tests never log through a destroyed callback. +struct DefaultLoggerGuard { + DefaultLoggerGuard() = default; + DefaultLoggerGuard(const DefaultLoggerGuard&) = delete; + DefaultLoggerGuard& operator=(const DefaultLoggerGuard&) = delete; + ~DefaultLoggerGuard() { svs_set_default_logger(nullptr, nullptr); } +}; + +// Builds a small index; Vamana logs at TRACE level through the global default logger. +void build_small_index() { + const size_t num_vectors = 100; + const size_t dimension = 16; + std::vector data; + generate_test_data(data, num_vectors, dimension); + + svs_error_h error = svs_error_create(); + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, dimension, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + CATCH_REQUIRE( + svs_index_builder_set_threadpool(builder, SVS_THREADPOOL_KIND_NATIVE, 2, error) + ); + svs_index_h index = svs_index_build(builder, data.data(), num_vectors, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); +} + +std::string read_file(const std::string& path) { + std::ifstream in(path); + return std::string( + std::istreambuf_iterator(in), std::istreambuf_iterator() + ); +} + +void write_file(const std::string& path, const std::string& content) { + std::ofstream out(path, std::ios::trunc); + out << content; +} + +} // namespace + +CATCH_TEST_CASE("C API Logger Handle", "[c_api][logging]") { + CATCH_SECTION("Create With Each Kind") { + svs_error_h error = svs_error_create(); + TempDir tmp; + const std::string path = (tmp.path() / "svs.log").string(); + + for (auto kind : + {SVS_LOGGING_KIND_NONE, SVS_LOGGING_KIND_STDOUT, SVS_LOGGING_KIND_STDERR}) { + svs_logger_h logger = svs_logger_create(kind, nullptr, error); + CATCH_REQUIRE(logger != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + svs_logger_free(logger); + + logger = svs_logger_create(kind, "ignored", error); + CATCH_REQUIRE(logger != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + svs_logger_free(logger); + } + + for (auto kind : {SVS_LOGGING_KIND_FILE_APPEND, SVS_LOGGING_KIND_FILE_TRUNCATE}) { + svs_logger_h logger = svs_logger_create(kind, path.c_str(), error); + CATCH_REQUIRE(logger != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + svs_logger_free(logger); + } + + svs_logger_h logger = svs_logger_create(SVS_LOGGING_KIND_NONE, nullptr, nullptr); + CATCH_REQUIRE(logger != nullptr); + svs_logger_free(logger); + svs_logger_free(nullptr); + svs_error_free(error); + } + + CATCH_SECTION("Create Invalid") { + svs_error_h error = svs_error_create(); + + CATCH_REQUIRE( + svs_logger_create(static_cast(5), nullptr, error) == nullptr + ); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + CATCH_REQUIRE( + svs_logger_create(static_cast(6), nullptr, error) == nullptr + ); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + + for (auto kind : {SVS_LOGGING_KIND_FILE_APPEND, SVS_LOGGING_KIND_FILE_TRUNCATE}) { + CATCH_REQUIRE(svs_logger_create(kind, nullptr, error) == nullptr); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + CATCH_REQUIRE(svs_logger_create(kind, "", error) == nullptr); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + } + + TempDir tmp; + CATCH_REQUIRE( + svs_logger_create( + SVS_LOGGING_KIND_FILE_TRUNCATE, tmp.string().c_str(), error + ) == nullptr + ); + CATCH_REQUIRE(svs_error_get_code(error) != SVS_OK); + + CATCH_REQUIRE( + svs_logger_create(SVS_LOGGING_KIND_FILE_APPEND, nullptr, nullptr) == nullptr + ); + + svs_error_free(error); + } + + CATCH_SECTION("Create Custom") { + svs_error_h error = svs_error_create(); + svs_logging_ops_t ops = SVS_INIT_LOGGING_OPS(noop_log); + svs_logging_t user_logger = {&ops, nullptr}; + + svs_logger_h logger = svs_logger_create_custom(&user_logger, error); + CATCH_REQUIRE(logger != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + svs_logger_free(logger); + + logger = svs_logger_create_custom(&user_logger, nullptr); + CATCH_REQUIRE(logger != nullptr); + svs_logger_free(logger); + svs_error_free(error); + } + + CATCH_SECTION("Create Custom Invalid") { + svs_error_h error = svs_error_create(); + + auto expect_invalid = [&](svs_logging_i user_logger) { + CATCH_REQUIRE(svs_logger_create_custom(user_logger, error) == nullptr); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + }; + + expect_invalid(nullptr); + + svs_logging_t no_ops = {nullptr, nullptr}; + expect_invalid(&no_ops); + + svs_logging_ops_t ops = SVS_INIT_LOGGING_OPS(noop_log); + ops.log = nullptr; + svs_logging_t no_log = {&ops, nullptr}; + expect_invalid(&no_log); + + svs_logging_ops_t bad_version = SVS_INIT_LOGGING_OPS(noop_log); + bad_version.version = svs_get_version() + 1; + svs_logging_t bad_version_logger = {&bad_version, nullptr}; + expect_invalid(&bad_version_logger); + + svs_logging_ops_t bad_size = SVS_INIT_LOGGING_OPS(noop_log); + bad_size.struct_size = sizeof(svs_logging_ops_t) - 1; + svs_logging_t bad_size_logger = {&bad_size, nullptr}; + expect_invalid(&bad_size_logger); + + CATCH_REQUIRE(svs_logger_create_custom(nullptr, nullptr) == nullptr); + + svs_error_free(error); + } + + CATCH_SECTION("NULL Arguments") { + svs_error_h error = svs_error_create(); + svs_logger_h logger = svs_logger_create(SVS_LOGGING_KIND_NONE, nullptr, error); + CATCH_REQUIRE(logger != nullptr); + + auto expect_invalid = [&](bool result) { + CATCH_REQUIRE(result == false); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + }; + + svs_log_level_t level; + const char* pattern = nullptr; + + expect_invalid(svs_logger_set_level(nullptr, SVS_LOG_LEVEL_INFO, error)); + expect_invalid(svs_logger_get_level(nullptr, &level, error)); + expect_invalid(svs_logger_get_level(logger, nullptr, error)); + expect_invalid(svs_logger_set_pattern(nullptr, "%v", error)); + expect_invalid(svs_logger_set_pattern(logger, nullptr, error)); + expect_invalid(svs_logger_get_pattern(nullptr, &pattern, error)); + expect_invalid(svs_logger_get_pattern(logger, nullptr, error)); + + CATCH_REQUIRE(svs_logger_set_level(nullptr, SVS_LOG_LEVEL_INFO, nullptr) == false); + + svs_logger_free(logger); + svs_error_free(error); + } + + CATCH_SECTION("Defaults After Create") { + svs_error_h error = svs_error_create(); + svs_logging_ops_t ops = SVS_INIT_LOGGING_OPS(noop_log); + svs_logging_t user_logger = {&ops, nullptr}; + TempDir tmp; + const std::string path = (tmp.path() / "svs.log").string(); + + auto check_defaults = [&](svs_logger_h logger) { + CATCH_REQUIRE(logger != nullptr); + svs_log_level_t level = SVS_LOG_LEVEL_OFF; + const char* pattern = nullptr; + CATCH_REQUIRE(svs_logger_get_level(logger, &level, error)); + CATCH_REQUIRE(level == SVS_LOG_LEVEL_WARN); + CATCH_REQUIRE(svs_logger_get_pattern(logger, &pattern, error)); + CATCH_REQUIRE(pattern != nullptr); + CATCH_REQUIRE(std::string(pattern) == "%v"); + svs_logger_free(logger); + }; + + check_defaults(svs_logger_create(SVS_LOGGING_KIND_NONE, nullptr, error)); + check_defaults(svs_logger_create(SVS_LOGGING_KIND_STDOUT, nullptr, error)); + check_defaults(svs_logger_create(SVS_LOGGING_KIND_STDERR, nullptr, error)); + check_defaults( + svs_logger_create(SVS_LOGGING_KIND_FILE_TRUNCATE, path.c_str(), error) + ); + check_defaults(svs_logger_create_custom(&user_logger, error)); + + svs_error_free(error); + } + + CATCH_SECTION("Level Round Trip") { + svs_error_h error = svs_error_create(); + svs_logger_h logger = svs_logger_create(SVS_LOGGING_KIND_NONE, nullptr, error); + CATCH_REQUIRE(logger != nullptr); + + for (auto level : + {SVS_LOG_LEVEL_TRACE, + SVS_LOG_LEVEL_DEBUG, + SVS_LOG_LEVEL_INFO, + SVS_LOG_LEVEL_WARN, + SVS_LOG_LEVEL_ERROR, + SVS_LOG_LEVEL_CRITICAL, + SVS_LOG_LEVEL_OFF}) { + CATCH_REQUIRE(svs_logger_set_level(logger, level, error)); + svs_log_level_t out_level = SVS_LOG_LEVEL_OFF; + CATCH_REQUIRE(svs_logger_get_level(logger, &out_level, error)); + CATCH_REQUIRE(out_level == level); + } + + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_WARN, error)); + CATCH_REQUIRE(!svs_logger_set_level(logger, static_cast(7), error) + ); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + svs_log_level_t out_level = SVS_LOG_LEVEL_OFF; + CATCH_REQUIRE(svs_logger_get_level(logger, &out_level, error)); + CATCH_REQUIRE(out_level == SVS_LOG_LEVEL_WARN); + + svs_logger_free(logger); + svs_error_free(error); + } + + CATCH_SECTION("Pattern Round Trip") { + svs_error_h error = svs_error_create(); + svs_logger_h logger = svs_logger_create(SVS_LOGGING_KIND_NONE, nullptr, error); + CATCH_REQUIRE(logger != nullptr); + + const char* pattern = nullptr; + CATCH_REQUIRE(svs_logger_get_pattern(logger, &pattern, error)); + CATCH_REQUIRE(pattern != nullptr); + CATCH_REQUIRE(std::string(pattern) == "%v"); + + CATCH_REQUIRE(svs_logger_set_pattern(logger, "[%l] %v", error)); + CATCH_REQUIRE(svs_logger_get_pattern(logger, &pattern, error)); + CATCH_REQUIRE(std::string(pattern) == "[%l] %v"); + + CATCH_REQUIRE(!svs_logger_set_pattern(logger, "", error)); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + CATCH_REQUIRE(svs_logger_get_pattern(logger, &pattern, error)); + CATCH_REQUIRE(std::string(pattern) == "[%l] %v"); + + svs_logger_free(logger); + svs_error_free(error); + } +} + +CATCH_TEST_CASE("C API Logger Output", "[c_api][logging]") { + CATCH_SECTION("Custom Callback Receives Messages") { + LogRecorder recorder; // Declared first: must outlive the guard. + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + + svs_logging_ops_t ops = SVS_INIT_LOGGING_OPS(record_log); + svs_logging_t user_logger = {&ops, &recorder}; + svs_logger_h logger = svs_logger_create_custom(&user_logger, error); + CATCH_REQUIRE(logger != nullptr); + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_TRACE, error)); + CATCH_REQUIRE(svs_set_default_logger(logger, error)); + CATCH_REQUIRE(svs_error_ok(error)); + + build_small_index(); + + // Vamana splits the 100 vectors into max(40, ceil(100 / 4096)) = 40 batches. + { + std::lock_guard lock{recorder.mutex}; + bool found = std::any_of( + recorder.messages.begin(), + recorder.messages.end(), + [](const auto& m) { + return m.first == SVS_LOG_LEVEL_TRACE && + m.second == "Number of syncs: 40"; + } + ); + CATCH_REQUIRE(found); + } + + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_logger_free(logger); + svs_error_free(error); + } + + CATCH_SECTION("None Output Is Silent") { + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + svs_logger_h logger = svs_logger_create(SVS_LOGGING_KIND_NONE, nullptr, error); + CATCH_REQUIRE(logger != nullptr); + + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_TRACE, error)); + CATCH_REQUIRE(svs_set_default_logger(logger, error)); + build_small_index(); + CATCH_REQUIRE(svs_error_ok(error)); + + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_logger_free(logger); + svs_error_free(error); + } + + CATCH_SECTION("Level Changes Reach Existing Users") { + LogRecorder recorder; // Declared first: must outlive the guard. + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + + svs_logging_ops_t ops = SVS_INIT_LOGGING_OPS(record_log); + svs_logging_t user_logger = {&ops, &recorder}; + svs_logger_h logger = svs_logger_create_custom(&user_logger, error); + CATCH_REQUIRE(logger != nullptr); + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_TRACE, error)); + CATCH_REQUIRE(svs_set_default_logger(logger, error)); + + build_small_index(); + CATCH_REQUIRE(recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + + { + std::lock_guard lock{recorder.mutex}; + recorder.messages.clear(); + } + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_OFF, error)); + build_small_index(); + { + std::lock_guard lock{recorder.mutex}; + CATCH_REQUIRE(recorder.messages.empty()); + } + + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_TRACE, error)); + build_small_index(); + CATCH_REQUIRE(recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_logger_free(logger); + svs_error_free(error); + } + + CATCH_SECTION("Pattern Not Applied To Custom Callback") { + LogRecorder recorder; // Declared first: must outlive the guard. + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + + svs_logging_ops_t ops = SVS_INIT_LOGGING_OPS(record_log); + svs_logging_t user_logger = {&ops, &recorder}; + svs_logger_h logger = svs_logger_create_custom(&user_logger, error); + CATCH_REQUIRE(logger != nullptr); + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_TRACE, error)); + CATCH_REQUIRE(svs_set_default_logger(logger, error)); + CATCH_REQUIRE(svs_logger_set_pattern(logger, "[x] %v", error)); + + build_small_index(); + + { + std::lock_guard lock{recorder.mutex}; + CATCH_REQUIRE(!recorder.messages.empty()); + for (const auto& [level, text] : recorder.messages) { + CATCH_REQUIRE(text.rfind("[x] ", 0) == std::string::npos); + } + bool found = std::any_of( + recorder.messages.begin(), + recorder.messages.end(), + [](const auto& m) { return m.second.rfind("Number of syncs: ", 0) == 0; } + ); + CATCH_REQUIRE(found); + } + + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_logger_free(logger); + svs_error_free(error); + } + + CATCH_SECTION("Level Filtering") { + LogRecorder recorder; // Declared first: must outlive the guard. + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + + svs_logging_ops_t ops = SVS_INIT_LOGGING_OPS(record_log); + svs_logging_t user_logger = {&ops, &recorder}; + svs_logger_h logger = svs_logger_create_custom(&user_logger, error); + CATCH_REQUIRE(logger != nullptr); + CATCH_REQUIRE(svs_set_default_logger(logger, error)); + build_small_index(); + CATCH_REQUIRE(!recorder.any_below(SVS_LOG_LEVEL_WARN)); + + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_INFO, error)); + build_small_index(); + CATCH_REQUIRE(!recorder.any_below(SVS_LOG_LEVEL_INFO)); + + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_TRACE, error)); + build_small_index(); + CATCH_REQUIRE(recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_logger_free(logger); + svs_error_free(error); + } + + CATCH_SECTION("Pattern Applied To File Output") { + TempDir tmp; + const std::string path = (tmp.path() / "svs.log").string(); + { + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + svs_logger_h logger = + svs_logger_create(SVS_LOGGING_KIND_FILE_TRUNCATE, path.c_str(), error); + CATCH_REQUIRE(logger != nullptr); + CATCH_REQUIRE(svs_logger_set_pattern(logger, "[x] %v", error)); + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_TRACE, error)); + CATCH_REQUIRE(svs_set_default_logger(logger, error)); + svs_logger_free(logger); + + build_small_index(); + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_error_free(error); + } + // Resetting the default logger released the last reference, which flushes the file. + auto content = read_file(path); + CATCH_REQUIRE(content.find("[x] Number of syncs") != std::string::npos); + } + + CATCH_SECTION("Default Logger Outlives Handle") { + LogRecorder recorder; // Declared first: must outlive the guard. + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + + svs_logging_ops_t ops = SVS_INIT_LOGGING_OPS(record_log); + svs_logging_t user_logger = {&ops, &recorder}; + svs_logger_h logger = svs_logger_create_custom(&user_logger, error); + CATCH_REQUIRE(logger != nullptr); + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_TRACE, error)); + CATCH_REQUIRE(svs_set_default_logger(logger, error)); + svs_logger_free(logger); + + build_small_index(); + + CATCH_REQUIRE(recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_error_free(error); + } + + CATCH_SECTION("Reset Default Logger With NULL") { + LogRecorder recorder; // Declared first: must outlive the guard. + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + + svs_logging_ops_t ops = SVS_INIT_LOGGING_OPS(record_log); + svs_logging_t user_logger = {&ops, &recorder}; + svs_logger_h logger = svs_logger_create_custom(&user_logger, error); + CATCH_REQUIRE(logger != nullptr); + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_TRACE, error)); + CATCH_REQUIRE(svs_set_default_logger(logger, error)); + + build_small_index(); + CATCH_REQUIRE(recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + CATCH_REQUIRE(svs_error_ok(error)); + { + std::lock_guard lock{recorder.mutex}; + recorder.messages.clear(); + } + build_small_index(); + { + std::lock_guard lock{recorder.mutex}; + CATCH_REQUIRE(recorder.messages.empty()); + } + + svs_logger_free(logger); + svs_error_free(error); + } + + CATCH_SECTION("Switch Default Logger To Another Logger") { + LogRecorder first; // Declared first: must outlive the guard. + LogRecorder second; // Declared first: must outlive the guard. + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + + svs_logging_ops_t ops = SVS_INIT_LOGGING_OPS(record_log); + svs_logging_t first_logger = {&ops, &first}; + svs_logging_t second_logger = {&ops, &second}; + svs_logger_h logger_a = svs_logger_create_custom(&first_logger, error); + svs_logger_h logger_b = svs_logger_create_custom(&second_logger, error); + CATCH_REQUIRE(logger_a != nullptr); + CATCH_REQUIRE(logger_b != nullptr); + CATCH_REQUIRE(svs_logger_set_level(logger_a, SVS_LOG_LEVEL_TRACE, error)); + CATCH_REQUIRE(svs_logger_set_level(logger_b, SVS_LOG_LEVEL_TRACE, error)); + + CATCH_REQUIRE(svs_set_default_logger(logger_a, error)); + build_small_index(); + CATCH_REQUIRE(first.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + { + std::lock_guard lock{second.mutex}; + CATCH_REQUIRE(second.messages.empty()); + } + + { + std::lock_guard lock{first.mutex}; + first.messages.clear(); + } + CATCH_REQUIRE(svs_set_default_logger(logger_b, error)); + build_small_index(); + CATCH_REQUIRE(second.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + { + std::lock_guard lock{first.mutex}; + CATCH_REQUIRE(first.messages.empty()); + } + + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_logger_free(logger_a); + svs_logger_free(logger_b); + svs_error_free(error); + } + + CATCH_SECTION("File Truncate") { + TempDir tmp; + const std::string path = (tmp.path() / "svs.log").string(); + write_file(path, "OLD CONTENT\n"); + { + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + svs_logger_h logger = + svs_logger_create(SVS_LOGGING_KIND_FILE_TRUNCATE, path.c_str(), error); + CATCH_REQUIRE(logger != nullptr); + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_TRACE, error)); + CATCH_REQUIRE(svs_set_default_logger(logger, error)); + svs_logger_free(logger); + + build_small_index(); + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_error_free(error); + } + auto content = read_file(path); + CATCH_REQUIRE(content.find("OLD CONTENT") == std::string::npos); + CATCH_REQUIRE(content.find("Number of syncs") != std::string::npos); + } + + CATCH_SECTION("File Append") { + TempDir tmp; + const std::string path = (tmp.path() / "svs.log").string(); + write_file(path, "OLD CONTENT\n"); + { + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + svs_logger_h logger = + svs_logger_create(SVS_LOGGING_KIND_FILE_APPEND, path.c_str(), error); + CATCH_REQUIRE(logger != nullptr); + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_TRACE, error)); + CATCH_REQUIRE(svs_set_default_logger(logger, error)); + svs_logger_free(logger); + + build_small_index(); + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_error_free(error); + } + auto content = read_file(path); + CATCH_REQUIRE(content.rfind("OLD CONTENT\n", 0) == 0); + CATCH_REQUIRE(content.find("Number of syncs") != std::string::npos); + } + + CATCH_SECTION("Default Logger Change Does Not Affect Built Index") { + LogRecorder recorder; // Declared first: must outlive the guard and the index. + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + + svs_logging_ops_t ops = SVS_INIT_LOGGING_OPS(record_log); + svs_logging_t user_logger = {&ops, &recorder}; + svs_logger_h logger = svs_logger_create_custom(&user_logger, error); + CATCH_REQUIRE(logger != nullptr); + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_TRACE, error)); + CATCH_REQUIRE(svs_set_default_logger(logger, error)); + + const size_t num_vectors = 100; + const size_t dimension = 16; + std::vector data; + generate_test_data(data, num_vectors, dimension); + std::vector ids(num_vectors); + for (size_t i = 0; i < num_vectors; ++i) { + ids[i] = i; + } + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, dimension, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + CATCH_REQUIRE( + svs_index_builder_set_threadpool(builder, SVS_THREADPOOL_KIND_NATIVE, 2, error) + ); + svs_index_h index = svs_index_build_dynamic( + builder, data.data(), ids.data(), num_vectors, 0, error + ); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + + // The index keeps the logger captured at build time after the default is restored. + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + { + std::lock_guard lock{recorder.mutex}; + recorder.messages.clear(); + } + std::vector new_data; + generate_test_data(new_data, num_vectors, dimension); + std::vector new_ids(num_vectors); + for (size_t i = 0; i < num_vectors; ++i) { + new_ids[i] = num_vectors + i; + } + CATCH_REQUIRE(svs_index_dynamic_add_points( + index, new_data.data(), new_ids.data(), num_vectors, nullptr, error + )); + // Deleting every original point makes consolidate log "Replacing entry point.". + CATCH_REQUIRE( + svs_index_dynamic_delete_points(index, ids.data(), num_vectors, nullptr, error) + ); + CATCH_REQUIRE(svs_index_dynamic_consolidate(index, error)); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(recorder.contains(SVS_LOG_LEVEL_DEBUG, "Replacing entry point")); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_logger_free(logger); + svs_error_free(error); + } +} + +namespace { + +constexpr size_t kBuilderLoggerNumVectors = 100; +constexpr size_t kBuilderLoggerDimension = 16; + +// Creates a logger handle that forwards every message (TRACE and up) to `recorder`. +svs_logger_h make_recording_logger(LogRecorder& recorder, svs_error_h error) { + // The callback sink copies the function pointer and `self`, so `ops` may be local. + svs_logging_ops_t ops = SVS_INIT_LOGGING_OPS(record_log); + svs_logging_t user_logger = {&ops, &recorder}; + svs_logger_h logger = svs_logger_create_custom(&user_logger, error); + CATCH_REQUIRE(logger != nullptr); + CATCH_REQUIRE(svs_logger_set_level(logger, SVS_LOG_LEVEL_TRACE, error)); + return logger; +} + +// Owns an algorithm and an index builder for small Vamana test indexes. +struct BuilderFixture { + svs_error_h error = svs_error_create(); + svs_algorithm_h algorithm = nullptr; + svs_index_builder_h builder = nullptr; + std::vector data; + + BuilderFixture() { + generate_test_data(data, kBuilderLoggerNumVectors, kBuilderLoggerDimension); + algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); + builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, kBuilderLoggerDimension, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + CATCH_REQUIRE( + svs_index_builder_set_threadpool(builder, SVS_THREADPOOL_KIND_NATIVE, 2, error) + ); + } + BuilderFixture(const BuilderFixture&) = delete; + BuilderFixture& operator=(const BuilderFixture&) = delete; + ~BuilderFixture() { + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + svs_index_h build() { + svs_index_h index = + svs_index_build(builder, data.data(), kBuilderLoggerNumVectors, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + return index; + } + + svs_index_h build_dynamic() { + svs_index_h index = svs_index_build_dynamic( + builder, data.data(), nullptr, kBuilderLoggerNumVectors, 0, error + ); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + return index; + } +}; + +size_t message_count(LogRecorder& recorder) { + std::lock_guard lock{recorder.mutex}; + return recorder.messages.size(); +} + +void clear_messages(LogRecorder& recorder) { + std::lock_guard lock{recorder.mutex}; + recorder.messages.clear(); +} + +} // namespace + +CATCH_TEST_CASE("C API Index Builder Logger", "[c_api][logging]") { + CATCH_SECTION("Two Builders Two Loggers") { + LogRecorder global_recorder; // Declared first: must outlive the guard. + LogRecorder recorder_a; + LogRecorder recorder_b; + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + + svs_logger_h global_logger = make_recording_logger(global_recorder, error); + CATCH_REQUIRE(svs_set_default_logger(global_logger, error)); + svs_logger_h logger_a = make_recording_logger(recorder_a, error); + svs_logger_h logger_b = make_recording_logger(recorder_b, error); + + { + BuilderFixture fixture_a; + BuilderFixture fixture_b; + CATCH_REQUIRE(svs_index_builder_set_logger(fixture_a.builder, logger_a, error)); + CATCH_REQUIRE(svs_index_builder_set_logger(fixture_b.builder, logger_b, error)); + CATCH_REQUIRE(svs_error_ok(error)); + + // Build A alone first: only logger A may receive anything. + svs_index_h index_a = fixture_a.build(); + CATCH_REQUIRE(recorder_a.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + CATCH_REQUIRE(message_count(recorder_b) == 0); + CATCH_REQUIRE(message_count(global_recorder) == 0); + + const size_t count_a = message_count(recorder_a); + svs_index_h index_b = fixture_b.build(); + CATCH_REQUIRE(recorder_b.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + CATCH_REQUIRE(message_count(recorder_a) == count_a); + CATCH_REQUIRE(message_count(global_recorder) == 0); + + svs_index_free(index_a); + svs_index_free(index_b); + } + + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_logger_free(logger_a); + svs_logger_free(logger_b); + svs_logger_free(global_logger); + svs_error_free(error); + } + + CATCH_SECTION("Builder Without Logger Uses Global") { + LogRecorder global_recorder; // Declared first: must outlive the guard. + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + svs_logger_h global_logger = make_recording_logger(global_recorder, error); + CATCH_REQUIRE(svs_set_default_logger(global_logger, error)); + + { + BuilderFixture fixture; + svs_index_free(fixture.build()); + } + CATCH_REQUIRE(global_recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_logger_free(global_logger); + svs_error_free(error); + } + + CATCH_SECTION("Dynamic Index Build And Load") { + LogRecorder global_recorder; // Declared first: must outlive the guard. + LogRecorder build_recorder; + LogRecorder load_recorder; + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + svs_logger_h global_logger = make_recording_logger(global_recorder, error); + CATCH_REQUIRE(svs_set_default_logger(global_logger, error)); + svs_logger_h build_logger = make_recording_logger(build_recorder, error); + svs_logger_h load_logger = make_recording_logger(load_recorder, error); + + TempDir tmp; + const std::string dir = tmp.string(); + + std::vector new_points; + generate_test_data(new_points, 50, kBuilderLoggerDimension); + std::vector new_ids(50); + for (size_t i = 0; i < new_ids.size(); ++i) { + new_ids[i] = kBuilderLoggerNumVectors + i; + } + + { + BuilderFixture fixture; + CATCH_REQUIRE(svs_index_builder_set_logger(fixture.builder, build_logger, error) + ); + svs_index_h index = fixture.build_dynamic(); + CATCH_REQUIRE(build_recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + + clear_messages(build_recorder); + CATCH_REQUIRE(svs_index_dynamic_add_points( + index, new_points.data(), new_ids.data(), 25, nullptr, error + )); + CATCH_REQUIRE(build_recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + + CATCH_REQUIRE(svs_index_save(index, dir.c_str(), error)); + svs_index_free(index); + } + + clear_messages(build_recorder); + { + BuilderFixture fixture; + CATCH_REQUIRE(svs_index_builder_set_logger(fixture.builder, load_logger, error) + ); + svs_index_h index = + svs_index_load_dynamic(fixture.builder, dir.c_str(), 0, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + CATCH_REQUIRE(svs_index_dynamic_add_points( + index, + new_points.data() + 25 * kBuilderLoggerDimension, + new_ids.data() + 25, + 25, + nullptr, + error + )); + CATCH_REQUIRE(load_recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + + // Delete every original point so the entry point is deleted and consolidate + // logs "Replacing entry point." at DEBUG level. + std::vector old_ids(kBuilderLoggerNumVectors); + for (size_t i = 0; i < old_ids.size(); ++i) { + old_ids[i] = i; + } + CATCH_REQUIRE(svs_index_dynamic_delete_points( + index, old_ids.data(), old_ids.size(), nullptr, error + )); + CATCH_REQUIRE(svs_index_dynamic_consolidate(index, error)); + CATCH_REQUIRE( + load_recorder.contains(SVS_LOG_LEVEL_DEBUG, "Replacing entry point") + ); + svs_index_free(index); + } + + CATCH_REQUIRE(message_count(build_recorder) == 0); + CATCH_REQUIRE(!global_recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + CATCH_REQUIRE( + !global_recorder.contains(SVS_LOG_LEVEL_DEBUG, "Replacing entry point") + ); + + svs_logger_free(build_logger); + svs_logger_free(load_logger); + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_logger_free(global_logger); + svs_error_free(error); + } + + CATCH_SECTION("Static Save And Load With Logger") { + LogRecorder global_recorder; // Declared first: must outlive the guard. + LogRecorder build_recorder; + LogRecorder load_recorder; + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + svs_logger_h global_logger = make_recording_logger(global_recorder, error); + CATCH_REQUIRE(svs_set_default_logger(global_logger, error)); + svs_logger_h build_logger = make_recording_logger(build_recorder, error); + svs_logger_h load_logger = make_recording_logger(load_recorder, error); + + TempDir tmp; + const std::string dir = tmp.string(); + { + BuilderFixture fixture; + CATCH_REQUIRE(svs_index_builder_set_logger(fixture.builder, build_logger, error) + ); + svs_index_h index = fixture.build(); + CATCH_REQUIRE(svs_index_save(index, dir.c_str(), error)); + svs_index_free(index); + } + clear_messages(build_recorder); + clear_messages(global_recorder); + { + BuilderFixture fixture; + CATCH_REQUIRE(svs_index_builder_set_logger(fixture.builder, load_logger, error) + ); + svs_index_h index = svs_index_load(fixture.builder, dir.c_str(), error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + svs_index_free(index); + } + // Static assemble may log nothing; no message may reach the other loggers. + CATCH_REQUIRE(message_count(build_recorder) == 0); + CATCH_REQUIRE(message_count(global_recorder) == 0); + + svs_logger_free(build_logger); + svs_logger_free(load_logger); + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_logger_free(global_logger); + svs_error_free(error); + } + + CATCH_SECTION("Converted Dynamic Index Uses Builder Logger") { + LogRecorder global_recorder; // Declared first: must outlive the guard. + LogRecorder src_recorder; + LogRecorder dst_recorder; + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + svs_logger_h global_logger = make_recording_logger(global_recorder, error); + CATCH_REQUIRE(svs_set_default_logger(global_logger, error)); + svs_logger_h src_logger = make_recording_logger(src_recorder, error); + svs_logger_h dst_logger = make_recording_logger(dst_recorder, error); + + std::vector new_points; + generate_test_data(new_points, 25, kBuilderLoggerDimension); + std::vector new_ids(25); + for (size_t i = 0; i < new_ids.size(); ++i) { + new_ids[i] = kBuilderLoggerNumVectors + i; + } + { + BuilderFixture src; + BuilderFixture dst; + CATCH_REQUIRE(svs_index_builder_set_logger(src.builder, src_logger, error)); + CATCH_REQUIRE(svs_index_builder_set_logger(dst.builder, dst_logger, error)); + svs_index_h src_index = src.build_dynamic(); + clear_messages(src_recorder); + + svs_index_h index = svs_index_convert_dynamic(dst.builder, src_index, 0, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(svs_index_dynamic_add_points( + index, new_points.data(), new_ids.data(), new_ids.size(), nullptr, error + )); + CATCH_REQUIRE(dst_recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + svs_index_free(index); + svs_index_free(src_index); + } + CATCH_REQUIRE(message_count(src_recorder) == 0); + CATCH_REQUIRE(message_count(global_recorder) == 0); + + svs_logger_free(src_logger); + svs_logger_free(dst_logger); + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_logger_free(global_logger); + svs_error_free(error); + } + + CATCH_SECTION("Synchronized Dynamic Index Uses Builder Logger") { + LogRecorder global_recorder; // Declared first: must outlive the guard. + LogRecorder recorder; + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + svs_logger_h global_logger = make_recording_logger(global_recorder, error); + CATCH_REQUIRE(svs_set_default_logger(global_logger, error)); + svs_logger_h logger = make_recording_logger(recorder, error); + { + BuilderFixture fixture; + CATCH_REQUIRE(svs_index_builder_set_logger(fixture.builder, logger, error)); + svs_dynamic_index_params_t params = SVS_INIT_DYNAMIC_INDEX_PARAMS(); + params.sync_kind = SVS_SYNC_KIND_GLOBAL; + svs_index_h index = svs_index_build_dynamic_ex( + fixture.builder, + fixture.data.data(), + nullptr, + kBuilderLoggerNumVectors, + ¶ms, + error + ); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + svs_index_free(index); + } + CATCH_REQUIRE(message_count(global_recorder) == 0); + + svs_logger_free(logger); + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_logger_free(global_logger); + svs_error_free(error); + } + + CATCH_SECTION("NULL Logger Clears And NULL Builder Fails") { + LogRecorder global_recorder; // Declared first: must outlive the guard. + LogRecorder builder_recorder; + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + svs_logger_h global_logger = make_recording_logger(global_recorder, error); + CATCH_REQUIRE(svs_set_default_logger(global_logger, error)); + svs_logger_h builder_logger = make_recording_logger(builder_recorder, error); + + { + BuilderFixture fixture; + CATCH_REQUIRE( + svs_index_builder_set_logger(fixture.builder, builder_logger, error) + ); + CATCH_REQUIRE(svs_index_builder_set_logger(fixture.builder, nullptr, error)); + CATCH_REQUIRE(svs_error_ok(error)); + svs_index_free(fixture.build()); + } + CATCH_REQUIRE(message_count(builder_recorder) == 0); + CATCH_REQUIRE(global_recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + + CATCH_REQUIRE( + svs_index_builder_set_logger(nullptr, builder_logger, error) == false + ); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + CATCH_REQUIRE(svs_index_builder_set_logger(nullptr, nullptr, error) == false); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + // NULL error handle is allowed. + CATCH_REQUIRE(svs_index_builder_set_logger(nullptr, nullptr, nullptr) == false); + + svs_logger_free(builder_logger); + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_logger_free(global_logger); + svs_error_free(error); + } + + CATCH_SECTION("Logger Handle Freed After Set") { + LogRecorder global_recorder; // Declared first: must outlive the guard. + LogRecorder recorder; + DefaultLoggerGuard guard; + svs_error_h error = svs_error_create(); + svs_logger_h global_logger = make_recording_logger(global_recorder, error); + CATCH_REQUIRE(svs_set_default_logger(global_logger, error)); + + { + BuilderFixture fixture; + svs_logger_h logger = make_recording_logger(recorder, error); + CATCH_REQUIRE(svs_index_builder_set_logger(fixture.builder, logger, error)); + svs_logger_free(logger); + svs_index_free(fixture.build()); + } + CATCH_REQUIRE(recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + CATCH_REQUIRE(!global_recorder.contains(SVS_LOG_LEVEL_TRACE, "Number of syncs")); + + CATCH_REQUIRE(svs_set_default_logger(nullptr, error)); + svs_logger_free(global_logger); + svs_error_free(error); + } +} diff --git a/bindings/c/tests/consumer/main.c b/bindings/c/tests/consumer/main.c index 07b41ebeb..9f61ac7c5 100644 --- a/bindings/c/tests/consumer/main.c +++ b/bindings/c/tests/consumer/main.c @@ -48,6 +48,33 @@ static void report(const char* name, svs_storage_h storage, svs_error_h error) { printf("%-24s %s (%s)\n", name, reason, svs_error_get_message(error)); } +/* Custom logging callback; counts the messages it receives. */ +static void count_log(void* self, enum svs_log_level level, const char* message) { + (void)level; + (void)message; + ++*(int*)self; +} + +/* Compile SVS_INIT_LOGGING_OPS as C and route a logger handle to a C callback. */ +static int check_logging(svs_error_h error) { + int count = 0; + svs_logging_ops_t ops = SVS_INIT_LOGGING_OPS(count_log); + svs_logging_t user_logger; + user_logger.ops = &ops; + user_logger.self = &count; + + svs_logger_h logger = svs_logger_create_custom(&user_logger, error); + if (logger == NULL) { + fprintf( + stderr, "failed to create a custom logger: %s\n", svs_error_get_message(error) + ); + return 0; + } + svs_logger_free(logger); + printf("%-24s available\n", "logging/custom"); + return 1; +} + int main(void) { svs_error_h error = svs_error_create(); if (error == NULL) { @@ -68,6 +95,11 @@ int main(void) { printf("%-24s available\n", "simple/float32"); svs_storage_free(simple); + if (!check_logging(error)) { + svs_error_free(error); + return EXIT_FAILURE; + } + report("sq/int8", svs_storage_create_sq(SVS_DATA_TYPE_INT8, error), error); report(