From e95a1c3a991b959bf09acb4f2bdc56898baa9c80 Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Fri, 22 May 2026 19:24:40 +0300 Subject: [PATCH 1/5] feat(prometheus): expose scrape counters and collect duration Add four built-in metrics to PrometheusHttpServerLogger so operators can observe scrape health and collection latency: - logit_prometheus_scrapes_total (counter) - logit_prometheus_scrape_errors_total (counter) - logit_prometheus_last_scrape_timestamp_ms (gauge) - logit_prometheus_collect_duration_seconds (gauge) Scrapes are counted in the HTTP handler before collect_payload(). Collection duration is measured inside collect_payload() using steady_clock. Failed scrapes (exceptions from collect_payload()) increment scrape_errors_total. All scrape metrics carry the logger="prometheus_http_server" label for consistency with existing built-in families. Not-tested: scrape_errors_total > 0 (requires an exception during collect_payload, which is hard to trigger deterministically in the current test setup). Co-Authored-By: Claude Opus 4.7 --- .../loggers/PrometheusHttpServerLogger.hpp | 78 +++++++++++++++++++ tests/prometheus_http_server_logger_test.cpp | 61 +++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/include/logit_cpp/logit/loggers/PrometheusHttpServerLogger.hpp b/include/logit_cpp/logit/loggers/PrometheusHttpServerLogger.hpp index 8d7cfda..59d1144 100644 --- a/include/logit_cpp/logit/loggers/PrometheusHttpServerLogger.hpp +++ b/include/logit_cpp/logit/loggers/PrometheusHttpServerLogger.hpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -65,6 +66,8 @@ namespace logit { [this](std::shared_ptr response, std::shared_ptr) { try { + m_scrapes_total.fetch_add(1); + m_last_scrape_timestamp_ms.store(LOGIT_CURRENT_TIMESTAMP_MS()); std::string payload = this->collect_payload(); response->write( SimpleWeb::StatusCode::success_ok, @@ -72,6 +75,7 @@ namespace logit { {{"Content-Type", "text/plain; version=0.0.4; charset=utf-8"}, {"Cache-Control", "no-store"}}); } catch (...) { + m_scrape_errors_total.fetch_add(1); response->write(SimpleWeb::StatusCode::server_error_internal_server_error); } }; @@ -134,12 +138,14 @@ namespace logit { /// \brief Collects current metrics and returns serialized Prometheus text payload. /// \return Complete Prometheus text exposition format string. std::string collect_payload() { + auto start = std::chrono::steady_clock::now(); std::vector families; { std::lock_guard lock(m_collect_mutex); m_metrics.build_builtin_metrics( families, m_config.format, "prometheus_http_server", LOGIT_CURRENT_TIMESTAMP_MS()); + build_scrape_metrics(families); if (m_config.on_collect) { try { m_config.on_collect(families); @@ -148,6 +154,9 @@ namespace logit { } } } + auto end = std::chrono::steady_clock::now(); + m_last_collect_duration_sec.store( + std::chrono::duration(end - start).count()); return build_prometheus_text_payload(families, m_config.format); } @@ -232,6 +241,75 @@ namespace logit { std::atomic m_log_level = ATOMIC_VAR_INIT(static_cast(LogLevel::LOG_LVL_TRACE)); + std::atomic m_scrapes_total{0}; + std::atomic m_scrape_errors_total{0}; + std::atomic m_last_scrape_timestamp_ms{0}; + std::atomic m_last_collect_duration_sec{0.0}; + + void build_scrape_metrics(std::vector& families) const { + const std::string& prefix = m_config.format.metric_prefix; + + // logit_prometheus_scrapes_total + { + PrometheusMetricFamily mf; + mf.name = prefix + "prometheus_scrapes_total"; + mf.help = "Total number of Prometheus scrape requests"; + mf.type = PrometheusMetricType::Counter; + PrometheusSample s; + s.name = mf.name; + s.value = static_cast(m_scrapes_total.load()); + add_scrape_labels(s); + mf.samples.push_back(s); + families.push_back(mf); + } + + // logit_prometheus_scrape_errors_total + { + PrometheusMetricFamily mf; + mf.name = prefix + "prometheus_scrape_errors_total"; + mf.help = "Total number of failed Prometheus scrape requests"; + mf.type = PrometheusMetricType::Counter; + PrometheusSample s; + s.name = mf.name; + s.value = static_cast(m_scrape_errors_total.load()); + add_scrape_labels(s); + mf.samples.push_back(s); + families.push_back(mf); + } + + // logit_prometheus_last_scrape_timestamp_ms + { + PrometheusMetricFamily mf; + mf.name = prefix + "prometheus_last_scrape_timestamp_ms"; + mf.help = "Timestamp of the last Prometheus scrape request"; + mf.type = PrometheusMetricType::Gauge; + PrometheusSample s; + s.name = mf.name; + s.value = static_cast(m_last_scrape_timestamp_ms.load()); + add_scrape_labels(s); + mf.samples.push_back(s); + families.push_back(mf); + } + + // logit_prometheus_collect_duration_seconds + { + PrometheusMetricFamily mf; + mf.name = prefix + "prometheus_collect_duration_seconds"; + mf.help = "Duration of the last metrics collection in seconds"; + mf.type = PrometheusMetricType::Gauge; + PrometheusSample s; + s.name = mf.name; + s.value = m_last_collect_duration_sec.load(); + add_scrape_labels(s); + mf.samples.push_back(s); + families.push_back(mf); + } + } + + static void add_scrape_labels(PrometheusSample& sample) { + sample.labels.push_back({"logger", "prometheus_http_server"}); + } + void stop() { if (!m_running.exchange(false)) { return; diff --git a/tests/prometheus_http_server_logger_test.cpp b/tests/prometheus_http_server_logger_test.cpp index 2d895b4..10f3218 100644 --- a/tests/prometheus_http_server_logger_test.cpp +++ b/tests/prometheus_http_server_logger_test.cpp @@ -270,6 +270,67 @@ int main() { logger.shutdown(); } + // Test 11: scrape metrics present after HTTP GET + { + logit::PrometheusHttpServerLogger::Config config; + config.port = 43201; + config.path = "/metrics"; + config.format.metric_prefix = "logit_"; + config.start_immediately = false; + + logit::PrometheusHttpServerLogger logger(config); + + logit::LogRecord record( + logit::LogLevel::LOG_LVL_INFO, 1710000000123LL, + "test.cpp", 80, "test_func", "scrape metrics test", "", + -1, false, false, false); + logger.log(record, "scrape metrics test"); + + logger.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + using HttpClient = SimpleWeb::Client; + HttpClient client("localhost:43201"); + auto response = client.request("GET", "/metrics"); + + assert(response->status_code.find("200") != std::string::npos); + + std::string body = response->content.string(); + assert(body.find("# HELP logit_prometheus_scrapes_total") != std::string::npos); + assert(body.find("# TYPE logit_prometheus_scrapes_total counter") != std::string::npos); + assert(body.find("logit_prometheus_scrapes_total") != std::string::npos); + + assert(body.find("# HELP logit_prometheus_scrape_errors_total") != std::string::npos); + assert(body.find("# TYPE logit_prometheus_scrape_errors_total counter") != std::string::npos); + assert(body.find("logit_prometheus_scrape_errors_total") != std::string::npos); + + assert(body.find("# HELP logit_prometheus_last_scrape_timestamp_ms") != std::string::npos); + assert(body.find("# TYPE logit_prometheus_last_scrape_timestamp_ms gauge") != std::string::npos); + + assert(body.find("# HELP logit_prometheus_collect_duration_seconds") != std::string::npos); + assert(body.find("# TYPE logit_prometheus_collect_duration_seconds gauge") != std::string::npos); + + logger.shutdown(); + } + + // Test 12: collect_payload includes scrape metrics with zero values + { + logit::PrometheusHttpServerLogger::Config config; + config.port = 43202; + config.start_immediately = false; + + logit::PrometheusHttpServerLogger logger(config); + + std::string payload = logger.collect_payload(); + + assert(payload.find("prometheus_scrapes_total") != std::string::npos); + assert(payload.find("prometheus_scrape_errors_total") != std::string::npos); + assert(payload.find("prometheus_last_scrape_timestamp_ms") != std::string::npos); + assert(payload.find("prometheus_collect_duration_seconds") != std::string::npos); + + logger.shutdown(); + } + return 0; } From b7a4d9d229f973e01cfa5917815edab1eba4eef3 Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Thu, 28 May 2026 10:52:01 +0300 Subject: [PATCH 2/5] feat(logging): add telemetry registry and context examples Add PrometheusRegistry for declarative custom application metrics and wire it into Prometheus examples and docs. Add MDC/NDC context support based on the existing local LogContext design, including formatter tokens and coverage. Fix OTLP graceful-shutdown worker wakeups and add zstd compression integration coverage. --- README.md | 45 +++++ docs/OtlpHttpLogger.md | 3 + docs/PrometheusLogger.md | 48 ++++-- examples/example_logit_mdc_ndc.cpp | 44 +++++ examples/example_logit_otlp_http.cpp | 57 ++++++- examples/example_logit_prometheus_payload.cpp | 31 +++- examples/example_logit_prometheus_server.cpp | 40 ++++- include/logit_cpp/logit/detail/LogContext.hpp | 98 +++++++++++ .../formatter/compiler/PatternCompiler.hpp | 65 ++++++- include/logit_cpp/logit/log_macros.hpp | 8 + include/logit_cpp/logit/loggers.hpp | 1 + .../logit/loggers/OtlpHttpLogger.hpp | 17 +- .../loggers/prometheus/PrometheusRegistry.hpp | 160 ++++++++++++++++++ include/logit_cpp/logit/utils.hpp | 1 + include/logit_cpp/logit/utils/LogRecord.hpp | 5 + tests/CMakeLists.txt | 8 +- tests/mdc_ndc_context_test.cpp | 91 ++++++++++ tests/otlp_http_logger_callback_test.cpp | 59 ++++++- tests/otlp_http_logger_zstd_test.cpp | 160 ++++++++++++++++++ tests/prometheus_registry_test.cpp | 140 +++++++++++++++ 20 files changed, 1035 insertions(+), 46 deletions(-) create mode 100644 examples/example_logit_mdc_ndc.cpp create mode 100644 include/logit_cpp/logit/detail/LogContext.hpp create mode 100644 include/logit_cpp/logit/loggers/prometheus/PrometheusRegistry.hpp create mode 100644 tests/mdc_ndc_context_test.cpp create mode 100644 tests/otlp_http_logger_zstd_test.cpp create mode 100644 tests/prometheus_registry_test.cpp diff --git a/README.md b/README.md index 8be3de6..433a4ff 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,13 @@ Internal headers under `logit/detail/` are private implementation details and sh See the macro examples below or browse the `examples/` folder for focused demonstrations, including queue tuning and crash handling. +Recent focused examples include: + +- `examples/example_logit_otlp_http.cpp` - OTLP/HTTP export with batching, retries, optional compression, and contextual trace/span fields. +- `examples/example_logit_prometheus_payload.cpp` - callback-based Prometheus payload emission with custom registry metrics. +- `examples/example_logit_prometheus_server.cpp` - embedded `/metrics` endpoint with built-in and application metrics. +- `examples/example_logit_mdc_ndc.cpp` - mapped and nested diagnostic context across scopes and threads. + ## Macro Examples ### Long-form macros @@ -100,6 +107,36 @@ void short_names_demo() { For a standalone program that brings everything together and intentionally aborts after logging a fatal message, check `examples/example_logit_minimal_crash.cpp`. +### Diagnostic context + +Mapped diagnostic context (MDC) stores thread-local key-value pairs, while nested +diagnostic context (NDC) stores a thread-local stack of scope names. The context +is captured into each `LogRecord` when the record is created. + +```cpp +#include + +int main() { + LOGIT_ADD_LOGGER( + logit::ConsoleLogger, (), + logit::SimpleLogFormatter, + ("[%T] request=%K{request_id} ndc=[%J] %v") + ); + + LOGIT_MDC_PUT("request_id", "req-42"); + LOGIT_NDC_PUSH("checkout"); + + { + LOGIT_NDC_GUARD("payment"); + LOGIT_INFO("charge started"); + } + + LOGIT_MDC_CLEAR(); + LOGIT_NDC_CLEAR(); + LOGIT_WAIT(); +} +``` + ### System error helpers `LOGIT_SYSERR_` captures the current `errno` (or `GetLastError()` on Windows) and appends the decoded information to the message, so failure details stay attached to the original context. The lower-level `LOGIT_PERROR_` and `LOGIT_WINERR_` families are also available if you want to explicitly choose the platform macro. @@ -521,6 +558,12 @@ Below is a list of supported formatting flags: - *Thread Flags*: - `%t`: Thread identifier + +- *Diagnostic Context Flags*: + + - `%K`: All mapped diagnostic context values as `key=value` pairs + - `%K{key}`: One mapped diagnostic context value by key + - `%J`: Nested diagnostic context stack - *Color Flags*: @@ -775,6 +818,8 @@ public: | `LOGIT__EVERY_N(n, ...)` | Log on every `n`th invocation. | | `LOGIT__THROTTLE(period_ms, ...)` | Log at most once per `period_ms` milliseconds. | | `LOGIT__TAG(({{"k", "v"}}), msg)` | Attach key-value tags to a message. | +| `LOGIT_MDC_PUT(key, value)`, `LOGIT_MDC_REMOVE(key)`, `LOGIT_MDC_CLEAR()` | Manage thread-local mapped diagnostic context. | +| `LOGIT_NDC_PUSH(value)`, `LOGIT_NDC_POP()`, `LOGIT_NDC_CLEAR()`, `LOGIT_NDC_GUARD(value)` | Manage thread-local nested diagnostic context. | | `LOGIT_RAW(msg)`, `LOGIT_RAW_TO(index, msg)`, `LOGIT_RAW_IF(condition, msg)` | Write already formatted text without applying level filters or formatter patterns. | | `LOGIT_SECTION(name)`, `LOGIT_SECTION_TO(index, name)`, `LOGIT_SECTION_IF(condition, name)` | Write raw section headers such as `[Proxy]`. | | `LOGIT__TO(index, ...)` | Target a specific logger index, including single-mode backends. | diff --git a/docs/OtlpHttpLogger.md b/docs/OtlpHttpLogger.md index 8e770ad..d9dd328 100644 --- a/docs/OtlpHttpLogger.md +++ b/docs/OtlpHttpLogger.md @@ -18,6 +18,9 @@ For Windows MinGW builds, the CMake integration enables kurlyk fallback options ## Usage +For a runnable version with environment overrides, graceful shutdown, optional +compression, and MDC trace/span fields, see `examples/example_logit_otlp_http.cpp`. + ```cpp #include diff --git a/docs/PrometheusLogger.md b/docs/PrometheusLogger.md index 7bae655..70e6ae6 100644 --- a/docs/PrometheusLogger.md +++ b/docs/PrometheusLogger.md @@ -24,7 +24,16 @@ LogIt++ provides two Prometheus backends for exposing internal log metrics in th | `logit_time_since_last_log_ms` | gauge | Time since last log (ms) | | `logit_build_info` | gauge | Build info (value=1, labels: version, compiler) | -The `metric_prefix` config option (default: `logit_`) is applied to all metric names. +The `metric_prefix` config option (default: `logit_`) is applied to built-in logger metric names. + +`PrometheusHttpServerLogger` also exposes scrape diagnostics: + +| Metric | Type | Description | +|--------|------|-------------| +| `logit_prometheus_scrapes_total` | counter | Total `/metrics` scrape requests | +| `logit_prometheus_scrape_errors_total` | counter | Failed scrape requests | +| `logit_prometheus_last_scrape_timestamp_ms` | gauge | Timestamp of the last scrape request | +| `logit_prometheus_collect_duration_seconds` | gauge | Duration of the last metrics collection | ## CMake Options @@ -38,6 +47,9 @@ option(LOGIT_WITH_PROMETHEUS_SERVER "Enable Prometheus HTTP server backend" OFF) ## Usage: PrometheusPayloadLogger +For a runnable callback example with custom application metrics, see +`examples/example_logit_prometheus_payload.cpp`. + ```cpp #include @@ -61,6 +73,9 @@ LOGIT_WAIT(); // triggers on_payload with current metrics ## Usage: PrometheusHttpServerLogger +For a runnable embedded `/metrics` server example, see +`examples/example_logit_prometheus_server.cpp`. + ```cpp #include @@ -81,22 +96,31 @@ LOGIT_INFO("Server started"); ## Custom Metrics -Use the `on_collect` callback to add application-specific metrics on each scrape: +Use `PrometheusRegistry` with the `on_collect` callback to register +application-specific metrics once and collect them on each scrape: ```cpp -config.on_collect = [](std::vector& families) { - logit::PrometheusMetricFamily mf; - mf.name = "myapp_queue_size"; - mf.help = "Current queue depth"; - mf.type = logit::PrometheusMetricType::Gauge; - logit::PrometheusSample s; - s.name = "myapp_queue_size"; - s.value = get_queue_depth(); - mf.samples.push_back(s); - families.push_back(mf); +#include + +logit::PrometheusRegistry registry("myapp_"); + +registry.set_gauge( + "queue_size", + "Current queue depth", + []() { return get_queue_depth(); }); + +config.on_collect = [®istry](std::vector& families) { + registry.collect(families); }; ``` +`PrometheusTextFormatConfig::metric_prefix` applies only to LogIt++ built-in +metrics. Custom metric names are written as supplied by the registry or manual +builders, so use the registry prefix for application metric namespaces. + +For low-level control, `on_collect` can still append `PrometheusMetricFamily` +objects directly or use helpers such as `add_prometheus_gauge()`. + ## Prometheus Scrape Config ```yaml diff --git a/examples/example_logit_mdc_ndc.cpp b/examples/example_logit_mdc_ndc.cpp new file mode 100644 index 0000000..ad67544 --- /dev/null +++ b/examples/example_logit_mdc_ndc.cpp @@ -0,0 +1,44 @@ +#include + +#include + +int main() { + LOGIT_ADD_CONSOLE( + "[%T] [%l] request=%K{request_id} user=%K{user_id} ndc=[%J] %v", + false); + + LOGIT_MDC_PUT("request_id", "req-42"); + LOGIT_MDC_PUT("user_id", "alice"); + LOGIT_NDC_PUSH("http"); + LOGIT_NDC_PUSH("POST /checkout"); + + LOGIT_INFO("request accepted"); + + { + LOGIT_NDC_GUARD("payment"); + LOGIT_WARN("payment provider latency is high"); + } + + LOGIT_INFO("payment scope has ended"); + + std::thread worker([]() { + LOGIT_MDC_PUT("request_id", "worker-7"); + LOGIT_MDC_PUT("user_id", "background"); + LOGIT_NDC_PUSH("worker"); + LOGIT_INFO("background task has its own thread-local context"); + LOGIT_MDC_CLEAR(); + LOGIT_NDC_CLEAR(); + }); + worker.join(); + + LOGIT_INFO("main thread context is unchanged"); + + LOGIT_MDC_REMOVE("user_id"); + LOGIT_NDC_POP(); + LOGIT_INFO("specific MDC keys can be removed and NDC can be popped"); + + LOGIT_MDC_CLEAR(); + LOGIT_NDC_CLEAR(); + LOGIT_SHUTDOWN(); + return 0; +} diff --git a/examples/example_logit_otlp_http.cpp b/examples/example_logit_otlp_http.cpp index 932ad44..4486532 100644 --- a/examples/example_logit_otlp_http.cpp +++ b/examples/example_logit_otlp_http.cpp @@ -1,5 +1,17 @@ #include +#include +#include + +namespace { + +std::string env_or(const char* name, const char* fallback) { + const char* value = std::getenv(name); + return (value && *value) ? std::string(value) : std::string(fallback); +} + +} // namespace + int main() { #ifndef LOGIT_WITH_OTLP LOGIT_ADD_CONSOLE_DEFAULT(); @@ -8,25 +20,52 @@ int main() { return 0; #else logit::OtlpHttpLogger::Config config; - config.host = "http://localhost:4318"; - config.path = "/v1/logs"; - config.format.service_name = "logit-otlp-example"; - config.format.deployment_environment = "dev"; - config.max_batch_size = 32; - config.export_interval_ms = 500; + config.host = env_or("LOGIT_OTLP_ENDPOINT", "http://localhost:4318"); + config.path = env_or("LOGIT_OTLP_PATH", "/v1/logs"); + config.format.service_name = env_or("LOGIT_SERVICE_NAME", "checkout-service"); + config.format.service_namespace = "examples"; + config.format.service_instance_id = "local-dev"; + config.format.deployment_environment = env_or("LOGIT_ENVIRONMENT", "dev"); + config.max_queue_size = 1024; + config.max_batch_size = 64; + config.max_in_flight_requests = 2; + config.export_interval_ms = 250; + config.request_timeout_sec = 3; + config.retry_attempts = 1; + config.retry_delay_ms = 100; + config.cancel_on_shutdown = false; + +#if defined(LOGIT_HAS_ZSTD) + config.compression = logit::OtlpCompression::Zstd; + config.compression_level = 3; +#elif defined(LOGIT_HAS_ZLIB) + config.compression = logit::OtlpCompression::Gzip; + config.compression_level = 6; +#endif LOGIT_ADD_LOGGER( logit::OtlpHttpLogger, (config), logit::SimpleLogFormatter, - ("%v") + ("[%l] trace=%K{trace_id} span=%K{span_id} %v") ); + LOGIT_MDC_PUT("trace_id", "7b3f1c8a2e914a99"); + LOGIT_MDC_PUT("span_id", "checkout-001"); + LOGIT_NDC_PUSH("checkout"); + LOGIT_INFO("OTLP logger started"); - LOGIT_WARN("Example warning message"); - LOGIT_ERROR("Example error message"); + LOGIT_WARN("payment provider latency is above threshold"); + + { + LOGIT_NDC_GUARD("submit-order"); + LOGIT_ERROR("order export failed; collector will count failed exports if HTTP fails"); + } LOGIT_WAIT(); + LOGIT_MDC_CLEAR(); + LOGIT_NDC_CLEAR(); + LOGIT_SHUTDOWN(); return 0; #endif } diff --git a/examples/example_logit_prometheus_payload.cpp b/examples/example_logit_prometheus_payload.cpp index c8f1a1e..dcd3071 100644 --- a/examples/example_logit_prometheus_payload.cpp +++ b/examples/example_logit_prometheus_payload.cpp @@ -1,5 +1,13 @@ #include +#ifdef LOGIT_WITH_PROMETHEUS +#include +#endif + +#include +#include +#include + int main() { #ifndef LOGIT_WITH_PROMETHEUS LOGIT_ADD_CONSOLE_DEFAULT(); @@ -7,14 +15,29 @@ int main() { LOGIT_WAIT(); return 0; #else + int queue_depth = 3; + unsigned long long jobs_processed = 41; + + logit::PrometheusRegistry registry("myapp_"); + registry.set_gauge( + "queue_depth", + "Current application queue depth", + [&queue_depth]() { return static_cast(queue_depth); }, + {{"queue", "orders"}}); + registry.set_counter( + "jobs_processed_total", + "Total processed jobs", + [&jobs_processed]() { return static_cast(jobs_processed); }); + logit::PrometheusPayloadLogger::Config config; config.format.metric_prefix = "myapp_"; config.format.include_build_info = true; config.emit_on_wait = true; + config.on_collect = [®istry](std::vector& families) { + registry.collect(families); + }; config.on_payload = [](std::string payload) { - // In a real application, send payload to your Prometheus push gateway - // or expose it via your own HTTP endpoint. - (void)payload; + std::cout << payload << std::endl; }; LOGIT_ADD_LOGGER( @@ -27,6 +50,8 @@ int main() { LOGIT_INFO("Prometheus payload logger started"); LOGIT_WARN("Example warning message"); LOGIT_ERROR("Example error message"); + queue_depth = 1; + ++jobs_processed; LOGIT_WAIT(); LOGIT_SHUTDOWN(); diff --git a/examples/example_logit_prometheus_server.cpp b/examples/example_logit_prometheus_server.cpp index 6eecf1c..ef7ad5b 100644 --- a/examples/example_logit_prometheus_server.cpp +++ b/examples/example_logit_prometheus_server.cpp @@ -1,9 +1,14 @@ #include #ifdef LOGIT_WITH_PROMETHEUS_SERVER -#include +#include #endif +#include +#include +#include +#include + int main() { #ifndef LOGIT_WITH_PROMETHEUS_SERVER LOGIT_ADD_CONSOLE_DEFAULT(); @@ -11,19 +16,36 @@ int main() { LOGIT_WAIT(); return 0; #else + const auto started_at = std::chrono::steady_clock::now(); + std::atomic jobs_processed{0}; + std::atomic queue_depth{0}; + + logit::PrometheusRegistry registry("myapp_"); + registry.set_gauge( + "uptime_seconds", + "Application uptime in seconds", + [started_at]() { + return std::chrono::duration( + std::chrono::steady_clock::now() - started_at).count(); + }); + registry.set_gauge( + "queue_depth", + "Current application queue depth", + [&queue_depth]() { return static_cast(queue_depth.load()); }, + {{"queue", "orders"}}); + registry.set_counter( + "jobs_processed_total", + "Total processed jobs", + [&jobs_processed]() { return static_cast(jobs_processed.load()); }); + logit::PrometheusHttpServerLogger::Config config; config.port = 9090; config.path = "/metrics"; config.format.metric_prefix = "myapp_"; config.format.include_build_info = true; - // Optional: add custom metrics on each scrape - config.on_collect = [](std::vector& families) { - logit::add_prometheus_gauge( - families, - "myapp_uptime_seconds", - "Application uptime in seconds", - 42.0); + config.on_collect = [®istry](std::vector& families) { + registry.collect(families); }; LOGIT_ADD_LOGGER( @@ -37,6 +59,8 @@ int main() { LOGIT_WARN("Scrape metrics at http://localhost:9090/metrics"); for (int i = 0; i < 5; ++i) { + queue_depth.store(5 - i); + jobs_processed.fetch_add(1); LOGIT_INFO("Logging iteration %d", (i + 1)); std::this_thread::sleep_for(std::chrono::seconds(1)); } diff --git a/include/logit_cpp/logit/detail/LogContext.hpp b/include/logit_cpp/logit/detail/LogContext.hpp new file mode 100644 index 0000000..ffa57cf --- /dev/null +++ b/include/logit_cpp/logit/detail/LogContext.hpp @@ -0,0 +1,98 @@ +#pragma once +#ifndef _LOGIT_LOG_CONTEXT_HPP_INCLUDED +#define _LOGIT_LOG_CONTEXT_HPP_INCLUDED + +/// \file LogContext.hpp +/// \brief Thread-local Mapped Diagnostic Context (MDC) and Nested Diagnostic Context (NDC). + +#include +#include +#include + +#ifndef LOGIT_DISABLE_THREAD_LOCAL_DESTRUCTORS +#define LOGIT_DISABLE_THREAD_LOCAL_DESTRUCTORS 1 +#endif + +namespace logit { + + namespace detail { + + inline std::map& mdc_map() { +# if LOGIT_DISABLE_THREAD_LOCAL_DESTRUCTORS + thread_local auto* instance = new std::map(); + return *instance; +# else + thread_local std::map instance; + return instance; +# endif + } + + inline std::vector& ndc_stack() { +# if LOGIT_DISABLE_THREAD_LOCAL_DESTRUCTORS + thread_local auto* instance = new std::vector(); + return *instance; +# else + thread_local std::vector instance; + return instance; +# endif + } + + } // namespace detail + + /// \brief Put a key-value pair into the MDC for the current thread. + inline void mdc_put(const std::string& key, const std::string& value) { + detail::mdc_map()[key] = value; + } + + /// \brief Remove a key from the MDC for the current thread. + inline void mdc_remove(const std::string& key) { + detail::mdc_map().erase(key); + } + + /// \brief Clear all MDC entries for the current thread. + inline void mdc_clear() { + detail::mdc_map().clear(); + } + + /// \brief Push a message onto the NDC stack for the current thread. + inline void ndc_push(const std::string& message) { + detail::ndc_stack().push_back(message); + } + + /// \brief Pop the top message from the NDC stack for the current thread. + inline void ndc_pop() { + std::vector& stack = detail::ndc_stack(); + if (!stack.empty()) { + stack.pop_back(); + } + } + + /// \brief Clear the NDC stack for the current thread. + inline void ndc_clear() { + detail::ndc_stack().clear(); + } + + /// \class NdcGuard + /// \brief RAII guard that pushes a message on construction and pops on destruction. + /// + /// Intended for strictly stack-scoped usage. If manual ndc_push()/ndc_pop() + /// calls interleave the guard lifetime, the guard still pops once. + class NdcGuard { + public: + explicit NdcGuard(const std::string& message) { + ndc_push(message); + } + + ~NdcGuard() { + ndc_pop(); + } + + NdcGuard(const NdcGuard&) = delete; + NdcGuard& operator=(const NdcGuard&) = delete; + NdcGuard(NdcGuard&&) = delete; + NdcGuard& operator=(NdcGuard&&) = delete; + }; + +} // namespace logit + +#endif // _LOGIT_LOG_CONTEXT_HPP_INCLUDED diff --git a/include/logit_cpp/logit/formatter/compiler/PatternCompiler.hpp b/include/logit_cpp/logit/formatter/compiler/PatternCompiler.hpp index e1fb5f2..0fb539f 100644 --- a/include/logit_cpp/logit/formatter/compiler/PatternCompiler.hpp +++ b/include/logit_cpp/logit/formatter/compiler/PatternCompiler.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -67,6 +68,11 @@ namespace logit { // Thread ThreadId, ///< %t: Thread identifier + // Diagnostic context + MappedDiagnosticContext, ///< %K: All mapped diagnostic context values + MappedDiagnosticContextValue, ///< %K{key}: Mapped diagnostic context value by key + NestedDiagnosticContext, ///< %J: Nested diagnostic context stack + // Color StartColor, ///< %^: Start of color range EndColor, ///< %$: End of color range @@ -85,6 +91,7 @@ namespace logit { bool center_align = false; ///< Center alignment flag. bool truncate = false; ///< Truncation flag. bool strip_ansi = false; ///< Removes ANSI escape codes (e.g., colors) if true. + std::string context_key; ///< Optional MDC key for context formatting. /// \brief Constructor for static text. /// \param context Compilation context for handling special cases. @@ -112,10 +119,12 @@ namespace logit { bool left = false, bool center = false, bool trunc = false, - bool strip_ansi = false) : + bool strip_ansi = false, + const std::string& context_key = std::string()) : context(context), type(type), width(width), left_align(left), center_align(center), - truncate(trunc), strip_ansi(strip_ansi) { + truncate(trunc), strip_ansi(strip_ansi), + context_key(context_key) { }; /// \brief Apply formatting considering alignment and width. @@ -257,6 +266,41 @@ namespace logit { temp_stream << record.thread_id; break; + // Diagnostic context + case FormatType::MappedDiagnosticContext: { + bool first = true; + for (std::map::const_iterator it = record.mdc.begin(); + it != record.mdc.end(); + ++it) { + if (!first) { + temp_stream << " "; + } + temp_stream << it->first << "=" << it->second; + first = false; + } + break; + } + case FormatType::MappedDiagnosticContextValue: { + std::map::const_iterator it = record.mdc.find(context_key); + if (it != record.mdc.end()) { + temp_stream << it->second; + } + break; + } + case FormatType::NestedDiagnosticContext: { + bool first = true; + for (std::vector::const_iterator it = record.ndc.begin(); + it != record.ndc.end(); + ++it) { + if (!first) { + temp_stream << " > "; + } + temp_stream << *it; + first = false; + } + break; + } + // Color case FormatType::StartColor: if (!strip_ansi) { @@ -609,6 +653,23 @@ namespace logit { instructions.emplace_back(context, FormatType::ThreadId, width, left_align, center_align, truncate, strip_ansi); break; + // Diagnostic context + case 'K': + if ((i + 1) < pattern.size() && pattern[i + 1] == '{') { + size_t end = pattern.find('}', i + 2); + if (end != std::string::npos) { + std::string key = pattern.substr(i + 2, end - i - 2); + instructions.emplace_back(context, FormatType::MappedDiagnosticContextValue, width, left_align, center_align, truncate, strip_ansi, key); + i = end; + break; + } + } + instructions.emplace_back(context, FormatType::MappedDiagnosticContext, width, left_align, center_align, truncate, strip_ansi); + break; + case 'J': + instructions.emplace_back(context, FormatType::NestedDiagnosticContext, width, left_align, center_align, truncate, strip_ansi); + break; + // File and Function case 'f': if ((i + 1) < pattern.size() && pattern[i + 1] == 'f' && (i + 2) < pattern.size() && pattern[i + 2] == 'n') { diff --git a/include/logit_cpp/logit/log_macros.hpp b/include/logit_cpp/logit/log_macros.hpp index ef85b13..faea688 100644 --- a/include/logit_cpp/logit/log_macros.hpp +++ b/include/logit_cpp/logit/log_macros.hpp @@ -50,6 +50,14 @@ /// \param y Second token. #define LOGIT_CONCAT(x, y) LOGIT_CONCAT_IMPL(x, y) +#define LOGIT_MDC_PUT(key, value) ::logit::mdc_put((key), (value)) +#define LOGIT_MDC_REMOVE(key) ::logit::mdc_remove((key)) +#define LOGIT_MDC_CLEAR() ::logit::mdc_clear() +#define LOGIT_NDC_PUSH(value) ::logit::ndc_push((value)) +#define LOGIT_NDC_POP() ::logit::ndc_pop() +#define LOGIT_NDC_CLEAR() ::logit::ndc_clear() +#define LOGIT_NDC_GUARD(value) ::logit::NdcGuard LOGIT_CONCAT(_logit_ndc_guard_, __COUNTER__)((value)) + #ifdef _LOGIT_ENUMS_HPP_INCLUDED static_assert(LOGIT_LEVEL_TRACE == static_cast(logit::LogLevel::LOG_LVL_TRACE), "LOGIT_LEVEL_TRACE mismatch"); diff --git a/include/logit_cpp/logit/loggers.hpp b/include/logit_cpp/logit/loggers.hpp index f72d3c4..5d27223 100644 --- a/include/logit_cpp/logit/loggers.hpp +++ b/include/logit_cpp/logit/loggers.hpp @@ -34,6 +34,7 @@ #endif #ifdef LOGIT_WITH_PROMETHEUS +#include "loggers/prometheus/PrometheusRegistry.hpp" #include "loggers/PrometheusPayloadLogger.hpp" #endif #ifdef LOGIT_WITH_PROMETHEUS_SERVER diff --git a/include/logit_cpp/logit/loggers/OtlpHttpLogger.hpp b/include/logit_cpp/logit/loggers/OtlpHttpLogger.hpp index 4eb647e..0b3e45a 100644 --- a/include/logit_cpp/logit/loggers/OtlpHttpLogger.hpp +++ b/include/logit_cpp/logit/loggers/OtlpHttpLogger.hpp @@ -291,16 +291,23 @@ namespace logit { lock, std::chrono::milliseconds(m_config.export_interval_ms), [this]() { - return m_state->stopping || - (!m_state->queue.empty() && - m_state->http_in_flight < m_config.max_in_flight_requests); + const bool can_submit = + !m_state->queue.empty() && + m_state->http_in_flight < m_config.max_in_flight_requests; + + const bool can_stop_now = + m_state->stopping && + (m_config.cancel_on_shutdown || + (m_state->queue.empty() && m_state->http_in_flight == 0)); + + return can_submit || can_stop_now; }); - if (m_state->stopping && m_state->queue.empty() && m_state->http_in_flight == 0) { + if (m_state->stopping && m_config.cancel_on_shutdown) { return; } - if (m_state->stopping && m_config.cancel_on_shutdown) { + if (m_state->stopping && m_state->queue.empty() && m_state->http_in_flight == 0) { return; } diff --git a/include/logit_cpp/logit/loggers/prometheus/PrometheusRegistry.hpp b/include/logit_cpp/logit/loggers/prometheus/PrometheusRegistry.hpp new file mode 100644 index 0000000..d9cbdf6 --- /dev/null +++ b/include/logit_cpp/logit/loggers/prometheus/PrometheusRegistry.hpp @@ -0,0 +1,160 @@ +#pragma once +#ifndef _LOGIT_PROMETHEUS_REGISTRY_HPP_INCLUDED +#define _LOGIT_PROMETHEUS_REGISTRY_HPP_INCLUDED + +/// \file PrometheusRegistry.hpp +/// \brief Declarative registry for custom Prometheus application metrics. + +#include "PrometheusTextFormatConfig.hpp" + +#include +#include +#include +#include +#include + +namespace logit { + + /// \class PrometheusRegistry + /// \brief Stores custom gauge and counter callbacks for Prometheus collection. + class PrometheusRegistry { + public: + /// \brief Constructs registry with an optional metric prefix. + explicit PrometheusRegistry(std::string metric_prefix = std::string()) + : m_metric_prefix(std::move(metric_prefix)) {} + + /// \brief Sets prefix applied to metrics collected by this registry. + void set_metric_prefix(const std::string& metric_prefix) { + m_metric_prefix = metric_prefix; + } + + /// \brief Returns prefix applied to metrics collected by this registry. + const std::string& metric_prefix() const { + return m_metric_prefix; + } + + /// \brief Registers or replaces a gauge metric callback. + void set_gauge( + const std::string& name, + const std::string& help, + std::function value_fn, + std::vector labels = {}) { + set_metric( + PrometheusMetricType::Gauge, + name, + help, + std::move(value_fn), + std::move(labels)); + } + + /// \brief Registers or replaces a counter metric callback. + void set_counter( + const std::string& name, + const std::string& help, + std::function value_fn, + std::vector labels = {}) { + set_metric( + PrometheusMetricType::Counter, + name, + help, + std::move(value_fn), + std::move(labels)); + } + + /// \brief Appends current registry metrics to the output vector. + void collect(std::vector& out) const { + std::vector collected; + + for (std::size_t i = 0; i < m_entries.size(); ++i) { + const Entry& entry = m_entries[i]; + const std::string full_name = m_metric_prefix + entry.name; + + PrometheusMetricFamily* family = + find_family(collected, full_name, entry.type); + if (family == nullptr) { + PrometheusMetricFamily mf; + mf.name = full_name; + mf.help = entry.help; + mf.type = entry.type; + collected.push_back(std::move(mf)); + family = &collected.back(); + } + + PrometheusSample sample; + sample.name = full_name; + sample.value = entry.value_fn(); + sample.labels = entry.labels; + family->samples.push_back(std::move(sample)); + } + + out.insert(out.end(), collected.begin(), collected.end()); + } + + private: + struct Entry { + PrometheusMetricType type = PrometheusMetricType::Untyped; + std::string name; + std::string help; + std::function value_fn; + std::vector labels; + }; + + std::string m_metric_prefix; + std::vector m_entries; + + void set_metric( + PrometheusMetricType type, + const std::string& name, + const std::string& help, + std::function value_fn, + std::vector labels) { + for (std::size_t i = 0; i < m_entries.size(); ++i) { + Entry& entry = m_entries[i]; + if (entry.name == name && labels_equal(entry.labels, labels)) { + entry.type = type; + entry.help = help; + entry.value_fn = std::move(value_fn); + entry.labels = std::move(labels); + return; + } + } + + Entry entry; + entry.type = type; + entry.name = name; + entry.help = help; + entry.value_fn = std::move(value_fn); + entry.labels = std::move(labels); + m_entries.push_back(std::move(entry)); + } + + static bool labels_equal( + const std::vector& lhs, + const std::vector& rhs) { + if (lhs.size() != rhs.size()) { + return false; + } + for (std::size_t i = 0; i < lhs.size(); ++i) { + if (lhs[i].name != rhs[i].name || lhs[i].value != rhs[i].value) { + return false; + } + } + return true; + } + + static PrometheusMetricFamily* find_family( + std::vector& families, + const std::string& name, + PrometheusMetricType type) { + for (std::size_t i = 0; i < families.size(); ++i) { + if (families[i].name == name && families[i].type == type) { + return &families[i]; + } + } + return nullptr; + } + }; + +} // namespace logit + +#endif // _LOGIT_PROMETHEUS_REGISTRY_HPP_INCLUDED diff --git a/include/logit_cpp/logit/utils.hpp b/include/logit_cpp/logit/utils.hpp index 6f90dc6..aa833fe 100644 --- a/include/logit_cpp/logit/utils.hpp +++ b/include/logit_cpp/logit/utils.hpp @@ -20,6 +20,7 @@ #include "utils/LogFileReadResult.hpp" #include "utils/encoding_utils.hpp" #include "utils/path_utils.hpp" +#include "detail/LogContext.hpp" #include "utils/LogRecord.hpp" #include "utils/tag_utils.hpp" diff --git a/include/logit_cpp/logit/utils/LogRecord.hpp b/include/logit_cpp/logit/utils/LogRecord.hpp index 0fc812e..28690d8 100644 --- a/include/logit_cpp/logit/utils/LogRecord.hpp +++ b/include/logit_cpp/logit/utils/LogRecord.hpp @@ -5,6 +5,7 @@ /// \file LogRecord.hpp /// \brief Contains the definition of the LogRecord structure for storing log data. +#include #include #include #include @@ -29,6 +30,8 @@ namespace logit { const bool print_mode : 1; ///< Flag to determine whether arguments are printed in a raw format without special symbols. const bool fmt_mode : 1; ///< Flag indicating if fmt formatting should be used. const bool raw_mode : 1; ///< Flag indicating if formatter and level filters should be bypassed. + mutable std::map mdc; ///< Mapped Diagnostic Context (thread-local key-value pairs). + mutable std::vector ndc; ///< Nested Diagnostic Context (thread-local stack). /// \brief Constructor with argument names. /// \param log_level Log severity level. @@ -66,6 +69,8 @@ namespace logit { print_mode(print_mode), fmt_mode(fmt_mode), raw_mode(raw_mode) { + mdc = detail::mdc_map(); + ndc = detail::ndc_stack(); }; }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 13a85ad..639665c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -48,10 +48,12 @@ else() memory_logger_backend_test.cpp memory_logger_concurrency_test.cpp memory_logger_integration_test.cpp + mdc_ndc_context_test.cpp os_error_macros_test.cpp otlp_http_logger_integration_test.cpp otlp_http_logger_callback_test.cpp otlp_http_logger_gzip_test.cpp + otlp_http_logger_zstd_test.cpp otlp_json_serializer_test.cpp otlp_payload_splitter_test.cpp otlp_structured_attributes_test.cpp @@ -60,6 +62,7 @@ else() prometheus_payload_logger_test.cpp prometheus_http_server_logger_test.cpp prometheus_metric_builders_test.cpp + prometheus_registry_test.cpp per_logger_isolation_test.cpp per_logger_mixed_mode_test.cpp printf_format_macros_test.cpp @@ -82,6 +85,7 @@ else() endif() if(NOT LOGIT_WITH_ZSTD) list(REMOVE_ITEM TEST_SOURCES file_logger_zstd_compression_test.cpp) + list(REMOVE_ITEM TEST_SOURCES otlp_http_logger_zstd_test.cpp) endif() if(NOT LOGIT_WITH_FMT) list(REMOVE_ITEM TEST_SOURCES fmt_macros_test.cpp) @@ -90,6 +94,7 @@ else() list(REMOVE_ITEM TEST_SOURCES otlp_http_logger_integration_test.cpp) list(REMOVE_ITEM TEST_SOURCES otlp_http_logger_callback_test.cpp) list(REMOVE_ITEM TEST_SOURCES otlp_http_logger_gzip_test.cpp) + list(REMOVE_ITEM TEST_SOURCES otlp_http_logger_zstd_test.cpp) list(REMOVE_ITEM TEST_SOURCES otlp_structured_attributes_test.cpp) list(REMOVE_ITEM TEST_SOURCES otlp_payload_splitter_test.cpp) list(REMOVE_ITEM TEST_SOURCES otlp_payload_logger_test.cpp) @@ -99,6 +104,7 @@ else() list(REMOVE_ITEM TEST_SOURCES prometheus_payload_logger_test.cpp) list(REMOVE_ITEM TEST_SOURCES prometheus_http_server_logger_test.cpp) list(REMOVE_ITEM TEST_SOURCES prometheus_metric_builders_test.cpp) + list(REMOVE_ITEM TEST_SOURCES prometheus_registry_test.cpp) endif() if(LOGIT_WITH_PROMETHEUS AND NOT LOGIT_WITH_PROMETHEUS_SERVER) list(REMOVE_ITEM TEST_SOURCES prometheus_http_server_logger_test.cpp) @@ -108,7 +114,7 @@ else() add_executable(${test_name} ${test_src}) target_link_libraries(${test_name} PRIVATE log-it-cpp) add_test(NAME ${test_name} COMMAND ${test_name}) - if(LOGIT_WITH_OTLP AND test_name MATCHES "^otlp_http_logger_(integration|callback|gzip)_test$") + if(LOGIT_WITH_OTLP AND test_name MATCHES "^otlp_http_logger_(integration|callback|gzip|zstd)_test$") target_include_directories(${test_name} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../external/kurlyk/external/Simple-Web-Server") endif() diff --git a/tests/mdc_ndc_context_test.cpp b/tests/mdc_ndc_context_test.cpp new file mode 100644 index 0000000..f400601 --- /dev/null +++ b/tests/mdc_ndc_context_test.cpp @@ -0,0 +1,91 @@ +#include + +#include +#include +#include +#include +#include + +int main() { + logit::MemoryLogger::Config mem_cfg; + logit::Logger::get_instance().add_logger( + std::unique_ptr(new logit::MemoryLogger(mem_cfg)), + std::unique_ptr(new logit::SimpleLogFormatter("%v [%K] [%J] req=%K{request_id}"))); + + LOGIT_MDC_PUT("request_id", "abc123"); + LOGIT_MDC_PUT("user_id", "42"); + LOGIT_INFO("msg1"); + + LOGIT_NDC_PUSH("outer"); + LOGIT_NDC_PUSH("inner"); + LOGIT_INFO("msg2"); + + logit::MemoryLogger::Config mem_cfg2; + logit::Logger::get_instance().add_logger( + std::unique_ptr(new logit::MemoryLogger(mem_cfg2)), + std::unique_ptr(new logit::SimpleLogFormatter("%v req=%K{request_id}"))); + LOGIT_INFO("msg3"); + + { + LOGIT_NDC_GUARD("guard_scope"); + LOGIT_INFO("msg4"); + } + LOGIT_INFO("msg5"); + + std::thread worker([]() { + LOGIT_MDC_PUT("request_id", "other"); + LOGIT_NDC_PUSH("worker"); + LOGIT_INFO("thread_msg"); + LOGIT_MDC_CLEAR(); + LOGIT_NDC_CLEAR(); + }); + worker.join(); + + LOGIT_INFO("msg6"); + + std::vector logs0 = + logit::Logger::get_instance().get_buffered_strings(0); + std::vector logs1 = + logit::Logger::get_instance().get_buffered_strings(1); + + LOGIT_MDC_CLEAR(); + LOGIT_NDC_CLEAR(); + LOGIT_SHUTDOWN(); + + assert(logs0.size() == 7); + + assert(logs0[0].find("msg1") != std::string::npos); + assert(logs0[0].find("request_id=abc123") != std::string::npos); + assert(logs0[0].find("user_id=42") != std::string::npos); + assert(logs0[0].find("req=abc123") != std::string::npos); + assert(logs0[0].find("[]") != std::string::npos); + + assert(logs0[1].find("msg2") != std::string::npos); + assert(logs0[1].find("outer > inner") != std::string::npos); + + assert(logs0[2].find("msg3") != std::string::npos); + + assert(logs0[3].find("msg4") != std::string::npos); + assert(logs0[3].find("guard_scope") != std::string::npos); + + assert(logs0[4].find("msg5") != std::string::npos); + assert(logs0[4].find("guard_scope") == std::string::npos); + + assert(logs0[5].find("thread_msg") != std::string::npos); + assert(logs0[5].find("request_id=other") != std::string::npos); + assert(logs0[5].find("worker") != std::string::npos); + + assert(logs0[6].find("msg6") != std::string::npos); + assert(logs0[6].find("request_id=abc123") != std::string::npos); + assert(logs0[6].find("request_id=other") == std::string::npos); + + assert(logs1.size() == 5); + assert(logs1[0].find("msg3") != std::string::npos); + assert(logs1[0].find("req=abc123") != std::string::npos); + assert(logs1[3].find("thread_msg") != std::string::npos); + assert(logs1[3].find("req=other") != std::string::npos); + assert(logs1[4].find("msg6") != std::string::npos); + assert(logs1[4].find("req=abc123") != std::string::npos); + + return 0; +} diff --git a/tests/otlp_http_logger_callback_test.cpp b/tests/otlp_http_logger_callback_test.cpp index 0a12fc5..18097a5 100644 --- a/tests/otlp_http_logger_callback_test.cpp +++ b/tests/otlp_http_logger_callback_test.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -168,7 +169,53 @@ int main() { stop_server(server, server_thread); } - // Test c: max_in_flight_requests=2 parallelism + // Test c: graceful shutdown drains queued backlog while in-flight is full + { + RequestCounter counter; + counter.delay_response = true; + counter.delay_ms = 300; + HttpServer server; + std::thread server_thread; + start_server(server, server_thread, counter, port); + + logit::OtlpHttpLogger::Config config; + config.host = "http://127.0.0.1:" + std::to_string(port); + config.path = "/v1/logs"; + config.format.service_name = "callback-test"; + config.max_batch_size = 1; + config.max_in_flight_requests = 1; + config.export_interval_ms = 10; + config.request_timeout_sec = 5; + config.cancel_on_shutdown = false; + + auto logger = std::unique_ptr(new logit::OtlpHttpLogger(config)); + + logit::LogRecord record1( + logit::LogLevel::LOG_LVL_WARN, 1710000000123LL, + "test.cpp", 120, "test_func", "shutdown backlog 1", "", + -1, false, false, false); + logit::LogRecord record2( + logit::LogLevel::LOG_LVL_WARN, 1710000000124LL, + "test.cpp", 121, "test_func", "shutdown backlog 2", "", + -1, false, false, false); + + logger->log(record1, "shutdown backlog msg 1"); + logger->log(record2, "shutdown backlog msg 2"); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto start = std::chrono::steady_clock::now(); + logger->shutdown(); + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start).count(); + + assert(counter.count.load() == 2); + assert(elapsed >= 300); + + stop_server(server, server_thread); + } + + // Test d: max_in_flight_requests=2 parallelism { RequestCounter counter; counter.delay_response = true; @@ -205,7 +252,7 @@ int main() { stop_server(server, server_thread); } - // Test d: HTTP 500 failure counting + // Test e: HTTP 500 failure counting { RequestCounter counter; HttpServer server; @@ -244,7 +291,7 @@ int main() { stop_server(server, server_thread); } - // Test e: wait() waits for callbacks + // Test f: wait() waits for callbacks { RequestCounter counter; counter.delay_response = true; @@ -281,7 +328,7 @@ int main() { stop_server(server, server_thread); } - // Test f: Graceful shutdown no UAF + // Test g: Graceful shutdown no UAF { RequestCounter counter; counter.delay_response = true; @@ -320,7 +367,7 @@ int main() { stop_server(server, server_thread); } - // Test g: cancel_on_shutdown=true fast shutdown + // Test h: cancel_on_shutdown=true fast shutdown { RequestCounter counter; counter.delay_response = true; @@ -359,7 +406,7 @@ int main() { stop_server(server, server_thread); } - // Test h: payload splitting produces multiple POSTs with small max_payload_bytes + // Test i: payload splitting produces multiple POSTs with small max_payload_bytes { RequestCounter counter; HttpServer server; diff --git a/tests/otlp_http_logger_zstd_test.cpp b/tests/otlp_http_logger_zstd_test.cpp new file mode 100644 index 0000000..42e7097 --- /dev/null +++ b/tests/otlp_http_logger_zstd_test.cpp @@ -0,0 +1,160 @@ +#include + +#if defined(LOGIT_WITH_OTLP) && defined(LOGIT_HAS_ZSTD) + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using HttpServer = SimpleWeb::Server; + +namespace { + +struct RequestCapture { + std::mutex mutex; + std::condition_variable cv; + std::atomic count{0}; + std::string last_body; + bool has_content_encoding_zstd = false; +}; + +bool wait_for_server(unsigned short port) { + for (int i = 0; i < 50; ++i) { + try { + kurlyk::HttpClient client("http://127.0.0.1:" + std::to_string(port)); + client.set_timeout(1); + auto future = client.get("/health", {}, {}); + if (future.wait_for(std::chrono::seconds(2)) == std::future_status::ready) { + auto response = future.get(); + if (response && response->status_code == 200) { + return true; + } + } + } catch (...) { + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + return false; +} + +void start_server(HttpServer& server, std::thread& thread, RequestCapture& capture, unsigned short port) { + server.config.port = port; + + server.resource["^/health$"]["GET"] = [](std::shared_ptr response, + std::shared_ptr) { + response->write(SimpleWeb::StatusCode::success_ok, "ok"); + }; + + server.resource["^/v1/logs$"]["POST"] = [&capture](std::shared_ptr response, + std::shared_ptr request) { + { + std::lock_guard lock(capture.mutex); + capture.last_body = request->content.string(); + + auto it = request->header.find("Content-Encoding"); + if (it != request->header.end() && it->second == "zstd") { + capture.has_content_encoding_zstd = true; + } + + capture.count.fetch_add(1); + } + capture.cv.notify_all(); + + response->write(SimpleWeb::StatusCode::success_ok, "{}"); + }; + + thread = std::thread([&server]() { + server.start(); + }); + + assert(wait_for_server(port)); +} + +void stop_server(HttpServer& server, std::thread& thread) { + server.stop(); + if (thread.joinable()) { + thread.join(); + } +} + +} // namespace + +int main() { + const unsigned short port = 43183; + + RequestCapture capture; + HttpServer server; + std::thread server_thread; + start_server(server, server_thread, capture, port); + + logit::OtlpHttpLogger::Config config; + config.host = "http://127.0.0.1:" + std::to_string(port); + config.path = "/v1/logs"; + config.format.service_name = "zstd-test"; + config.compression = logit::OtlpCompression::Zstd; + config.compression_level = 3; + config.max_batch_size = 256; + config.export_interval_ms = 50; + config.request_timeout_sec = 2; + + auto logger = std::unique_ptr(new logit::OtlpHttpLogger(config)); + + for (int i = 0; i < 3; ++i) { + logit::LogRecord record( + logit::LogLevel::LOG_LVL_WARN, 1710000000123LL + i, + "test.cpp", 100 + i, "test_func", "zstd test", "", + -1, false, false, false); + logger->log(record, "zstd compression test message " + std::to_string(i)); + } + + logger->wait(); + logger->shutdown(); + + { + std::unique_lock lock(capture.mutex); + capture.cv.wait_for(lock, std::chrono::seconds(3), [&capture]() { + return capture.count.load() >= 1; + }); + } + + assert(capture.count.load() >= 1); + assert(capture.has_content_encoding_zstd); + assert(capture.last_body.find("resourceLogs") == std::string::npos); + + unsigned long long raw_size = + ZSTD_getFrameContentSize(capture.last_body.data(), capture.last_body.size()); + assert(raw_size != ZSTD_CONTENTSIZE_ERROR); + assert(raw_size != ZSTD_CONTENTSIZE_UNKNOWN); + + std::vector decompressed(static_cast(raw_size)); + std::size_t result = ZSTD_decompress( + decompressed.data(), decompressed.size(), + capture.last_body.data(), capture.last_body.size()); + assert(!ZSTD_isError(result)); + + std::string json(decompressed.data(), result); + assert(json.find("\"resourceLogs\"") != std::string::npos); + assert(json.find("zstd compression test message") != std::string::npos); + + stop_server(server, server_thread); + + return 0; +} + +#else + +int main() { + return 0; +} + +#endif diff --git a/tests/prometheus_registry_test.cpp b/tests/prometheus_registry_test.cpp new file mode 100644 index 0000000..857ac7b --- /dev/null +++ b/tests/prometheus_registry_test.cpp @@ -0,0 +1,140 @@ +#include +#include + +#ifdef LOGIT_WITH_PROMETHEUS + +#include +#include +#include +#include + +int main() { + // Test 1: gauge callback is collected with the current value + { + int queue_size = 3; + logit::PrometheusRegistry registry; + registry.set_gauge( + "app_queue_size", + "Current application queue size", + [&queue_size]() { return static_cast(queue_size); }); + + std::vector families; + registry.collect(families); + + assert(families.size() == 1); + assert(families[0].name == "app_queue_size"); + assert(families[0].type == logit::PrometheusMetricType::Gauge); + assert(families[0].samples.size() == 1); + assert(families[0].samples[0].value == 3.0); + + queue_size = 7; + families.clear(); + registry.collect(families); + assert(families[0].samples[0].value == 7.0); + } + + // Test 2: counter callback and labels are preserved + { + logit::PrometheusRegistry registry; + registry.set_counter( + "requests_total", + "Total requests", + []() { return 12.0; }, + {{"method", "GET"}}); + + std::vector families; + registry.collect(families); + + assert(families.size() == 1); + assert(families[0].type == logit::PrometheusMetricType::Counter); + assert(families[0].samples[0].labels.size() == 1); + assert(families[0].samples[0].labels[0].name == "method"); + assert(families[0].samples[0].labels[0].value == "GET"); + } + + // Test 3: same raw name and labels replace the existing series + { + logit::PrometheusRegistry registry; + registry.set_gauge("worker_load", "Worker load", []() { return 1.0; }, {{"worker", "a"}}); + registry.set_gauge("worker_load", "Worker load", []() { return 2.0; }, {{"worker", "a"}}); + + std::vector families; + registry.collect(families); + + assert(families.size() == 1); + assert(families[0].samples.size() == 1); + assert(families[0].samples[0].value == 2.0); + } + + // Test 4: same raw name with different labels is grouped into one family + { + logit::PrometheusRegistry registry; + registry.set_gauge("worker_load", "Worker load", []() { return 1.0; }, {{"worker", "a"}}); + registry.set_gauge("worker_load", "Worker load", []() { return 2.0; }, {{"worker", "b"}}); + + std::vector families; + registry.collect(families); + + assert(families.size() == 1); + assert(families[0].samples.size() == 2); + assert(families[0].samples[0].labels[0].value == "a"); + assert(families[0].samples[1].labels[0].value == "b"); + } + + // Test 5: registry prefix is independent from serializer config + { + logit::PrometheusRegistry registry("myapp_"); + registry.set_metric_prefix("myapp_"); + assert(registry.metric_prefix() == "myapp_"); + + registry.set_gauge( + "queue_size", + "Queue size", + []() { return 5.0; }, + {{"component", "api"}}); + + std::vector families; + registry.collect(families); + + assert(families.size() == 1); + assert(families[0].name == "myapp_queue_size"); + assert(families[0].samples[0].name == "myapp_queue_size"); + + logit::PrometheusTextFormatConfig config; + config.metric_prefix = "logit_"; + std::string payload = logit::build_prometheus_text_payload(families, config); + + assert(payload.find("# HELP myapp_queue_size Queue size") != std::string::npos); + assert(payload.find("# TYPE myapp_queue_size gauge") != std::string::npos); + assert(payload.find("myapp_queue_size{component=\"api\"} 5") != std::string::npos); + assert(payload.find("logit_myapp_queue_size") == std::string::npos); + } + + // Test 6: value callback exceptions propagate to caller + { + logit::PrometheusRegistry registry; + registry.set_gauge("broken_metric", "Broken metric", []() -> double { + throw std::runtime_error("broken metric"); + }); + + std::vector families; + bool caught = false; + try { + registry.collect(families); + } catch (const std::runtime_error&) { + caught = true; + } + + assert(caught); + } + + return 0; +} + +#else + +int main() { + return 0; +} + +#endif From 32cbe04feada72aa8b4f6d2cba3f9698019521b6 Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Thu, 28 May 2026 14:46:36 +0300 Subject: [PATCH 3/5] fix(context): make MDC and NDC opt-in Add LOGIT_WITH_CONTEXT so diagnostic context support does not affect the default logging hot path. Store context as an optional shared snapshot only when the thread context is non-empty, and keep ASan builds away from intentionally leaked thread-local storage. --- CMakeLists.txt | 7 +- README.md | 15 +- docs/OtlpHttpLogger.md | 4 +- examples/example_logit_mdc_ndc.cpp | 7 + examples/example_logit_otlp_http.cpp | 10 ++ include/logit_cpp/logit/detail/LogContext.hpp | 139 +++++++++++++++--- .../formatter/compiler/PatternCompiler.hpp | 30 +++- include/logit_cpp/logit/log_macros.hpp | 10 ++ include/logit_cpp/logit/utils/LogRecord.hpp | 14 +- tests/CMakeLists.txt | 3 + tests/mdc_ndc_context_test.cpp | 4 + 11 files changed, 206 insertions(+), 37 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ccf588b..66a7c8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,7 @@ option(LOGIT_BENCH_WITH_SPDLOG "Enable spdlog comparison benchmarks" OFF) option(LOGIT_WITH_GZIP "Enable gzip via zlib" OFF) option(LOGIT_WITH_ZSTD "Enable zstd" OFF) option(LOGIT_WITH_FMT "Enable fmt support" OFF) +option(LOGIT_WITH_CONTEXT "Enable MDC/NDC diagnostic context support" OFF) option(LOGIT_WITH_OTLP "Enable OTLP/HTTP log export via optional kurlyk dependency" OFF) option(LOGIT_WITH_PROMETHEUS "Enable Prometheus text payload support" OFF) option(LOGIT_WITH_PROMETHEUS_SERVER "Enable Prometheus HTTP server backend" OFF) @@ -73,6 +74,10 @@ if(LOGIT_FORCE_ASYNC_OFF) target_compile_definitions(log-it-cpp INTERFACE LOGIT_DEFAULT_ASYNC_OFF=1) endif() +if(LOGIT_WITH_CONTEXT) + target_compile_definitions(log-it-cpp INTERFACE LOGIT_WITH_CONTEXT=1) +endif() + if(LOGIT_WITH_SYSLOG AND (UNIX OR APPLE) AND NOT EMSCRIPTEN) target_compile_definitions(log-it-cpp INTERFACE LOGIT_HAS_SYSLOG=1) endif() @@ -298,4 +303,4 @@ write_basic_package_version_file( install(FILES "${CMAKE_CURRENT_BINARY_DIR}/log-it-cpp.pc" DESTINATION share/pkgconfig - ) \ No newline at end of file + ) diff --git a/README.md b/README.md index 433a4ff..d9dffe8 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,12 @@ For a standalone program that brings everything together and intentionally abort Mapped diagnostic context (MDC) stores thread-local key-value pairs, while nested diagnostic context (NDC) stores a thread-local stack of scope names. The context -is captured into each `LogRecord` when the record is created. +is available when the library is configured with `-DLOGIT_WITH_CONTEXT=ON`. +When this option is off, `LogRecord` keeps the original hot-path shape and the +context macros become no-ops. + +With context enabled, a `LogRecord` captures a shared snapshot only when the +current thread has non-empty MDC or NDC values. ```cpp #include @@ -818,8 +823,8 @@ public: | `LOGIT__EVERY_N(n, ...)` | Log on every `n`th invocation. | | `LOGIT__THROTTLE(period_ms, ...)` | Log at most once per `period_ms` milliseconds. | | `LOGIT__TAG(({{"k", "v"}}), msg)` | Attach key-value tags to a message. | -| `LOGIT_MDC_PUT(key, value)`, `LOGIT_MDC_REMOVE(key)`, `LOGIT_MDC_CLEAR()` | Manage thread-local mapped diagnostic context. | -| `LOGIT_NDC_PUSH(value)`, `LOGIT_NDC_POP()`, `LOGIT_NDC_CLEAR()`, `LOGIT_NDC_GUARD(value)` | Manage thread-local nested diagnostic context. | +| `LOGIT_MDC_PUT(key, value)`, `LOGIT_MDC_REMOVE(key)`, `LOGIT_MDC_CLEAR()` | Manage thread-local mapped diagnostic context when `LOGIT_WITH_CONTEXT` is enabled. | +| `LOGIT_NDC_PUSH(value)`, `LOGIT_NDC_POP()`, `LOGIT_NDC_CLEAR()`, `LOGIT_NDC_GUARD(value)` | Manage thread-local nested diagnostic context when `LOGIT_WITH_CONTEXT` is enabled. | | `LOGIT_RAW(msg)`, `LOGIT_RAW_TO(index, msg)`, `LOGIT_RAW_IF(condition, msg)` | Write already formatted text without applying level filters or formatter patterns. | | `LOGIT_SECTION(name)`, `LOGIT_SECTION_TO(index, name)`, `LOGIT_SECTION_IF(condition, name)` | Write raw section headers such as `[Proxy]`. | | `LOGIT__TO(index, ...)` | Target a specific logger index, including single-mode backends. | @@ -924,7 +929,9 @@ The following toggles cover all build-time features: - `LOGIT_CPP_BUILD_EXAMPLES` (default: OFF) — build the example programs. - `LOGIT_BENCH_ENABLE` (default: OFF) — build benchmarks; `LOGIT_BENCH_WITH_SPDLOG` (default: OFF) also builds the spdlog comparisons. - `LOGIT_WITH_GZIP` / `LOGIT_WITH_ZSTD` (defaults: OFF) — enable gzip or zstd support for rotated files. -- `LOGIT_WITH_FMT` (default: OFF) — include the `{}`-style formatting macros; `LOGIT_USE_SUBMODULES` (default: OFF) allows bundled optional dependency fallbacks such as fmt, zlib, and zstd when system packages are missing. +- `LOGIT_WITH_FMT` (default: OFF) — include the `{}`-style formatting macros. +- `LOGIT_WITH_CONTEXT` (default: OFF) — enable MDC/NDC helpers and `%K`, `%K{key}`, `%J` formatter tokens. +- `LOGIT_USE_SUBMODULES` (default: OFF) allows bundled optional dependency fallbacks such as fmt, zlib, and zstd when system packages are missing. - `LOGIT_WITH_SYSLOG` (default: ON on Unix-like targets) — build the syslog backend. - `LOGIT_WITH_WIN_EVENT_LOG` (default: ON on Windows) — build the Windows Event Log backend. - `LOGIT_FORCE_ASYNC_OFF` (default: OFF) — force synchronous logging even in multi-threaded builds. diff --git a/docs/OtlpHttpLogger.md b/docs/OtlpHttpLogger.md index d9dd328..31bb499 100644 --- a/docs/OtlpHttpLogger.md +++ b/docs/OtlpHttpLogger.md @@ -19,7 +19,9 @@ For Windows MinGW builds, the CMake integration enables kurlyk fallback options ## Usage For a runnable version with environment overrides, graceful shutdown, optional -compression, and MDC trace/span fields, see `examples/example_logit_otlp_http.cpp`. +compression, and optional MDC trace/span fields, see +`examples/example_logit_otlp_http.cpp`. Build with `-DLOGIT_WITH_CONTEXT=ON` +if you want the MDC trace/span fields to be populated. ```cpp #include diff --git a/examples/example_logit_mdc_ndc.cpp b/examples/example_logit_mdc_ndc.cpp index ad67544..ebc4171 100644 --- a/examples/example_logit_mdc_ndc.cpp +++ b/examples/example_logit_mdc_ndc.cpp @@ -3,6 +3,12 @@ #include int main() { +#ifndef LOGIT_WITH_CONTEXT + LOGIT_ADD_CONSOLE_DEFAULT(); + LOGIT_WARN("MDC/NDC example requires LOGIT_WITH_CONTEXT=ON"); + LOGIT_WAIT(); + return 0; +#else LOGIT_ADD_CONSOLE( "[%T] [%l] request=%K{request_id} user=%K{user_id} ndc=[%J] %v", false); @@ -41,4 +47,5 @@ int main() { LOGIT_NDC_CLEAR(); LOGIT_SHUTDOWN(); return 0; +#endif } diff --git a/examples/example_logit_otlp_http.cpp b/examples/example_logit_otlp_http.cpp index 4486532..8cd812d 100644 --- a/examples/example_logit_otlp_http.cpp +++ b/examples/example_logit_otlp_http.cpp @@ -47,24 +47,34 @@ int main() { logit::OtlpHttpLogger, (config), logit::SimpleLogFormatter, +#ifdef LOGIT_WITH_CONTEXT ("[%l] trace=%K{trace_id} span=%K{span_id} %v") +#else + ("[%l] %v") +#endif ); +#ifdef LOGIT_WITH_CONTEXT LOGIT_MDC_PUT("trace_id", "7b3f1c8a2e914a99"); LOGIT_MDC_PUT("span_id", "checkout-001"); LOGIT_NDC_PUSH("checkout"); +#endif LOGIT_INFO("OTLP logger started"); LOGIT_WARN("payment provider latency is above threshold"); { +#ifdef LOGIT_WITH_CONTEXT LOGIT_NDC_GUARD("submit-order"); +#endif LOGIT_ERROR("order export failed; collector will count failed exports if HTTP fails"); } LOGIT_WAIT(); +#ifdef LOGIT_WITH_CONTEXT LOGIT_MDC_CLEAR(); LOGIT_NDC_CLEAR(); +#endif LOGIT_SHUTDOWN(); return 0; #endif diff --git a/include/logit_cpp/logit/detail/LogContext.hpp b/include/logit_cpp/logit/detail/LogContext.hpp index ffa57cf..548aabd 100644 --- a/include/logit_cpp/logit/detail/LogContext.hpp +++ b/include/logit_cpp/logit/detail/LogContext.hpp @@ -6,34 +6,106 @@ /// \brief Thread-local Mapped Diagnostic Context (MDC) and Nested Diagnostic Context (NDC). #include +#include #include #include +#ifndef LOGIT_HAS_FEATURE +#if defined(__has_feature) +#define LOGIT_HAS_FEATURE(x) __has_feature(x) +#else +#define LOGIT_HAS_FEATURE(x) 0 +#endif +#endif + #ifndef LOGIT_DISABLE_THREAD_LOCAL_DESTRUCTORS +#if defined(__SANITIZE_ADDRESS__) || LOGIT_HAS_FEATURE(address_sanitizer) +#define LOGIT_DISABLE_THREAD_LOCAL_DESTRUCTORS 0 +#else #define LOGIT_DISABLE_THREAD_LOCAL_DESTRUCTORS 1 #endif +#endif namespace logit { +#ifdef LOGIT_WITH_CONTEXT + + /// \struct LogContextSnapshot + /// \brief Immutable MDC/NDC snapshot captured by a log record. + struct LogContextSnapshot { + std::map mdc; ///< Mapped Diagnostic Context. + std::vector ndc; ///< Nested Diagnostic Context. + }; + namespace detail { - inline std::map& mdc_map() { # if LOGIT_DISABLE_THREAD_LOCAL_DESTRUCTORS - thread_local auto* instance = new std::map(); - return *instance; + + inline std::map*& mdc_map_storage() { + static thread_local std::map* instance = nullptr; + return instance; + } + + inline std::vector*& ndc_stack_storage() { + static thread_local std::vector* instance = nullptr; + return instance; + } + # else - thread_local std::map instance; + + inline std::unique_ptr >& mdc_map_storage() { + static thread_local std::unique_ptr > instance; + return instance; + } + + inline std::unique_ptr >& ndc_stack_storage() { + static thread_local std::unique_ptr > instance; return instance; + } + +# endif + + inline std::map& mutable_mdc_map() { +# if LOGIT_DISABLE_THREAD_LOCAL_DESTRUCTORS + if (!mdc_map_storage()) { + mdc_map_storage() = new std::map(); + } + return *mdc_map_storage(); +# else + if (!mdc_map_storage()) { + mdc_map_storage().reset(new std::map()); + } + return *mdc_map_storage(); # endif } - inline std::vector& ndc_stack() { + inline std::vector& mutable_ndc_stack() { # if LOGIT_DISABLE_THREAD_LOCAL_DESTRUCTORS - thread_local auto* instance = new std::vector(); - return *instance; + if (!ndc_stack_storage()) { + ndc_stack_storage() = new std::vector(); + } + return *ndc_stack_storage(); # else - thread_local std::vector instance; - return instance; + if (!ndc_stack_storage()) { + ndc_stack_storage().reset(new std::vector()); + } + return *ndc_stack_storage(); +# endif + } + + inline const std::map* mdc_map_if_exists() { +# if LOGIT_DISABLE_THREAD_LOCAL_DESTRUCTORS + return mdc_map_storage(); +# else + return mdc_map_storage().get(); +# endif + } + + inline const std::vector* ndc_stack_if_exists() { +# if LOGIT_DISABLE_THREAD_LOCAL_DESTRUCTORS + return ndc_stack_storage(); +# else + return ndc_stack_storage().get(); # endif } @@ -41,35 +113,66 @@ namespace logit { /// \brief Put a key-value pair into the MDC for the current thread. inline void mdc_put(const std::string& key, const std::string& value) { - detail::mdc_map()[key] = value; + detail::mutable_mdc_map()[key] = value; } /// \brief Remove a key from the MDC for the current thread. inline void mdc_remove(const std::string& key) { - detail::mdc_map().erase(key); + std::map* values = + const_cast*>(detail::mdc_map_if_exists()); + if (values != nullptr) { + values->erase(key); + } } /// \brief Clear all MDC entries for the current thread. inline void mdc_clear() { - detail::mdc_map().clear(); + std::map* values = + const_cast*>(detail::mdc_map_if_exists()); + if (values != nullptr) { + values->clear(); + } } /// \brief Push a message onto the NDC stack for the current thread. inline void ndc_push(const std::string& message) { - detail::ndc_stack().push_back(message); + detail::mutable_ndc_stack().push_back(message); } /// \brief Pop the top message from the NDC stack for the current thread. inline void ndc_pop() { - std::vector& stack = detail::ndc_stack(); - if (!stack.empty()) { - stack.pop_back(); + std::vector* stack = + const_cast*>(detail::ndc_stack_if_exists()); + if (stack != nullptr && !stack->empty()) { + stack->pop_back(); } } /// \brief Clear the NDC stack for the current thread. inline void ndc_clear() { - detail::ndc_stack().clear(); + std::vector* stack = + const_cast*>(detail::ndc_stack_if_exists()); + if (stack != nullptr) { + stack->clear(); + } + } + + /// \brief Captures current thread MDC/NDC if either context is non-empty. + inline std::shared_ptr capture_log_context() { + const std::map* mdc = detail::mdc_map_if_exists(); + const std::vector* ndc = detail::ndc_stack_if_exists(); + if ((mdc == nullptr || mdc->empty()) && (ndc == nullptr || ndc->empty())) { + return std::shared_ptr(); + } + + std::shared_ptr snapshot(new LogContextSnapshot()); + if (mdc != nullptr) { + snapshot->mdc = *mdc; + } + if (ndc != nullptr) { + snapshot->ndc = *ndc; + } + return snapshot; } /// \class NdcGuard @@ -93,6 +196,8 @@ namespace logit { NdcGuard& operator=(NdcGuard&&) = delete; }; +#endif // LOGIT_WITH_CONTEXT + } // namespace logit #endif // _LOGIT_LOG_CONTEXT_HPP_INCLUDED diff --git a/include/logit_cpp/logit/formatter/compiler/PatternCompiler.hpp b/include/logit_cpp/logit/formatter/compiler/PatternCompiler.hpp index 0fb539f..33c8ae0 100644 --- a/include/logit_cpp/logit/formatter/compiler/PatternCompiler.hpp +++ b/include/logit_cpp/logit/formatter/compiler/PatternCompiler.hpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -268,9 +267,13 @@ namespace logit { // Diagnostic context case FormatType::MappedDiagnosticContext: { +#ifdef LOGIT_WITH_CONTEXT + if (!record.context) { + break; + } bool first = true; - for (std::map::const_iterator it = record.mdc.begin(); - it != record.mdc.end(); + for (std::map::const_iterator it = record.context->mdc.begin(); + it != record.context->mdc.end(); ++it) { if (!first) { temp_stream << " "; @@ -278,19 +281,29 @@ namespace logit { temp_stream << it->first << "=" << it->second; first = false; } +#endif break; } case FormatType::MappedDiagnosticContextValue: { - std::map::const_iterator it = record.mdc.find(context_key); - if (it != record.mdc.end()) { - temp_stream << it->second; +#ifdef LOGIT_WITH_CONTEXT + if (record.context) { + std::map::const_iterator it = + record.context->mdc.find(context_key); + if (it != record.context->mdc.end()) { + temp_stream << it->second; + } } +#endif break; } case FormatType::NestedDiagnosticContext: { +#ifdef LOGIT_WITH_CONTEXT + if (!record.context) { + break; + } bool first = true; - for (std::vector::const_iterator it = record.ndc.begin(); - it != record.ndc.end(); + for (std::vector::const_iterator it = record.context->ndc.begin(); + it != record.context->ndc.end(); ++it) { if (!first) { temp_stream << " > "; @@ -298,6 +311,7 @@ namespace logit { temp_stream << *it; first = false; } +#endif break; } diff --git a/include/logit_cpp/logit/log_macros.hpp b/include/logit_cpp/logit/log_macros.hpp index faea688..9a6c57c 100644 --- a/include/logit_cpp/logit/log_macros.hpp +++ b/include/logit_cpp/logit/log_macros.hpp @@ -50,6 +50,7 @@ /// \param y Second token. #define LOGIT_CONCAT(x, y) LOGIT_CONCAT_IMPL(x, y) +#ifdef LOGIT_WITH_CONTEXT #define LOGIT_MDC_PUT(key, value) ::logit::mdc_put((key), (value)) #define LOGIT_MDC_REMOVE(key) ::logit::mdc_remove((key)) #define LOGIT_MDC_CLEAR() ::logit::mdc_clear() @@ -57,6 +58,15 @@ #define LOGIT_NDC_POP() ::logit::ndc_pop() #define LOGIT_NDC_CLEAR() ::logit::ndc_clear() #define LOGIT_NDC_GUARD(value) ::logit::NdcGuard LOGIT_CONCAT(_logit_ndc_guard_, __COUNTER__)((value)) +#else +#define LOGIT_MDC_PUT(key, value) do { } while (0) +#define LOGIT_MDC_REMOVE(key) do { } while (0) +#define LOGIT_MDC_CLEAR() do { } while (0) +#define LOGIT_NDC_PUSH(value) do { } while (0) +#define LOGIT_NDC_POP() do { } while (0) +#define LOGIT_NDC_CLEAR() do { } while (0) +#define LOGIT_NDC_GUARD(value) do { } while (0) +#endif #ifdef _LOGIT_ENUMS_HPP_INCLUDED static_assert(LOGIT_LEVEL_TRACE == static_cast(logit::LogLevel::LOG_LVL_TRACE), diff --git a/include/logit_cpp/logit/utils/LogRecord.hpp b/include/logit_cpp/logit/utils/LogRecord.hpp index 28690d8..06d0a78 100644 --- a/include/logit_cpp/logit/utils/LogRecord.hpp +++ b/include/logit_cpp/logit/utils/LogRecord.hpp @@ -5,7 +5,6 @@ /// \file LogRecord.hpp /// \brief Contains the definition of the LogRecord structure for storing log data. -#include #include #include #include @@ -30,8 +29,9 @@ namespace logit { const bool print_mode : 1; ///< Flag to determine whether arguments are printed in a raw format without special symbols. const bool fmt_mode : 1; ///< Flag indicating if fmt formatting should be used. const bool raw_mode : 1; ///< Flag indicating if formatter and level filters should be bypassed. - mutable std::map mdc; ///< Mapped Diagnostic Context (thread-local key-value pairs). - mutable std::vector ndc; ///< Nested Diagnostic Context (thread-local stack). +#ifdef LOGIT_WITH_CONTEXT + const std::shared_ptr context; ///< Optional MDC/NDC snapshot. +#endif /// \brief Constructor with argument names. /// \param log_level Log severity level. @@ -68,9 +68,11 @@ namespace logit { logger_index(logger_index), print_mode(print_mode), fmt_mode(fmt_mode), - raw_mode(raw_mode) { - mdc = detail::mdc_map(); - ndc = detail::ndc_stack(); + raw_mode(raw_mode) +#ifdef LOGIT_WITH_CONTEXT + , context(capture_log_context()) +#endif + { }; }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 639665c..f1a9dad 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -90,6 +90,9 @@ else() if(NOT LOGIT_WITH_FMT) list(REMOVE_ITEM TEST_SOURCES fmt_macros_test.cpp) endif() + if(NOT LOGIT_WITH_CONTEXT) + list(REMOVE_ITEM TEST_SOURCES mdc_ndc_context_test.cpp) + endif() if(NOT LOGIT_WITH_OTLP) list(REMOVE_ITEM TEST_SOURCES otlp_http_logger_integration_test.cpp) list(REMOVE_ITEM TEST_SOURCES otlp_http_logger_callback_test.cpp) diff --git a/tests/mdc_ndc_context_test.cpp b/tests/mdc_ndc_context_test.cpp index f400601..346bebe 100644 --- a/tests/mdc_ndc_context_test.cpp +++ b/tests/mdc_ndc_context_test.cpp @@ -7,6 +7,9 @@ #include int main() { +#ifndef LOGIT_WITH_CONTEXT + return 0; +#else logit::MemoryLogger::Config mem_cfg; logit::Logger::get_instance().add_logger( std::unique_ptr(new logit::MemoryLogger(mem_cfg)), @@ -88,4 +91,5 @@ int main() { assert(logs1[4].find("req=abc123") != std::string::npos); return 0; +#endif } From 318b08d40c60071320d307f2c18cde907f02e89b Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Fri, 29 May 2026 08:24:59 +0300 Subject: [PATCH 4/5] fix(prometheus): honor scrape metric label config Apply PrometheusTextFormatConfig logger and instance label settings to PrometheusHttpServerLogger scrape diagnostics. Add coverage for custom logger label names, instance labels, and disabled logger labels on scrape metrics. --- .../loggers/PrometheusHttpServerLogger.hpp | 13 ++++- tests/prometheus_http_server_logger_test.cpp | 48 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/include/logit_cpp/logit/loggers/PrometheusHttpServerLogger.hpp b/include/logit_cpp/logit/loggers/PrometheusHttpServerLogger.hpp index 59d1144..c93fa4b 100644 --- a/include/logit_cpp/logit/loggers/PrometheusHttpServerLogger.hpp +++ b/include/logit_cpp/logit/loggers/PrometheusHttpServerLogger.hpp @@ -306,8 +306,17 @@ namespace logit { } } - static void add_scrape_labels(PrometheusSample& sample) { - sample.labels.push_back({"logger", "prometheus_http_server"}); + void add_scrape_labels(PrometheusSample& sample) const { + if (m_config.format.include_logger_label) { + sample.labels.push_back({ + m_config.format.logger_label_name, + "prometheus_http_server"}); + } + if (m_config.format.include_instance_label) { + sample.labels.push_back({ + m_config.format.instance_label_name, + m_config.format.instance_label_value}); + } } void stop() { diff --git a/tests/prometheus_http_server_logger_test.cpp b/tests/prometheus_http_server_logger_test.cpp index 10f3218..23f3ceb 100644 --- a/tests/prometheus_http_server_logger_test.cpp +++ b/tests/prometheus_http_server_logger_test.cpp @@ -331,6 +331,54 @@ int main() { logger.shutdown(); } + // Test 13: scrape metrics follow configured label policy + { + logit::PrometheusHttpServerLogger::Config config; + config.port = 43203; + config.start_immediately = false; + config.format.logger_label_name = "backend"; + config.format.include_instance_label = true; + config.format.instance_label_name = "node"; + config.format.instance_label_value = "node-a"; + + logit::PrometheusHttpServerLogger logger(config); + + std::string payload = logger.collect_payload(); + + assert(payload.find( + "logit_prometheus_scrapes_total{backend=\"prometheus_http_server\",node=\"node-a\"}") != std::string::npos); + assert(payload.find( + "logit_prometheus_scrape_errors_total{backend=\"prometheus_http_server\",node=\"node-a\"}") != std::string::npos); + assert(payload.find( + "logit_prometheus_last_scrape_timestamp_ms{backend=\"prometheus_http_server\",node=\"node-a\"}") != std::string::npos); + assert(payload.find( + "logit_prometheus_collect_duration_seconds{backend=\"prometheus_http_server\",node=\"node-a\"}") != std::string::npos); + + logger.shutdown(); + } + + // Test 14: scrape metrics can suppress logger label + { + logit::PrometheusHttpServerLogger::Config config; + config.port = 43204; + config.start_immediately = false; + config.format.include_logger_label = false; + config.format.include_instance_label = true; + config.format.instance_label_name = "node"; + config.format.instance_label_value = "node-b"; + + logit::PrometheusHttpServerLogger logger(config); + + std::string payload = logger.collect_payload(); + + assert(payload.find( + "logit_prometheus_scrapes_total{node=\"node-b\"}") != std::string::npos); + assert(payload.find( + "logit_prometheus_scrapes_total{logger=") == std::string::npos); + + logger.shutdown(); + } + return 0; } From 1bfde927b2ff779eb4ef9089a19b8c65769db7ee Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Fri, 29 May 2026 09:15:59 +0300 Subject: [PATCH 5/5] fix(prometheus): keep collecting healthy registry metrics Continue collecting PrometheusRegistry entries after a value callback throws, append successfully collected samples, and then rethrow the first exception so Prometheus loggers still count the collection failure. Update registry coverage and docs for the partial-success behavior. --- docs/PrometheusLogger.md | 5 ++++ .../loggers/prometheus/PrometheusRegistry.hpp | 17 ++++++++++++- tests/prometheus_registry_test.cpp | 25 ++++++++++++++++++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/PrometheusLogger.md b/docs/PrometheusLogger.md index 70e6ae6..77c2146 100644 --- a/docs/PrometheusLogger.md +++ b/docs/PrometheusLogger.md @@ -118,6 +118,11 @@ config.on_collect = [®istry](std::vector& fami metrics. Custom metric names are written as supplied by the registry or manual builders, so use the registry prefix for application metric namespaces. +If a registry value callback throws, the registry skips that sample, keeps +collecting later metrics, appends the healthy samples, and then rethrows the +first exception. The Prometheus loggers catch that exception from `on_collect` +and increment their failed export counter. + For low-level control, `on_collect` can still append `PrometheusMetricFamily` objects directly or use helpers such as `add_prometheus_gauge()`. diff --git a/include/logit_cpp/logit/loggers/prometheus/PrometheusRegistry.hpp b/include/logit_cpp/logit/loggers/prometheus/PrometheusRegistry.hpp index d9cbdf6..cd78264 100644 --- a/include/logit_cpp/logit/loggers/prometheus/PrometheusRegistry.hpp +++ b/include/logit_cpp/logit/loggers/prometheus/PrometheusRegistry.hpp @@ -8,6 +8,7 @@ #include "PrometheusTextFormatConfig.hpp" #include +#include #include #include #include @@ -64,10 +65,21 @@ namespace logit { /// \brief Appends current registry metrics to the output vector. void collect(std::vector& out) const { std::vector collected; + std::exception_ptr first_exception; for (std::size_t i = 0; i < m_entries.size(); ++i) { const Entry& entry = m_entries[i]; const std::string full_name = m_metric_prefix + entry.name; + double value = 0.0; + + try { + value = entry.value_fn(); + } catch (...) { + if (!first_exception) { + first_exception = std::current_exception(); + } + continue; + } PrometheusMetricFamily* family = find_family(collected, full_name, entry.type); @@ -82,12 +94,15 @@ namespace logit { PrometheusSample sample; sample.name = full_name; - sample.value = entry.value_fn(); + sample.value = value; sample.labels = entry.labels; family->samples.push_back(std::move(sample)); } out.insert(out.end(), collected.begin(), collected.end()); + if (first_exception) { + std::rethrow_exception(first_exception); + } } private: diff --git a/tests/prometheus_registry_test.cpp b/tests/prometheus_registry_test.cpp index 857ac7b..9714edf 100644 --- a/tests/prometheus_registry_test.cpp +++ b/tests/prometheus_registry_test.cpp @@ -110,12 +110,22 @@ int main() { assert(payload.find("logit_myapp_queue_size") == std::string::npos); } - // Test 6: value callback exceptions propagate to caller + // Test 6: value callback exceptions report failure without dropping healthy samples { logit::PrometheusRegistry registry; + registry.set_gauge( + "healthy_metric", + "Healthy metric", + []() { return 1.0; }, + {{"slot", "before"}}); registry.set_gauge("broken_metric", "Broken metric", []() -> double { throw std::runtime_error("broken metric"); }); + registry.set_gauge( + "healthy_metric", + "Healthy metric", + []() { return 2.0; }, + {{"slot", "after"}}); std::vector families; bool caught = false; @@ -126,6 +136,19 @@ int main() { } assert(caught); + assert(families.size() == 1); + assert(families[0].name == "healthy_metric"); + assert(families[0].samples.size() == 2); + assert(families[0].samples[0].value == 1.0); + assert(families[0].samples[0].labels[0].value == "before"); + assert(families[0].samples[1].value == 2.0); + assert(families[0].samples[1].labels[0].value == "after"); + + std::string payload = logit::build_prometheus_text_payload( + families, logit::PrometheusTextFormatConfig{}); + assert(payload.find("healthy_metric{slot=\"before\"} 1") != std::string::npos); + assert(payload.find("healthy_metric{slot=\"after\"} 2") != std::string::npos); + assert(payload.find("broken_metric") == std::string::npos); } return 0;