Skip to content

Commit c69733c

Browse files
LimiNodeclaude
andauthored
feat(logging): add telemetry registry and context examples
* 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 <[email protected]> * 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. * 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. * 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. * 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. --------- Co-authored-by: Claude Opus 4.7 <[email protected]>
1 parent 98cea64 commit c69733c

23 files changed

Lines changed: 1446 additions & 49 deletions

CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ option(LOGIT_BENCH_WITH_SPDLOG "Enable spdlog comparison benchmarks" OFF)
88
option(LOGIT_WITH_GZIP "Enable gzip via zlib" OFF)
99
option(LOGIT_WITH_ZSTD "Enable zstd" OFF)
1010
option(LOGIT_WITH_FMT "Enable fmt support" OFF)
11+
option(LOGIT_WITH_CONTEXT "Enable MDC/NDC diagnostic context support" OFF)
1112
option(LOGIT_WITH_OTLP "Enable OTLP/HTTP log export via optional kurlyk dependency" OFF)
1213
option(LOGIT_WITH_PROMETHEUS "Enable Prometheus text payload support" OFF)
1314
option(LOGIT_WITH_PROMETHEUS_SERVER "Enable Prometheus HTTP server backend" OFF)
@@ -73,6 +74,10 @@ if(LOGIT_FORCE_ASYNC_OFF)
7374
target_compile_definitions(log-it-cpp INTERFACE LOGIT_DEFAULT_ASYNC_OFF=1)
7475
endif()
7576

77+
if(LOGIT_WITH_CONTEXT)
78+
target_compile_definitions(log-it-cpp INTERFACE LOGIT_WITH_CONTEXT=1)
79+
endif()
80+
7681
if(LOGIT_WITH_SYSLOG AND (UNIX OR APPLE) AND NOT EMSCRIPTEN)
7782
target_compile_definitions(log-it-cpp INTERFACE LOGIT_HAS_SYSLOG=1)
7883
endif()
@@ -298,4 +303,4 @@ write_basic_package_version_file(
298303
install(FILES
299304
"${CMAKE_CURRENT_BINARY_DIR}/log-it-cpp.pc"
300305
DESTINATION share/pkgconfig
301-
)
306+
)

README.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ Internal headers under `logit/detail/` are private implementation details and sh
4646

4747
See the macro examples below or browse the `examples/` folder for focused demonstrations, including queue tuning and crash handling.
4848

49+
Recent focused examples include:
50+
51+
- `examples/example_logit_otlp_http.cpp` - OTLP/HTTP export with batching, retries, optional compression, and contextual trace/span fields.
52+
- `examples/example_logit_prometheus_payload.cpp` - callback-based Prometheus payload emission with custom registry metrics.
53+
- `examples/example_logit_prometheus_server.cpp` - embedded `/metrics` endpoint with built-in and application metrics.
54+
- `examples/example_logit_mdc_ndc.cpp` - mapped and nested diagnostic context across scopes and threads.
55+
4956
## Macro Examples
5057

