Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ All notable changes to this project will be documented in this file.
follow-up layers.
- Added side-effect-free built-in configuration validation and default
materialization for file, memory, MDBX, OTLP, and Prometheus plans.
- Added a binding/resolution and atomic caller-visible publication contract for
materialized runtime logger plans; factory side effects are not transactional,
and parser frontends and hot reload remain outside this layer.

### Fixed

Expand Down
1 change: 1 addition & 0 deletions include/logit_cpp/logit.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "logit/utils.hpp"
#include "logit/formatter.hpp"
#include "logit/loggers.hpp"
#include "logit/configuration_runtime.hpp"
#include "logit/Logger.hpp"
#include "logit/log_macros.hpp"

Expand Down
6 changes: 5 additions & 1 deletion include/logit_cpp/logit/configuration.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ namespace logit {
MissingProgrammaticBinding,
UnknownField,
BackendValidationFailed,
MaterializationFailed
MaterializationFailed,
BindingResolutionFailed,
InstantiationFailed
};

inline const char* configuration_error_code_name(ConfigurationErrorCode code) {
Expand All @@ -47,6 +49,8 @@ namespace logit {
case ConfigurationErrorCode::UnknownField: return "unknown_field";
case ConfigurationErrorCode::BackendValidationFailed: return "backend_validation_failed";
case ConfigurationErrorCode::MaterializationFailed: return "materialization_failed";
case ConfigurationErrorCode::BindingResolutionFailed: return "binding_resolution_failed";
case ConfigurationErrorCode::InstantiationFailed: return "instantiation_failed";
}
return "unknown_configuration_error";
}
Expand Down
179 changes: 179 additions & 0 deletions include/logit_cpp/logit/configuration_runtime.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#pragma once
#ifndef LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_CONFIGURATION_RUNTIME_HPP_INCLUDED
#define LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_CONFIGURATION_RUNTIME_HPP_INCLUDED

/// \file configuration_runtime.hpp
/// \brief Binding and runtime-instantiation contract for materialized plans.

#include "configuration.hpp"
#include "loggers.hpp"

#include <map>
#include <memory>
#include <string>
#include <typeindex>
#include <vector>

