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 8be3de6..d9dffe8 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,41 @@ 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 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 + +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 +563,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 +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 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. | @@ -879,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 8e770ad..31bb499 100644 --- a/docs/OtlpHttpLogger.md +++ b/docs/OtlpHttpLogger.md @@ -18,6 +18,11 @@ For Windows MinGW builds, the CMake integration enables kurlyk fallback options ## Usage +For a runnable version with environment overrides, graceful shutdown, optional +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/docs/PrometheusLogger.md b/docs/PrometheusLogger.md index 7bae655..77c2146 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,36 @@ 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. + +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()`. + ## 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..ebc4171 --- /dev/null +++ b/examples/example_logit_mdc_ndc.cpp @@ -0,0 +1,51 @@ +#include + +#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); + + 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; +#endif +} diff --git a/examples/example_logit_otlp_http.cpp b/examples/example_logit_otlp_http.cpp index 932ad44..8cd812d 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,62 @@ 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") +#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("Example warning message"); - LOGIT_ERROR("Example error message"); + 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/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..548aabd --- /dev/null +++ b/include/logit_cpp/logit/detail/LogContext.hpp @@ -0,0 +1,203 @@ +#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 +#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 { + +# if LOGIT_DISABLE_THREAD_LOCAL_DESTRUCTORS + + 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 + + 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& mutable_ndc_stack() { +# if LOGIT_DISABLE_THREAD_LOCAL_DESTRUCTORS + if (!ndc_stack_storage()) { + ndc_stack_storage() = new std::vector(); + } + return *ndc_stack_storage(); +# else + 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 + } + + } // 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::mutable_mdc_map()[key] = value; + } + + /// \brief Remove a key from the MDC for the current thread. + inline void mdc_remove(const std::string& 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() { + 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::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 = + 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() { + 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 + /// \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; + }; + +#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 e1fb5f2..33c8ae0 100644 --- a/include/logit_cpp/logit/formatter/compiler/PatternCompiler.hpp +++ b/include/logit_cpp/logit/formatter/compiler/PatternCompiler.hpp @@ -67,6 +67,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 +90,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 +118,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 +265,56 @@ namespace logit { temp_stream << record.thread_id; break; + // Diagnostic context + case FormatType::MappedDiagnosticContext: { +#ifdef LOGIT_WITH_CONTEXT + if (!record.context) { + break; + } + bool first = true; + for (std::map::const_iterator it = record.context->mdc.begin(); + it != record.context->mdc.end(); + ++it) { + if (!first) { + temp_stream << " "; + } + temp_stream << it->first << "=" << it->second; + first = false; + } +#endif + break; + } + case FormatType::MappedDiagnosticContextValue: { +#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.context->ndc.begin(); + it != record.context->ndc.end(); + ++it) { + if (!first) { + temp_stream << " > "; + } + temp_stream << *it; + first = false; + } +#endif + break; + } + // Color case FormatType::StartColor: if (!strip_ansi) { @@ -609,6 +667,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..9a6c57c 100644 --- a/include/logit_cpp/logit/log_macros.hpp +++ b/include/logit_cpp/logit/log_macros.hpp @@ -50,6 +50,24 @@ /// \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() +#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)) +#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), "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/PrometheusHttpServerLogger.hpp b/include/logit_cpp/logit/loggers/PrometheusHttpServerLogger.hpp index 8d7cfda..c93fa4b 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,84 @@ 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); + } + } + + 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() { if (!m_running.exchange(false)) { 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..cd78264 --- /dev/null +++ b/include/logit_cpp/logit/loggers/prometheus/PrometheusRegistry.hpp @@ -0,0 +1,175 @@ +#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 +#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; + 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); + 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 = 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: + 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..06d0a78 100644 --- a/include/logit_cpp/logit/utils/LogRecord.hpp +++ b/include/logit_cpp/logit/utils/LogRecord.hpp @@ -29,6 +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. +#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. @@ -65,7 +68,11 @@ namespace logit { logger_index(logger_index), print_mode(print_mode), fmt_mode(fmt_mode), - raw_mode(raw_mode) { + raw_mode(raw_mode) +#ifdef LOGIT_WITH_CONTEXT + , context(capture_log_context()) +#endif + { }; }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 13a85ad..f1a9dad 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,14 +85,19 @@ 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) 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) 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 +107,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 +117,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..346bebe --- /dev/null +++ b/tests/mdc_ndc_context_test.cpp @@ -0,0 +1,95 @@ +#include + +#include +#include +#include +#include +#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)), + 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; +#endif +} 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_http_server_logger_test.cpp b/tests/prometheus_http_server_logger_test.cpp index 2d895b4..23f3ceb 100644 --- a/tests/prometheus_http_server_logger_test.cpp +++ b/tests/prometheus_http_server_logger_test.cpp @@ -270,6 +270,115 @@ 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(); + } + + // 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; } diff --git a/tests/prometheus_registry_test.cpp b/tests/prometheus_registry_test.cpp new file mode 100644 index 0000000..9714edf --- /dev/null +++ b/tests/prometheus_registry_test.cpp @@ -0,0 +1,163 @@ +#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 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; + try { + registry.collect(families); + } catch (const std::runtime_error&) { + caught = true; + } + + 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; +} + +#else + +int main() { + return 0; +} + +#endif