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
This commit is contained in:
2026-05-22 22:00:38 -04:00
committed by GitHub
parent 2ee7b3d2a2
commit 6a66619c70
44 changed files with 1445 additions and 439 deletions

View File

@@ -2,8 +2,6 @@
* @file wikipedia/fetch_extract.cc
*/
#include <spdlog/spdlog.h>
#include <boost/json.hpp>
#include <chrono>
#include <format>
@@ -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;
}
}

View File

@@ -3,8 +3,6 @@
* @brief WikipediaService::GetLocationContext() implementation.
*/
#include <spdlog/spdlog.h>
#include <chrono>
#include <format>
#include <string>
@@ -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;
}

View File

@@ -8,5 +8,5 @@
#include <utility>
WikipediaEnrichmentService::WikipediaEnrichmentService(
std::unique_ptr<WebClient> client)
: client_(std::move(client)) {}
std::unique_ptr<WebClient> client, std::shared_ptr<ILogger> logger)
: client_(std::move(client)), logger_(std::move(logger)) {}

View File

@@ -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 <spdlog/spdlog.h>
#include <string>
#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<LogEntry>& 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<std::thread::id>{}(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;
}

View File

@@ -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 <chrono>
#include <optional>
#include <string>
#include <string_view>
#include "concurrency/bounded_channel.h"
#include "services/logging/log_entry.h"
LogProducer::LogProducer(BoundedChannel<LogEntry>& channel)
: channel_(channel) {}
void LogProducer::DoLog(LogEntry entry) { channel_.Send(std::move(entry)); }

View File

@@ -6,20 +6,25 @@
#include "services/prompting/prompt_directory.h"
#include <spdlog/spdlog.h>
#include <chrono>
#include <filesystem>
#include <format>
#include <fstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
// ---------------------------------------------------------------------------
// 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<ILogger> 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 <prompt_dir>/<key>.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<char>(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;

View File

@@ -1,5 +1,6 @@
#include "services/database/sqlite_connection_helpers.h"
#include <format>
#include <stdexcept>
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));
}
}

View File

@@ -4,6 +4,7 @@
*/
#include <filesystem>
#include <format>
#include <memory>
#include <stdexcept>
#include <string>
@@ -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;