From b9016ea46008c5fcf6f1d72bcda3edb6369d334e Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Sun, 11 Oct 2026 22:14:08 +0300 Subject: [PATCH 1/3] feat(config): add built-in materialization adapters Add a parser-neutral built-in materializer that validates backend field schemas and applies defaults matching the public FileLogger, UniqueFileLogger, MemoryLogger, MdbxLogger, OTLP, and Prometheus configurations. Keep optional backends and callback-only payload exporters fail-closed, and preserve side-effect-free plan materialization with nested format validation. --- CHANGELOG.md | 2 + include/logit_cpp/logit.hpp | 1 + .../logit/configuration_builtins.hpp | 558 ++++++++++++++++++ tests/configuration_contract_test.cpp | 68 +++ 4 files changed, 629 insertions(+) create mode 100644 include/logit_cpp/logit/configuration_builtins.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 63e4d10..9803506 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ All notable changes to this project will be documented in this file. validation, and side-effect-free materialization contract for Configuration Loading v1. JSON/properties parsing and logger instantiation remain separate follow-up layers. +- Added side-effect-free built-in configuration validation and default + materialization for file, memory, MDBX, OTLP, and Prometheus plans. ### Fixed diff --git a/include/logit_cpp/logit.hpp b/include/logit_cpp/logit.hpp index c4ff98e..643e8a4 100644 --- a/include/logit_cpp/logit.hpp +++ b/include/logit_cpp/logit.hpp @@ -12,6 +12,7 @@ #include "logit/config.hpp" #include "logit/enums.hpp" #include "logit/configuration.hpp" +#include "logit/configuration_builtins.hpp" #include "logit/utils.hpp" #include "logit/formatter.hpp" #include "logit/loggers.hpp" diff --git a/include/logit_cpp/logit/configuration_builtins.hpp b/include/logit_cpp/logit/configuration_builtins.hpp new file mode 100644 index 0000000..8e78c3f --- /dev/null +++ b/include/logit_cpp/logit/configuration_builtins.hpp @@ -0,0 +1,558 @@ +#pragma once +#ifndef LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_CONFIGURATION_BUILTINS_HPP_INCLUDED +#define LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_CONFIGURATION_BUILTINS_HPP_INCLUDED + +/// \file configuration_builtins.hpp +/// \brief Side-effect-free adapters for built-in backend configuration schemas. + +#include "configuration.hpp" + +namespace logit { + + /// \class BuiltinConfigurationMaterializer + /// \brief Validates and materializes data-only plans for built-in backends. + /// + /// This adapter deliberately does not include backend implementation headers, + /// allocate resources, invoke callbacks, or instantiate loggers. It applies + /// the defaults documented by the public backend Config structures and leaves + /// executable bindings to a later instantiation phase. + class BuiltinConfigurationMaterializer : public ConfigurationMaterializer { + public: + bool validate_backend(const ConfigurationLoggerSpec& logger, + ConfigurationDiagnostics& diagnostics) const override { + if (!is_known_configuration_backend(logger.type)) { + diagnostics.add(ConfigurationErrorCode::UnknownBackendType, + "logger.type", + "Built-in materializer does not know this backend"); + return false; + } + + if (!backend_available(logger.type)) { + diagnostics.add(ConfigurationErrorCode::UnavailableBackend, + "logger.type", + "The selected optional backend is not enabled in this build"); + return false; + } + + if (!adapter_supported(logger.type)) { + diagnostics.add(ConfigurationErrorCode::UnavailableBackend, + "logger.type", + "No built-in semantic adapter is defined for this backend yet"); + return false; + } + + if (logger.type == "otlp_payload" || logger.type == "prometheus_payload") { + diagnostics.add(ConfigurationErrorCode::MissingProgrammaticBinding, + "logger.backend.on_payload", + "Payload export requires a programmatic on_payload binding"); + return false; + } + + bool valid = true; + for (ConfigurationFieldMap::const_iterator it = logger.backend.begin(); + it != logger.backend.end(); ++it) { + if (!field_allowed(logger.type, it->first)) { + diagnostics.add(ConfigurationErrorCode::UnknownField, + "logger.backend." + it->first, + "Field is not part of the selected built-in backend"); + valid = false; + } + } + + valid = validate_common_types(logger, diagnostics) && valid; + if (logger.type == "file" || logger.type == "unique_file") { + valid = validate_file_types(logger, diagnostics) && valid; + } else if (logger.type == "memory") { + valid = validate_memory_types(logger, diagnostics) && valid; + } else if (logger.type == "mdbx") { + valid = validate_mdbx_types(logger, diagnostics) && valid; + } else if (logger.type == "otlp_http") { + valid = validate_otlp_types(logger, diagnostics) && valid; + } else if (logger.type == "prometheus_http" || + logger.type == "prometheus_payload") { + valid = validate_prometheus_types(logger, diagnostics) && valid; + } + return valid; + } + + bool materialize_backend(const ConfigurationLoggerSpec& logger, + ConfigurationMaterializedLogger& output, + ConfigurationDiagnostics& diagnostics) const override { + if (!validate_backend(logger, diagnostics)) { + return false; + } + + output.id = logger.id; + output.type = logger.type; + output.strategy = logger.strategy; + output.backend = logger.backend; + apply_defaults(logger.type, output.backend); + return true; + } + + private: + static bool backend_available(const std::string& type) { + if (type == "mdbx") { +#if defined(LOGIT_WITH_MDBX) + return true; +#else + return false; +#endif + } + if (type == "otlp_http" || type == "otlp_payload") { +#if defined(LOGIT_WITH_OTLP) + return true; +#else + return false; +#endif + } + if (type == "prometheus_http") { +#if defined(LOGIT_WITH_PROMETHEUS_SERVER) + return true; +#else + return false; +#endif + } + if (type == "prometheus_payload") { +#if defined(LOGIT_WITH_PROMETHEUS) + return true; +#else + return false; +#endif + } + return true; + } + + static bool adapter_supported(const std::string& type) { + return type == "file" || type == "unique_file" || type == "memory" || + type == "mdbx" || type == "otlp_http" || + type == "otlp_payload" || type == "prometheus_http" || + type == "prometheus_payload"; + } + + static bool field_allowed(const std::string& type, const std::string& field) { + if (type == "file") { + return field == "directory" || field == "async" || + field == "auto_delete_days" || field == "max_file_size_bytes" || + field == "max_rotated_files" || field == "compress" || + field == "compress_level" || field == "compress_async" || + field == "external_cmd" || field == "naming" || + field == "seq_width" || field == "use_dedicated_executor" || + field == "queue_capacity" || field == "queue_policy"; + } + if (type == "unique_file") { + return field == "directory" || field == "async" || + field == "auto_delete_days" || field == "hash_length" || + field == "use_dedicated_executor" || field == "queue_capacity" || + field == "queue_policy"; + } + if (type == "memory") { + return field == "max_records" || field == "max_bytes" || + field == "max_age_ms"; + } + if (type == "mdbx") { + return field == "path" || field == "app_name" || field == "session_id" || + field == "async" || field == "drop_on_overflow" || + field == "max_queue_size" || field == "max_batch_size" || + field == "flush_interval_ms" || field == "large_payload_threshold" || + field == "payload_preview_size" || + field == "store_large_payloads_separately" || + field == "payload_compression" || + field == "payload_compression_level"; + } + if (type == "otlp_http") { + return field == "format" || field == "host" || field == "path" || + field == "max_queue_size" || field == "max_batch_size" || + field == "max_in_flight_requests" || field == "export_interval_ms" || + field == "request_timeout_sec" || field == "retry_attempts" || + field == "retry_delay_ms" || field == "max_payload_bytes" || + field == "compression" || field == "compression_level" || + field == "drop_on_overflow" || field == "async" || + field == "cancel_on_shutdown"; + } + if (type == "prometheus_http") { + return field == "format" || field == "address" || field == "port" || + field == "path" || field == "health_path" || + field == "enable_health_endpoint" || field == "start_immediately"; + } + if (type == "prometheus_payload") { + return field == "format"; + } + return false; + } + + static bool has(const ConfigurationFieldMap& fields, const char* name) { + return fields.find(name) != fields.end(); + } + + static bool type_is(const ConfigurationFieldMap& fields, const char* name, + ConfigurationValue::Type type) { + ConfigurationFieldMap::const_iterator it = fields.find(name); + return it == fields.end() || it->second.type() == type; + } + + static bool validate_type(const ConfigurationLoggerSpec& logger, + const char* name, ConfigurationValue::Type type, + ConfigurationDiagnostics& diagnostics) { + if (type_is(logger.backend, name, type)) return true; + diagnostics.add(ConfigurationErrorCode::InvalidValue, + "logger.backend." + std::string(name), + "Field has an invalid value type"); + return false; + } + + static bool validate_common_types(const ConfigurationLoggerSpec& logger, + ConfigurationDiagnostics& diagnostics) { + bool valid = true; + valid = validate_type(logger, "async", ConfigurationValue::Type::Boolean, + diagnostics) && valid; + valid = validate_type(logger, "use_dedicated_executor", + ConfigurationValue::Type::Boolean, diagnostics) && valid; + valid = validate_type(logger, "queue_capacity", + ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; + valid = validate_type(logger, "queue_policy", ConfigurationValue::Type::String, + diagnostics) && valid; + return valid; + } + + static bool validate_file_types(const ConfigurationLoggerSpec& logger, + ConfigurationDiagnostics& diagnostics) { + bool valid = true; + valid = validate_type(logger, "directory", ConfigurationValue::Type::String, + diagnostics) && valid; + valid = validate_type(logger, "auto_delete_days", ConfigurationValue::Type::SignedInteger, + diagnostics) && valid; + valid = validate_type(logger, "max_file_size_bytes", + ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; + valid = validate_type(logger, "max_rotated_files", + ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; + valid = validate_type(logger, "compress", ConfigurationValue::Type::String, + diagnostics) && valid; + valid = validate_type(logger, "compress_level", ConfigurationValue::Type::SignedInteger, + diagnostics) && valid; + valid = validate_type(logger, "compress_async", ConfigurationValue::Type::Boolean, + diagnostics) && valid; + valid = validate_type(logger, "external_cmd", ConfigurationValue::Type::String, + diagnostics) && valid; + valid = validate_type(logger, "naming", ConfigurationValue::Type::String, + diagnostics) && valid; + valid = validate_type(logger, "seq_width", ConfigurationValue::Type::UnsignedInteger, + diagnostics) && valid; + valid = validate_enum(logger, "compress", "none", "gzip", "zstd", "external_cmd", + diagnostics) && valid; + valid = validate_enum(logger, "naming", "sequence", "timestamp", "timestamp_ms", + diagnostics) && valid; + if (logger.type == "unique_file") { + valid = validate_type(logger, "hash_length", ConfigurationValue::Type::UnsignedInteger, + diagnostics) && valid; + } + return valid; + } + + static bool validate_memory_types(const ConfigurationLoggerSpec& logger, + ConfigurationDiagnostics& diagnostics) { + bool valid = true; + valid = validate_type(logger, "max_records", ConfigurationValue::Type::UnsignedInteger, + diagnostics) && valid; + valid = validate_type(logger, "max_bytes", ConfigurationValue::Type::UnsignedInteger, + diagnostics) && valid; + valid = validate_type(logger, "max_age_ms", ConfigurationValue::Type::SignedInteger, + diagnostics) && valid; + return valid; + } + + static bool validate_mdbx_types(const ConfigurationLoggerSpec& logger, + ConfigurationDiagnostics& diagnostics) { + bool valid = true; + valid = validate_type(logger, "path", ConfigurationValue::Type::String, diagnostics) && valid; + valid = validate_type(logger, "app_name", ConfigurationValue::Type::String, diagnostics) && valid; + valid = validate_type(logger, "session_id", ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; + valid = validate_type(logger, "async", ConfigurationValue::Type::Boolean, diagnostics) && valid; + valid = validate_type(logger, "drop_on_overflow", ConfigurationValue::Type::Boolean, diagnostics) && valid; + valid = validate_type(logger, "max_queue_size", ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; + valid = validate_type(logger, "max_batch_size", ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; + valid = validate_type(logger, "flush_interval_ms", ConfigurationValue::Type::SignedInteger, diagnostics) && valid; + valid = validate_type(logger, "large_payload_threshold", ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; + valid = validate_type(logger, "payload_preview_size", ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; + valid = validate_type(logger, "store_large_payloads_separately", ConfigurationValue::Type::Boolean, diagnostics) && valid; + valid = validate_type(logger, "payload_compression", ConfigurationValue::Type::String, diagnostics) && valid; + valid = validate_type(logger, "payload_compression_level", ConfigurationValue::Type::SignedInteger, diagnostics) && valid; + valid = validate_enum(logger, "payload_compression", "none", "gzip", "zstd", + diagnostics) && valid; + return valid; + } + + static bool validate_otlp_types(const ConfigurationLoggerSpec& logger, + ConfigurationDiagnostics& diagnostics) { + bool valid = true; + valid = validate_otlp_format(logger, diagnostics) && valid; + valid = validate_type(logger, "host", ConfigurationValue::Type::String, diagnostics) && valid; + valid = validate_type(logger, "path", ConfigurationValue::Type::String, diagnostics) && valid; + valid = validate_type(logger, "max_queue_size", ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; + valid = validate_type(logger, "max_batch_size", ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; + valid = validate_type(logger, "max_in_flight_requests", ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; + valid = validate_type(logger, "export_interval_ms", ConfigurationValue::Type::SignedInteger, diagnostics) && valid; + valid = validate_type(logger, "request_timeout_sec", ConfigurationValue::Type::SignedInteger, diagnostics) && valid; + valid = validate_type(logger, "retry_attempts", ConfigurationValue::Type::SignedInteger, diagnostics) && valid; + valid = validate_type(logger, "retry_delay_ms", ConfigurationValue::Type::SignedInteger, diagnostics) && valid; + valid = validate_type(logger, "max_payload_bytes", ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; + valid = validate_type(logger, "compression", ConfigurationValue::Type::String, diagnostics) && valid; + valid = validate_type(logger, "compression_level", ConfigurationValue::Type::SignedInteger, diagnostics) && valid; + valid = validate_type(logger, "drop_on_overflow", ConfigurationValue::Type::Boolean, diagnostics) && valid; + valid = validate_type(logger, "async", ConfigurationValue::Type::Boolean, diagnostics) && valid; + valid = validate_type(logger, "cancel_on_shutdown", ConfigurationValue::Type::Boolean, diagnostics) && valid; + valid = validate_enum(logger, "compression", "none", "gzip", "zstd", diagnostics) && valid; + return valid; + } + + static bool validate_enum(const ConfigurationLoggerSpec& logger, const char* name, + const char* first, const char* second, const char* third, + const char* fourth, ConfigurationDiagnostics& diagnostics) { + ConfigurationFieldMap::const_iterator it = logger.backend.find(name); + if (it == logger.backend.end()) return true; + std::string value; + if (!it->second.get_string(value) || + (value != first && value != second && value != third && value != fourth)) { + diagnostics.add(ConfigurationErrorCode::InvalidValue, + "logger.backend." + std::string(name), + "Field has an unsupported named value"); + return false; + } + return true; + } + + static bool validate_enum(const ConfigurationLoggerSpec& logger, const char* name, + const char* first, const char* second, const char* third, + ConfigurationDiagnostics& diagnostics) { + ConfigurationFieldMap::const_iterator it = logger.backend.find(name); + if (it == logger.backend.end()) return true; + std::string value; + if (!it->second.get_string(value) || + (value != first && value != second && value != third)) { + diagnostics.add(ConfigurationErrorCode::InvalidValue, + "logger.backend." + std::string(name), + "Field has an unsupported named value"); + return false; + } + return true; + } + + static bool validate_prometheus_types(const ConfigurationLoggerSpec& logger, + ConfigurationDiagnostics& diagnostics) { + bool valid = validate_prometheus_format(logger, diagnostics); + if (logger.type == "prometheus_http") { + valid = validate_type(logger, "address", ConfigurationValue::Type::String, diagnostics) && valid; + valid = validate_type(logger, "port", ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; + valid = validate_type(logger, "path", ConfigurationValue::Type::String, diagnostics) && valid; + valid = validate_type(logger, "health_path", ConfigurationValue::Type::String, diagnostics) && valid; + valid = validate_type(logger, "enable_health_endpoint", ConfigurationValue::Type::Boolean, diagnostics) && valid; + valid = validate_type(logger, "start_immediately", ConfigurationValue::Type::Boolean, diagnostics) && valid; + } + return valid; + } + + static bool validate_otlp_format(const ConfigurationLoggerSpec& logger, + ConfigurationDiagnostics& diagnostics) { + ConfigurationFieldMap::const_iterator it = logger.backend.find("format"); + if (it == logger.backend.end()) return true; + if (it->second.type() != ConfigurationValue::Type::Object) { + diagnostics.add(ConfigurationErrorCode::InvalidValue, + "logger.backend.format", + "format must be an object"); + return false; + } + std::map fields; + it->second.get_object(fields); + bool valid = true; + for (std::map::const_iterator field = fields.begin(); + field != fields.end(); ++field) { + const bool string_field = field->first == "service_name" || + field->first == "service_namespace" || field->first == "service_instance_id" || + field->first == "deployment_environment" || field->first == "args_prefix"; + const bool bool_field = field->first == "include_source" || + field->first == "include_thread_id" || field->first == "include_format" || + field->first == "include_arg_names" || field->first == "include_args"; + if (!string_field && !bool_field) { + diagnostics.add(ConfigurationErrorCode::UnknownField, + "logger.backend.format." + field->first, + "Field is not part of OtlpJsonFormatConfig"); + valid = false; + } else if ((string_field && field->second.type() != ConfigurationValue::Type::String) || + (bool_field && field->second.type() != ConfigurationValue::Type::Boolean)) { + diagnostics.add(ConfigurationErrorCode::InvalidValue, + "logger.backend.format." + field->first, + "Field has an invalid value type"); + valid = false; + } + } + return valid; + } + + static bool validate_prometheus_format(const ConfigurationLoggerSpec& logger, + ConfigurationDiagnostics& diagnostics) { + ConfigurationFieldMap::const_iterator it = logger.backend.find("format"); + if (it == logger.backend.end()) return true; + if (it->second.type() != ConfigurationValue::Type::Object) { + diagnostics.add(ConfigurationErrorCode::InvalidValue, + "logger.backend.format", + "format must be an object"); + return false; + } + std::map fields; + it->second.get_object(fields); + bool valid = true; + for (std::map::const_iterator field = fields.begin(); + field != fields.end(); ++field) { + const bool string_field = field->first == "metric_prefix" || + field->first == "logger_label_name" || field->first == "instance_label_name" || + field->first == "instance_label_value"; + const bool bool_field = field->first == "include_help" || + field->first == "include_type" || field->first == "include_timestamp" || + field->first == "include_logger_label" || field->first == "include_instance_label" || + field->first == "include_build_info"; + const bool labels_field = field->first == "const_labels"; + if (!string_field && !bool_field && !labels_field) { + diagnostics.add(ConfigurationErrorCode::UnknownField, + "logger.backend.format." + field->first, + "Field is not part of PrometheusTextFormatConfig"); + valid = false; + } else if ((string_field && field->second.type() != ConfigurationValue::Type::String) || + (bool_field && field->second.type() != ConfigurationValue::Type::Boolean) || + (labels_field && field->second.type() != ConfigurationValue::Type::Array)) { + diagnostics.add(ConfigurationErrorCode::InvalidValue, + "logger.backend.format." + field->first, + "Field has an invalid value type"); + valid = false; + } + if (labels_field && field->second.type() == ConfigurationValue::Type::Array) { + std::vector labels; + field->second.get_array(labels); + for (std::size_t index = 0; index < labels.size(); ++index) { + std::map label; + if (!labels[index].get_object(label)) { + diagnostics.add(ConfigurationErrorCode::InvalidValue, + "logger.backend.format.const_labels[" + + std::to_string(index) + "]", + "Each constant label must be an object"); + valid = false; + continue; + } + if (label.find("name") == label.end() || + label.find("value") == label.end() || + label["name"].type() != ConfigurationValue::Type::String || + label["value"].type() != ConfigurationValue::Type::String) { + diagnostics.add(ConfigurationErrorCode::InvalidValue, + "logger.backend.format.const_labels[" + + std::to_string(index) + "]", + "Constant labels require string name and value"); + valid = false; + } + } + } + } + return valid; + } + + static void set_default(ConfigurationFieldMap& fields, const char* name, + const ConfigurationValue& value) { + if (!has(fields, name)) fields[name] = value; + } + + static void apply_defaults(const std::string& type, ConfigurationFieldMap& fields) { + if (type == "file") { + set_default(fields, "directory", ConfigurationValue::make_string("logs")); + set_default(fields, "async", ConfigurationValue::make_boolean(true)); + set_default(fields, "auto_delete_days", ConfigurationValue::make_signed(30)); + set_default(fields, "max_file_size_bytes", ConfigurationValue::make_unsigned(0)); + set_default(fields, "max_rotated_files", ConfigurationValue::make_unsigned(0)); + set_default(fields, "compress", ConfigurationValue::make_string("none")); + set_default(fields, "compress_level", ConfigurationValue::make_signed(1)); + set_default(fields, "compress_async", ConfigurationValue::make_boolean(true)); + set_default(fields, "external_cmd", ConfigurationValue::make_string("")); + set_default(fields, "naming", ConfigurationValue::make_string("sequence")); + set_default(fields, "seq_width", ConfigurationValue::make_unsigned(3)); + set_default(fields, "use_dedicated_executor", ConfigurationValue::make_boolean(false)); + set_default(fields, "queue_capacity", ConfigurationValue::make_unsigned(0)); + set_default(fields, "queue_policy", ConfigurationValue::make_string("block")); + } else if (type == "unique_file") { + set_default(fields, "directory", ConfigurationValue::make_string("unique_logs")); + set_default(fields, "async", ConfigurationValue::make_boolean(true)); + set_default(fields, "auto_delete_days", ConfigurationValue::make_signed(30)); + set_default(fields, "hash_length", ConfigurationValue::make_unsigned(8)); + set_default(fields, "use_dedicated_executor", ConfigurationValue::make_boolean(false)); + set_default(fields, "queue_capacity", ConfigurationValue::make_unsigned(0)); + set_default(fields, "queue_policy", ConfigurationValue::make_string("block")); + } else if (type == "memory") { + set_default(fields, "max_records", ConfigurationValue::make_unsigned(1000)); + set_default(fields, "max_bytes", ConfigurationValue::make_unsigned(1024 * 1024)); + set_default(fields, "max_age_ms", ConfigurationValue::make_signed(24LL * 60 * 60 * 1000)); + } else if (type == "mdbx") { + set_default(fields, "path", ConfigurationValue::make_string("logs.mdbx")); + set_default(fields, "app_name", ConfigurationValue::make_string("")); + set_default(fields, "session_id", ConfigurationValue::make_unsigned(0)); + set_default(fields, "async", ConfigurationValue::make_boolean(true)); + set_default(fields, "drop_on_overflow", ConfigurationValue::make_boolean(true)); + set_default(fields, "max_queue_size", ConfigurationValue::make_unsigned(8192)); + set_default(fields, "max_batch_size", ConfigurationValue::make_unsigned(256)); + set_default(fields, "flush_interval_ms", ConfigurationValue::make_signed(100)); + set_default(fields, "large_payload_threshold", ConfigurationValue::make_unsigned(4096)); + set_default(fields, "payload_preview_size", ConfigurationValue::make_unsigned(512)); + set_default(fields, "store_large_payloads_separately", ConfigurationValue::make_boolean(true)); + set_default(fields, "payload_compression", ConfigurationValue::make_string("none")); + set_default(fields, "payload_compression_level", ConfigurationValue::make_signed(6)); + } else if (type == "otlp_http") { + ConfigurationFieldMap format; + format["service_name"] = ConfigurationValue::make_string("logit-app"); + format["service_namespace"] = ConfigurationValue::make_string(""); + format["service_instance_id"] = ConfigurationValue::make_string(""); + format["deployment_environment"] = ConfigurationValue::make_string(""); + format["include_source"] = ConfigurationValue::make_boolean(true); + format["include_thread_id"] = ConfigurationValue::make_boolean(true); + format["include_format"] = ConfigurationValue::make_boolean(true); + format["include_arg_names"] = ConfigurationValue::make_boolean(false); + format["include_args"] = ConfigurationValue::make_boolean(true); + format["args_prefix"] = ConfigurationValue::make_string("logit.arg."); + set_default(fields, "format", ConfigurationValue::make_object(format)); + set_default(fields, "host", ConfigurationValue::make_string("http://localhost:4318")); + set_default(fields, "path", ConfigurationValue::make_string("/v1/logs")); + set_default(fields, "max_queue_size", ConfigurationValue::make_unsigned(8192)); + set_default(fields, "max_batch_size", ConfigurationValue::make_unsigned(256)); + set_default(fields, "max_in_flight_requests", ConfigurationValue::make_unsigned(1)); + set_default(fields, "export_interval_ms", ConfigurationValue::make_signed(1000)); + set_default(fields, "request_timeout_sec", ConfigurationValue::make_signed(3)); + set_default(fields, "retry_attempts", ConfigurationValue::make_signed(2)); + set_default(fields, "retry_delay_ms", ConfigurationValue::make_signed(250)); + set_default(fields, "max_payload_bytes", ConfigurationValue::make_unsigned(1024 * 1024)); + set_default(fields, "compression", ConfigurationValue::make_string("none")); + set_default(fields, "compression_level", ConfigurationValue::make_signed(6)); + set_default(fields, "drop_on_overflow", ConfigurationValue::make_boolean(true)); + set_default(fields, "async", ConfigurationValue::make_boolean(true)); + set_default(fields, "cancel_on_shutdown", ConfigurationValue::make_boolean(false)); + } else if (type == "prometheus_http") { + ConfigurationFieldMap format; + format["include_help"] = ConfigurationValue::make_boolean(true); + format["include_type"] = ConfigurationValue::make_boolean(true); + format["include_timestamp"] = ConfigurationValue::make_boolean(false); + format["metric_prefix"] = ConfigurationValue::make_string("logit_"); + format["const_labels"] = ConfigurationValue::make_array( + std::vector()); + format["include_logger_label"] = ConfigurationValue::make_boolean(true); + format["logger_label_name"] = ConfigurationValue::make_string("logger"); + format["include_instance_label"] = ConfigurationValue::make_boolean(false); + format["instance_label_name"] = ConfigurationValue::make_string("instance"); + format["instance_label_value"] = ConfigurationValue::make_string(""); + format["include_build_info"] = ConfigurationValue::make_boolean(true); + set_default(fields, "format", ConfigurationValue::make_object(format)); + set_default(fields, "address", ConfigurationValue::make_string("0.0.0.0")); + set_default(fields, "port", ConfigurationValue::make_unsigned(9090)); + set_default(fields, "path", ConfigurationValue::make_string("/metrics")); + set_default(fields, "health_path", ConfigurationValue::make_string("/health")); + set_default(fields, "enable_health_endpoint", ConfigurationValue::make_boolean(true)); + set_default(fields, "start_immediately", ConfigurationValue::make_boolean(true)); + } + } + }; + +} // namespace logit + +#endif // LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_CONFIGURATION_BUILTINS_HPP_INCLUDED diff --git a/tests/configuration_contract_test.cpp b/tests/configuration_contract_test.cpp index b83b96e..d6e6b74 100644 --- a/tests/configuration_contract_test.cpp +++ b/tests/configuration_contract_test.cpp @@ -173,6 +173,71 @@ void test_materializer_is_not_called_after_validation_failure() { assert(materializer.materialize_calls == 0); } +void test_builtin_materializer_applies_file_defaults() { + logit::ConfigurationDocument document; + document.schema_version = 1; + logit::ConfigurationLoggerSpec logger; + logger.id = "file-defaults"; + logger.type = "file"; + document.loggers.push_back(logger); + + logit::BuiltinConfigurationMaterializer materializer; + logit::ConfigurationPlan plan; + logit::ConfigurationDiagnostics diagnostics; + assert(logit::materialize_configuration(document, materializer, plan, diagnostics)); + assert(diagnostics.empty()); + + std::string directory; + std::uint64_t queue_capacity = 1; + bool async = false; + assert(plan.loggers[0].backend.at("directory").get_string(directory)); + assert(directory == "logs"); + assert(plan.loggers[0].backend.at("async").get_boolean(async)); + assert(async); + assert(plan.loggers[0].backend.at("queue_capacity").get_unsigned(queue_capacity)); + assert(queue_capacity == 0); +} + +void test_builtin_materializer_rejects_unknown_field() { + logit::ConfigurationDocument document; + document.schema_version = 1; + logit::ConfigurationLoggerSpec logger; + logger.id = "memory-invalid"; + logger.type = "memory"; + logger.backend["not_a_memory_option"] = logit::ConfigurationValue::make_boolean(true); + document.loggers.push_back(logger); + + logit::BuiltinConfigurationMaterializer materializer; + logit::ConfigurationPlan plan; + logit::ConfigurationDiagnostics diagnostics; + assert(!logit::materialize_configuration(document, materializer, plan, diagnostics)); + assert(diagnostics.has_code(logit::ConfigurationErrorCode::UnknownField)); +} + +void test_builtin_materializer_handles_optional_backends_explicitly() { + logit::ConfigurationDocument document; + document.schema_version = 1; + logit::ConfigurationLoggerSpec logger; + logger.id = "mdbx-or-optional"; + logger.type = "mdbx"; + document.loggers.push_back(logger); + + logit::BuiltinConfigurationMaterializer materializer; + logit::ConfigurationPlan plan; + logit::ConfigurationDiagnostics diagnostics; + const bool materialized = + logit::materialize_configuration(document, materializer, plan, diagnostics); +#if defined(LOGIT_WITH_MDBX) + assert(materialized); + assert(diagnostics.empty()); + assert(plan.loggers[0].backend.at("path").type() == + logit::ConfigurationValue::Type::String); +#else + assert(!materialized); + assert(diagnostics.has_code(logit::ConfigurationErrorCode::UnavailableBackend)); +#endif +} + } // namespace int main() { @@ -182,5 +247,8 @@ int main() { test_diagnostics_fail_closed(); test_plan_is_preserved_on_late_failure(); test_materializer_is_not_called_after_validation_failure(); + test_builtin_materializer_applies_file_defaults(); + test_builtin_materializer_rejects_unknown_field(); + test_builtin_materializer_handles_optional_backends_explicitly(); return 0; } From 585f6ea183d6959767406041fb51f66f428cb36e Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Mon, 12 Oct 2026 00:21:53 +0300 Subject: [PATCH 2/3] fix(config): close built-in materialization contract Merge nested OTLP and Prometheus format defaults recursively, preserve user values, and reject unknown nested label fields. Rebase backend diagnostics to the document logger index and carry required callback bindings in materialized plans so payload backends remain data-only until instantiation. --- include/logit_cpp/logit/configuration.hpp | 39 ++++-- .../logit/configuration_builtins.hpp | 122 ++++++++++++++---- tests/configuration_contract_test.cpp | 118 ++++++++++++++++- 3 files changed, 240 insertions(+), 39 deletions(-) diff --git a/include/logit_cpp/logit/configuration.hpp b/include/logit_cpp/logit/configuration.hpp index 003ec3a..d7fdc3c 100644 --- a/include/logit_cpp/logit/configuration.hpp +++ b/include/logit_cpp/logit/configuration.hpp @@ -87,6 +87,17 @@ namespace logit { return false; } + void append_rebased(const ConfigurationDiagnostics& source, + const std::string& prefix) { + for (std::vector::const_iterator it = source.m_items.begin(); + it != source.m_items.end(); ++it) { + const std::string path = it->path.empty() + ? prefix + : prefix + "." + it->path; + add(it->code, path, it->message); + } + } + private: std::vector m_items; }; @@ -242,6 +253,7 @@ namespace logit { std::string type; ConfigurationStrategySpec strategy; ConfigurationFieldMap backend; + std::vector required_bindings; }; /// \struct ConfigurationPlan @@ -425,13 +437,17 @@ namespace logit { } for (std::size_t index = 0; index < document.loggers.size(); ++index) { - const std::size_t diagnostic_count = diagnostics.size(); - const bool valid = materializer.validate_backend(document.loggers[index], diagnostics); - if (!valid || diagnostics.size() != diagnostic_count) { - if (diagnostics.size() == diagnostic_count) { + ConfigurationDiagnostics local_diagnostics; + const bool valid = materializer.validate_backend(document.loggers[index], + local_diagnostics); + if (!valid || !local_diagnostics.empty()) { + const std::string prefix = "loggers[" + std::to_string(index) + "]"; + if (local_diagnostics.empty()) { diagnostics.add(ConfigurationErrorCode::BackendValidationFailed, - "loggers[" + std::to_string(index) + "].backend", + prefix + ".backend", "Backend materializer rejected the logger configuration"); + } else { + diagnostics.append_rebased(local_diagnostics, prefix); } return false; } @@ -442,14 +458,17 @@ namespace logit { candidate.loggers.reserve(document.loggers.size()); for (std::size_t index = 0; index < document.loggers.size(); ++index) { ConfigurationMaterializedLogger output; - const std::size_t diagnostic_count = diagnostics.size(); + ConfigurationDiagnostics local_diagnostics; const bool materialized = materializer.materialize_backend( - document.loggers[index], output, diagnostics); - if (!materialized || diagnostics.size() != diagnostic_count) { - if (diagnostics.size() == diagnostic_count) { + document.loggers[index], output, local_diagnostics); + if (!materialized || !local_diagnostics.empty()) { + const std::string prefix = "loggers[" + std::to_string(index) + "]"; + if (local_diagnostics.empty()) { diagnostics.add(ConfigurationErrorCode::MaterializationFailed, - "loggers[" + std::to_string(index) + "]", + prefix, "Backend materializer could not produce a logger plan"); + } else { + diagnostics.append_rebased(local_diagnostics, prefix); } return false; } diff --git a/include/logit_cpp/logit/configuration_builtins.hpp b/include/logit_cpp/logit/configuration_builtins.hpp index 8e78c3f..f818408 100644 --- a/include/logit_cpp/logit/configuration_builtins.hpp +++ b/include/logit_cpp/logit/configuration_builtins.hpp @@ -22,38 +22,31 @@ namespace logit { ConfigurationDiagnostics& diagnostics) const override { if (!is_known_configuration_backend(logger.type)) { diagnostics.add(ConfigurationErrorCode::UnknownBackendType, - "logger.type", + "type", "Built-in materializer does not know this backend"); return false; } if (!backend_available(logger.type)) { diagnostics.add(ConfigurationErrorCode::UnavailableBackend, - "logger.type", + "type", "The selected optional backend is not enabled in this build"); return false; } if (!adapter_supported(logger.type)) { diagnostics.add(ConfigurationErrorCode::UnavailableBackend, - "logger.type", + "type", "No built-in semantic adapter is defined for this backend yet"); return false; } - if (logger.type == "otlp_payload" || logger.type == "prometheus_payload") { - diagnostics.add(ConfigurationErrorCode::MissingProgrammaticBinding, - "logger.backend.on_payload", - "Payload export requires a programmatic on_payload binding"); - return false; - } - bool valid = true; for (ConfigurationFieldMap::const_iterator it = logger.backend.begin(); it != logger.backend.end(); ++it) { if (!field_allowed(logger.type, it->first)) { diagnostics.add(ConfigurationErrorCode::UnknownField, - "logger.backend." + it->first, + "backend." + it->first, "Field is not part of the selected built-in backend"); valid = false; } @@ -66,7 +59,7 @@ namespace logit { valid = validate_memory_types(logger, diagnostics) && valid; } else if (logger.type == "mdbx") { valid = validate_mdbx_types(logger, diagnostics) && valid; - } else if (logger.type == "otlp_http") { + } else if (logger.type == "otlp_http" || logger.type == "otlp_payload") { valid = validate_otlp_types(logger, diagnostics) && valid; } else if (logger.type == "prometheus_http" || logger.type == "prometheus_payload") { @@ -87,6 +80,9 @@ namespace logit { output.strategy = logger.strategy; output.backend = logger.backend; apply_defaults(logger.type, output.backend); + if (logger.type == "otlp_payload" || logger.type == "prometheus_payload") { + output.required_bindings.push_back("on_payload"); + } return true; } @@ -170,6 +166,12 @@ namespace logit { field == "drop_on_overflow" || field == "async" || field == "cancel_on_shutdown"; } + if (type == "otlp_payload") { + return field == "format" || field == "async" || + field == "max_payload_bytes" || field == "max_batch_size" || + field == "max_queue_size" || field == "drop_on_overflow" || + field == "export_interval_ms"; + } if (type == "prometheus_http") { return field == "format" || field == "address" || field == "port" || field == "path" || field == "health_path" || @@ -196,7 +198,7 @@ namespace logit { ConfigurationDiagnostics& diagnostics) { if (type_is(logger.backend, name, type)) return true; diagnostics.add(ConfigurationErrorCode::InvalidValue, - "logger.backend." + std::string(name), + "backend." + std::string(name), "Field has an invalid value type"); return false; } @@ -291,7 +293,11 @@ namespace logit { valid = validate_type(logger, "max_queue_size", ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; valid = validate_type(logger, "max_batch_size", ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; valid = validate_type(logger, "max_in_flight_requests", ConfigurationValue::Type::UnsignedInteger, diagnostics) && valid; - valid = validate_type(logger, "export_interval_ms", ConfigurationValue::Type::SignedInteger, diagnostics) && valid; + valid = validate_type(logger, "export_interval_ms", + logger.type == "otlp_payload" + ? ConfigurationValue::Type::UnsignedInteger + : ConfigurationValue::Type::SignedInteger, + diagnostics) && valid; valid = validate_type(logger, "request_timeout_sec", ConfigurationValue::Type::SignedInteger, diagnostics) && valid; valid = validate_type(logger, "retry_attempts", ConfigurationValue::Type::SignedInteger, diagnostics) && valid; valid = validate_type(logger, "retry_delay_ms", ConfigurationValue::Type::SignedInteger, diagnostics) && valid; @@ -314,7 +320,7 @@ namespace logit { if (!it->second.get_string(value) || (value != first && value != second && value != third && value != fourth)) { diagnostics.add(ConfigurationErrorCode::InvalidValue, - "logger.backend." + std::string(name), + "backend." + std::string(name), "Field has an unsupported named value"); return false; } @@ -330,7 +336,7 @@ namespace logit { if (!it->second.get_string(value) || (value != first && value != second && value != third)) { diagnostics.add(ConfigurationErrorCode::InvalidValue, - "logger.backend." + std::string(name), + "backend." + std::string(name), "Field has an unsupported named value"); return false; } @@ -357,7 +363,7 @@ namespace logit { if (it == logger.backend.end()) return true; if (it->second.type() != ConfigurationValue::Type::Object) { diagnostics.add(ConfigurationErrorCode::InvalidValue, - "logger.backend.format", + "backend.format", "format must be an object"); return false; } @@ -374,13 +380,13 @@ namespace logit { field->first == "include_arg_names" || field->first == "include_args"; if (!string_field && !bool_field) { diagnostics.add(ConfigurationErrorCode::UnknownField, - "logger.backend.format." + field->first, + "backend.format." + field->first, "Field is not part of OtlpJsonFormatConfig"); valid = false; } else if ((string_field && field->second.type() != ConfigurationValue::Type::String) || (bool_field && field->second.type() != ConfigurationValue::Type::Boolean)) { diagnostics.add(ConfigurationErrorCode::InvalidValue, - "logger.backend.format." + field->first, + "backend.format." + field->first, "Field has an invalid value type"); valid = false; } @@ -394,7 +400,7 @@ namespace logit { if (it == logger.backend.end()) return true; if (it->second.type() != ConfigurationValue::Type::Object) { diagnostics.add(ConfigurationErrorCode::InvalidValue, - "logger.backend.format", + "backend.format", "format must be an object"); return false; } @@ -413,14 +419,14 @@ namespace logit { const bool labels_field = field->first == "const_labels"; if (!string_field && !bool_field && !labels_field) { diagnostics.add(ConfigurationErrorCode::UnknownField, - "logger.backend.format." + field->first, + "backend.format." + field->first, "Field is not part of PrometheusTextFormatConfig"); valid = false; } else if ((string_field && field->second.type() != ConfigurationValue::Type::String) || (bool_field && field->second.type() != ConfigurationValue::Type::Boolean) || (labels_field && field->second.type() != ConfigurationValue::Type::Array)) { diagnostics.add(ConfigurationErrorCode::InvalidValue, - "logger.backend.format." + field->first, + "backend.format." + field->first, "Field has an invalid value type"); valid = false; } @@ -431,18 +437,28 @@ namespace logit { std::map label; if (!labels[index].get_object(label)) { diagnostics.add(ConfigurationErrorCode::InvalidValue, - "logger.backend.format.const_labels[" + + "backend.format.const_labels[" + std::to_string(index) + "]", "Each constant label must be an object"); valid = false; continue; } + for (std::map::const_iterator member = label.begin(); + member != label.end(); ++member) { + if (member->first != "name" && member->first != "value") { + diagnostics.add(ConfigurationErrorCode::UnknownField, + "backend.format.const_labels[" + + std::to_string(index) + "]." + member->first, + "Field is not part of PrometheusLabel"); + valid = false; + } + } if (label.find("name") == label.end() || label.find("value") == label.end() || label["name"].type() != ConfigurationValue::Type::String || label["value"].type() != ConfigurationValue::Type::String) { diagnostics.add(ConfigurationErrorCode::InvalidValue, - "logger.backend.format.const_labels[" + + "backend.format.const_labels[" + std::to_string(index) + "]", "Constant labels require string name and value"); valid = false; @@ -458,6 +474,24 @@ namespace logit { if (!has(fields, name)) fields[name] = value; } + static void merge_object_defaults(ConfigurationFieldMap& fields, const char* name, + const ConfigurationFieldMap& defaults) { + ConfigurationFieldMap::iterator it = fields.find(name); + if (it == fields.end()) { + fields[name] = ConfigurationValue::make_object(defaults); + return; + } + ConfigurationFieldMap observed; + if (!it->second.get_object(observed)) return; + for (ConfigurationFieldMap::const_iterator field = defaults.begin(); + field != defaults.end(); ++field) { + if (observed.find(field->first) == observed.end()) { + observed[field->first] = field->second; + } + } + it->second = ConfigurationValue::make_object(observed); + } + static void apply_defaults(const std::string& type, ConfigurationFieldMap& fields) { if (type == "file") { set_default(fields, "directory", ConfigurationValue::make_string("logs")); @@ -512,7 +546,7 @@ namespace logit { format["include_arg_names"] = ConfigurationValue::make_boolean(false); format["include_args"] = ConfigurationValue::make_boolean(true); format["args_prefix"] = ConfigurationValue::make_string("logit.arg."); - set_default(fields, "format", ConfigurationValue::make_object(format)); + merge_object_defaults(fields, "format", format); set_default(fields, "host", ConfigurationValue::make_string("http://localhost:4318")); set_default(fields, "path", ConfigurationValue::make_string("/v1/logs")); set_default(fields, "max_queue_size", ConfigurationValue::make_unsigned(8192)); @@ -528,6 +562,25 @@ namespace logit { set_default(fields, "drop_on_overflow", ConfigurationValue::make_boolean(true)); set_default(fields, "async", ConfigurationValue::make_boolean(true)); set_default(fields, "cancel_on_shutdown", ConfigurationValue::make_boolean(false)); + } else if (type == "otlp_payload") { + ConfigurationFieldMap format; + format["service_name"] = ConfigurationValue::make_string("logit-app"); + format["service_namespace"] = ConfigurationValue::make_string(""); + format["service_instance_id"] = ConfigurationValue::make_string(""); + format["deployment_environment"] = ConfigurationValue::make_string(""); + format["include_source"] = ConfigurationValue::make_boolean(true); + format["include_thread_id"] = ConfigurationValue::make_boolean(true); + format["include_format"] = ConfigurationValue::make_boolean(true); + format["include_arg_names"] = ConfigurationValue::make_boolean(false); + format["include_args"] = ConfigurationValue::make_boolean(true); + format["args_prefix"] = ConfigurationValue::make_string("logit.arg."); + merge_object_defaults(fields, "format", format); + set_default(fields, "async", ConfigurationValue::make_boolean(true)); + set_default(fields, "max_payload_bytes", ConfigurationValue::make_unsigned(1024 * 1024)); + set_default(fields, "max_batch_size", ConfigurationValue::make_unsigned(256)); + set_default(fields, "max_queue_size", ConfigurationValue::make_unsigned(1024)); + set_default(fields, "drop_on_overflow", ConfigurationValue::make_boolean(true)); + set_default(fields, "export_interval_ms", ConfigurationValue::make_unsigned(100)); } else if (type == "prometheus_http") { ConfigurationFieldMap format; format["include_help"] = ConfigurationValue::make_boolean(true); @@ -542,13 +595,30 @@ namespace logit { format["instance_label_name"] = ConfigurationValue::make_string("instance"); format["instance_label_value"] = ConfigurationValue::make_string(""); format["include_build_info"] = ConfigurationValue::make_boolean(true); - set_default(fields, "format", ConfigurationValue::make_object(format)); + merge_object_defaults(fields, "format", format); set_default(fields, "address", ConfigurationValue::make_string("0.0.0.0")); set_default(fields, "port", ConfigurationValue::make_unsigned(9090)); set_default(fields, "path", ConfigurationValue::make_string("/metrics")); set_default(fields, "health_path", ConfigurationValue::make_string("/health")); set_default(fields, "enable_health_endpoint", ConfigurationValue::make_boolean(true)); set_default(fields, "start_immediately", ConfigurationValue::make_boolean(true)); + } else if (type == "prometheus_payload") { + ConfigurationFieldMap format; + format["include_help"] = ConfigurationValue::make_boolean(true); + format["include_type"] = ConfigurationValue::make_boolean(true); + format["include_timestamp"] = ConfigurationValue::make_boolean(false); + format["metric_prefix"] = ConfigurationValue::make_string("logit_"); + format["const_labels"] = ConfigurationValue::make_array( + std::vector()); + format["include_logger_label"] = ConfigurationValue::make_boolean(true); + format["logger_label_name"] = ConfigurationValue::make_string("logger"); + format["include_instance_label"] = ConfigurationValue::make_boolean(false); + format["instance_label_name"] = ConfigurationValue::make_string("instance"); + format["instance_label_value"] = ConfigurationValue::make_string(""); + format["include_build_info"] = ConfigurationValue::make_boolean(true); + merge_object_defaults(fields, "format", format); + set_default(fields, "emit_on_log", ConfigurationValue::make_boolean(false)); + set_default(fields, "emit_on_wait", ConfigurationValue::make_boolean(true)); } } }; diff --git a/tests/configuration_contract_test.cpp b/tests/configuration_contract_test.cpp index d6e6b74..5cf43a5 100644 --- a/tests/configuration_contract_test.cpp +++ b/tests/configuration_contract_test.cpp @@ -12,14 +12,16 @@ struct RecordingMaterializer : logit::ConfigurationMaterializer { mutable int materialize_calls = 0; bool add_validation_diagnostic = false; bool add_materialization_diagnostic = false; + int validation_diagnostic_at = -1; int fail_materialization_at = -1; bool validate_backend(const logit::ConfigurationLoggerSpec& logger, logit::ConfigurationDiagnostics& diagnostics) const override { ++validate_calls; - if (add_validation_diagnostic) { + if (add_validation_diagnostic && + (validation_diagnostic_at < 0 || validate_calls == validation_diagnostic_at)) { diagnostics.add(logit::ConfigurationErrorCode::UnavailableBackend, - "loggers[0].type", + "type", "test validation diagnostic"); } if (logger.type == "file" && logger.backend.find("directory") == logger.backend.end()) { @@ -37,7 +39,7 @@ struct RecordingMaterializer : logit::ConfigurationMaterializer { ++materialize_calls; if (add_materialization_diagnostic) { diagnostics.add(logit::ConfigurationErrorCode::MissingProgrammaticBinding, - "loggers[0].binding", + "binding", "test materialization diagnostic"); } if (fail_materialization_at == materialize_calls) { @@ -238,6 +240,113 @@ void test_builtin_materializer_handles_optional_backends_explicitly() { #endif } +void test_materializer_diagnostics_are_rebased_to_logger_index() { + logit::ConfigurationDocument document = valid_document(); + document.loggers.push_back(document.loggers[0]); + document.loggers[1].id = "file-secondary"; + + RecordingMaterializer materializer; + materializer.add_validation_diagnostic = true; + materializer.validation_diagnostic_at = 2; + logit::ConfigurationPlan plan; + logit::ConfigurationDiagnostics diagnostics; + assert(!logit::materialize_configuration(document, materializer, plan, diagnostics)); + assert(diagnostics.size() == 1); + assert(diagnostics.items()[0].path == "loggers[1].type"); +} + +void test_callback_backends_record_required_bindings_when_available() { +#if defined(LOGIT_WITH_OTLP) + logit::ConfigurationDocument document; + document.schema_version = 1; + logit::ConfigurationLoggerSpec logger; + logger.id = "otlp-payload"; + logger.type = "otlp_payload"; + document.loggers.push_back(logger); + + logit::BuiltinConfigurationMaterializer materializer; + logit::ConfigurationPlan plan; + logit::ConfigurationDiagnostics diagnostics; + assert(logit::materialize_configuration(document, materializer, plan, diagnostics)); + assert(diagnostics.empty()); + assert(plan.loggers[0].required_bindings.size() == 1); + assert(plan.loggers[0].required_bindings[0] == "on_payload"); +#endif +} + +void test_optional_format_defaults_merge_recursively() { +#if defined(LOGIT_WITH_OTLP) + { + logit::ConfigurationDocument document; + document.schema_version = 1; + logit::ConfigurationLoggerSpec logger; + logger.id = "otlp-partial-format"; + logger.type = "otlp_http"; + std::map format; + format["service_name"] = logit::ConfigurationValue::make_string("orders"); + logger.backend["format"] = logit::ConfigurationValue::make_object(format); + document.loggers.push_back(logger); + + logit::BuiltinConfigurationMaterializer materializer; + logit::ConfigurationPlan plan; + logit::ConfigurationDiagnostics diagnostics; + assert(logit::materialize_configuration(document, materializer, plan, diagnostics)); + std::map observed; + assert(plan.loggers[0].backend["format"].get_object(observed)); + std::string service_name; + bool include_source = false; + assert(observed["service_name"].get_string(service_name)); + assert(service_name == "orders"); + assert(observed["include_source"].get_boolean(include_source)); + assert(include_source); + } +#endif +#if defined(LOGIT_WITH_PROMETHEUS) || defined(LOGIT_WITH_PROMETHEUS_SERVER) + { + logit::ConfigurationDocument document; + document.schema_version = 1; + logit::ConfigurationLoggerSpec logger; + logger.id = "prometheus-partial-format"; +#if defined(LOGIT_WITH_PROMETHEUS_SERVER) + logger.type = "prometheus_http"; +#else + logger.type = "prometheus_payload"; +#endif + std::map format; + format["metric_prefix"] = logit::ConfigurationValue::make_string("app_"); + logger.backend["format"] = logit::ConfigurationValue::make_object(format); + document.loggers.push_back(logger); + + logit::BuiltinConfigurationMaterializer materializer; + logit::ConfigurationPlan plan; + logit::ConfigurationDiagnostics diagnostics; + assert(logit::materialize_configuration(document, materializer, plan, diagnostics)); + std::map observed; + assert(plan.loggers[0].backend["format"].get_object(observed)); + std::string metric_prefix; + bool include_help = false; + assert(observed["metric_prefix"].get_string(metric_prefix)); + assert(metric_prefix == "app_"); + assert(observed["include_help"].get_boolean(include_help)); + assert(include_help); + + std::map invalid_label; + invalid_label["name"] = logit::ConfigurationValue::make_string("env"); + invalid_label["value"] = logit::ConfigurationValue::make_string("prod"); + invalid_label["typo"] = logit::ConfigurationValue::make_unsigned(1); + std::vector labels; + labels.push_back(logit::ConfigurationValue::make_object(invalid_label)); + document.loggers[0].backend["format"] = + logit::ConfigurationValue::make_object( + std::map{ + {"const_labels", logit::ConfigurationValue::make_array(labels)}}); + diagnostics = logit::ConfigurationDiagnostics(); + assert(!logit::materialize_configuration(document, materializer, plan, diagnostics)); + assert(diagnostics.has_code(logit::ConfigurationErrorCode::UnknownField)); + } +#endif +} + } // namespace int main() { @@ -250,5 +359,8 @@ int main() { test_builtin_materializer_applies_file_defaults(); test_builtin_materializer_rejects_unknown_field(); test_builtin_materializer_handles_optional_backends_explicitly(); + test_materializer_diagnostics_are_rebased_to_logger_index(); + test_callback_backends_record_required_bindings_when_available(); + test_optional_format_defaults_merge_recursively(); return 0; } From 48504fcaaeeb163b78fd59c2bcac5a817aa07eab Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Mon, 12 Oct 2026 00:50:08 +0300 Subject: [PATCH 3/3] fix(config): complete prometheus payload schema Accept and validate PrometheusPayloadLogger emit_on_log and emit_on_wait fields, preserving explicit values while materializing the documented defaults. Document the relative diagnostic path contract for backend adapters. --- include/logit_cpp/logit/configuration.hpp | 7 ++++++- include/logit_cpp/logit/configuration_builtins.hpp | 6 +++++- tests/configuration_contract_test.cpp | 9 +++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/include/logit_cpp/logit/configuration.hpp b/include/logit_cpp/logit/configuration.hpp index d7fdc3c..0c47005 100644 --- a/include/logit_cpp/logit/configuration.hpp +++ b/include/logit_cpp/logit/configuration.hpp @@ -273,11 +273,16 @@ namespace logit { /// /// Returning true with a diagnostic is still a failed phase. Adapters /// must add only error diagnostics; warnings are not part of this v1 - /// contract. + /// contract. Diagnostic paths are relative to the current logger root + /// (for example `backend.foo`, `type`, or `binding`); the document + /// layer prefixes them with `loggers[N]`. virtual bool validate_backend(const ConfigurationLoggerSpec& logger, ConfigurationDiagnostics& diagnostics) const = 0; /// \brief Materialize a DTO after all document validation has succeeded. + /// + /// Diagnostic paths follow the same relative-to-logger-root rule as + /// `validate_backend()`; the document layer adds the logger index. virtual bool materialize_backend(const ConfigurationLoggerSpec& logger, ConfigurationMaterializedLogger& output, ConfigurationDiagnostics& diagnostics) const = 0; diff --git a/include/logit_cpp/logit/configuration_builtins.hpp b/include/logit_cpp/logit/configuration_builtins.hpp index f818408..77fa2cc 100644 --- a/include/logit_cpp/logit/configuration_builtins.hpp +++ b/include/logit_cpp/logit/configuration_builtins.hpp @@ -178,7 +178,8 @@ namespace logit { field == "enable_health_endpoint" || field == "start_immediately"; } if (type == "prometheus_payload") { - return field == "format"; + return field == "format" || field == "emit_on_log" || + field == "emit_on_wait"; } return false; } @@ -353,6 +354,9 @@ namespace logit { valid = validate_type(logger, "health_path", ConfigurationValue::Type::String, diagnostics) && valid; valid = validate_type(logger, "enable_health_endpoint", ConfigurationValue::Type::Boolean, diagnostics) && valid; valid = validate_type(logger, "start_immediately", ConfigurationValue::Type::Boolean, diagnostics) && valid; + } else if (logger.type == "prometheus_payload") { + valid = validate_type(logger, "emit_on_log", ConfigurationValue::Type::Boolean, diagnostics) && valid; + valid = validate_type(logger, "emit_on_wait", ConfigurationValue::Type::Boolean, diagnostics) && valid; } return valid; } diff --git a/tests/configuration_contract_test.cpp b/tests/configuration_contract_test.cpp index 5cf43a5..aa0a708 100644 --- a/tests/configuration_contract_test.cpp +++ b/tests/configuration_contract_test.cpp @@ -311,6 +311,7 @@ void test_optional_format_defaults_merge_recursively() { logger.type = "prometheus_http"; #else logger.type = "prometheus_payload"; + logger.backend["emit_on_log"] = logit::ConfigurationValue::make_boolean(true); #endif std::map format; format["metric_prefix"] = logit::ConfigurationValue::make_string("app_"); @@ -329,6 +330,14 @@ void test_optional_format_defaults_merge_recursively() { assert(metric_prefix == "app_"); assert(observed["include_help"].get_boolean(include_help)); assert(include_help); +#if !defined(LOGIT_WITH_PROMETHEUS_SERVER) + bool emit_on_log = false; + bool emit_on_wait = false; + assert(plan.loggers[0].backend["emit_on_log"].get_boolean(emit_on_log)); + assert(plan.loggers[0].backend["emit_on_wait"].get_boolean(emit_on_wait)); + assert(emit_on_log); + assert(emit_on_wait); +#endif std::map invalid_label; invalid_label["name"] = logit::ConfigurationValue::make_string("env");