5158
### Long-form macros
@@ -100,6 +107,41 @@ void short_names_demo() {
100107

101108
For a standalone program that brings everything together and intentionally aborts after logging a fatal message, check `examples/example_logit_minimal_crash.cpp`.
102109

110+
### Diagnostic context
111+
112+
Mapped diagnostic context (MDC) stores thread-local key-value pairs, while nested
113+
diagnostic context (NDC) stores a thread-local stack of scope names. The context
114+
is available when the library is configured with `-DLOGIT_WITH_CONTEXT=ON`.
115+
When this option is off, `LogRecord` keeps the original hot-path shape and the
116+
context macros become no-ops.
117+
118+
With context enabled, a `LogRecord` captures a shared snapshot only when the
119+
current thread has non-empty MDC or NDC values.
120+
121+
```cpp
122+
#include <logit.hpp>
123+
124+
int main() {
125+
LOGIT_ADD_LOGGER(
126+
logit::ConsoleLogger, (),
127+
logit::SimpleLogFormatter,
128+
("[%T] request=%K{request_id} ndc=[%J] %v")
129+
);
130+
131+
LOGIT_MDC_PUT("request_id", "req-42");
132+
LOGIT_NDC_PUSH("checkout");
133+
134+
{
135+
LOGIT_NDC_GUARD("payment");
136+
LOGIT_INFO("charge started");
137+
}
138+
139+
LOGIT_MDC_CLEAR();
140+
LOGIT_NDC_CLEAR();
141+
LOGIT_WAIT();
142+
}
143+
```
144+
103145
### System error helpers
104146

105147
`LOGIT_SYSERR_<LEVEL>` 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_<LEVEL>` and `LOGIT_WINERR_<LEVEL>` 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:
521563
- *Thread Flags*:
522564

523565
- `%t`: Thread identifier
566+
567+
- *Diagnostic Context Flags*:
568+
569+
- `%K`: All mapped diagnostic context values as `key=value` pairs
570+
- `%K{key}`: One mapped diagnostic context value by key
571+
- `%J`: Nested diagnostic context stack
524572

525573
- *Color Flags*:
526574

@@ -775,6 +823,8 @@ public:
775823
| `LOGIT_<LEVEL>_EVERY_N(n, ...)` | Log on every `n`th invocation. |
776824
| `LOGIT_<LEVEL>_THROTTLE(period_ms, ...)` | Log at most once per `period_ms` milliseconds. |
777825
| `LOGIT_<LEVEL>_TAG(({{"k", "v"}}), msg)` | Attach key-value tags to a message. |
826+
| `LOGIT_MDC_PUT(key, value)`, `LOGIT_MDC_REMOVE(key)`, `LOGIT_MDC_CLEAR()` | Manage thread-local mapped diagnostic context when `LOGIT_WITH_CONTEXT` is enabled. |
827+
| `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. |
778828
| `LOGIT_RAW(msg)`, `LOGIT_RAW_TO(index, msg)`, `LOGIT_RAW_IF(condition, msg)` | Write already formatted text without applying level filters or formatter patterns. |
779829
| `LOGIT_SECTION(name)`, `LOGIT_SECTION_TO(index, name)`, `LOGIT_SECTION_IF(condition, name)` | Write raw section headers such as `[Proxy]`. |
780830
| `LOGIT_<LEVEL>_TO(index, ...)` | Target a specific logger index, including single-mode backends. |
@@ -879,7 +929,9 @@ The following toggles cover all build-time features:
879929
- `LOGIT_CPP_BUILD_EXAMPLES` (default: OFF) — build the example programs.
880930
- `LOGIT_BENCH_ENABLE` (default: OFF) — build benchmarks; `LOGIT_BENCH_WITH_SPDLOG` (default: OFF) also builds the spdlog comparisons.
881931
- `LOGIT_WITH_GZIP` / `LOGIT_WITH_ZSTD` (defaults: OFF) — enable gzip or zstd support for rotated files.
882-
- `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.
932+
- `LOGIT_WITH_FMT` (default: OFF) — include the `{}`-style formatting macros.
933+
- `LOGIT_WITH_CONTEXT` (default: OFF) — enable MDC/NDC helpers and `%K`, `%K{key}`, `%J` formatter tokens.
934+
- `LOGIT_USE_SUBMODULES` (default: OFF) allows bundled optional dependency fallbacks such as fmt, zlib, and zstd when system packages are missing.
883935
- `LOGIT_WITH_SYSLOG` (default: ON on Unix-like targets) — build the syslog backend.
884936
- `LOGIT_WITH_WIN_EVENT_LOG` (default: ON on Windows) — build the Windows Event Log backend.
885937
- `LOGIT_FORCE_ASYNC_OFF` (default: OFF) — force synchronous logging even in multi-threaded builds.

docs/OtlpHttpLogger.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ For Windows MinGW builds, the CMake integration enables kurlyk fallback options
1818

1919
## Usage
2020

21+
For a runnable version with environment overrides, graceful shutdown, optional
22+
compression, and optional MDC trace/span fields, see
23+
`examples/example_logit_otlp_http.cpp`. Build with `-DLOGIT_WITH_CONTEXT=ON`
24+
if you want the MDC trace/span fields to be populated.
25+
2126
```cpp
2227
#include <logit.hpp>
2328

docs/PrometheusLogger.md

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,16 @@ LogIt++ provides two Prometheus backends for exposing internal log metrics in th
2424
| `logit_time_since_last_log_ms` | gauge | Time since last log (ms) |
2525
| `logit_build_info` | gauge | Build info (value=1, labels: version, compiler) |
2626

27-
The `metric_prefix` config option (default: `logit_`) is applied to all metric names.
27+
The `metric_prefix` config option (default: `logit_`) is applied to built-in logger metric names.
28+
29+
`PrometheusHttpServerLogger` also exposes scrape diagnostics:
30+
31+
| Metric | Type | Description |
32+
|--------|------|-------------|
33+
| `logit_prometheus_scrapes_total` | counter | Total `/metrics` scrape requests |
34+
| `logit_prometheus_scrape_errors_total` | counter | Failed scrape requests |
35+
| `logit_prometheus_last_scrape_timestamp_ms` | gauge | Timestamp of the last scrape request |
36+
| `logit_prometheus_collect_duration_seconds` | gauge | Duration of the last metrics collection |
2837

2938
## CMake Options
3039

@@ -38,6 +47,9 @@ option(LOGIT_WITH_PROMETHEUS_SERVER "Enable Prometheus HTTP server backend" OFF)
3847

3948
## Usage: PrometheusPayloadLogger
4049

50+
For a runnable callback example with custom application metrics, see
51+
`examples/example_logit_prometheus_payload.cpp`.
52+
4153
```cpp
4254
#include <logit.hpp>
4355

@@ -61,6 +73,9 @@ LOGIT_WAIT(); // triggers on_payload with current metrics
6173

6274
## Usage: PrometheusHttpServerLogger
6375

76+
For a runnable embedded `/metrics` server example, see
77+
`examples/example_logit_prometheus_server.cpp`.
78+
6479
```cpp
6580
#include <logit.hpp>
6681

@@ -81,22 +96,36 @@ LOGIT_INFO("Server started");
8196

8297
## Custom Metrics
8398

84-
Use the `on_collect` callback to add application-specific metrics on each scrape:
99+
Use `PrometheusRegistry` with the `on_collect` callback to register
100+
application-specific metrics once and collect them on each scrape:
85101

86102
```cpp
87-
config.on_collect = [](std::vector<logit::PrometheusMetricFamily>& families) {
88-
logit::PrometheusMetricFamily mf;
89-
mf.name = "myapp_queue_size";
90-
mf.help = "Current queue depth";
91-
mf.type = logit::PrometheusMetricType::Gauge;
92-
logit::PrometheusSample s;
93-
s.name = "myapp_queue_size";
94-
s.value = get_queue_depth();
95-
mf.samples.push_back(s);
96-
families.push_back(mf);
103+
#include <logit/loggers/prometheus/PrometheusRegistry.hpp>
104+
105+
logit::PrometheusRegistry registry("myapp_");
106+
107+
registry.set_gauge(
108+
"queue_size",
109+
"Current queue depth",
110+
[]() { return get_queue_depth(); });
111+
112+
config.on_collect = [&registry](std::vector<logit::PrometheusMetricFamily>& families) {
113+
registry.collect(families);
97114
};
98115
```
99116
117+
`PrometheusTextFormatConfig::metric_prefix` applies only to LogIt++ built-in
118+
metrics. Custom metric names are written as supplied by the registry or manual
119+
builders, so use the registry prefix for application metric namespaces.
120+
121+
If a registry value callback throws, the registry skips that sample, keeps
122+
collecting later metrics, appends the healthy samples, and then rethrows the
123+
first exception. The Prometheus loggers catch that exception from `on_collect`
124+
and increment their failed export counter.
125+
126+
For low-level control, `on_collect` can still append `PrometheusMetricFamily`
127+
objects directly or use helpers such as `add_prometheus_gauge()`.
128+
100129
## Prometheus Scrape Config
101130
102131
```yaml

examples/example_logit_mdc_ndc.cpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#include <logit.hpp>
2+
3+
#include <thread>
4+
5+
int main() {
6+
#ifndef LOGIT_WITH_CONTEXT
7+
LOGIT_ADD_CONSOLE_DEFAULT();
8+
LOGIT_WARN("MDC/NDC example requires LOGIT_WITH_CONTEXT=ON");
9+
LOGIT_WAIT();
10+
return 0;
11+
#else
12+
LOGIT_ADD_CONSOLE(
13+
"[%T] [%l] request=%K{request_id} user=%K{user_id} ndc=[%J] %v",
14+
false);
15+
16+
LOGIT_MDC_PUT("request_id", "req-42");
17+
LOGIT_MDC_PUT("user_id", "alice");
18+
LOGIT_NDC_PUSH("http");
19+
LOGIT_NDC_PUSH("POST /checkout");
20+
21+
LOGIT_INFO("request accepted");
22+
23+
{
24+
LOGIT_NDC_GUARD("payment");
25+
LOGIT_WARN("payment provider latency is high");
26+
}
27+
28+
LOGIT_INFO("payment scope has ended");
29+
30+
std::thread worker([]() {
31+
LOGIT_MDC_PUT("request_id", "worker-7");
32+
LOGIT_MDC_PUT("user_id", "background");
33+
LOGIT_NDC_PUSH("worker");
34+
LOGIT_INFO("background task has its own thread-local context");
35+
LOGIT_MDC_CLEAR();
36+
LOGIT_NDC_CLEAR();
37+
});
38+
worker.join();
39+
40+
LOGIT_INFO("main thread context is unchanged");
41+
42+
LOGIT_MDC_REMOVE("user_id");
43+
LOGIT_NDC_POP();
44+
LOGIT_INFO("specific MDC keys can be removed and NDC can be popped");
45+
46+
LOGIT_MDC_CLEAR();
47+
LOGIT_NDC_CLEAR();
48+
LOGIT_SHUTDOWN();
49+
return 0;
50+
#endif
51+
}
Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
#include <logit.hpp>
22

3+
#include <cstdlib>
4+
#include <string>
5+
6+
namespace {
7+
8+
std::string env_or(const char* name, const char* fallback) {
9+
const char* value = std::getenv(name);
10+
return (value && *value) ? std::string(value) : std::string(fallback);
11+
}
12+
13+
} // namespace
14+
315
int main() {
416
#ifndef LOGIT_WITH_OTLP
517
LOGIT_ADD_CONSOLE_DEFAULT();
@@ -8,25 +20,62 @@ int main() {
820
return 0;
921
#else
1022
logit::OtlpHttpLogger::Config config;
11-
config.host = "http://localhost:4318";
12-
config.path = "/v1/logs";
13-
config.format.service_name = "logit-otlp-example";
14-
config.format.deployment_environment = "dev";
15-
config.max_batch_size = 32;
16-
config.export_interval_ms = 500;
23+
config.host = env_or("LOGIT_OTLP_ENDPOINT", "http://localhost:4318");
24+
config.path = env_or("LOGIT_OTLP_PATH", "/v1/logs");
25+
config.format.service_name = env_or("LOGIT_SERVICE_NAME", "checkout-service");
26+
config.format.service_namespace = "examples";
27+
config.format.service_instance_id = "local-dev";
28+
config.format.deployment_environment = env_or("LOGIT_ENVIRONMENT", "dev");
29+
config.max_queue_size = 1024;
30+
config.max_batch_size = 64;
31+
config.max_in_flight_requests = 2;
32+
config.export_interval_ms = 250;
33+
config.request_timeout_sec = 3;
34+
config.retry_attempts = 1;
35+
config.retry_delay_ms = 100;
36+
config.cancel_on_shutdown = false;
37+
38+
#if defined(LOGIT_HAS_ZSTD)
39+
config.compression = logit::OtlpCompression::Zstd;
40+
config.compression_level = 3;
41+
#elif defined(LOGIT_HAS_ZLIB)
42+
config.compression = logit::OtlpCompression::Gzip;
43+
config.compression_level = 6;
44+
#endif
1745

1846
LOGIT_ADD_LOGGER(
1947
logit::OtlpHttpLogger,
2048
(config),
2149
logit::SimpleLogFormatter,
22-
("%v")
50+
#ifdef LOGIT_WITH_CONTEXT
51+
("[%l] trace=%K{trace_id} span=%K{span_id} %v")
52+
#else
53+
("[%l] %v")
54+
#endif
2355
);
2456

57+
#ifdef LOGIT_WITH_CONTEXT
58+
LOGIT_MDC_PUT("trace_id", "7b3f1c8a2e914a99");
59+
LOGIT_MDC_PUT("span_id", "checkout-001");
60+
LOGIT_NDC_PUSH("checkout");
61+
#endif
62+
2563
LOGIT_INFO("OTLP logger started");
26-
LOGIT_WARN("Example warning message");
27-
LOGIT_ERROR("Example error message");
64+
LOGIT_WARN("payment provider latency is above threshold");
65+
66+
{
67+
#ifdef LOGIT_WITH_CONTEXT
68+
LOGIT_NDC_GUARD("submit-order");
69+
#endif
70+
LOGIT_ERROR("order export failed; collector will count failed exports if HTTP fails");
71+
}
2872

2973
LOGIT_WAIT();
74+
#ifdef LOGIT_WITH_CONTEXT
75+
LOGIT_MDC_CLEAR();
76+
LOGIT_NDC_CLEAR();
77+
#endif
78+
LOGIT_SHUTDOWN();
3079
return 0;
3180
#endif
3281
}

0 commit comments

Comments
 (0)