From 6a66619c709510ecb48e94d96e83f9368c83b967 Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Fri, 22 May 2026 22:00:38 -0400 Subject: [PATCH] Add multithreaded logging infrastructure for preparation for future designs (#225) * Update class diagrams * Implement BoundedChannel and multithreaded logging infra * Integrate logging channel system * Update string concatenations to use std::format * Add pretty print log --- docs/pipeline/diagrams/current/class.puml | 48 +++++ docs/pipeline/diagrams/planned/class.puml | 52 +++-- tooling/pipeline/CMakeLists.txt | 41 ++-- ...r.h => biergarten_pipeline_orchestrator.h} | 50 +++-- .../includes/concurrency/bounded_channel.h | 73 ++++++++ .../includes/concurrency/bounded_channel.tcc | 57 ++++++ .../data_generation/llama_generator.h | 4 +- tooling/pipeline/includes/data_model/models.h | 6 +- .../includes/json_handling/json_loader.h | 5 +- .../includes/json_handling/pretty_print.h | 109 +++++++++++ .../services/enrichment/wikipedia_service.h | 5 +- .../services/logging/log_dispatcher.h | 53 ++++++ .../includes/services/logging/log_entry.h | 88 +++++++++ .../includes/services/logging/log_producer.h | 53 ++++++ .../includes/services/logging/logger.h | 64 +++++++ .../services/prompting/prompt_directory.h | 6 + .../includes/web_client/http_web_client.h | 9 +- .../application_options/parse_arguments.cc | 98 +++++++--- .../generate_breweries.cc | 58 ------ .../biergarten_data_generator/log_results.cc | 26 --- .../query_cities_with_countries.cc | 41 ---- .../src/biergarten_data_generator/run.cc | 52 ----- .../biergarten_pipeline_orchestrator.cc} | 10 +- .../generate_breweries.cc | 68 +++++++ .../log_results.cc | 37 ++++ .../query_cities_with_countries.cc | 51 +++++ .../biergarten_pipeline_orchestrator/run.cc | 63 +++++++ .../data_generation/llama/generate_brewery.cc | 44 +++-- .../data_generation/llama/generate_user.cc | 5 +- .../src/data_generation/llama/helpers.cc | 81 ++++---- .../src/data_generation/llama/infer.cc | 18 +- .../data_generation/llama/llama_generator.cc | 2 + .../src/data_generation/llama/load.cc | 17 +- .../pipeline/src/json_handling/json_loader.cc | 20 +- tooling/pipeline/src/main.cc | 177 ++++++++++++++---- .../enrichment/wikipedia/fetch_extract.cc | 80 ++++++-- .../enrichment/wikipedia/get_summary.cc | 28 ++- .../enrichment/wikipedia/wikipedia_service.cc | 4 +- .../src/services/logging/log_dispatcher.cc | 74 ++++++++ .../src/services/logging/log_producer.cc | 19 ++ .../pipeline/src/services/prompt_directory.cc | 43 +++-- .../helpers/sqlite_connection_helpers.cc | 5 +- .../src/services/sqlite/initialize.cc | 5 +- .../src/web_client/http_web_client.cc | 35 ++-- 44 files changed, 1445 insertions(+), 439 deletions(-) rename tooling/pipeline/includes/{biergarten_data_generator.h => biergarten_pipeline_orchestrator.h} (52%) create mode 100644 tooling/pipeline/includes/concurrency/bounded_channel.h create mode 100644 tooling/pipeline/includes/concurrency/bounded_channel.tcc create mode 100644 tooling/pipeline/includes/json_handling/pretty_print.h create mode 100644 tooling/pipeline/includes/services/logging/log_dispatcher.h create mode 100644 tooling/pipeline/includes/services/logging/log_entry.h create mode 100644 tooling/pipeline/includes/services/logging/log_producer.h create mode 100644 tooling/pipeline/includes/services/logging/logger.h delete mode 100644 tooling/pipeline/src/biergarten_data_generator/generate_breweries.cc delete mode 100644 tooling/pipeline/src/biergarten_data_generator/log_results.cc delete mode 100644 tooling/pipeline/src/biergarten_data_generator/query_cities_with_countries.cc delete mode 100644 tooling/pipeline/src/biergarten_data_generator/run.cc rename tooling/pipeline/src/{biergarten_data_generator/biergarten_data_generator.cc => biergarten_pipeline_orchestrator/biergarten_pipeline_orchestrator.cc} (56%) create mode 100644 tooling/pipeline/src/biergarten_pipeline_orchestrator/generate_breweries.cc create mode 100644 tooling/pipeline/src/biergarten_pipeline_orchestrator/log_results.cc create mode 100644 tooling/pipeline/src/biergarten_pipeline_orchestrator/query_cities_with_countries.cc create mode 100644 tooling/pipeline/src/biergarten_pipeline_orchestrator/run.cc create mode 100644 tooling/pipeline/src/services/logging/log_dispatcher.cc create mode 100644 tooling/pipeline/src/services/logging/log_producer.cc diff --git a/docs/pipeline/diagrams/current/class.puml b/docs/pipeline/diagrams/current/class.puml index 76955e2..0bfb7f2 100644 --- a/docs/pipeline/diagrams/current/class.puml +++ b/docs/pipeline/diagrams/current/class.puml @@ -26,6 +26,7 @@ skinparam note { title The Biergarten Data Pipeline - Class Diagram class BiergartenDataGenerator { + - logger_ : std::shared_ptr - context_service_ : std::unique_ptr - generator_ : std::unique_ptr - exporter_ : std::unique_ptr @@ -36,6 +37,46 @@ class BiergartenDataGenerator { - LogResults() : void } +class LogLevel <> { + Debug + Info + Warn + Error +} + +class PipelinePhase <> { + Startup + UserGeneration + BreweryAndBeerGeneration + CheckinGeneration + RatingGeneration + FollowGeneration + Teardown +} + +struct LogEntry { + + timestamp : std::chrono::system_clock::time_point + + level : LogLevel + + phase : PipelinePhase + + message : std::string + + worker : std::optional +} + +interface ILogger <> { + + Log(entry : const LogEntry&) : void +} + +class LogProducer { + - channel_ : BoundedChannel& + + Log(entry : const LogEntry&) : void +} + +class LogDispatcher { + - channel_ : BoundedChannel& + + Run() : void + - ToSpdlogLevel(level) : spdlog::level::level_enum +} + interface IEnrichmentService <> { + GetLocationContext(loc : const Location&) : std::string } @@ -123,10 +164,17 @@ class SystemDateTimeProvider { } ' Structural Relationships / Dependency Injection +BiergartenDataGenerator *-- ILogger : owns BiergartenDataGenerator *-- IEnrichmentService : owns BiergartenDataGenerator *-- DataGenerator : owns BiergartenDataGenerator *-- IExportService : owns +LogEntry *-- LogLevel +LogEntry *-- PipelinePhase +ILogger <|.. LogProducer : implements +LogProducer ..> LogEntry : emits +LogDispatcher ..> LogEntry : consumes + IEnrichmentService <|.. WikipediaService : implements WikipediaService *-- WebClient : owns diff --git a/docs/pipeline/diagrams/planned/class.puml b/docs/pipeline/diagrams/planned/class.puml index ad1a819..0ad1e8b 100644 --- a/docs/pipeline/diagrams/planned/class.puml +++ b/docs/pipeline/diagrams/planned/class.puml @@ -1,4 +1,4 @@ -@startuml +@startuml class_diagram ' ========================================== ' CONFIGURATION & STYLING @@ -8,6 +8,8 @@ skinparam classAttributeFontSize 9 skinparam defaultFontSize 25 skinparam titleFontSize 30 +title Biergarten Data Pipeline — Class Diagram + package "Domain: Models" { class Location { @@ -141,7 +143,7 @@ package "Domain: Models" { LocationContext *-- Completeness } -@startuml + package "Domain: Application Configuration" { class SamplingOptions { + temperature: float = 1.0F @@ -167,12 +169,10 @@ package "Domain: Application Configuration" { + pipeline: PipelineOptions } - ' --- Domain Model Relationships --- ApplicationOptions *-- GeneratorOptions ApplicationOptions *-- PipelineOptions GeneratorOptions o-- SamplingOptions } -@endum package "Domain: Policy" { @@ -275,33 +275,29 @@ package "Infrastructure: Logging" { + level : LogLevel + phase : PipelinePhase + message : std::string - + city : std::optional - + entity_id : std::optional + worker : std::optional } - interface Logger <> { - + Log(level, phase, message,\n city, entity_id, worker) : void + interface ILogger <> { + + Log(entry : const LogEntry&) : void } - class PipelineLogger { - - log_ch_ : BoundedChannel& - + Log(level, phase, message,\n city, entity_id, worker) : void + class LogProducer { + - channel_ : BoundedChannel& + + Log(entry : const LogEntry&) : void } - class LogWorker { - - log_ch_ : BoundedChannel& + class LogDispatcher { + - channel_ : BoundedChannel& + Run() : void - - FormatTimestamp(tp) : std::string - ToSpdlogLevel(level) : spdlog::level::level_enum - - ToString(phase) : std::string } - ' --- Logging Relationships --- LogEntry *-- LogLevel LogEntry *-- PipelinePhase - PipelineLogger ..> LogEntry : emits - LogWorker ..> LogEntry : consumes + ILogger <|.. LogProducer + LogProducer ..> LogEntry : emits + LogDispatcher ..> LogEntry : consumes } package "Infrastructure: Pipeline Channel" { @@ -363,6 +359,22 @@ package "Infrastructure: Enrichment" { } +package "Infrastructure: Prompting" { + + interface IPromptDirectory <> { + + Load(key : std::string_view) : std::string + } + + class PromptDirectory { + - prompt_dir_ : std::filesystem::path + - cache_ : std::unordered_map + + PromptDirectory(prompt_dir : const std::filesystem::path&) + + Load(key : std::string_view) : std::string + } + + IPromptDirectory <|.. PromptDirectory +} + package "Infrastructure: Data Generation" { interface DataGenerator <> { @@ -386,6 +398,7 @@ package "Infrastructure: Data Generation" { - model_ : ModelHandle - context_ : ContextHandle - prompt_formatter_ : std::unique_ptr + - prompt_directory_ : std::unique_ptr - rng_ : std::mt19937 + GenerateBrewery(...) : BreweryResult + GenerateBeer(...) : BeerResult @@ -459,8 +472,6 @@ package "Infrastructure: Data Export" { } - - class BiergartenPipelineOrchestrator { - preloader_ : std::unique_ptr - enrichment_service_ : std::unique_ptr @@ -531,6 +542,7 @@ DateTimeProvider <|.. SystemDateTimeProvider WikipediaService *-- WebClient WikipediaService ..> ContextStrategy LlamaGenerator *-- PromptFormatter +LlamaGenerator *-- IPromptDirectory LlamaGenerator ..> GeneratorOptions SqliteExportService *-- DateTimeProvider diff --git a/tooling/pipeline/CMakeLists.txt b/tooling/pipeline/CMakeLists.txt index abdf592..b31ce3e 100644 --- a/tooling/pipeline/CMakeLists.txt +++ b/tooling/pipeline/CMakeLists.txt @@ -138,7 +138,8 @@ FetchContent_MakeAvailable(cpp-httplib) # 5. Executable & Sources add_executable(${PROJECT_NAME} - includes/services/enrichment/mock_enrichment.h) + includes/services/enrichment/mock_enrichment.h + includes/json_handling/pretty_print.h) # --- Entry point --- target_sources(${PROJECT_NAME} PRIVATE @@ -155,13 +156,13 @@ target_sources(${PROJECT_NAME} PRIVATE src/application_options/parse_arguments.cc ) -# --- biergarten_data_generator --- +# --- biergarten_pipeline_orchestrator --- target_sources(${PROJECT_NAME} PRIVATE - src/biergarten_data_generator/log_results.cc - src/biergarten_data_generator/biergarten_data_generator.cc - src/biergarten_data_generator/generate_breweries.cc - src/biergarten_data_generator/run.cc - src/biergarten_data_generator/query_cities_with_countries.cc + src/biergarten_pipeline_orchestrator/log_results.cc + src/biergarten_pipeline_orchestrator/biergarten_pipeline_orchestrator.cc + src/biergarten_pipeline_orchestrator/generate_breweries.cc + src/biergarten_pipeline_orchestrator/run.cc + src/biergarten_pipeline_orchestrator/query_cities_with_countries.cc ) # --- web_client --- @@ -202,17 +203,23 @@ target_sources(${PROJECT_NAME} PRIVATE # --- services: sqlite --- target_sources(${PROJECT_NAME} PRIVATE - src/services/sqlite/process_record.cc - src/services/sqlite/sqlite_export_service.cc - src/services/sqlite/finalize.cc - src/services/sqlite/initialize.cc - src/services/sqlite/helpers/sqlite_connection_helpers.cc - src/services/sqlite/helpers/sqlite_statement_helpers.cc + src/services/sqlite/process_record.cc + src/services/sqlite/sqlite_export_service.cc + src/services/sqlite/finalize.cc + src/services/sqlite/initialize.cc + src/services/sqlite/helpers/sqlite_connection_helpers.cc + src/services/sqlite/helpers/sqlite_statement_helpers.cc +) + +# --- services: logging --- +target_sources(${PROJECT_NAME} PRIVATE + "src/services/logging/log_producer.cc" + src/services/logging/log_dispatcher.cc ) # --- services (top-level) --- target_sources(${PROJECT_NAME} PRIVATE - src/services/prompt_directory.cc + src/services/prompt_directory.cc ) # 6. Include Directories, Link Libraries & Compile Definitions @@ -242,6 +249,11 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE $<$:DEBUG> ) +target_compile_options(biergarten-pipeline PRIVATE + -fmacro-prefix-map=${CMAKE_SOURCE_DIR}/tooling/pipeline/src/= +) + + # 7. Runtime Assets configure_file( ${CMAKE_SOURCE_DIR}/locations.json @@ -253,3 +265,4 @@ add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD ${CMAKE_SOURCE_DIR}/prompts ${CMAKE_BINARY_DIR}/prompts ) + diff --git a/tooling/pipeline/includes/biergarten_data_generator.h b/tooling/pipeline/includes/biergarten_pipeline_orchestrator.h similarity index 52% rename from tooling/pipeline/includes/biergarten_data_generator.h rename to tooling/pipeline/includes/biergarten_pipeline_orchestrator.h index e74ba02..514d843 100644 --- a/tooling/pipeline/includes/biergarten_data_generator.h +++ b/tooling/pipeline/includes/biergarten_pipeline_orchestrator.h @@ -3,7 +3,10 @@ /** * @file biergarten_data_generator.h - * @brief Core orchestration class for pipeline data generation. + * @brief Orchestration for end-to-end brewery data generation pipeline. + * + * Intent: Coordinates location loading, enrichment, and generation phases + * to produce a complete dataset. Coordinates dependencies via composition root. */ #include @@ -15,25 +18,30 @@ #include "services/database/export_service.h" #include "services/enrichment/enrichment_service.h" +#include "services/logging/logger.h" + /** * @brief Main data generator class for the Biergarten pipeline. * * This class encapsulates the core logic for generating brewery data. * It handles location loading, city enrichment, and brewery generation. */ -class BiergartenDataGenerator { +class BiergartenPipelineOrchestrator { public: - /** - * @brief Construct a BiergartenDataGenerator with injected dependencies. - * - * @param context_service Context provider for sampled locations. - * @param generator Brewery and user data generator. - * @param exporter Storage backend for generated brewery data. - */ - BiergartenDataGenerator(std::unique_ptr context_service, - std::unique_ptr generator, - std::unique_ptr exporter, - const ApplicationOptions& application_options); +/** + * @brief Constructs the orchestrator with injected pipeline dependencies. + * + * @param context_service Provides regional context for locations. + * @param generator Implementation (Llama or Mock) for brewery/user generation. + * @param exporter Database backend for persisting generated records. + * @param application_options CLI configuration and paths. + */ + BiergartenPipelineOrchestrator( + std::shared_ptr logger, + std::unique_ptr context_service, + std::unique_ptr generator, + std::unique_ptr exporter, + const ApplicationOptions& application_options); /** * @brief Run the data generation pipeline. @@ -43,21 +51,31 @@ class BiergartenDataGenerator { * 2. Resolve context for each city using the injected context service * 3. Generate brewery data for sampled cities * + * @note STRUCTURAL CONCURRENCY REQUIREMENT: + * When transitioned to a multithreaded design, this method MUST structurally + * enforce that all deployed worker threads are joined before returning (e.g. + * by using std::jthread or a structured concurrency primitive). This ensures + * workers do not attempt to log to a closed channel during application teardown. + * * @return true if successful, false if not */ bool Run(); private: + /// @brief Logger instance for emitting pipeline messages. + std::shared_ptr logger_; + /// @brief Owning context provider dependency. std::unique_ptr context_service_; /// @brief Generator dependency selected in the composition root. std::unique_ptr generator_; - /// @brief Storage backend for generated brewery records. - std::unique_ptr exporter_; + /// @brief Storage backend for generated brewery records. + std::unique_ptr exporter_; - const ApplicationOptions application_options_; + /// @brief CLI configuration: paths, model settings, generation parameters. + ApplicationOptions application_options_; /** * @brief Load locations from JSON and sample cities. diff --git a/tooling/pipeline/includes/concurrency/bounded_channel.h b/tooling/pipeline/includes/concurrency/bounded_channel.h new file mode 100644 index 0000000..ff4fdc2 --- /dev/null +++ b/tooling/pipeline/includes/concurrency/bounded_channel.h @@ -0,0 +1,73 @@ +#ifndef BIERGARTEN_PIPELINE_INCLUDES_CONCURRENCY_BOUNDED_CHANNEL_H_ +#define BIERGARTEN_PIPELINE_INCLUDES_CONCURRENCY_BOUNDED_CHANNEL_H_ + +#include +#include +#include +#include +#include + +/** + * @file bounded_channel.h + * @brief Thread-safe, bounded multi-producer/multi-consumer synchronous channel. + * + * Intent: Enables asynchronous inter-thread communication with backpressure. + * Models a synchronous channel where producers/consumers block on capacity limits. + */ + +/** + * @class BoundedChannel + * @brief MPMC channel with fixed capacity and blocking semantics. + * + * Producers block when buffer is full; consumers block when empty. + * Close() unblocks all waiters and signals channel exhaustion. + */ +template +class BoundedChannel { + // ------------------------------------------------------------------------- + // Internal state — all access must be guarded by mutex_. + // ------------------------------------------------------------------------- + + std::queue queue_; + + std::mutex mutex_; + + std::condition_variable not_full_; + + std::condition_variable not_empty_; + + std::size_t capacity_; + + bool closed_ = false; + + public: + /** + * @brief Construct a bounded channel with the given capacity. + * @param capacity Maximum number of items the channel may hold. + */ + explicit BoundedChannel(std::size_t capacity) : capacity_(capacity) {} + + /** + * @brief Send an item into the channel. Blocks when the channel is full. + * @param item Move-only item to enqueue. + */ + void Send(T item); + + /** + * @brief Receive an item from the channel. Blocks when the channel is + * empty. + * @return std::optional containing the item, or std::nullopt when the + * channel is closed and drained. + */ + std::optional Receive(); + + /** + * @brief Close the channel and unblock all waiting threads. Idempotent. + */ + void Close(); +}; + +// Include the template implementation +#include "bounded_channel.tcc" + +#endif // BIERGARTEN_PIPELINE_INCLUDES_CONCURRENCY_BOUNDED_CHANNEL_H_ diff --git a/tooling/pipeline/includes/concurrency/bounded_channel.tcc b/tooling/pipeline/includes/concurrency/bounded_channel.tcc new file mode 100644 index 0000000..951bfbd --- /dev/null +++ b/tooling/pipeline/includes/concurrency/bounded_channel.tcc @@ -0,0 +1,57 @@ +#include "bounded_channel.h" + +template +void BoundedChannel::Send(T item) { + // Acquire exclusive ownership of the mutex; released automatically on scope exit. + std::unique_lock lock(mutex_); + + // Block until there is space in the queue or the channel has been closed. + // The predicate guards against spurious wakeups. + not_full_.wait(lock, [&] { return queue_.size() < capacity_ || closed_; }); + + // If the channel was closed while waiting, discard the item and return. + if (closed_) return; + + // Move the item into the queue to avoid an unnecessary copy. + queue_.push(std::move(item)); + + // Wake one blocked Receive() call to signal that data is now available. + not_empty_.notify_one(); +} + +template +std::optional BoundedChannel::Receive() { + // Acquire exclusive ownership of the mutex. + std::unique_lock lock(mutex_); + + // Block until the queue is non-empty or the channel has been closed. + // The predicate guards against spurious wakeups. + not_empty_.wait(lock, [&] { return !queue_.empty() || closed_; }); + + // If woken due to closure and no items remain, signal exhaustion via nullopt. + if (queue_.empty()) return std::nullopt; + + // Move the front item out of the queue to avoid an unnecessary copy. + T item = std::move(queue_.front()); + queue_.pop(); + + // Wake one blocked Send() call to signal that a slot has opened. + not_full_.notify_one(); + + return item; +} + +template +void BoundedChannel::Close() { + // Acquire exclusive ownership of the mutex to ensure visibility of the flag. + std::unique_lock lock(mutex_); + + // Mark the channel as closed; subsequent Send() calls will be dropped. + closed_ = true; + + // Wake all blocked Send() callers so they can observe the closed flag and exit. + not_full_.notify_all(); + + // Wake all blocked Receive() callers so they can drain remaining items or return nullopt. + not_empty_.notify_all(); +} \ No newline at end of file diff --git a/tooling/pipeline/includes/data_generation/llama_generator.h b/tooling/pipeline/includes/data_generation/llama_generator.h index 35c1340..ef33b0c 100644 --- a/tooling/pipeline/includes/data_generation/llama_generator.h +++ b/tooling/pipeline/includes/data_generation/llama_generator.h @@ -18,6 +18,7 @@ #include "data_generation/data_generator.h" #include "data_generation/prompt_formatting/prompt_formatter.h" #include "data_model/models.h" +#include "services/logging/logger.h" struct llama_model; struct llama_context; @@ -37,7 +38,7 @@ class LlamaGenerator final : public DataGenerator { * @param prompt_directory Directory service for loading named prompt files. */ LlamaGenerator(const ApplicationOptions& options, - const std::string& model_path, + const std::string& model_path, std::shared_ptr logger, std::unique_ptr prompt_formatter, std::unique_ptr prompt_directory); @@ -130,6 +131,7 @@ class LlamaGenerator final : public DataGenerator { std::mt19937 rng_; uint32_t n_ctx_ = kDefaultContextSize; int n_gpu_layers_ = 0; + std::shared_ptr logger_; std::unique_ptr prompt_formatter_; std::unique_ptr prompt_directory_; }; diff --git a/tooling/pipeline/includes/data_model/models.h b/tooling/pipeline/includes/data_model/models.h index 9346b01..40c0d62 100644 --- a/tooling/pipeline/includes/data_model/models.h +++ b/tooling/pipeline/includes/data_model/models.h @@ -10,11 +10,14 @@ #include #include #include +#include #include #include #include #include +class ILogger; + namespace prog_opts = boost::program_options; // ============================================================================ @@ -136,6 +139,7 @@ struct ApplicationOptions { // Function Declarations // ============================================================================ -std::optional ParseArguments(const int argc, char** argv); +std::optional ParseArguments(const int argc, char** argv, + std::shared_ptr logger = nullptr); #endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_MODELS_H_ diff --git a/tooling/pipeline/includes/json_handling/json_loader.h b/tooling/pipeline/includes/json_handling/json_loader.h index 6cf288f..3fc9bba 100644 --- a/tooling/pipeline/includes/json_handling/json_loader.h +++ b/tooling/pipeline/includes/json_handling/json_loader.h @@ -7,16 +7,19 @@ */ #include +#include #include #include "data_model/models.h" +#include "services/logging/logger.h" /// @brief Loads curated world locations from a JSON file into memory. class JsonLoader { public: /// @brief Parses a JSON array file and returns all location records. static std::vector LoadLocations( - const std::filesystem::path& filepath); + const std::filesystem::path& filepath, + std::shared_ptr logger = nullptr); }; #endif // BIERGARTEN_PIPELINE_INCLUDES_JSON_HANDLING_JSON_LOADER_H_ diff --git a/tooling/pipeline/includes/json_handling/pretty_print.h b/tooling/pipeline/includes/json_handling/pretty_print.h new file mode 100644 index 0000000..51e7486 --- /dev/null +++ b/tooling/pipeline/includes/json_handling/pretty_print.h @@ -0,0 +1,109 @@ +#ifndef BIERGARTEN_PIPELINE_INCLUDES_JSON_HANDLING_PRETTY_PRINT_H_ +#define BIERGARTEN_PIPELINE_INCLUDES_JSON_HANDLING_PRETTY_PRINT_H_ + +/** + * @file json_handling/pretty_print.h + * @brief Pretty-printing utilities for JSON values. + * + * Provides formatting capability for boost::json::value with indentation and + * readable output. Adapted from Boost JSON library examples. + */ + +#include +#include +#include + +/** + * @brief Pretty-prints a JSON value to an output stream with indentation. + * + * Recursively formats JSON objects and arrays with consistent 4-space + * indentation. Adapted from: + * https://raw.githubusercontent.com/boostorg/json/refs/heads/develop/example/pretty.cpp + * + * @param outstream Output stream to write formatted JSON. + * @param json_val JSON value to format. + * @param indent Optional indentation string (managed internally on first call). + */ +inline void PrettyPrint(std::ostream& outstream, + boost::json::value const& json_val, + std::string* indent = nullptr) { + std::string str; + if (indent == nullptr) { + indent = &str; + } + switch (json_val.kind()) { + case boost::json::kind::object: { + outstream << "{\n"; + indent->append(4, ' '); + auto const& obj = json_val.get_object(); + if (!obj.empty()) { + const auto* iter = obj.begin(); + for (;;) { + outstream << *indent << boost::json::serialize(iter->key()) << " : "; + PrettyPrint(outstream, iter->value(), indent); + iter = std::next(iter); + if (iter == obj.end()) { + break; + } + + outstream << ",\n"; + } + } + outstream << "\n"; + indent->resize(indent->size() - 4); + outstream << *indent << "}"; + break; + } + + case boost::json::kind::array: { + outstream << "[\n"; + indent->append(4, ' '); + auto const& arr = json_val.get_array(); + if (!arr.empty()) { + const auto* iter = arr.begin(); + for (;;) { + outstream << *indent; + PrettyPrint(outstream, *iter, indent); + iter = std::next(iter); + if (iter == arr.end()) { + break; + } + outstream << ",\n"; + } + } + outstream << "\n"; + indent->resize(indent->size() - 4); + outstream << *indent << "]"; + break; + } + + case boost::json::kind::string: { + outstream << serialize(json_val.get_string()); + break; + } + + case boost::json::kind::uint64: + case boost::json::kind::int64: + case boost::json::kind::double_: + outstream << json_val; + break; + + case boost::json::kind::bool_: + if (json_val.get_bool()) { + outstream << "true"; + } else { + outstream << "false"; + } + break; + + case boost::json::kind::null: + outstream << "null"; + break; + } + + if (indent->empty()) { + outstream << "\n"; + } +} + +#endif diff --git a/tooling/pipeline/includes/services/enrichment/wikipedia_service.h b/tooling/pipeline/includes/services/enrichment/wikipedia_service.h index c51ff28..82a97aa 100644 --- a/tooling/pipeline/includes/services/enrichment/wikipedia_service.h +++ b/tooling/pipeline/includes/services/enrichment/wikipedia_service.h @@ -12,13 +12,15 @@ #include #include "enrichment_service.h" +#include "services/logging/logger.h" #include "web_client/web_client.h" /// @brief Provides Wikipedia summary lookups backed by cached raw extracts. class WikipediaEnrichmentService final : public IEnrichmentService { public: /// @brief Creates a new Wikipedia service with the provided web client. - explicit WikipediaEnrichmentService(std::unique_ptr client); + explicit WikipediaEnrichmentService(std::unique_ptr client, + std::shared_ptr logger); /// @brief Returns the Wikipedia-derived context for a location. [[nodiscard]] std::string GetLocationContext(const Location& loc) override; @@ -26,6 +28,7 @@ class WikipediaEnrichmentService final : public IEnrichmentService { private: std::string FetchExtract(std::string_view query); std::unique_ptr client_; + std::shared_ptr logger_; /// @brief Canonical cache for raw Wikipedia query extracts. std::unordered_map extract_cache_; }; diff --git a/tooling/pipeline/includes/services/logging/log_dispatcher.h b/tooling/pipeline/includes/services/logging/log_dispatcher.h new file mode 100644 index 0000000..c2104bf --- /dev/null +++ b/tooling/pipeline/includes/services/logging/log_dispatcher.h @@ -0,0 +1,53 @@ +/** + * @file services/logging/log_dispatcher.h + * @brief Dedicated log dispatcher for asynchronous pipeline logging. + * + * The dispatcher drains LogEntry values from a bounded channel and forwards + * them to spdlog on a dedicated thread. + */ + +#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_LOGGING_LOG_DISPATCHER_H_ +#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_LOGGING_LOG_DISPATCHER_H_ + +#include + +#include "concurrency/bounded_channel.h" +#include "services/logging/log_entry.h" + +/** + * @class LogDispatcher + * @brief Consumes log entries from a channel and forwards them to spdlog. + * + * Non-copyable and non-movable. Intended to run on its own dedicated thread + * and exit once the channel has been closed and drained. + */ +class LogDispatcher { + public: + /** + * @brief Construct a log dispatcher. + * + * @param channel Reference to the bounded channel used for log retrieval. + */ + explicit LogDispatcher(BoundedChannel& channel); + + LogDispatcher(const LogDispatcher&) = delete; + LogDispatcher& operator=(const LogDispatcher&) = delete; + LogDispatcher(LogDispatcher&&) = delete; + LogDispatcher& operator=(LogDispatcher&&) = delete; + ~LogDispatcher() = default; + + /** + * @brief Drain the channel and forward entries to spdlog. + * + * Intended to be called once on a dedicated thread. The loop returns after + * the channel has been closed and all queued entries have been processed. + */ + void Run(); + + private: + BoundedChannel& channel_; + + static spdlog::level::level_enum ToSpdlogLevel(LogLevel level); +}; + +#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_LOGGING_LOG_DISPATCHER_H_ diff --git a/tooling/pipeline/includes/services/logging/log_entry.h b/tooling/pipeline/includes/services/logging/log_entry.h new file mode 100644 index 0000000..1227430 --- /dev/null +++ b/tooling/pipeline/includes/services/logging/log_entry.h @@ -0,0 +1,88 @@ +/** + * @file services/logging/log_entry.h + * @brief Structured log record shared by the pipeline logging infra. + * + * LogEntry is a lightweight value type that can be passed safely between the + * logging producer and dispatcher through BoundedChannel. + */ + +#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_LOGGING_LOG_ENTRY_H_ +#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_LOGGING_LOG_ENTRY_H_ + +#include +#include +#include +#include +#include + +/** + * @enum LogLevel + * @brief Severity levels supported by the logging infra. + */ +enum class LogLevel { + Debug, ///< Development/debugging information. + Info, ///< General informational messages. + Warn, ///< Warning conditions. + Error, ///< Error conditions. +}; + +/** + * @enum PipelinePhase + * @brief Pipeline execution phases used to tag log records. + * + * The phase tag makes it easier to correlate log output with the part of the + * pipeline that emitted it. + */ +enum class PipelinePhase { + Startup, ///< Initialization and validation. + UserGeneration, ///< User profile generation. + BreweryAndBeerGeneration, ///< Brewery and beer data generation. + CheckinGeneration, ///< Checkin (visit) record generation. + RatingGeneration, ///< Rating and review generation. + FollowGeneration, ///< Follow relationship generation. + Teardown, ///< Finalization and cleanup. +}; + +/** + * @struct LogDTO + * @brief User-provided subset of log fields. Used to capture call-site info transparently. + */ +struct LogDTO { + LogLevel level; + PipelinePhase phase; + std::string message; +}; + +/** + * @struct LogEntry + * @brief Single structured log event. + * + * All fields are value types, which keeps transfer across the bounded channel + * simple and avoids shared ownership. + * + * NOTE: timestamp, thread_id, and origin must be populated by ILogger::Log() + * before the entry is dispatched. + */ +struct LogEntry { + /// @brief Timestamp when the entry was created. + std::chrono::system_clock::time_point timestamp{}; + + /// @brief Source location where the log call was made. + std::source_location origin{}; + + /// @brief Thread responsible for emitting the log. + std::thread::id thread_id{}; + + + /// @brief Severity level of this entry. + LogLevel level; + + /// @brief Pipeline phase associated with the entry. + PipelinePhase phase; + + /// @brief Log message text. + std::string message; + +}; + +#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_LOGGING_LOG_ENTRY_H_ diff --git a/tooling/pipeline/includes/services/logging/log_producer.h b/tooling/pipeline/includes/services/logging/log_producer.h new file mode 100644 index 0000000..d032f89 --- /dev/null +++ b/tooling/pipeline/includes/services/logging/log_producer.h @@ -0,0 +1,53 @@ +/** + * @file services/logging/log_producer.h + * @brief Channel-backed log producer for asynchronous pipeline logging. + * + * The producer captures log records from application code and forwards them to + * a bounded channel for later processing by the dispatcher. + */ + +#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_LOGGING_CHANNEL_LOGGER_H_ +#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_LOGGING_CHANNEL_LOGGER_H_ + +#include + +#include "concurrency/bounded_channel.h" +#include "services/logging/log_entry.h" +#include "services/logging/logger.h" + +/** + * @class LogProducer + * @brief ILogger implementation that forwards entries to a bounded channel. + * + * Non-copyable and non-movable. The channel reference is non-owning and must + * remain valid for the lifetime of the producer. + */ +class LogProducer final : public ILogger { + public: + /** + * @brief Construct a channel-backed producer. + * + * @param channel Reference to the bounded channel used for log transfer. + */ + explicit LogProducer(BoundedChannel& channel); + + LogProducer(const LogProducer&) = delete; + LogProducer& operator=(const LogProducer&) = delete; + LogProducer(LogProducer&&) = delete; + LogProducer& operator=(LogProducer&&) = delete; + + ~LogProducer() override = default; + + /** + * @brief Queue a log message for asynchronous processing. + * + * Blocks while the channel applies backpressure. This blocking behavior + * under heavy load is an accepted trade-off for simplicity. + */ + void DoLog(LogEntry log_entry) override; + + private: + BoundedChannel& channel_; +}; + +#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_LOGGING_CHANNEL_LOGGER_H_ diff --git a/tooling/pipeline/includes/services/logging/logger.h b/tooling/pipeline/includes/services/logging/logger.h new file mode 100644 index 0000000..5171744 --- /dev/null +++ b/tooling/pipeline/includes/services/logging/logger.h @@ -0,0 +1,64 @@ +/** + * @file services/logging/logger.h + * @brief Abstract logging interface used by pipeline components. + * + * The interface keeps application code independent from the concrete logging + * transport, buffering, and formatting implementation. + */ + +#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_LOGGING_LOGGER_H_ +#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_LOGGING_LOGGER_H_ + +#include +#include +#include + +#include "services/logging/log_entry.h" + +/** + * @class ILogger + * @brief Minimal interface for submitting structured log messages. + * + * Implementations are non-copyable and non-movable. They are typically owned + * by the composition root and injected into services that emit diagnostics. + */ +class ILogger { + public: + ILogger() = default; + ILogger(const ILogger&) = delete; + ILogger& operator=(const ILogger&) = delete; + ILogger(ILogger&&) = delete; + ILogger& operator=(ILogger&&) = delete; + virtual ~ILogger() = default; + + /** + * @brief Submit a log message to the logging subsystem. + * + * @param payload User-provided log data (level, phase, message). + * @param origin Auto-captured source location of the call site. + */ + void Log(LogDTO payload, + std::source_location origin = std::source_location::current(), + std::chrono::system_clock::time_point timestamp = std::chrono::system_clock::now(), + std::thread::id thread_id = std::this_thread::get_id()) { + LogEntry entry; + entry.timestamp = timestamp; + entry.thread_id = thread_id; + entry.level = payload.level; + entry.phase = payload.phase; + entry.message = std::move(payload.message); + entry.origin = origin; + DoLog(std::move(entry)); + } + + protected: + /** + * @brief Underlying implementation to transport the log entry. + * + * Implementations must be thread-safe as DoLog can be called concurrently + * from multiple worker threads. + */ + virtual void DoLog(LogEntry log_entry) = 0; +}; + +#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_LOGGING_LOGGER_H_ diff --git a/tooling/pipeline/includes/services/prompting/prompt_directory.h b/tooling/pipeline/includes/services/prompting/prompt_directory.h index 8b2d4c6..973c715 100644 --- a/tooling/pipeline/includes/services/prompting/prompt_directory.h +++ b/tooling/pipeline/includes/services/prompting/prompt_directory.h @@ -12,11 +12,14 @@ */ #include +#include #include #include #include #include +#include "services/logging/logger.h" + /** * @brief Interface for loading named prompt files. */ @@ -56,6 +59,8 @@ class PromptDirectory final : public IPromptDirectory { * directory. */ explicit PromptDirectory(const std::filesystem::path& prompt_dir); + PromptDirectory(const std::filesystem::path& prompt_dir, + std::shared_ptr logger); /** * @brief Loads the prompt for @p key, caching the result. @@ -70,6 +75,7 @@ class PromptDirectory final : public IPromptDirectory { private: std::filesystem::path prompt_dir_; + std::shared_ptr logger_; std::unordered_map cache_; }; diff --git a/tooling/pipeline/includes/web_client/http_web_client.h b/tooling/pipeline/includes/web_client/http_web_client.h index a38beba..803169a 100644 --- a/tooling/pipeline/includes/web_client/http_web_client.h +++ b/tooling/pipeline/includes/web_client/http_web_client.h @@ -8,8 +8,11 @@ #include "web_client/web_client.h" +#include "services/logging/logger.h" +#include #include +#include /** * @brief WebClient implementation backed by cpp-httplib. @@ -24,7 +27,8 @@ */ class HttpWebClient final : public WebClient { public: - HttpWebClient() = default; + explicit HttpWebClient(std::shared_ptr logger) + : logger_(std::move(logger)) {} ~HttpWebClient() override = default; /** @@ -43,6 +47,9 @@ public: * @return Percent-encoded string safe for use in a URL. */ std::string EncodeURL(const std::string& value) override; + + private: + std::shared_ptr logger_; }; diff --git a/tooling/pipeline/src/application_options/parse_arguments.cc b/tooling/pipeline/src/application_options/parse_arguments.cc index b2995d1..5e9ae84 100644 --- a/tooling/pipeline/src/application_options/parse_arguments.cc +++ b/tooling/pipeline/src/application_options/parse_arguments.cc @@ -1,12 +1,15 @@ -#include - +#include +#include +#include #include #include #include #include "data_model/models.h" +#include "services/logging/logger.h" -std::optional ParseArguments(const int argc, char** argv) { +std::optional ParseArguments( + const int argc, char** argv, std::shared_ptr logger) { prog_opts::options_description desc("Pipeline Options"); auto opt = desc.add_options(); @@ -61,10 +64,20 @@ std::optional ParseArguments(const int argc, char** argv) { // No flags provided — treat as a help request rather than an error. if (argc == 1) { - spdlog::info("Biergarten Pipeline"); - std::stringstream usage_stream; - usage_stream << "\nUsage: biergarten-pipeline [options]\n\n" << desc; - spdlog::info(usage_stream.str()); + const std::string title = "Biergarten Pipeline"; + const std::string usage = ([&] { + std::stringstream usage_stream; + usage_stream << "\nUsage: biergarten-pipeline [options]\n\n" << desc; + return usage_stream.str(); + })(); + if (logger) { + logger->Log(LogDTO{.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = title}); + logger->Log(LogDTO{.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = usage}); + } return std::nullopt; } @@ -76,7 +89,11 @@ std::optional ParseArguments(const int argc, char** argv) { if (var_map.contains("help")) { std::stringstream help_stream; help_stream << "\n" << desc; - spdlog::info(help_stream.str()); + if (logger) { + logger->Log(LogDTO{.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = help_stream.str()}); + } return std::nullopt; } @@ -85,8 +102,7 @@ std::optional ParseArguments(const int argc, char** argv) { options.pipeline.output_path = var_map["output"].as(); options.pipeline.log_path = var_map["log-path"].as(); options.pipeline.prompt_dir = var_map["prompt-dir"].as(); - options.pipeline.location_count = - var_map["location-count"].as(); + options.pipeline.location_count = var_map["location-count"].as(); const bool use_mocked = var_map["mocked"].as(); const std::string model_path = var_map["model"].as(); @@ -94,23 +110,43 @@ std::optional ParseArguments(const int argc, char** argv) { // Enforce mutual exclusivity before any further configuration is applied. if (use_mocked && !model_path.empty()) { - spdlog::error( - "Invalid arguments: --mocked and --model are mutually exclusive"); + const std::string msg = + "Invalid arguments: --mocked and --model are mutually exclusive"; + if (logger) { + logger->Log(LogDTO{.level = LogLevel::Error, + .phase = PipelinePhase::Startup, + .message = msg}); + } else { + std::cerr << msg << std::endl; + } return std::nullopt; } if (!use_mocked && model_path.empty()) { - spdlog::error( - "Invalid arguments: either --mocked or --model must be specified"); + const std::string msg = + "Invalid arguments: either --mocked or --model must be specified"; + if (logger) { + logger->Log(LogDTO{.level = LogLevel::Error, + .phase = PipelinePhase::Startup, + .message = msg}); + } else { + std::cerr << msg << std::endl; + } return std::nullopt; } // Prompt directory is only meaningful for live inference — the mock // generator has no use for it and should not require it to be present. if (!use_mocked && options.pipeline.prompt_dir.empty()) { - spdlog::error( - "Invalid arguments: --prompt-dir is required when not using " - "--mocked"); + const std::string msg = + "Invalid arguments: --prompt-dir is required when not using --mocked"; + if (logger) { + logger->Log({.level = LogLevel::Error, + .phase = PipelinePhase::Startup, + .message = msg}); + } else { + std::cerr << msg << std::endl; + } return std::nullopt; } @@ -131,7 +167,15 @@ std::optional ParseArguments(const int argc, char** argv) { // Warn but do not fail — the run is still valid, the flags are just // silently irrelevant when no model is loaded. if (use_mocked) { - spdlog::warn("Sampling parameters are ignored when using --mocked"); + const std::string msg = + "Sampling parameters are ignored when using --mocked"; + if (logger) { + logger->Log(LogDTO{.level = LogLevel::Warn, + .phase = PipelinePhase::Startup, + .message = msg}); + } else { + std::cerr << msg << std::endl; + } } else { SamplingOptions sampling; sampling.temperature = var_map["temperature"].as(); @@ -148,11 +192,23 @@ std::optional ParseArguments(const int argc, char** argv) { return options; } catch (const std::exception& exception) { - spdlog::error("Failed to parse command-line arguments: {}", - exception.what()); + const std::string msg = + std::string("Failed to parse command-line arguments: ") + + exception.what(); + if (logger) { + logger->Log(LogDTO{.level = LogLevel::Error, + .phase = PipelinePhase::Startup, + .message = msg}); + } return std::nullopt; } catch (...) { - spdlog::error("Failed to parse command-line arguments: unknown error"); + const std::string msg = + "Failed to parse command-line arguments: unknown error"; + if (logger) { + logger->Log(LogDTO{.level = LogLevel::Error, + .phase = PipelinePhase::Startup, + .message = msg}); + } return std::nullopt; } } diff --git a/tooling/pipeline/src/biergarten_data_generator/generate_breweries.cc b/tooling/pipeline/src/biergarten_data_generator/generate_breweries.cc deleted file mode 100644 index 934c3dd..0000000 --- a/tooling/pipeline/src/biergarten_data_generator/generate_breweries.cc +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @file biergarten_data_generator/generate_breweries.cc - * @brief BiergartenDataGenerator::GenerateBreweries() implementation. - */ - -#include - -#include "biergarten_data_generator.h" - -void BiergartenDataGenerator::GenerateBreweries( - std::span cities) { - spdlog::info("\n=== SAMPLE BREWERY GENERATION ==="); - - generated_breweries_.clear(); - size_t skipped_count = 0; - size_t export_failed_count = 0; - - for (const auto& [location, region_context] : cities) { - try { - const BreweryResult brewery = - generator_->GenerateBrewery(location, region_context); - - const GeneratedBrewery gen{.location = location, .brewery = brewery}; - - generated_breweries_.push_back(gen); - - try { - exporter_->ProcessRecord(gen); - } catch (const std::exception& export_exception) { - ++export_failed_count; - - spdlog::warn( - "[Pipeline] Generated brewery for '{}' ({}) but SQLite export " - "failed: {}", - location.city, location.country, export_exception.what()); - } - } catch (const std::exception& e) { - ++skipped_count; - - spdlog::warn( - "[Pipeline] Skipping city '{}' ({}): brewery generation failed: " - "{}", - location.city, location.country, e.what()); - } - } - - if (skipped_count > 0) { - spdlog::warn("[Pipeline] Skipped {} city/cities due to generation errors", - skipped_count); - } - - if (export_failed_count > 0) { - spdlog::warn( - "[Pipeline] Failed to export {} generated brewery/breweries to " - "SQLite", - export_failed_count); - } -} diff --git a/tooling/pipeline/src/biergarten_data_generator/log_results.cc b/tooling/pipeline/src/biergarten_data_generator/log_results.cc deleted file mode 100644 index 975729e..0000000 --- a/tooling/pipeline/src/biergarten_data_generator/log_results.cc +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @file biergarten_data_generator/log_results.cc - * @brief BiergartenDataGenerator::LogResults() implementation. - */ - -#include - -#include "biergarten_data_generator.h" - -void BiergartenDataGenerator::LogResults() const { - spdlog::info("\n=== GENERATED DATA DUMP ==="); - size_t index = 1; - for (const auto& [location, brewery] : generated_breweries_) { - spdlog::info( - "{}. city=\"{}\" country=\"{}\" state=\"{}\" " - "iso3166_2={} lat={} lon={}", - index, location.city, location.country, location.state_province, - location.iso3166_2, location.latitude, location.longitude); - spdlog::info(" brewery_name_en=\"{}\"", brewery.name_en); - spdlog::info(" brewery_description_en=\"{}\"", brewery.description_en); - spdlog::info(" brewery_name_local=\"{}\"", brewery.name_local); - spdlog::info(" brewery_description_local=\"{}\"", - brewery.description_local); - ++index; - } -} diff --git a/tooling/pipeline/src/biergarten_data_generator/query_cities_with_countries.cc b/tooling/pipeline/src/biergarten_data_generator/query_cities_with_countries.cc deleted file mode 100644 index c17654f..0000000 --- a/tooling/pipeline/src/biergarten_data_generator/query_cities_with_countries.cc +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @file biergarten_data_generator/query_cities_with_countries.cc - * @brief BiergartenDataGenerator::QueryCitiesWithCountries() implementation. - */ - -#include - -#include -#include -#include -#include - -#include "biergarten_data_generator.h" -#include "json_handling/json_loader.h" - -std::vector BiergartenDataGenerator::QueryCitiesWithCountries() { - spdlog::info("\n=== GEOGRAPHIC DATA OVERVIEW ==="); - - const std::filesystem::path locations_path = "locations.json"; - - auto all_locations = JsonLoader::LoadLocations(locations_path); - spdlog::info(" Locations available: {}", all_locations.size()); - - const size_t sample_count = std::min( - static_cast(application_options_.pipeline.location_count), - all_locations.size()); - - const auto sample_count_signed = - static_cast>( - sample_count); - - std::vector sampled_locations; - sampled_locations.reserve(sample_count); - - std::random_device random_generator; - std::ranges::sample(all_locations, std::back_inserter(sampled_locations), - sample_count_signed, random_generator); - - spdlog::info(" Sampled locations: {}", sampled_locations.size()); - return sampled_locations; -} diff --git a/tooling/pipeline/src/biergarten_data_generator/run.cc b/tooling/pipeline/src/biergarten_data_generator/run.cc deleted file mode 100644 index 4ee2b46..0000000 --- a/tooling/pipeline/src/biergarten_data_generator/run.cc +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @file biergarten_data_generator/run.cc - * @brief BiergartenDataGenerator::Run() implementation. - */ - -#include - -#include - -#include "biergarten_data_generator.h" - -bool BiergartenDataGenerator::Run() { - try { - exporter_->Initialize(); - - std::vector cities = QueryCitiesWithCountries(); - std::vector enriched; - enriched.reserve(cities.size()); - - size_t skipped_count = 0; - for (auto& city : cities) { - try { - std::string region_context = context_service_->GetLocationContext(city); - // spdlog::debug("[Pipeline] Context for '{}' ({}) gathered:\n{}", - // city.city, city.iso3166_2, region_context); - - enriched.push_back( - EnrichedCity{.location = std::move(city), - .region_context = std::move(region_context)}); - } catch (const std::exception& exception) { - ++skipped_count; - spdlog::warn( - "[Pipeline] Skipping city '{}' ({}): context lookup failed: {}", - city.city, city.country, exception.what()); - } - } - - if (skipped_count > 0) { - spdlog::warn( - "[Pipeline] Skipped {} city/cities due to context lookup errors", - skipped_count); - } - - this->GenerateBreweries(enriched); - exporter_->Finalize(); - this->LogResults(); - return true; - } catch (const std::exception& e) { - spdlog::error("Pipeline execution failed with error: {}", e.what()); - return false; - } -} diff --git a/tooling/pipeline/src/biergarten_data_generator/biergarten_data_generator.cc b/tooling/pipeline/src/biergarten_pipeline_orchestrator/biergarten_pipeline_orchestrator.cc similarity index 56% rename from tooling/pipeline/src/biergarten_data_generator/biergarten_data_generator.cc rename to tooling/pipeline/src/biergarten_pipeline_orchestrator/biergarten_pipeline_orchestrator.cc index 71875b3..31a158e 100644 --- a/tooling/pipeline/src/biergarten_data_generator/biergarten_data_generator.cc +++ b/tooling/pipeline/src/biergarten_pipeline_orchestrator/biergarten_pipeline_orchestrator.cc @@ -1,18 +1,20 @@ /** - * @file biergarten_data_generator/biergarten_data_generator.cc + * @file biergarten_pipeline_orchestrator/biergarten_pipeline_orchestrator.cc * @brief BiergartenDataGenerator constructor implementation. */ -#include "biergarten_data_generator.h" +#include "biergarten_pipeline_orchestrator.h" #include -BiergartenDataGenerator::BiergartenDataGenerator( +BiergartenPipelineOrchestrator::BiergartenPipelineOrchestrator( + std::shared_ptr logger, std::unique_ptr context_service, std::unique_ptr generator, std::unique_ptr exporter, const ApplicationOptions &app_options) - : context_service_(std::move(context_service)), + : logger_(std::move(logger)), + context_service_(std::move(context_service)), generator_(std::move(generator)), exporter_(std::move(exporter)), application_options_(app_options) {} diff --git a/tooling/pipeline/src/biergarten_pipeline_orchestrator/generate_breweries.cc b/tooling/pipeline/src/biergarten_pipeline_orchestrator/generate_breweries.cc new file mode 100644 index 0000000..09dfa82 --- /dev/null +++ b/tooling/pipeline/src/biergarten_pipeline_orchestrator/generate_breweries.cc @@ -0,0 +1,68 @@ +/** + * @file biergarten_pipeline_orchestrator/generate_breweries.cc + * @brief BiergartenDataGenerator::GenerateBreweries() implementation. + */ + +#include +#include + +#include "biergarten_pipeline_orchestrator.h" +#include "services/logging/logger.h" + +void BiergartenPipelineOrchestrator::GenerateBreweries( + std::span cities) { + logger_->Log({.level = LogLevel::Info, + .phase = PipelinePhase::BreweryAndBeerGeneration, + .message = "=== SAMPLE BREWERY GENERATION ==="}); + + generated_breweries_.clear(); + size_t skipped_count = 0; + size_t export_failed_count = 0; + + for (const auto& [location, region_context] : cities) { + try { + const BreweryResult brewery = + generator_->GenerateBrewery(location, region_context); + + const GeneratedBrewery gen{.location = location, .brewery = brewery}; + + generated_breweries_.push_back(gen); + + try { + exporter_->ProcessRecord(gen); + } catch (const std::exception& export_exception) { + ++export_failed_count; + + logger_->Log( + {.level = LogLevel::Warn, + .phase = PipelinePhase::BreweryAndBeerGeneration, + .message = + std::format("[Pipeline] Generated brewery for '{}' ({}) but SQLite export failed: {}", + location.city, location.country, export_exception.what())}); + } + } catch (const std::exception& e) { + ++skipped_count; + + logger_->Log({.level = LogLevel::Warn, + .phase = PipelinePhase::BreweryAndBeerGeneration, + .message = std::format("[Pipeline] Skipping city '{}' ({}): brewery generation failed: {}", + location.city, location.country, e.what())}); + } + } + + if (skipped_count > 0) { + logger_->Log({.level = LogLevel::Warn, + .phase = PipelinePhase::BreweryAndBeerGeneration, + .message = std::format( + "[Pipeline] Skipped {} city/cities due to generation errors", + skipped_count)}); + } + + if (export_failed_count > 0) { + logger_->Log({.level = LogLevel::Warn, + .phase = PipelinePhase::Teardown, + .message = std::format( + "[Pipeline] Failed to export {} generated brewery/breweries to SQLite", + export_failed_count)}); + } +} diff --git a/tooling/pipeline/src/biergarten_pipeline_orchestrator/log_results.cc b/tooling/pipeline/src/biergarten_pipeline_orchestrator/log_results.cc new file mode 100644 index 0000000..21db335 --- /dev/null +++ b/tooling/pipeline/src/biergarten_pipeline_orchestrator/log_results.cc @@ -0,0 +1,37 @@ +/** + * @file biergarten_pipeline_orchestrator/log_results.cc + * @brief BiergartenDataGenerator::LogResults() implementation. + */ + +#include +#include +#include + +#include "../../includes/json_handling/pretty_print.h" +#include "biergarten_pipeline_orchestrator.h" +#include "services/logging/logger.h" +void BiergartenPipelineOrchestrator::LogResults() const { + boost::json::array output; + + for (const auto& [location, brewery] : generated_breweries_) { + output.push_back(boost::json::object{ + {"name_en", brewery.name_en}, + {"description_en", brewery.description_en}, + {"name_local", brewery.name_local}, + {"description_local", brewery.description_local}, + {"location", boost::json::object{ + {"city", location.city}, + {"country", location.country}, + {"state_province", location.state_province}, + {"iso3166_2", location.iso3166_2}, + {"latitude", location.latitude}, + {"longitude", location.longitude}, + }}}); + } + + std::ostringstream oss; + PrettyPrint(oss, output); + logger_->Log({.level = LogLevel::Info, + .phase = PipelinePhase::Teardown, + .message = oss.str()}); +} diff --git a/tooling/pipeline/src/biergarten_pipeline_orchestrator/query_cities_with_countries.cc b/tooling/pipeline/src/biergarten_pipeline_orchestrator/query_cities_with_countries.cc new file mode 100644 index 0000000..6c1b62b --- /dev/null +++ b/tooling/pipeline/src/biergarten_pipeline_orchestrator/query_cities_with_countries.cc @@ -0,0 +1,51 @@ +/** + * @file biergarten_pipeline_orchestrator/query_cities_with_countries.cc + * @brief BiergartenDataGenerator::QueryCitiesWithCountries() implementation. + */ + +#include +#include +#include +#include +#include +#include + +#include "biergarten_pipeline_orchestrator.h" +#include "json_handling/json_loader.h" +#include "services/logging/logger.h" + +std::vector +BiergartenPipelineOrchestrator::QueryCitiesWithCountries() { + logger_->Log({.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = "=== GEOGRAPHIC DATA OVERVIEW ==="}); + + const std::filesystem::path locations_path = "locations.json"; + + auto all_locations = JsonLoader::LoadLocations(locations_path, logger_); + + const size_t sample_count = std::min( + static_cast(application_options_.pipeline.location_count), + all_locations.size()); + + const auto sample_count_signed = + static_cast>( + sample_count); + + std::vector sampled_locations; + sampled_locations.reserve(sample_count); + + std::random_device random_generator; + std::ranges::sample(all_locations, std::back_inserter(sampled_locations), + sample_count_signed, random_generator); + + logger_->Log({.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = std::format(" Locations available: {}", + all_locations.size())}); + logger_->Log({.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = std::format(" Sampled locations: {}", + sampled_locations.size())}); + return sampled_locations; +} diff --git a/tooling/pipeline/src/biergarten_pipeline_orchestrator/run.cc b/tooling/pipeline/src/biergarten_pipeline_orchestrator/run.cc new file mode 100644 index 0000000..c98805f --- /dev/null +++ b/tooling/pipeline/src/biergarten_pipeline_orchestrator/run.cc @@ -0,0 +1,63 @@ +/** + * @file biergarten_pipeline_orchestrator/run.cc + * @brief BiergartenDataGenerator::Run() implementation. + */ + +#include +#include +#include + +#include "biergarten_pipeline_orchestrator.h" +#include "services/logging/logger.h" + +bool BiergartenPipelineOrchestrator::Run() { + try { + exporter_->Initialize(); + + std::vector cities = QueryCitiesWithCountries(); + std::vector enriched; + enriched.reserve(cities.size()); + + size_t skipped_count = 0; + for (auto& city : cities) { + try { + std::string region_context = context_service_->GetLocationContext(city); + // logger_->Log(LogLevel::Debug, PipelinePhase::UserGeneration, + // "[Pipeline] Context for '" + city.city + "' (" + + // city.iso3166_2 + ") gathered:\n" + region_context); + + enriched.push_back( + EnrichedCity{.location = std::move(city), + .region_context = std::move(region_context)}); + } catch (const std::exception& exception) { + ++skipped_count; + logger_->Log( + {.level = LogLevel::Warn, + .phase = PipelinePhase::UserGeneration, + .message = std::format( + "[Pipeline] Skipping city '{}' ({}): context lookup failed: {}", + city.city, city.country, exception.what())}); + } + } + + if (skipped_count > 0) { + logger_->Log({.level = LogLevel::Warn, + .phase = PipelinePhase::UserGeneration, + .message = std::format( + "[Pipeline] Skipped {} city/cities due to context lookup errors", + skipped_count)}); + } + + this->GenerateBreweries(enriched); + exporter_->Finalize(); + this->LogResults(); + return true; + } catch (const std::exception& e) { + logger_->Log( + {.level = LogLevel::Error, + .phase = PipelinePhase::Teardown, + .message = + std::format("Pipeline execution failed with error: {}", e.what())}); + return false; + } +} diff --git a/tooling/pipeline/src/data_generation/llama/generate_brewery.cc b/tooling/pipeline/src/data_generation/llama/generate_brewery.cc index 569036b..f6d5159 100644 --- a/tooling/pipeline/src/data_generation/llama/generate_brewery.cc +++ b/tooling/pipeline/src/data_generation/llama/generate_brewery.cc @@ -4,8 +4,7 @@ * inference, and validates structured JSON output for brewery records. */ -#include - +#include #include #include #include @@ -100,8 +99,13 @@ BreweryResult LlamaGenerator::GenerateBrewery( // Generate brewery data from LLM raw = this->Infer(system_prompt, user_prompt, max_tokens, kBreweryJsonGrammar); - spdlog::debug("LlamaGenerator: raw output (attempt {}): {}", attempt + 1, - raw); + if (logger_) { + logger_->Log( + {.level = LogLevel::Debug, + .phase = PipelinePhase::BreweryAndBeerGeneration, + .message = std::format("LlamaGenerator: raw output (attempt {}): {}", + attempt + 1, raw)}); + } // Validate output: parse JSON and check required fields @@ -112,9 +116,13 @@ BreweryResult LlamaGenerator::GenerateBrewery( if (!validation_error.has_value()) { // Success: return parsed brewery data - spdlog::info( - "LlamaGenerator: successfully generated brewery data on attempt {}", - attempt + 1); + if (logger_) { + logger_->Log( + {.level = LogLevel::Info, + .phase = PipelinePhase::BreweryAndBeerGeneration, + .message = std::format("LlamaGenerator: successfully generated brewery data on attempt {}", + attempt + 1)}); + } return brewery; } @@ -122,8 +130,14 @@ BreweryResult LlamaGenerator::GenerateBrewery( // Validation failed: log error and prepare corrective feedback last_error = *validation_error; - spdlog::warn("LlamaGenerator: malformed brewery JSON (attempt {}): {}", - attempt + 1, *validation_error); + if (logger_) { + logger_->Log( + {.level = LogLevel::Warn, + .phase = PipelinePhase::BreweryAndBeerGeneration, + .message = + std::format("LlamaGenerator: malformed brewery JSON (attempt {}): {}", + attempt + 1, *validation_error)}); + } // Update prompt with error details to guide LLM toward correct output. user_prompt = std::format( @@ -140,9 +154,13 @@ BreweryResult LlamaGenerator::GenerateBrewery( } // All retry attempts exhausted: log failure and throw exception - spdlog::error( - "LlamaGenerator: malformed brewery response after {} attempts: " - "{}", - max_attempts, last_error.empty() ? raw : last_error); + if (logger_) { + logger_->Log( + {.level = LogLevel::Error, + .phase = PipelinePhase::BreweryAndBeerGeneration, + .message = std::format( + "LlamaGenerator: malformed brewery response after {} attempts: {}", + max_attempts, last_error.empty() ? raw : last_error)}); + } throw std::runtime_error("LlamaGenerator: malformed brewery response"); } diff --git a/tooling/pipeline/src/data_generation/llama/generate_user.cc b/tooling/pipeline/src/data_generation/llama/generate_user.cc index 7ed6426..8afc776 100644 --- a/tooling/pipeline/src/data_generation/llama/generate_user.cc +++ b/tooling/pipeline/src/data_generation/llama/generate_user.cc @@ -4,9 +4,8 @@ * retry handling, and output sanitization for downstream parsing. */ -#include -#include +#include #include #include "data_generation/llama_generator.h" @@ -21,5 +20,5 @@ // 4. Return locale-aware username and biography UserResult LlamaGenerator::GenerateUser(const std::string& locale) { return {.username = "test_user", - .bio = "This is a test user profile from " + locale + "."}; + .bio = std::format("This is a test user profile from {}.", locale)}; } diff --git a/tooling/pipeline/src/data_generation/llama/helpers.cc b/tooling/pipeline/src/data_generation/llama/helpers.cc index 1433523..d8b2715 100644 --- a/tooling/pipeline/src/data_generation/llama/helpers.cc +++ b/tooling/pipeline/src/data_generation/llama/helpers.cc @@ -16,11 +16,11 @@ #include "data_generation/llama_generator_helpers.h" #include "llama.h" - +namespace { /** * String trimming: removes leading and trailing whitespace */ -static std::string Trim(std::string_view value) { +std::string Trim(std::string_view value) { constexpr std::string_view whitespace = " \t\n\r\f\v"; const size_t first_index = value.find_first_not_of(whitespace); if (first_index == std::string_view::npos) { @@ -35,7 +35,7 @@ static std::string Trim(std::string_view value) { * Normalize whitespace: collapses multiple spaces/tabs/newlines into single * spaces */ -static std::string CondenseWhitespace(std::string_view text) { +std::string CondenseWhitespace(std::string_view text) { std::string out; out.reserve(text.size()); @@ -61,7 +61,37 @@ static std::string CondenseWhitespace(std::string_view text) { // Guard against truncating in the first half of the string. // This preserves the critical opening content and avoids cutting critical // context words early in the region description. -static constexpr size_t kTruncationGuardDivisor = 2; +constexpr size_t kTruncationGuardDivisor = 2; + +bool ReadRequiredTrimmedStringField(const boost::json::object& obj, + std::string_view key, std::string& out, + std::string* error_out) { + const boost::json::value* field = obj.if_contains(key); + if (field == nullptr || !field->is_string()) { + return false; + } + + const auto& string_value = field->as_string(); + out = Trim(std::string_view(string_value.data(), string_value.size())); + return !out.empty(); +} + +bool HasSchemaPlaceholder(const std::array& values) { + for (const std::string* value : values) { + std::string lowered = *value; + std::ranges::transform(lowered, lowered.begin(), + [](const unsigned char character) { + return static_cast(std::tolower(character)); + }); + + if (lowered == "string") { + return true; + } + } + + return false; +} +} // namespace /** * Truncate region context to fit within max length while preserving word @@ -121,47 +151,6 @@ void AppendTokenPiece(const llama_vocab* vocab, llama_token token, "LlamaGenerator: failed to decode sampled token piece"); } -static bool ReadRequiredTrimmedStringField(const boost::json::object& obj, - std::string_view key, - std::string& out, - std::string* error_out) { - const boost::json::value* field = obj.if_contains(key); - if (field == nullptr || !field->is_string()) { - if (error_out != nullptr) { - *error_out = - "JSON field '" + std::string(key) + "' is missing or not a string"; - } - return false; - } - - const auto& string_value = field->as_string(); - out = Trim(std::string_view(string_value.data(), string_value.size())); - if (out.empty()) { - if (error_out != nullptr) { - *error_out = "JSON field '" + std::string(key) + "' must not be empty"; - } - return false; - } - - return true; -} - -static bool HasSchemaPlaceholder(const std::array& values) { - for (const std::string* value : values) { - std::string lowered = *value; - std::ranges::transform(lowered, lowered.begin(), - [](unsigned char character) { - return static_cast(std::tolower(character)); - }); - - if (lowered == "string") { - return true; - } - } - - return false; -} - std::optional ValidateBreweryJson(const std::string& raw, BreweryResult& brewery_out) { boost::system::error_code error_code; @@ -209,7 +198,7 @@ std::optional ValidateBreweryJson(const std::string& raw, return validation_error; } - const std::array schema_placeholders = { + const std::array schema_placeholders = { &brewery_out.name_en, &brewery_out.description_en, &brewery_out.name_local, &brewery_out.description_local}; if (HasSchemaPlaceholder(schema_placeholders)) { diff --git a/tooling/pipeline/src/data_generation/llama/infer.cc b/tooling/pipeline/src/data_generation/llama/infer.cc index dc06d0b..fd9f2ee 100644 --- a/tooling/pipeline/src/data_generation/llama/infer.cc +++ b/tooling/pipeline/src/data_generation/llama/infer.cc @@ -5,9 +5,9 @@ * output tokens back to text for system+user chat prompts. */ -#include - #include +#include +#include #include #include #include @@ -107,7 +107,7 @@ std::string LlamaGenerator::InferFormatted(const std::string& formatted_prompt, .top_p = sampling_top_p_, .seed = static_cast(rng_()), }; - auto sampler = MakeSamplerChain(vocab, sampler_config, grammar); + const auto sampler = MakeSamplerChain(vocab, sampler_config, grammar); /** * Clear KV cache to ensure clean inference state (no residual context) @@ -171,10 +171,14 @@ std::string LlamaGenerator::InferFormatted(const std::string& formatted_prompt, */ prompt_tokens.resize(static_cast(token_count)); if (token_count > prompt_budget) { - spdlog::warn( - "LlamaGenerator: prompt too long ({} tokens), truncating to {} " - "tokens to fit n_batch/n_ctx limits", - token_count, prompt_budget); + if (logger_) { + logger_->Log({.level = LogLevel::Warn, + .phase = PipelinePhase::BreweryAndBeerGeneration, + .message = std::format( + "LlamaGenerator: prompt too long ({} tokens), " + "truncating to {} tokens to fit n_batch/n_ctx limits", + token_count, prompt_budget)}); + } prompt_tokens.resize(static_cast(prompt_budget)); token_count = prompt_budget; } diff --git a/tooling/pipeline/src/data_generation/llama/llama_generator.cc b/tooling/pipeline/src/data_generation/llama/llama_generator.cc index d780f2f..cc095dd 100644 --- a/tooling/pipeline/src/data_generation/llama/llama_generator.cc +++ b/tooling/pipeline/src/data_generation/llama/llama_generator.cc @@ -32,9 +32,11 @@ void LlamaGenerator::ContextDeleter::operator()( LlamaGenerator::LlamaGenerator( const ApplicationOptions& options, const std::string& model_path, + std::shared_ptr logger, std::unique_ptr prompt_formatter, std::unique_ptr prompt_directory) : rng_(std::random_device{}()), + logger_(std::move(logger)), prompt_formatter_(std::move(prompt_formatter)), prompt_directory_(std::move(prompt_directory)) { if (model_path.empty()) { diff --git a/tooling/pipeline/src/data_generation/llama/load.cc b/tooling/pipeline/src/data_generation/llama/load.cc index 0829efb..1866b2a 100644 --- a/tooling/pipeline/src/data_generation/llama/load.cc +++ b/tooling/pipeline/src/data_generation/llama/load.cc @@ -4,9 +4,8 @@ * context, and resets prior resources during model initialization. */ -#include - #include +#include #include #include #include @@ -29,8 +28,10 @@ void LlamaGenerator::Load(const std::string& model_path) { llama_model_params model_params = llama_model_default_params(); model_params.n_gpu_layers = n_gpu_layers_; - LlamaGenerator::ModelHandle loaded_model( + + ModelHandle loaded_model( llama_model_load_from_file(model_path.c_str(), model_params)); + if (!loaded_model) { throw std::runtime_error( "LlamaGenerator: failed to load model from path: " + model_path); @@ -40,8 +41,9 @@ void LlamaGenerator::Load(const std::string& model_path) { context_params.n_ctx = n_ctx_; context_params.n_batch = std::min(n_ctx_, kMaxBatchSize); - LlamaGenerator::ContextHandle loaded_context( + ContextHandle loaded_context( llama_init_from_model(loaded_model.get(), context_params)); + if (!loaded_context) { throw std::runtime_error("LlamaGenerator: failed to create context"); } @@ -49,5 +51,10 @@ void LlamaGenerator::Load(const std::string& model_path) { model_ = std::move(loaded_model); context_ = std::move(loaded_context); - spdlog::info("[LlamaGenerator] Loaded model: {}", model_path); + if (logger_) { + logger_->Log({.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = std::format("[LlamaGenerator] Loaded model: {} ", + model_path)}); + } } diff --git a/tooling/pipeline/src/json_handling/json_loader.cc b/tooling/pipeline/src/json_handling/json_loader.cc index 5ca70e9..6311991 100644 --- a/tooling/pipeline/src/json_handling/json_loader.cc +++ b/tooling/pipeline/src/json_handling/json_loader.cc @@ -6,7 +6,9 @@ #include "json_handling/json_loader.h" -#include +#include +#include "services/logging/logger.h" +#include #include #include @@ -19,8 +21,8 @@ static std::string ReadRequiredString(const boost::json::object& object, const char* key) { const boost::json::value* value = object.if_contains(key); if (value == nullptr || !value->is_string()) { - throw std::runtime_error(std::string("Missing or invalid string field: ") + - key); + throw std::runtime_error( + std::format("Missing or invalid string field: {}", key)); } const std::string_view text = value->as_string(); return std::string(text); @@ -30,8 +32,8 @@ static double ReadRequiredNumber(const boost::json::object& object, const char* key) { const boost::json::value* value = object.if_contains(key); if (value == nullptr || !value->is_number()) { - throw std::runtime_error(std::string("Missing or invalid numeric field: ") + - key); + throw std::runtime_error( + std::format("Missing or invalid numeric field: {}", key)); } return value->to_number(); } @@ -41,7 +43,7 @@ static std::vector ReadRequiredStringArray( const boost::json::value* value = object.if_contains(key); if (value == nullptr || !value->is_array()) { throw std::runtime_error( - std::string("Missing or invalid string array field: ") + key); + std::format("Missing or invalid string array field: {}", key)); } const auto& array = value->as_array(); @@ -50,7 +52,7 @@ static std::vector ReadRequiredStringArray( for (const auto& item : array) { if (!item.is_string()) { throw std::runtime_error( - std::string("Missing or invalid string array field: ") + key); + std::format("Missing or invalid string array field: {}", key)); } items.emplace_back(item.as_string()); } @@ -58,7 +60,7 @@ static std::vector ReadRequiredStringArray( } std::vector JsonLoader::LoadLocations( - const std::filesystem::path& filepath) { + const std::filesystem::path& filepath, std::shared_ptr logger) { std::ifstream input(filepath); if (!input.is_open()) { throw std::runtime_error("Failed to open locations file: " + @@ -104,7 +106,5 @@ std::vector JsonLoader::LoadLocations( }); } - spdlog::info("[JsonLoader] Loaded {} locations from {}", locations.size(), - filepath.string()); return locations; } diff --git a/tooling/pipeline/src/main.cc b/tooling/pipeline/src/main.cc index cd204c9..25ed693 100644 --- a/tooling/pipeline/src/main.cc +++ b/tooling/pipeline/src/main.cc @@ -4,16 +4,22 @@ * initializes shared infrastructure, and executes the pipeline entry flow. */ +#include #include #include #include +#include #include +#include +#include #include #include #include +#include -#include "biergarten_data_generator.h" +#include "biergarten_pipeline_orchestrator.h" +#include "concurrency/bounded_channel.h" #include "data_generation/llama_generator.h" #include "data_generation/mock_generator.h" #include "data_generation/prompt_formatting/gemma4_jinja_prompt_formatter.h" @@ -25,28 +31,50 @@ #include "services/enrichment/enrichment_service.h" #include "services/enrichment/mock_enrichment.h" #include "services/enrichment/wikipedia_service.h" +#include "services/logging/log_dispatcher.h" +#include "services/logging/log_entry.h" +#include "services/logging/log_producer.h" +#include "services/logging/logger.h" #include "services/prompting/prompt_directory.h" #include "web_client/http_web_client.h" namespace di = boost::di; +static constexpr size_t kLogMaxCount = 512; + int main(const int argc, char** argv) { + spdlog::set_level(spdlog::level::debug); + spdlog::set_pattern("│ %Y-%m-%d %H:%M:%S.%e │ %^%-7l%$ │ %v"); + BoundedChannel log_channel(kLogMaxCount); + + auto log_dispatcher = // + std::make_unique(log_channel); + std::shared_ptr log_producer = + std::make_shared(log_channel); + + std::thread log_thread([&log_dispatcher] { log_dispatcher->Run(); }); + auto shutdown = [&](const int exit_code) { + log_channel.Close(); + log_thread.join(); + return exit_code; + }; + try { Timer timer; - spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v"); #ifndef BIERGARTEN_MOCK_ONLY const LlamaBackendState llama_backend_state; #endif -#ifdef DEBUG - spdlog::set_level(spdlog::level::debug); -#endif + + log_producer->Log({.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = "STARTING PIPELINE"}); const std::optional parsed_options = - ParseArguments(argc, argv); + ParseArguments(argc, argv, log_producer); if (!parsed_options.has_value()) { - return 0; + return shutdown(EXIT_FAILURE); } const auto options = *parsed_options; @@ -55,65 +83,136 @@ int main(const int argc, char** argv) { options.generator.sampling.value_or(SamplingOptions{}); std::unique_ptr prompt_directory; + if (!options.generator.use_mocked) { try { - prompt_directory = - std::make_unique(options.pipeline.prompt_dir); + prompt_directory = std::make_unique( + options.pipeline.prompt_dir, log_producer); } catch (const std::exception& dir_error) { - spdlog::error("[Startup] Invalid --prompt-dir: {}", dir_error.what()); - return 1; + log_producer->Log({.level = LogLevel::Error, + .phase = PipelinePhase::Startup, + .message = std::format("Invalid --prompt-dir: {}", + dir_error.what())}); + + return shutdown(EXIT_FAILURE); } } const auto injector = di::make_injector( + di::bind().to(log_producer), di::bind().to(options), di::bind().to(model_path), - di::bind().to(), di::bind().to(), - di::bind().to(), + di::bind().to([options, log_producer] { + if (options.generator.use_mocked) { + { + log_producer->Log( + {.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = "Prompt formatter: none (mock mode)"}); + } + return std::unique_ptr(nullptr); + } + { + log_producer->Log( + {.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = "Prompt formatter: Gemma4JinjaPromptFormatter"}); + } + return std::unique_ptr( + std::make_unique()); + }), + di::bind().to([options, log_producer] { + if (options.generator.use_mocked) { + { + log_producer->Log({.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = "Web client: none (mock mode)"}); + } + return std::unique_ptr(nullptr); + } + { + log_producer->Log({.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = "Web client: HttpWebClient"}); + } + return std::unique_ptr( + std::make_unique(log_producer)); + }), di::bind().to( - [options](const auto& inj) -> std::unique_ptr { + [options, &log_producer]( + const auto& inj) -> std::unique_ptr { if (options.generator.use_mocked) { + { + log_producer->Log({.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = "Enrichment: mock"}); + } return std::make_unique(); } - + { + log_producer->Log({.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = "Enrichment: Wikipedia"}); + } return std::make_unique( - inj.template create>()); + inj.template create>(), + log_producer); }), di::bind().to( - [options, model_path, sampling, &prompt_directory]( - const auto& inj) -> std::unique_ptr { + [&options, &model_path, &sampling, &prompt_directory, + &log_producer](const auto& inj) -> std::unique_ptr { if (options.generator.use_mocked) { - spdlog::info( - "[Generator] Using MockGenerator (no model path provided)"); + { + log_producer->Log({.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = "Generator: mock"}); + } return std::make_unique(); } - - spdlog::info( - "[Generator] Using LlamaGenerator: {} (temperature={}, " - "top-p={}, top-k={}, n_ctx={}, seed={})", - model_path, sampling.temperature, sampling.top_p, - sampling.top_k, sampling.n_ctx, sampling.seed); + { + log_producer->Log( + {.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = std::format( + "Generator: LlamaGenerator | model={} | temp={:.2f} " + "top_p={:.2f} top_k={} n_ctx={} seed={}", + model_path, sampling.temperature, sampling.top_p, + sampling.top_k, sampling.n_ctx, sampling.seed)}); + } return std::make_unique( - options, model_path, + options, model_path, log_producer, inj.template create>(), std::move(prompt_directory)); - }) + })); - ); + const auto orchestrator = + injector.create>(); - const auto generator = - injector.create>(); - - if (!generator->Run()) { - spdlog::error("Pipeline execution failed"); - return 1; + if (!orchestrator->Run()) { + log_producer->Log({.level = LogLevel::Error, + .phase = PipelinePhase::Teardown, + .message = "Pipeline execution failed"}); + return shutdown(EXIT_FAILURE); } - spdlog::info("Pipeline executed successfully in {} ms", timer.Elapsed()); - return 0; + log_producer->Log({.level = LogLevel::Info, + .phase = PipelinePhase::Teardown, + .message = std::format("Pipeline complete in {} ms", + timer.Elapsed())}); + + return shutdown(EXIT_SUCCESS); + } catch (const std::exception& exception) { - spdlog::critical("Unhandled fatal error in main: {}", exception.what()); - return 1; + const LogDTO log_entry{.level = LogLevel::Error, + .phase = PipelinePhase::Teardown, + .message = exception.what()}; + if (log_producer) { + log_producer->Log(log_entry); + } else { + std::cerr << log_entry.message << std::endl; + } + + return shutdown(EXIT_FAILURE); } } diff --git a/tooling/pipeline/src/services/enrichment/wikipedia/fetch_extract.cc b/tooling/pipeline/src/services/enrichment/wikipedia/fetch_extract.cc index 94f2f1e..0afd490 100644 --- a/tooling/pipeline/src/services/enrichment/wikipedia/fetch_extract.cc +++ b/tooling/pipeline/src/services/enrichment/wikipedia/fetch_extract.cc @@ -2,8 +2,6 @@ * @file wikipedia/fetch_extract.cc */ -#include - #include #include #include @@ -16,13 +14,16 @@ using namespace boost; std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) { - const std::string cache_key(query); // 1. Cache Lookup if (const auto cache_it = this->extract_cache_.find(cache_key); cache_it != this->extract_cache_.end()) { - spdlog::debug("Wikipedia: Cache hit for {}!", cache_key); + if (logger_) { + logger_->Log({.level = LogLevel::Debug, + .phase = PipelinePhase::UserGeneration, + .message = std::format("Wikipedia: Cache hit for {}!", cache_key)}); + } return cache_it->second; } @@ -32,7 +33,6 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) { "api.php?action=query&titles={}&prop=extracts&explaintext=1&format=json", encoded); - const std::string body = this->client_->Get(url); { using namespace std::literals::chrono_literals; @@ -44,15 +44,27 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) { json::value doc = json::parse(body, ec); if (ec) { - spdlog::warn("WikipediaService: JSON parse error for '{}': {}", query, - ec.message()); + if (logger_) { + logger_->Log( + {.level = LogLevel::Warn, + .phase = PipelinePhase::UserGeneration, + .message = std::format("WikipediaService: JSON parse error for '{}': {}", + std::string(query), ec.message())}); + } return {}; } // 3. Safe Extraction const json::object* obj = doc.if_object(); if (obj == nullptr) { - spdlog::warn("WikipediaService: Expected root object for '{}'", query); + if (logger_) { + logger_->Log( + {.level = LogLevel::Warn, + .phase = PipelinePhase::UserGeneration, + .message = + std::format("WikipediaService: Expected root object for '{}'", + std::string(query))}); + } return {}; } @@ -63,14 +75,27 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) { : nullptr; if ((pages_ptr == nullptr) || !pages_ptr->is_object()) { - spdlog::warn("WikipediaService: Missing query.pages for '{}'", query); + if (logger_) { + logger_->Log( + {.level = LogLevel::Warn, + .phase = PipelinePhase::UserGeneration, + .message = + std::format("WikipediaService: Missing query.pages for '{}'", + std::string(query))}); + } return {}; } const json::object& pages = pages_ptr->get_object(); if (pages.empty()) { - spdlog::warn("WikipediaService: No pages returned for '{}'", query); + if (logger_) { + logger_->Log( + {.level = LogLevel::Warn, + .phase = PipelinePhase::UserGeneration, + .message = std::format("WikipediaService: No pages returned for '{}'", + std::string(query))}); + } this->extract_cache_.emplace(cache_key, ""); return {}; } @@ -80,7 +105,14 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) { const json::value& page_val = pages.begin()->value(); if (!page_val.is_object()) { - spdlog::warn("WikipediaService: Unexpected page format for '{}'", query); + if (logger_) { + logger_->Log( + {.level = LogLevel::Warn, + .phase = PipelinePhase::UserGeneration, + .message = + std::format("WikipediaService: Unexpected page format for '{}'", + std::string(query))}); + } return {}; } @@ -88,7 +120,12 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) { // Handle 404/Missing status if (page.contains("missing")) { - spdlog::warn("WikipediaService: Page '{}' does not exist", query); + if (logger_) { + logger_->Log({.level = LogLevel::Warn, + .phase = PipelinePhase::UserGeneration, + .message = std::format("WikipediaService: Page '{}' does not exist", + std::string(query))}); + } this->extract_cache_.emplace(cache_key, ""); return {}; } @@ -96,17 +133,28 @@ std::string WikipediaEnrichmentService::FetchExtract(std::string_view query) { const json::value* extract_ptr = page.if_contains("extract"); if ((extract_ptr == nullptr) || !extract_ptr->is_string()) { - spdlog::warn("WikipediaService: No extract string found for '{}'", query); + if (logger_) { + logger_->Log( + {.level = LogLevel::Warn, + .phase = PipelinePhase::UserGeneration, + .message = + std::format("WikipediaService: No extract string found for '{}'", + std::string(query))}); + } this->extract_cache_.emplace(cache_key, ""); return {}; } // 4. Success std::string extract(extract_ptr->as_string()); - spdlog::info("WikipediaService: Fetched {} chars for '{}'", extract.size(), - query); + if (logger_) { + logger_->Log({.level = LogLevel::Info, + .phase = PipelinePhase::UserGeneration, + .message = std::format("WikipediaService: Fetched {} chars for '{}'", + extract.size(), std::string(query))}); + } this->extract_cache_.insert_or_assign(cache_key, extract); return extract; -} \ No newline at end of file +} diff --git a/tooling/pipeline/src/services/enrichment/wikipedia/get_summary.cc b/tooling/pipeline/src/services/enrichment/wikipedia/get_summary.cc index d19a420..1df272f 100644 --- a/tooling/pipeline/src/services/enrichment/wikipedia/get_summary.cc +++ b/tooling/pipeline/src/services/enrichment/wikipedia/get_summary.cc @@ -3,8 +3,6 @@ * @brief WikipediaService::GetLocationContext() implementation. */ -#include - #include #include #include @@ -12,10 +10,15 @@ #include "services/enrichment/wikipedia_service.h" -std::string WikipediaEnrichmentService::GetLocationContext(const Location& loc) { +std::string WikipediaEnrichmentService::GetLocationContext( + const Location& loc) { using namespace std::literals::chrono_literals; if (!this->client_) { - spdlog::warn("Client is nullptr."); + if (logger_) { + logger_->Log({.level = LogLevel::Warn, + .phase = PipelinePhase::UserGeneration, + .message = "Wikipedia client is nullptr."}); + } return {}; } @@ -46,13 +49,22 @@ std::string WikipediaEnrichmentService::GetLocationContext(const Location& loc) try { append_extract(FetchExtract(brewing_query)); append_extract(FetchExtract(beer_query)); - spdlog::info("Done fetching for {}. Sleeping for 10 seconds.", - location_query); + if (logger_) { + logger_->Log({.level = LogLevel::Info, + .phase = PipelinePhase::UserGeneration, + .message = std::format("Done fetching for {}. Sleeping for 10 seconds.", + location_query)}); + } std::this_thread::sleep_for(10s); } catch (const std::runtime_error& e) { - spdlog::debug("WikipediaService lookup failed for '{}': {}", location_query, - e.what()); + if (logger_) { + logger_->Log( + {.level = LogLevel::Debug, + .phase = PipelinePhase::UserGeneration, + .message = std::format("WikipediaService lookup failed for '{}': {}", + location_query, e.what())}); + } } return result; } diff --git a/tooling/pipeline/src/services/enrichment/wikipedia/wikipedia_service.cc b/tooling/pipeline/src/services/enrichment/wikipedia/wikipedia_service.cc index dbccd5e..df32618 100644 --- a/tooling/pipeline/src/services/enrichment/wikipedia/wikipedia_service.cc +++ b/tooling/pipeline/src/services/enrichment/wikipedia/wikipedia_service.cc @@ -8,5 +8,5 @@ #include WikipediaEnrichmentService::WikipediaEnrichmentService( - std::unique_ptr client) - : client_(std::move(client)) {} + std::unique_ptr client, std::shared_ptr logger) + : client_(std::move(client)), logger_(std::move(logger)) {} diff --git a/tooling/pipeline/src/services/logging/log_dispatcher.cc b/tooling/pipeline/src/services/logging/log_dispatcher.cc new file mode 100644 index 0000000..3655047 --- /dev/null +++ b/tooling/pipeline/src/services/logging/log_dispatcher.cc @@ -0,0 +1,74 @@ +/** + * @brief LogDispatcher implementation for asynchronous pipeline logging. + * + * LogDispatcher drains LogEntry items from a BoundedChannel and forwards them + * to spdlog for final output. + */ +#include "services/logging/log_dispatcher.h" + +#include + +#include + +#include "concurrency/bounded_channel.h" +#include "services/logging/log_entry.h" + +namespace { +[[nodiscard]] constexpr std::string_view PipelinePhaseToString( + PipelinePhase phase) { + switch (phase) { + case PipelinePhase::Startup: + return "Startup"; + case PipelinePhase::UserGeneration: + return "User Generation"; + case PipelinePhase::BreweryAndBeerGeneration: + return "Brewery & Beer Gen"; + case PipelinePhase::CheckinGeneration: + return "Checkin Gen"; + case PipelinePhase::RatingGeneration: + return "Rating Gen"; + case PipelinePhase::FollowGeneration: + return "Follow Gen"; + case PipelinePhase::Teardown: + return "Teardown"; + } + return "Unknown"; +} +} // namespace + +LogDispatcher::LogDispatcher(BoundedChannel& channel) + : channel_(channel) {} + +void LogDispatcher::Run() { + auto logger = spdlog::default_logger(); + + while (true) { + auto entry = channel_.Receive(); + if (!entry.has_value()) { + // Channel is closed and drained. + break; + } + + const auto& log = entry.value(); + + logger->log(ToSpdlogLevel(log.level), + "{:<20} │ thread: {:016x} │ [{}:{}] │ {}", + PipelinePhaseToString(log.phase), + std::hash{}(log.thread_id), + log.origin.file_name(), log.origin.line(), log.message); + } +} + +spdlog::level::level_enum LogDispatcher::ToSpdlogLevel(LogLevel level) { + switch (level) { + case LogLevel::Debug: + return spdlog::level::debug; + case LogLevel::Info: + return spdlog::level::info; + case LogLevel::Warn: + return spdlog::level::warn; + case LogLevel::Error: + return spdlog::level::err; + } + return spdlog::level::info; +} diff --git a/tooling/pipeline/src/services/logging/log_producer.cc b/tooling/pipeline/src/services/logging/log_producer.cc new file mode 100644 index 0000000..18278ad --- /dev/null +++ b/tooling/pipeline/src/services/logging/log_producer.cc @@ -0,0 +1,19 @@ +/** + * @file src/services/logging/log_producer.cc + * @brief LogProducer implementation for asynchronous pipeline logging. + */ + +#include "services/logging/log_producer.h" + +#include +#include +#include +#include + +#include "concurrency/bounded_channel.h" +#include "services/logging/log_entry.h" + +LogProducer::LogProducer(BoundedChannel& channel) + : channel_(channel) {} + +void LogProducer::DoLog(LogEntry entry) { channel_.Send(std::move(entry)); } diff --git a/tooling/pipeline/src/services/prompt_directory.cc b/tooling/pipeline/src/services/prompt_directory.cc index 7583f2e..ded77b4 100644 --- a/tooling/pipeline/src/services/prompt_directory.cc +++ b/tooling/pipeline/src/services/prompt_directory.cc @@ -6,20 +6,25 @@ #include "services/prompting/prompt_directory.h" -#include - +#include #include +#include #include #include #include #include +#include // --------------------------------------------------------------------------- // PromptDirectory // --------------------------------------------------------------------------- PromptDirectory::PromptDirectory(const std::filesystem::path& prompt_dir) - : prompt_dir_(prompt_dir) { + : PromptDirectory(prompt_dir, nullptr) {} + +PromptDirectory::PromptDirectory(const std::filesystem::path& prompt_dir, + std::shared_ptr logger) + : prompt_dir_(prompt_dir), logger_(std::move(logger)) { std::error_code ec; // Scenario 4: directory must exist. @@ -40,12 +45,18 @@ PromptDirectory::PromptDirectory(const std::filesystem::path& prompt_dir) std::filesystem::directory_iterator probe(prompt_dir_, ec); if (ec) { throw std::runtime_error( - "PromptDirectory: prompt directory is not readable: " + - prompt_dir_.string() + " (" + ec.message() + ")"); + std::format("PromptDirectory: prompt directory is not readable: {} ({})", + prompt_dir_.string(), ec.message())); } - spdlog::info("[PromptDirectory] Resolved prompt directory: {}", - prompt_dir_.string()); + if (logger_) { + logger_->Log( + {.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = + std::string("[PromptDirectory] Resolved prompt directory: ") + + prompt_dir_.string()}); + } } std::string PromptDirectory::Load(std::string_view key) { @@ -59,13 +70,13 @@ std::string PromptDirectory::Load(std::string_view key) { // Scenario 3: resolve /.md and require it to exist. const std::filesystem::path file_path = - prompt_dir_ / std::filesystem::path(key_str + ".md"); + prompt_dir_ / std::filesystem::path(std::format("{}.md", key_str)); std::ifstream file(file_path); if (!file.is_open()) { throw std::runtime_error( - "PromptDirectory: prompt file not found for key '" + key_str + - "': " + file_path.string()); + std::format("PromptDirectory: prompt file not found for key '{}': {}", + key_str, file_path.string())); } std::string content((std::istreambuf_iterator(file)), @@ -73,12 +84,16 @@ std::string PromptDirectory::Load(std::string_view key) { file.close(); if (content.empty()) { - throw std::runtime_error("PromptDirectory: prompt file for key '" + - key_str + "' is empty: " + file_path.string()); + throw std::runtime_error(std::format("PromptDirectory: prompt file for key '{}' is empty: {}", + key_str, file_path.string())); } - spdlog::info("[PromptDirectory] Loaded prompt '{}' from '{}' ({} chars)", - key_str, file_path.string(), content.size()); + if (logger_) { + logger_->Log({.level = LogLevel::Info, + .phase = PipelinePhase::Startup, + .message = std::format("[PromptDirectory] Loaded prompt '{}' from '{}' ({} chars)", + key_str, file_path.string(), content.size())}); + } cache_.emplace(key_str, content); return content; diff --git a/tooling/pipeline/src/services/sqlite/helpers/sqlite_connection_helpers.cc b/tooling/pipeline/src/services/sqlite/helpers/sqlite_connection_helpers.cc index c527aa3..3602002 100644 --- a/tooling/pipeline/src/services/sqlite/helpers/sqlite_connection_helpers.cc +++ b/tooling/pipeline/src/services/sqlite/helpers/sqlite_connection_helpers.cc @@ -1,5 +1,6 @@ #include "services/database/sqlite_connection_helpers.h" +#include #include namespace sqlite_export_service_internal { @@ -20,7 +21,7 @@ void SqliteStatementDeleter::operator()( void ThrowSqliteError(sqlite3* db_handle, std::string_view action) { const std::string message = db_handle != nullptr ? sqlite3_errmsg(db_handle) : "unknown SQLite error"; - throw std::runtime_error(std::string(action) + ": " + message); + throw std::runtime_error(std::format("{}: {}", action, message)); } SqliteDatabaseHandle OpenDatabase(const std::filesystem::path& path) { @@ -50,7 +51,7 @@ void ExecSql(const SqliteDatabaseHandle& db_handle, std::string_view sql, ? error_message : sqlite3_errmsg(db_handle.get()); sqlite3_free(error_message); - throw std::runtime_error(std::string(action) + ": " + message); + throw std::runtime_error(std::format("{}: {}", action, message)); } } diff --git a/tooling/pipeline/src/services/sqlite/initialize.cc b/tooling/pipeline/src/services/sqlite/initialize.cc index fb66bc9..b7da162 100644 --- a/tooling/pipeline/src/services/sqlite/initialize.cc +++ b/tooling/pipeline/src/services/sqlite/initialize.cc @@ -4,6 +4,7 @@ */ #include +#include #include #include #include @@ -18,8 +19,8 @@ std::filesystem::path SqliteExportService::BuildDatabasePath() const { for (int suffix = 1; std::filesystem::exists(candidate); ++suffix) { candidate = output_path_ / - std::filesystem::path("biergarten_seed_" + run_timestamp_utc_ + - "-" + std::to_string(suffix) + ".sqlite"); + std::filesystem::path(std::format("biergarten_seed_{}-{}.sqlite", + run_timestamp_utc_, suffix)); } return candidate; diff --git a/tooling/pipeline/src/web_client/http_web_client.cc b/tooling/pipeline/src/web_client/http_web_client.cc index 4653102..dabd669 100644 --- a/tooling/pipeline/src/web_client/http_web_client.cc +++ b/tooling/pipeline/src/web_client/http_web_client.cc @@ -1,5 +1,5 @@ /** -* @file web_client/http_web_client.cc + * @file web_client/http_web_client.cc * @brief cpp-httplib implementation of WebClient. */ @@ -7,12 +7,14 @@ #include +#include +#include #include #include #include #include -#include "spdlog/spdlog.h" +#include "services/logging/logger.h" namespace { constexpr time_t kConnectionTimeoutSeconds = 5; @@ -31,7 +33,7 @@ std::pair SplitUrl(const std::string& url) { return {match[1].str(), match[2].matched ? match[2].str() : "/"}; } -} // namespace +} // namespace std::string HttpWebClient::Get(const std::string& url) { const auto [origin, path] = SplitUrl(url); @@ -40,24 +42,27 @@ std::string HttpWebClient::Get(const std::string& url) { client.set_follow_location(true); client.set_connection_timeout(kConnectionTimeoutSeconds); client.set_read_timeout(kReadTimeoutSeconds); - client.set_default_headers({ - {"Accept", "application/json"}, - {"User-Agent", "biergarten-pipeline/1.0"} - }); + client.set_default_headers({{"Accept", "application/json"}, + {"User-Agent", "biergarten-pipeline/1.0"}}); const httplib::Result result = client.Get(path); if (!result) { - throw std::runtime_error( - "[HttpWebClient] Request failed for URL: " + url + - " — " + httplib::to_string(result.error())); + throw std::runtime_error(std::format( + "[HttpWebClient] Request failed for URL: {} — {}", url, + httplib::to_string(result.error()))); } if (result->status < kSuccessMin || result->status >= kSuccessMax) { - spdlog::error("[HttpWebClient] Request failed for URL: " + url); - throw std::runtime_error( - "[HttpWebClient] HTTP " + std::to_string(result->status) + - " for URL: " + url); + if (logger_) { + logger_->Log( + {.level = LogLevel::Error, + .phase = PipelinePhase::UserGeneration, + .message = + std::format("[HttpWebClient] Request failed for URL: {}", url)}); + } + throw std::runtime_error(std::format("[HttpWebClient] HTTP {} for URL: {}", + result->status, url)); } return result->body; @@ -65,4 +70,4 @@ std::string HttpWebClient::Get(const std::string& url) { std::string HttpWebClient::EncodeURL(const std::string& value) { return httplib::encode_uri_component(value); -} \ No newline at end of file +}