namespace logit {

/// \class ConfigurationBindingSet
/// \brief Type-erased programmatic resources resolved for one logger.
///
/// Bindings deliberately remain outside the parser-neutral document. A
/// resolver and its factory agree on the concrete type associated with each
/// name; the configuration layer only owns the lifetime of the shared value.
class ConfigurationBindingSet {
public:
bool empty() const { return m_values.empty(); }

bool contains(const std::string& name) const {
std::map<std::string, Entry>::const_iterator it = m_values.find(name);
return it != m_values.end() && static_cast<bool>(it->second.value);
}

template <typename T>
void set(const std::string& name, const std::shared_ptr<T>& value) {
m_values[name] = Entry(value, std::type_index(typeid(T)));
}

template <typename T>
std::shared_ptr<T> get(const std::string& name) const {
std::map<std::string, Entry>::const_iterator it = m_values.find(name);
if (it == m_values.end() || !it->second.value ||
it->second.type != std::type_index(typeid(T))) {
return std::shared_ptr<T>();
}
return std::static_pointer_cast<T>(it->second.value);
}

private:
struct Entry {
Entry() : value(), type(typeid(void)) {}
Entry(const std::shared_ptr<void>& value, const std::type_index& type)
: value(value), type(type) {}

std::shared_ptr<void> value;
std::type_index type;
};

std::map<std::string, Entry> m_values;
};

/// \class ConfigurationBindingResolver
/// \brief Resolves required programmatic bindings without constructing a logger.
class ConfigurationBindingResolver {
public:
virtual ~ConfigurationBindingResolver() {}

/// \brief Resolve bindings for one materialized logger.
///
/// Diagnostic paths are relative to the logger root. The document layer
/// prefixes them with `loggers[N]`.
virtual bool resolve(const ConfigurationMaterializedLogger& logger,
ConfigurationBindingSet& bindings,
ConfigurationDiagnostics& diagnostics) const = 0;
};

/// \class ConfigurationLoggerFactory
/// \brief Creates one runtime logger after bindings have been resolved.
/// Factory side effects are owned by the caller and are not transactional;
/// only publication of the caller-visible logger vector is atomic.
/// A factory owns the concrete binding types it expects and must retrieve
/// required values through `ConfigurationBindingSet::get<T>()`. If a
/// required value has the wrong type, the factory must fail closed by
/// adding a diagnostic or returning an empty logger pointer.
class ConfigurationLoggerFactory {
public:
virtual ~ConfigurationLoggerFactory() {}

/// \brief Instantiate one logger from a complete plan entry.
///
/// Returning an empty pointer is a failed instantiation. Diagnostic
/// paths are relative to the logger root and are rebased by the document
/// layer.
virtual std::shared_ptr<ILogger> instantiate(
const ConfigurationMaterializedLogger& logger,
const ConfigurationBindingSet& bindings,
ConfigurationDiagnostics& diagnostics) const = 0;
};

/// \brief Resolve bindings and publish a complete logger set atomically.
///
/// The caller-visible logger vector is updated only after every binding has
/// resolved and every factory call has returned a logger. Factories may
/// still perform external side effects while constructing instances; those
/// effects are not transactional and are not rolled back if a later factory
/// fails.
inline bool instantiate_configuration(
const ConfigurationPlan& plan,
const ConfigurationBindingResolver& resolver,
const ConfigurationLoggerFactory& factory,
std::vector<std::shared_ptr<ILogger>>& loggers,
ConfigurationDiagnostics& diagnostics) {
diagnostics = ConfigurationDiagnostics();
std::vector<ConfigurationBindingSet> resolved_bindings;
resolved_bindings.reserve(plan.loggers.size());
bool bindings_ok = true;
for (std::size_t index = 0; index < plan.loggers.size(); ++index) {
const ConfigurationMaterializedLogger& entry = plan.loggers[index];
ConfigurationBindingSet bindings;
ConfigurationDiagnostics local_diagnostics;
const bool resolved = resolver.resolve(entry, bindings, local_diagnostics);

bool missing_binding = false;
for (std::vector<std::string>::const_iterator required =
entry.required_bindings.begin();
required != entry.required_bindings.end(); ++required) {
if (!bindings.contains(*required)) {
local_diagnostics.add(ConfigurationErrorCode::MissingProgrammaticBinding,
"binding." + *required,
"Required programmatic binding was not resolved");
missing_binding = true;
}
}

const std::string prefix = "loggers[" + std::to_string(index) + "]";
if (!resolved || !local_diagnostics.empty() || missing_binding) {
bindings_ok = false;
if (local_diagnostics.empty()) {
diagnostics.add(ConfigurationErrorCode::BindingResolutionFailed,
prefix + ".binding",
"Programmatic bindings could not be resolved");
} else {
diagnostics.append_rebased(local_diagnostics, prefix);
}
}
resolved_bindings.push_back(bindings);
}

if (!bindings_ok) return false;

std::vector<std::shared_ptr<ILogger>> candidate;
candidate.reserve(plan.loggers.size());
for (std::size_t index = 0; index < plan.loggers.size(); ++index) {
const ConfigurationMaterializedLogger& entry = plan.loggers[index];
const std::string prefix = "loggers[" + std::to_string(index) + "]";

ConfigurationDiagnostics factory_diagnostics;
std::shared_ptr<ILogger> runtime_logger =
factory.instantiate(entry, resolved_bindings[index], factory_diagnostics);
if (!runtime_logger || !factory_diagnostics.empty()) {
if (factory_diagnostics.empty()) {
diagnostics.add(ConfigurationErrorCode::InstantiationFailed,
prefix,
"Logger factory could not instantiate the plan entry");
} else {
diagnostics.append_rebased(factory_diagnostics, prefix);
}
return false;
}
candidate.push_back(runtime_logger);
}

loggers = candidate;
return true;
}

} // namespace logit

#endif // LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_CONFIGURATION_RUNTIME_HPP_INCLUDED
121 changes: 121 additions & 0 deletions tests/configuration_contract_test.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <cassert>
#include <map>
#include <memory>
#include <string>
#include <vector>

Expand Down Expand Up @@ -53,6 +54,51 @@ struct RecordingMaterializer : logit::ConfigurationMaterializer {
}
};

struct RecordingBindingResolver : logit::ConfigurationBindingResolver {
bool provide_binding = true;
bool provide_null_binding = false;
bool provide_wrong_type = false;
int missing_at = -1;
mutable int calls = 0;

bool resolve(const logit::ConfigurationMaterializedLogger& logger,
logit::ConfigurationBindingSet& bindings,
logit::ConfigurationDiagnostics&) const override {
++calls;
if (provide_binding && !logger.required_bindings.empty() &&
(missing_at < 0 || calls != missing_at)) {
if (provide_null_binding) {
bindings.set("on_payload", std::shared_ptr<int>());
} else if (provide_wrong_type) {
bindings.set("on_payload", std::shared_ptr<std::string>(new std::string("wrong")));
} else {
bindings.set("on_payload", std::shared_ptr<int>(new int(1)));
}
}
return true;
}
};

