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

@@ -4,8 +4,7 @@
* inference, and validates structured JSON output for brewery records.
*/
#include <spdlog/spdlog.h>
#include <chrono>
#include <format>
#include <optional>
#include <stdexcept>
@@ -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");
}

View File

@@ -4,9 +4,8 @@
* retry handling, and output sanitization for downstream parsing.
*/
#include <spdlog/spdlog.h>
#include <stdexcept>
#include <format>
#include <string>
#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)};
}

View File

@@ -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<std::string*, 4>& values) {
for (const std::string* value : values) {
std::string lowered = *value;
std::ranges::transform(lowered, lowered.begin(),
[](const unsigned char character) {
return static_cast<char>(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<std::string*, 4>& values) {
for (const std::string* value : values) {
std::string lowered = *value;
std::ranges::transform(lowered, lowered.begin(),
[](unsigned char character) {
return static_cast<char>(std::tolower(character));
});
if (lowered == "string") {
return true;
}
}
return false;
}
std::optional<std::string> ValidateBreweryJson(const std::string& raw,
BreweryResult& brewery_out) {
boost::system::error_code error_code;
@@ -209,7 +198,7 @@ std::optional<std::string> ValidateBreweryJson(const std::string& raw,
return validation_error;
}
const std::array<std::string*, 4> 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)) {

View File

@@ -5,9 +5,9 @@
* output tokens back to text for system+user chat prompts.
*/
#include <spdlog/spdlog.h>
#include <algorithm>
#include <chrono>
#include <format>
#include <memory>
#include <stdexcept>
#include <string>
@@ -107,7 +107,7 @@ std::string LlamaGenerator::InferFormatted(const std::string& formatted_prompt,
.top_p = sampling_top_p_,
.seed = static_cast<uint32_t>(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<size_t>(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<size_t>(prompt_budget));
token_count = prompt_budget;
}

View File

@@ -32,9 +32,11 @@ void LlamaGenerator::ContextDeleter::operator()(
LlamaGenerator::LlamaGenerator(
const ApplicationOptions& options, const std::string& model_path,
std::shared_ptr<ILogger> logger,
std::unique_ptr<IPromptFormatter> prompt_formatter,
std::unique_ptr<IPromptDirectory> 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()) {

View File

@@ -4,9 +4,8 @@
* context, and resets prior resources during model initialization.
*/
#include <spdlog/spdlog.h>
#include <algorithm>
#include <chrono>
#include <stdexcept>
#include <string>
#include <utility>
@@ -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)});
}
}