feat(otlp): callback-based HTTP exporter with in-flight limit - #103
Merged
Conversation
Replace the blocking future.wait_for() export loop with kurlyk callback API. This lets the worker thread submit multiple HTTP requests without waiting for each round-trip, controlled by max_in_flight_requests. Key changes: - OtlpHttpLoggerState holds shared mutex, cv, http_in_flight and failed_exports counters. The HTTP callback captures a weak_ptr to this state, eliminating use-after-free on logger destruction. - worker_loop no longer blocks on export_batch(); it submits via submit_batch_async() and loops immediately while http_in_flight stays below max_in_flight_requests. - wait() now waits on http_in_flight == 0 and queue empty via the shared state condition variable. - stop() uses m_client.wait_requests() for graceful drain, or m_client.cancel_requests() when cancel_on_shutdown is true. - OtlpHttpLoggerConfig gains max_in_flight_requests (default 1) and cancel_on_shutdown (default false) to preserve prior semantics. Tests added in otlp_http_logger_callback_test.cpp cover: - single batch callback export - max_in_flight=1 blocking second batch - max_in_flight=2 parallel export - HTTP 500 failure counting - wait() blocks until callback completes - graceful shutdown without UAF - cancel_on_shutdown fast path Constraint: callback must not capture `this`; weak_ptr<State> only. Rejected: kurlyk future-based blocking API | blocks worker thread, serializes all batches. Confidence: high Scope-risk: moderate Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add .claude/ agents, rules, commands, and .cbmignore project config. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Blocker 1 (stop ordering): move m_client.cancel_requests() before worker.join() so cancel_on_shutdown actually fast-paths. Graceful path now calls wait_requests() after join(). Blocker 2 (worker wakeup): unify queue + in-flight state under single OtlpHttpLoggerState with one mutex and one cv. The HTTP callback wakes worker via state->cv without capturing this, eliminating stalled worker between export_interval_ms timeouts. Blocker 3 (CMake install): replace blanket if(NOT LOGIT_WITH_OTLP) guard with IMPORTED-target-aware conditional. install(EXPORT) is skipped only when kurlyk is a non-IMPORTED submodule; it still works when kurlyk is pre-installed, keeping the package usable. Also: - Normalize max_in_flight_requests==0 to 1 in constructor. - Move http_in_flight under state mutex (non-atomic) to avoid mixed synchronization model. - Add .gitignore to exclude local agent configs. - Remove .claude/proxy-config.json and settings.local.json from repo. Rejected: per-field atomics + separate mutexes — mixed model is harder to reason about and callback could not wake worker safely. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… bundled kurlyk Blocker 1: cancel_on_shutdown=true could still drain and submit queued batches after cancel_requests() because worker_loop() only checked stopping + empty queue. Now stop() clears the queue under state mutex and increments m_dropped; worker_loop() returns immediately when stopping && cancel_on_shutdown, preventing new submissions. Blocker 2: CMake install/export with bundled kurlyk now hard-fails at install time (install(CODE FATAL_ERROR)) instead of producing a broken installed package. install(EXPORT) is conditional: included only when kurlyk is pre-installed (IMPORTED) or absent. Development builds with submodules still configure and compile normally. Also: removed unused #include <future>. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add OtlpPayloadLogger backend that forms OTLP JSON payloads and passes them to a user-provided std::function<void(std::string)> callback. This allows applications with their own telemetry pipeline to reuse LogIt++'s OTLP formatting and batching without duplicating HTTP infrastructure. Includes OtlpPayloadLoggerConfig, queue/worker batching, sync/async modes, overflow drop policy, and integration tests. Constraint: OtlpPayloadLogger.hpp and tests guarded by LOGIT_WITH_OTLP. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wrap on_payload in try/catch in both sync and async paths. Add FailedExportCount counter and param getter. Fix m_idle so it is set true only after callback completes, preventing wait()/shutdown() hang if callback throws. Add tests for slow callback and throwing callback. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extract serialization settings into shared OtlpJsonFormatConfig. Convert OtlpHttpLogger and OtlpPayloadLogger to use nested Config structs (OtlpHttpLogger::Config, OtlpPayloadLogger::Config) following the library pattern used by other backends. Serialization fields live under a `format` sub-member. Delete standalone OtlpHttpLoggerConfig.hpp and OtlpPayloadLoggerConfig.hpp. Update all tests, example, and docs. All 55 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Update tool-priority.md to elevate Codebase Memory to first-pass codebase discovery before Grep/Read/LSP. Add explicit Codebase Memory Usage Policy section. Update CLAUDE.md specific overrides to reflect Codebase Memory as the lightest path for non-trivial codebase understanding. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the blocking
future.wait_for()HTTP export loop inOtlpHttpLoggerwith kurlyk callback API, enabling non-blocking batched exports up to a configurablemax_in_flight_requestslimit. Also introduces a newOtlpPayloadLoggerbackend that forms OTLP JSON payloads and passes them to a user-provided callback instead of sending HTTP itself.Motivation
The previous implementation blocked the dedicated worker thread on every HTTP round-trip (
future.wait_for(...) -> future.get()). This serialized all batch exports and prevented the worker from dequeuing new logs while a slow collector endpoint was being processed. Under backpressure the queue filled up and triggereddrop_on_overfloweven though the worker was idle.Changes
OtlpHttpLogger.hpp
OtlpHttpLoggerState(shared_ptr-held) withmutex,cv,http_in_flightandfailed_exportscounters.weak_ptr<OtlpHttpLoggerState>instead ofthis, preventing use-after-free if the logger is destroyed before the callback fires.worker_loop()no longer blocks insideexport_batch(). It incrementshttp_in_flight, callssubmit_batch_async(), and immediately loops back to dequeue the next batch whilehttp_in_flight < max_in_flight_requests.wait()now waits onhttp_in_flight == 0 && m_queue.empty()via the shared-state condition variable.stop()usesm_client.wait_requests()for graceful drain, orm_client.cancel_requests()whencancel_on_shutdown = true.OtlpHttpLogger::ConfigwithOtlpJsonFormatConfig formatsub-member.OtlpPayloadLogger.hpp (new)
std::function<void(std::string)>.FailedExportCount/DroppedLogCountdiagnostics.OtlpPayloadLogger::ConfigsharingOtlpJsonFormatConfig format.OtlpJsonFormatConfig.hpp (new)
include_*flags,args_prefix) from the old standaloneOtlpHttpLoggerConfig.OtlpHttpLogger,OtlpPayloadLogger, andbuild_otlp_logs_json_payload().Deleted
otlp/OtlpHttpLoggerConfig.hpp— replaced byOtlpHttpLogger::Config.otlp/OtlpPayloadLoggerConfig.hpp— replaced byOtlpPayloadLogger::Config.CMakeLists.txt / tests/CMakeLists.txt
install(EXPORT)when OTLP is enabled (kurlyk target not exported).LOGIT_WITH_OTLP=ON(required by kurlyk).target_include_directoriesforSimple-Web-Serverheaders used by OTLP HTTP integration tests.otlp_payload_logger_testto the test list (guarded byLOGIT_WITH_OTLP).New tests
otlp_http_logger_callback_test.cpp— 7 scenarios:max_in_flight=1blocking — second batch is not submitted until the first callback completes.max_in_flight=2parallelism — two batches are submitted concurrently.FailedExportCountincrements correctly.wait()blocks until callback completes — measured elapsed time >= server delay.cancel_on_shutdown=false, shutdown waits for callback and completes cleanly.cancel_on_shutdown=truefast path — slow server, shutdown completes in < 1s.otlp_payload_logger_test.cpp— 8 scenarios:"resourceLogs".FailedExportCountwithout escaping fromlog().drop_on_overflow=true—DroppedLogCount> 0.wait()blocks until queue drain.shutdown()stops worker cleanly without deadlock.wait()blocks until callback finishes (>= 400 ms).FailedExportCountincrements.Verification
otlp_http_logger_callback_testPASS in ~3.9s.otlp_payload_logger_testPASS.Scope risk
Moderate — the threading model changes from "worker thread blocks on HTTP" to "worker thread submits and moves on". The shared-state + weak_ptr pattern ensures safety, but concurrent callback delivery from kurlyk's
NetworkWorkerthread now decrementshttp_in_flightasynchronously.