struct RecordingLoggerFactory : logit::ConfigurationLoggerFactory {
mutable int calls = 0;
int fail_at = -1;

std::shared_ptr<logit::ILogger> instantiate(
const logit::ConfigurationMaterializedLogger&,
const logit::ConfigurationBindingSet& bindings,
logit::ConfigurationDiagnostics& diagnostics) const override {
++calls;
if (!bindings.get<int>("on_payload")) {
diagnostics.add(logit::ConfigurationErrorCode::InstantiationFailed,
"binding.on_payload",
"Test factory rejected a binding with the wrong type");
return std::shared_ptr<logit::ILogger>();
}
if (fail_at == calls) return std::shared_ptr<logit::ILogger>();
return std::shared_ptr<logit::ILogger>(new logit::MemoryLogger());
}
};

logit::ConfigurationDocument valid_document() {
logit::ConfigurationDocument document;
document.schema_version = 1;
Expand Down Expand Up @@ -356,6 +402,80 @@ void test_optional_format_defaults_merge_recursively() {
#endif
}

void test_runtime_binding_and_instantiation_are_atomic() {
logit::ConfigurationPlan plan;
plan.schema_version = 1;
logit::ConfigurationMaterializedLogger first;
first.id = "callback-entry";
first.required_bindings.push_back("on_payload");
plan.loggers.push_back(first);

RecordingBindingResolver resolver;
RecordingLoggerFactory factory;
std::vector<std::shared_ptr<logit::ILogger>> loggers;
std::shared_ptr<logit::ILogger> sentinel(new logit::MemoryLogger());
loggers.push_back(sentinel);
logit::ConfigurationDiagnostics diagnostics;
assert(logit::instantiate_configuration(plan, resolver, factory, loggers, diagnostics));
assert(diagnostics.empty());
assert(loggers.size() == 1);
assert(loggers[0] != sentinel);

resolver.provide_binding = false;
loggers.clear();
loggers.push_back(sentinel);
assert(!logit::instantiate_configuration(plan, resolver, factory, loggers, diagnostics));
assert(diagnostics.items()[0].path == "loggers[0].binding.on_payload");
assert(loggers.size() == 1);
assert(loggers[0] == sentinel);

resolver.provide_binding = true;
resolver.provide_null_binding = true;
factory.calls = 0;
assert(!logit::instantiate_configuration(plan, resolver, factory, loggers, diagnostics));
assert(diagnostics.has_code(logit::ConfigurationErrorCode::MissingProgrammaticBinding));
assert(factory.calls == 0);

resolver.provide_null_binding = false;
resolver.provide_wrong_type = true;
logit::ConfigurationBindingSet bindings;
bindings.set("value", std::shared_ptr<int>(new int(1)));
assert(bindings.get<int>("value"));
assert(!bindings.get<std::string>("value"));
resolver.provide_wrong_type = false;

resolver.provide_binding = true;
resolver.provide_wrong_type = true;
factory.calls = 0;
loggers.clear();
loggers.push_back(sentinel);
assert(!logit::instantiate_configuration(plan, resolver, factory, loggers, diagnostics));
assert(diagnostics.has_code(logit::ConfigurationErrorCode::InstantiationFailed));
assert(factory.calls == 1);
assert(loggers.size() == 1);
assert(loggers[0] == sentinel);
resolver.provide_wrong_type = false;

resolver.provide_binding = true;
factory.calls = 0;
logit::ConfigurationMaterializedLogger second = first;
second.id = "second";
plan.loggers.push_back(second);
resolver.calls = 0;
resolver.missing_at = 2;
assert(!logit::instantiate_configuration(plan, resolver, factory, loggers, diagnostics));
assert(diagnostics.has_code(logit::ConfigurationErrorCode::MissingProgrammaticBinding));
assert(factory.calls == 0);
assert(loggers.size() == 1);
assert(loggers[0] == sentinel);

resolver.provide_binding = true;
resolver.missing_at = -1;
factory.fail_at = 2;
assert(!logit::instantiate_configuration(plan, resolver, factory, loggers, diagnostics));
assert(diagnostics.has_code(logit::ConfigurationErrorCode::InstantiationFailed));
}

} // namespace

int main() {
Expand All @@ -371,5 +491,6 @@ int main() {
test_materializer_diagnostics_are_rebased_to_logger_index();
test_callback_backends_record_required_bindings_when_available();
test_optional_format_defaults_merge_recursively();
test_runtime_binding_and_instantiation_are_atomic();
return 0;
}
Loading