LogIt++
Header-only, macro-first C++ logging library
Loading...
Searching...
No Matches
OtlpHttpLogger.hpp
Go to the documentation of this file.
1#pragma once
2#ifndef LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_LOGGERS_OTLPHTTPLOGGER_HPP_INCLUDED
3#define LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_LOGGERS_OTLPHTTPLOGGER_HPP_INCLUDED
4
7
8#ifndef LOGIT_WITH_OTLP
9# error "OtlpHttpLogger requires LOGIT_WITH_OTLP=1 and the optional kurlyk dependency. Enable LOGIT_WITH_OTLP in CMake."
10#endif
11
12#include "ILogger.hpp"
18
19#ifndef KURLYK_WEBSOCKET_SUPPORT
20# define KURLYK_WEBSOCKET_SUPPORT 0
21#endif
22#ifndef KURLYK_HTTP_SUPPORT
23# define KURLYK_HTTP_SUPPORT 1
24#endif
25#include <kurlyk.hpp>
26
27#include <atomic>
28#include <chrono>
29#include <condition_variable>
30#include <cstdint>
31#include <deque>
32#include <limits>
33#include <mutex>
34#include <stdexcept>
35#include <string>
36#include <thread>
37#include <vector>
38
39namespace logit {
40
42 std::mutex mutex;
43 std::condition_variable cv;
44 std::condition_variable space_cv;
45 std::deque<OtlpLogItem> queue;
46 std::size_t http_in_flight = 0;
47 std::atomic<uint64_t> failed_exports{0};
48 bool stopping = false;
49 };
50
57 class OtlpHttpLogger final : public ILogger {
58 public:
59 struct Config {
61 std::string host = "http://localhost:4318";
62 std::string path = "/v1/logs";
63 std::size_t max_queue_size = 8192;
64 std::size_t max_batch_size = 256;
65 std::size_t max_in_flight_requests = 1;
69 long retry_delay_ms = 250;
70 std::size_t max_payload_bytes = 1024 * 1024;
73 bool drop_on_overflow = true;
74 bool async = true;
75 bool cancel_on_shutdown = false;
76 };
77
80
83 explicit OtlpHttpLogger(const Config& config)
84 : m_config(config),
85 m_client(config.host),
86 m_state(std::make_shared<OtlpHttpLoggerState>()) {
88 m_config.max_in_flight_requests = 1;
89 }
90 m_client.set_content_type("application/json");
91 m_client.set_timeout(config.request_timeout_sec);
92 m_client.set_retry_attempts(config.retry_attempts, config.retry_delay_ms);
93
95#if !defined(LOGIT_HAS_ZLIB)
96 throw std::runtime_error("OtlpHttpLogger: gzip compression requested but LOGIT_WITH_GZIP is not enabled");
97#endif
98 }
100#if !defined(LOGIT_HAS_ZSTD)
101 throw std::runtime_error("OtlpHttpLogger: zstd compression requested but LOGIT_WITH_ZSTD is not enabled");
102#endif
103 }
104
105 if (m_config.async) {
106 m_worker = std::thread(&OtlpHttpLogger::worker_loop, this);
107 }
108 }
109
111 ~OtlpHttpLogger() override {
112 stop();
113 }
114
117
121 void log(const LogRecord& record, const std::string& message) override {
122 std::unique_lock<std::mutex> lifecycle_lock(m_lifecycle_mutex);
123
124 if (m_state->stopping) {
125 ++m_dropped;
126 return;
127 }
129
130 OtlpLogItem item;
131 item.record = make_otlp_record_snapshot(record);
132 item.message = message;
133
134 if (!m_config.async) {
135 std::vector<OtlpLogItem> batch;
136 batch.push_back(item);
137 submit_batch_async(batch);
138 try {
139 m_client.wait_requests();
140 } catch (...) {
141 }
142 return;
143 }
144
145 for (;;) {
146 std::unique_lock<std::mutex> lock(m_state->mutex);
147 if (m_state->stopping) {
148 ++m_dropped;
149 return;
150 }
151
152 if (m_state->queue.size() >= m_config.max_queue_size) {
154 ++m_dropped;
155 return;
156 }
157
158 lifecycle_lock.unlock();
159 m_state->space_cv.wait(lock, [this]() {
160 return m_state->stopping || m_state->queue.size() < m_config.max_queue_size;
161 });
162 lock.unlock();
163 lifecycle_lock.lock();
164
165 if (m_state->stopping) {
166 ++m_dropped;
167 return;
168 }
169
170 continue;
171 }
172
173 m_state->queue.push_back(item);
174 lock.unlock();
175 m_state->cv.notify_one();
176 return;
177 }
178 }
179
181 void wait() override {
182 if (!m_config.async) {
183 return;
184 }
185
186 std::unique_lock<std::mutex> lock(m_state->mutex);
187 m_state->cv.wait(lock, [this]() {
188 return m_state->queue.empty() && m_state->http_in_flight == 0;
189 });
190 }
191
193 void shutdown() override {
194 stop();
195 }
196
200 std::string get_string_param(const LoggerParam& param) const override {
201 switch (param) {
202 case LoggerParam::LastLogTimestamp: return std::to_string(get_last_log_ts());
203 case LoggerParam::TimeSinceLastLog: return std::to_string(get_time_since_last_log());
204 case LoggerParam::DroppedLogCount: return std::to_string(dropped_count());
205 case LoggerParam::FailedExportCount: return std::to_string(failed_export_count());
206 default:
207 break;
208 }
209 return std::string();
210 }
211
215 int64_t get_int_param(const LoggerParam& param) const override {
216 switch (param) {
221 default:
222 break;
223 }
224 return 0;
225 }
226
230 double get_float_param(const LoggerParam& param) const override {
231 switch (param) {
233 return static_cast<double>(get_last_log_ts()) / 1000.0;
235 return static_cast<double>(get_time_since_last_log()) / 1000.0;
237 return static_cast<double>(dropped_count());
239 return static_cast<double>(failed_export_count());
240 default:
241 break;
242 }
243 return 0.0;
244 }
245
248 void set_log_level(LogLevel level) override {
249 m_log_level = static_cast<int>(level);
250 }
251
254 LogLevel get_log_level() const override {
255 return static_cast<LogLevel>(m_log_level.load());
256 }
257
260 uint64_t dropped_count() const {
261 return m_dropped.load();
262 }
263
266 uint64_t failed_export_count() const {
267 return m_state->failed_exports.load();
268 }
269
270 private:
272 kurlyk::HttpClient m_client;
273
274 mutable std::mutex m_lifecycle_mutex;
275 std::thread m_worker;
276
277 std::shared_ptr<OtlpHttpLoggerState> m_state;
278
279 std::atomic<int> m_log_level = ATOMIC_VAR_INIT(static_cast<int>(LogLevel::LOG_LVL_TRACE));
280 std::atomic<int64_t> m_last_log_ts = ATOMIC_VAR_INIT(0);
281 std::atomic<uint64_t> m_dropped = ATOMIC_VAR_INIT(0);
282
284 void worker_loop() {
285 while (true) {
286 std::vector<OtlpLogItem> batch;
287 batch.reserve(m_config.max_batch_size);
288
289 {
290 std::unique_lock<std::mutex> lock(m_state->mutex);
291 m_state->cv.wait_for(
292 lock,
293 std::chrono::milliseconds(m_config.export_interval_ms),
294 [this]() {
295 const bool can_submit =
296 !m_state->queue.empty() &&
297 m_state->http_in_flight < m_config.max_in_flight_requests;
298
299 const bool can_stop_now =
300 m_state->stopping &&
301 (m_config.cancel_on_shutdown ||
302 (m_state->queue.empty() && m_state->http_in_flight == 0));
303
304 return can_submit || can_stop_now;
305 });
306
307 if (m_state->stopping && m_config.cancel_on_shutdown) {
308 return;
309 }
310
311 if (m_state->stopping && m_state->queue.empty() && m_state->http_in_flight == 0) {
312 return;
313 }
314
315 if (m_state->queue.empty() ||
316 m_state->http_in_flight >= m_config.max_in_flight_requests) {
317 continue;
318 }
319
320 while (!m_state->queue.empty() && batch.size() < m_config.max_batch_size) {
321 batch.push_back(m_state->queue.front());
322 m_state->queue.pop_front();
323 }
324
325 m_state->space_cv.notify_all();
326 }
327
328 if (!batch.empty()) {
329 submit_batch_async(batch);
330 }
331 }
332 }
333
336 void submit_batch_async(const std::vector<OtlpLogItem>& batch) {
339
340 if (chunks.empty()) {
341 return;
342 }
343
344 kurlyk::Headers headers;
345 headers.emplace("Content-Type", "application/json");
346
347 auto weak_state = std::weak_ptr<OtlpHttpLoggerState>(m_state);
348
349 for (auto& chunk : chunks) {
350 std::string post_content;
351 kurlyk::Headers chunk_headers = headers;
352
354 if (!detail::compress_string_gzip(chunk, post_content, m_config.compression_level)) {
355 // Compression failed: fallback to uncompressed payload.
356 // Count this as a failed export attempt so operators can observe compression issues.
357 m_state->failed_exports.fetch_add(1);
358 post_content = std::move(chunk);
359 } else {
360 chunk_headers.emplace("Content-Encoding", "gzip");
361 }
363 if (!detail::compress_string_zstd(chunk, post_content, m_config.compression_level)) {
364 // Compression failed: fallback to uncompressed payload.
365 // Count this as a failed export attempt so operators can observe compression issues.
366 m_state->failed_exports.fetch_add(1);
367 post_content = std::move(chunk);
368 } else {
369 chunk_headers.emplace("Content-Encoding", "zstd");
370 }
371 } else {
372 post_content = std::move(chunk);
373 }
374
375 {
376 std::unique_lock<std::mutex> lock(m_state->mutex);
377 m_state->cv.wait(lock, [this]() {
378 return (m_state->stopping && m_config.cancel_on_shutdown) ||
379 m_state->http_in_flight < m_config.max_in_flight_requests;
380 });
381
382 if (m_state->stopping && m_config.cancel_on_shutdown) {
383 m_state->failed_exports.fetch_add(1);
384 continue;
385 }
386
387 ++m_state->http_in_flight;
388 }
389
390 bool submitted = false;
391
392 try {
393 submitted = m_client.post(
394 m_config.path, {}, chunk_headers, std::move(post_content),
395 [weak_state](kurlyk::HttpResponsePtr response) {
396 auto state = weak_state.lock();
397 if (!state) {
398 return;
399 }
400
401 std::lock_guard<std::mutex> lock(state->mutex);
402 if (!response || response->status_code < 200 || response->status_code >= 300) {
403 state->failed_exports.fetch_add(1);
404 }
405 if (state->http_in_flight > 0) {
406 --state->http_in_flight;
407 }
408 state->cv.notify_all();
409 });
410 } catch (...) {
411 submitted = false;
412 }
413
414 if (!submitted) {
415 auto state = m_state;
416 std::lock_guard<std::mutex> lock(state->mutex);
417 state->failed_exports.fetch_add(1);
418 if (state->http_in_flight > 0) {
419 --state->http_in_flight;
420 }
421 state->cv.notify_all();
422 }
423 }
424 }
425
427 void stop() {
428 {
429 std::lock_guard<std::mutex> lifecycle_lock(m_lifecycle_mutex);
430 std::lock_guard<std::mutex> lock(m_state->mutex);
431 if (m_state->stopping) {
432 return;
433 }
434 m_state->stopping = true;
435
436 if (m_config.cancel_on_shutdown) {
437 m_dropped.fetch_add(m_state->queue.size());
438 m_state->queue.clear();
439 m_state->space_cv.notify_all();
440 }
441 }
442
443 if (m_config.cancel_on_shutdown) {
444 try {
445 m_client.cancel_requests();
446 } catch (...) {
447 }
448 }
449
450 m_state->cv.notify_all();
451 m_state->space_cv.notify_all();
452
453 if (m_worker.joinable()) {
454 m_worker.join();
455 }
456
457 if (!m_config.cancel_on_shutdown) {
458 try {
459 m_client.wait_requests();
460 } catch (...) {
461 }
462 }
463 }
464
467 int64_t get_last_log_ts() const {
468 return m_last_log_ts.load();
469 }
470
473 int64_t get_time_since_last_log() const {
474 const int64_t last = get_last_log_ts();
475 if (last <= 0) {
476 return 0;
477 }
478 const int64_t now = LOGIT_CURRENT_TIMESTAMP_MS();
479 return now > last ? now - last : 0;
480 }
481
485 static int64_t counter_to_int64(uint64_t value) {
486 const uint64_t max_value = static_cast<uint64_t>((std::numeric_limits<int64_t>::max)());
487 return value > max_value ? (std::numeric_limits<int64_t>::max)() : static_cast<int64_t>(value);
488 }
489 };
490
491} // namespace logit
492
493#endif // LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_LOGGERS_OTLPHTTPLOGGER_HPP_INCLUDED
Shared gzip/zstd compression helpers used by OTLP and MDBX backends.
Defines the interface for loggers used in the logging system.
Compression algorithm selected for OTLP/HTTP payloads.
Defines serialization-related configuration shared by OTLP loggers.
Defines OTLP/HTTP JSON serialization helpers for logs.
Splits oversized OTLP log batches into multiple JSON payload chunks.
Optional buffered snapshot access returns empty results by default.
Definition ILogger.hpp:47
Exports logs to an OTLP/HTTP endpoint using kurlyk.
std::thread m_worker
Export worker thread.
kurlyk::HttpClient m_client
HTTP client used for OTLP export.
int64_t get_int_param(const LoggerParam &param) const override
Retrieves an integer parameter from the logger.
OtlpHttpLogger(const OtlpHttpLogger &)=delete
uint64_t dropped_count() const
Returns number of dropped records.
void wait() override
Waits until queued and in-flight exports finish.
double get_float_param(const LoggerParam &param) const override
Retrieves a floating-point parameter from the logger.
OtlpHttpLogger()
Constructs OTLP HTTP logger with default configuration.
Config m_config
Export configuration.
void log(const LogRecord &record, const std::string &message) override
Queues or exports a log message.
LogLevel get_log_level() const override
Gets minimal log level for this logger.
std::mutex m_lifecycle_mutex
Serializes log() with shutdown.
OtlpHttpLogger & operator=(const OtlpHttpLogger &)=delete
int64_t get_time_since_last_log() const
Returns elapsed time since last log.
std::atomic< int64_t > m_last_log_ts
void submit_batch_async(const std::vector< OtlpLogItem > &batch)
Submits one batch asynchronously, splitting into payload chunks.
std::atomic< int > m_log_level
std::string get_string_param(const LoggerParam &param) const override
Retrieves a string parameter from the logger.
void worker_loop()
Worker thread loop.
uint64_t failed_export_count() const
Returns number of failed export attempts.
~OtlpHttpLogger() override
Stops worker and cancels pending HTTP requests.
int64_t get_last_log_ts() const
Returns last log timestamp.
OtlpHttpLogger(const Config &config)
Constructs OTLP HTTP logger with custom configuration.
std::atomic< uint64_t > m_dropped
void set_log_level(LogLevel level) override
Sets minimal log level for this logger.
void stop()
Stops worker and cancels pending requests.
std::shared_ptr< OtlpHttpLoggerState > m_state
static int64_t counter_to_int64(uint64_t value)
Converts unsigned counter value to int64_t with saturation.
void shutdown() override
Stops the OTLP worker after draining queued exports.
#define LOGIT_CURRENT_TIMESTAMP_MS()
Macro to get the current timestamp in milliseconds.
Definition config.hpp:84
bool compress_string_gzip(const std::string &input, std::string &output, int level)
Compress a string with gzip.
bool compress_string_zstd(const std::string &input, std::string &output, int level)
Compress a string with zstd.
The primary namespace for the LogIt++ library.
OtlpCompression
Compression algorithm used for an OTLP/HTTP request payload.
LogLevel
Logging levels.
Definition enums.hpp:15
@ LOG_LVL_TRACE
Trace level logging.
std::vector< std::string > build_otlp_logs_json_payload_chunks(const std::vector< OtlpLogItem > &batch, const OtlpJsonFormatConfig &format, std::size_t max_payload_bytes)
Builds OTLP JSON payload chunks from a batch, splitting when a chunk would exceed max_payload_bytes.
OtlpRecordSnapshot make_otlp_record_snapshot(const LogRecord &record)
Creates an owned snapshot from a LogRecord.
LoggerParam
Enumeration for different logger parameters that can be retrieved.
Definition enums.hpp:47
@ TimeSinceLastLog
The time elapsed since the last log in seconds.
@ LastLogTimestamp
The timestamp of the last log.
@ DroppedLogCount
Number of log records dropped by the backend.
@ FailedExportCount
Number of failed export attempts reported by the backend.
Stores log metadata and content.
Definition LogRecord.hpp:17
const int64_t timestamp_ms
Timestamp in milliseconds.
Definition LogRecord.hpp:19
std::condition_variable space_cv
std::condition_variable cv
std::deque< OtlpLogItem > queue
std::atomic< uint64_t > failed_exports
Serialization settings for OTLP JSON payload construction.
Owns one formatted log message and its structured metadata.
OtlpRecordSnapshot record
Structured log metadata snapshot.
std::string message
Formatted message body.