LogIt++
Header-only, macro-first C++ logging library
Loading...
Searching...
No Matches
MdbxLogger.hpp
Go to the documentation of this file.
1#pragma once
2#ifndef LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_LOGGERS_MDBXLOGGER_HPP_INCLUDED
3#define LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_LOGGERS_MDBXLOGGER_HPP_INCLUDED
4
7
8#include <mdbx_containers/KeyValueTable.hpp>
9#include <algorithm>
10#include <cstring>
11#include <limits>
12#include <map>
13#include <memory>
14#include <optional>
15#include <stdexcept>
16#include <unordered_map>
17#include <utility>
18
19namespace logit {
20
24 None = 0,
25 Gzip = 1,
26 Zstd = 2
27 };
28
29 namespace detail {
30 class MdbxReadException : public std::runtime_error {
31 public:
32 MdbxReadException(LogReadError error, const std::string& message)
33 : std::runtime_error(message), m_error(error) {}
34
35 LogReadError error() const noexcept {
36 return m_error;
37 }
38
39 private:
40 LogReadError m_error;
41 };
42 }
43
46 class MdbxLogger final : public ILogger, public ILogReader, public ILogSubscriber {
47 public:
50 struct Config {
51 std::string path = "logs.mdbx";
52 std::string app_name;
53 uint64_t session_id = 0;
54 bool async = true;
55 bool drop_on_overflow = true;
56 std::size_t max_queue_size = 8192;
57 std::size_t max_batch_size = 256;
59 std::size_t large_payload_threshold = 4096;
60 std::size_t payload_preview_size = 512;
64 std::function<void(const std::string&)> on_error;
65 };
66
69 struct SessionView {
70 std::string app_name;
71 int64_t start_time_ms = 0;
72 int64_t end_time_ms = 0;
73 uint64_t process_id = 0;
74 uint32_t schema_version = 1;
75 };
76
84
86
87 explicit MdbxLogger(const Config& config)
88 : m_config(config) {
89 try {
94 if (m_config.async) {
95 m_worker = std::thread(&MdbxLogger::worker_loop, this);
96 }
97 } catch (const std::exception& e) {
98 report_init_error(std::string("MdbxLogger initialization error: ") + e.what());
99 throw;
100 } catch (...) {
101 report_init_error("MdbxLogger initialization error");
102 throw;
103 }
104 }
105
106 ~MdbxLogger() override {
107 shutdown();
108 }
109
110 MdbxLogger(const MdbxLogger&) = delete;
111 MdbxLogger& operator=(const MdbxLogger&) = delete;
112
114 void log(const LogRecord& record, const std::string& message) override {
115 MdbxLogItem item;
116 item.level = record.log_level;
117 item.timestamp_ms = record.timestamp_ms;
118 item.file = record.file;
119 item.function = record.function;
120 item.line = record.line;
121 item.message = message;
122
123 if (!m_config.async) {
124 {
125 std::lock_guard<std::mutex> lock(m_mutex);
126 if (m_stopping) {
127 ++m_dropped;
128 return;
129 }
130 }
132 std::vector<MdbxLogItem> batch;
133 batch.push_back(std::move(item));
134 write_batch(batch);
135 return;
136 }
137
138 std::unique_lock<std::mutex> lock(m_mutex);
139 if (m_stopping) {
140 ++m_dropped;
141 return;
142 }
143
145
148 ++m_dropped;
149 return;
150 }
151
152 m_space_cv.wait(lock, [this]() {
153 return m_stopping ||
156 });
157
158 if (m_stopping) {
159 ++m_dropped;
160 return;
161 }
162 }
163
164 m_queue.push_back(std::move(item));
165 lock.unlock();
166 m_cv.notify_one();
167 }
168
170 void wait() override {
171 if (!m_config.async) {
172 return;
173 }
174
175 std::unique_lock<std::mutex> lock(m_mutex);
176 m_cv.wait(lock, [this]() {
177 return m_queue.empty() && m_idle;
178 });
179 }
180
182 void shutdown() override {
183 bool expected = false;
184 if (!m_shutdown.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
185 return;
186 }
187
188 {
189 std::lock_guard<std::mutex> lock(m_mutex);
190 m_stopping = true;
191 }
192 m_cv.notify_all();
193 m_space_cv.notify_all();
194
195 if (m_worker.joinable()) {
196 m_worker.join();
197 }
198
199 try {
201 } catch (...) {
203 }
204 }
205
207 uint64_t session_id() const {
208 return m_session_id;
209 }
210
212 std::vector<LogRecordSnapshot> read_range(
213 int64_t from_ms,
214 int64_t to_ms,
215 std::size_t limit = 0) const override {
216 auto result = read_range_result(from_ms, to_ms, limit);
217 return result.value ? *result.value : std::vector<LogRecordSnapshot>();
218 }
219
221 LogReadResult<std::vector<LogRecordSnapshot>> read_range_result(
222 int64_t from_ms,
223 int64_t to_ms,
224 std::size_t limit = 0) const override {
225 LogReadResult<std::vector<LogRecordSnapshot>> result;
226 result.value.emplace();
227 if (to_ms <= from_ms) {
228 return result;
229 }
230
231 try {
232 const std::string from_key = detail::make_mdbx_record_key(from_ms, 0);
233 const std::string to_key = detail::make_mdbx_record_key(
234 to_ms - 1,
235 (std::numeric_limits<uint32_t>::max)());
236
237 std::lock_guard<std::mutex> db_lock(m_db_mutex);
238 m_records->for_each_range(from_key, to_key,
239 [&result, limit](const std::string&, const Record& record) -> bool {
240 result.value->push_back(to_log_record_snapshot(record));
241 return limit == 0 || result.value->size() < limit;
242 });
243 } catch (const detail::MdbxReadException& e) {
244 result.value.reset();
245 result.error = e.error();
246 result.message = e.what();
247 } catch (const mdbxc::MdbxException& e) {
248 result.value.reset();
249 result.error = LogReadError::StorageError;
250 result.message = e.what();
251 } catch (const std::exception& e) {
252 result.value.reset();
253 result.error = LogReadError::DecodeError;
254 result.message = e.what();
255 } catch (...) {
256 result.value.reset();
257 result.error = LogReadError::DecodeError;
258 result.message = "MdbxLogger: unknown read_range error";
259 }
260
261 return result;
262 }
263
269 std::vector<LogRecordSnapshot> read_recent(
270 std::size_t limit,
271 int64_t period_ms = 0,
272 LogReadOrder order = LogReadOrder::Ascending) const override {
273 auto result = read_recent_result(limit, period_ms, order);
274 return result.value ? *result.value : std::vector<LogRecordSnapshot>();
275 }
276
278 LogReadResult<std::vector<LogRecordSnapshot>> read_recent_result(
279 std::size_t limit,
280 int64_t period_ms = 0,
281 LogReadOrder order = LogReadOrder::Ascending) const override {
282 const int64_t now_ms = LOGIT_CURRENT_TIMESTAMP_MS();
283 const int64_t from_ms = (period_ms > 0) ? (now_ms - period_ms) : 0;
284 auto result = read_range_result(from_ms, now_ms + 1, 0);
285 if (!result.value) {
286 return result;
287 }
288 if (limit > 0 && result.value->size() > limit) {
289 result.value->erase(
290 result.value->begin(),
291 result.value->begin() + static_cast<std::ptrdiff_t>(result.value->size() - limit));
292 }
293 if (order == LogReadOrder::Descending && !result.value->empty()) {
294 std::reverse(result.value->begin(), result.value->end());
295 }
296 return result;
297 }
298
300 std::optional<PayloadView> read_payload(uint64_t payload_id) const {
301 return read_payload_result(payload_id).value;
302 }
303
305 LogReadResult<PayloadView> read_payload_result(uint64_t payload_id) const {
306 LogReadResult<PayloadView> result;
307 if (payload_id == 0) {
308 result.error = LogReadError::NotFound;
309 result.message = "MdbxLogger: payload id is zero";
310 return result;
311 }
312
313 try {
314 std::lock_guard<std::mutex> db_lock(m_db_mutex);
315#if __cplusplus >= 201703L
316 auto value = m_payloads->find(payload_id);
317 if (!value) return make_not_found_result<PayloadView>("MdbxLogger: payload not found");
318 result.value = to_view(*value);
319#else
320 std::pair<bool, Payload> value = m_payloads->find(payload_id);
321 if (!value.first) return make_not_found_result<PayloadView>("MdbxLogger: payload not found");
322 result.value = to_view(value.second);
323#endif
324 } catch (const detail::MdbxReadException& e) {
325 result.error = e.error();
326 result.message = e.what();
327 } catch (const mdbxc::MdbxException& e) {
328 result.error = LogReadError::StorageError;
329 result.message = e.what();
330 } catch (const std::exception& e) {
331 result.error = LogReadError::DecodeError;
332 result.message = e.what();
333 } catch (...) {
334 result.error = LogReadError::DecodeError;
335 result.message = "MdbxLogger: unknown payload read error";
336 }
337 return result;
338 }
339
342 std::optional<std::string> read_payload_data(uint64_t payload_id) const {
343 return read_payload_data_result(payload_id).value;
344 }
345
347 LogReadResult<std::string> read_payload_data_result(uint64_t payload_id) const {
348 LogReadResult<std::string> result;
349 auto view = read_payload_result(payload_id);
350 if (!view.value) {
351 result.error = view.error;
352 result.message = view.message;
353 return result;
354 }
355 if (view.value->compression == MdbxPayloadCompression::None) {
356 result.value = view.value->data;
357 return result;
358 }
359 std::string output;
360 bool ok = false;
361 if (view.value->compression == MdbxPayloadCompression::Gzip) {
362 ok = detail::decompress_string_gzip(view.value->data, output);
363 } else if (view.value->compression == MdbxPayloadCompression::Zstd) {
364 ok = detail::decompress_string_zstd(view.value->data, output);
365 }
366 if (ok) {
367 result.value = std::move(output);
368 } else {
369 result.error = LogReadError::DecompressionError;
370 result.message = "MdbxLogger: failed to decompress payload data";
371 }
372 return result;
373 }
374
376 std::optional<SessionView> read_session(uint64_t session_id) const {
377 return read_session_result(session_id).value;
378 }
379
381 LogReadResult<SessionView> read_session_result(uint64_t session_id) const {
382 LogReadResult<SessionView> result;
383 if (session_id == 0) {
384 result.error = LogReadError::NotFound;
385 result.message = "MdbxLogger: session id is zero";
386 return result;
387 }
388
389 try {
390 std::lock_guard<std::mutex> db_lock(m_db_mutex);
391#if __cplusplus >= 201703L
392 auto value = m_sessions->find(session_id);
393 if (!value) return make_not_found_result<SessionView>("MdbxLogger: session not found");
394 result.value = to_view(*value);
395#else
396 std::pair<bool, Session> value = m_sessions->find(session_id);
397 if (!value.first) return make_not_found_result<SessionView>("MdbxLogger: session not found");
398 result.value = to_view(value.second);
399#endif
400 } catch (const detail::MdbxReadException& e) {
401 result.error = e.error();
402 result.message = e.what();
403 } catch (const mdbxc::MdbxException& e) {
404 result.error = LogReadError::StorageError;
405 result.message = e.what();
406 } catch (const std::exception& e) {
407 result.error = LogReadError::DecodeError;
408 result.message = e.what();
409 } catch (...) {
410 result.error = LogReadError::DecodeError;
411 result.message = "MdbxLogger: unknown session read error";
412 }
413 return result;
414 }
415
416 std::string get_string_param(const LoggerParam& param) const override {
417 switch (param) {
418 case LoggerParam::LastLogTimestamp: return std::to_string(get_last_log_ts());
419 case LoggerParam::TimeSinceLastLog: return std::to_string(get_time_since_last_log());
420 case LoggerParam::DroppedLogCount: return std::to_string(dropped_count());
421 case LoggerParam::FailedExportCount: return std::to_string(failed_export_count());
422 default:
423 break;
424 }
425 return std::string();
426 }
427
428 int64_t get_int_param(const LoggerParam& param) const override {
429 switch (param) {
434 default:
435 break;
436 }
437 return 0;
438 }
439
440 double get_float_param(const LoggerParam& param) const override {
441 switch (param) {
443 return static_cast<double>(get_last_log_ts()) / 1000.0;
445 return static_cast<double>(get_time_since_last_log()) / 1000.0;
447 return static_cast<double>(dropped_count());
449 return static_cast<double>(failed_export_count());
450 default:
451 break;
452 }
453 return 0.0;
454 }
455
456 void set_log_level(LogLevel level) override {
457 m_log_level.store(static_cast<int>(level), std::memory_order_release);
458 }
459
460 LogLevel get_log_level() const override {
461 return static_cast<LogLevel>(m_log_level.load(std::memory_order_acquire));
462 }
463
465 wait();
466
467 LogClearResult result;
468 try {
469 {
470 std::lock_guard<std::mutex> db_lock(m_db_mutex);
471 auto txn = m_connection->transaction(mdbxc::TransactionMode::WRITABLE);
472 if (options.include_persistent_records) {
473 result.cleared_records = m_records->count(txn);
474 m_records->clear(txn);
475 }
476 if (options.include_payloads) {
477 m_payloads->clear(txn);
478 }
479 if (options.include_sessions) {
480 m_sessions->clear(txn);
481 Session session;
482 session.app_name = m_config.app_name;
484 session.end_time_ms = 0;
486 session.schema_version = 1;
488 m_sessions->insert_or_assign(m_session_id, session, txn);
489 }
490 txn.commit();
491 }
492
493 {
494 std::lock_guard<std::mutex> lock(m_mutex);
496 }
497 m_last_log_ts.store(0, std::memory_order_release);
498 m_last_log_mono_ts.store(0, std::memory_order_release);
499
500 result.ok = true;
502 result.message = "cleared";
503 } catch (const std::exception& e) {
504 result.ok = false;
506 result.message = std::string("MdbxLogger clear error: ") + e.what();
507 } catch (...) {
508 result.ok = false;
510 result.message = "MdbxLogger clear error";
511 }
512 return result;
513 }
514
515 uint64_t dropped_count() const {
516 return m_dropped.load(std::memory_order_acquire);
517 }
518
519 uint64_t failed_export_count() const {
520 return m_failed_writes.load(std::memory_order_acquire);
521 }
522
523 uint64_t add_log_callback(Callback callback) override {
524 std::lock_guard<std::mutex> lock(m_callbacks_mutex);
525 const uint64_t id = m_next_callback_id.fetch_add(1, std::memory_order_relaxed);
526 m_callbacks.emplace(id, std::move(callback));
527 return id;
528 }
529
530 bool remove_log_callback(uint64_t callback_id) override {
531 std::lock_guard<std::mutex> lock(m_callbacks_mutex);
532 return m_callbacks.erase(callback_id) > 0;
533 }
534
535 private:
536 struct MdbxLogItem {
538 int64_t timestamp_ms = 0;
539 std::string file;
540 std::string function;
541 int line = 0;
542 std::string message;
543 };
544
545 struct Session {
546 std::string app_name;
547 int64_t start_time_ms = 0;
548 int64_t end_time_ms = 0;
549 uint64_t process_id = 0;
550 uint32_t schema_version = 1;
551
552 std::vector<uint8_t> to_bytes() const;
553 static Session from_bytes(const void* data, size_t size);
554 };
555
556 struct Record {
557 uint64_t session_id = 0;
558 int64_t timestamp_ms = 0;
559 uint32_t sequence = 0;
561 std::string message;
562 uint64_t payload_id = 0;
563 std::string file;
564 std::string function;
565 int line = 0;
566
567 std::vector<uint8_t> to_bytes() const;
568 static Record from_bytes(const void* data, size_t size);
569 };
570
571 struct Payload {
572 uint64_t payload_id = 0;
574 std::string data;
575
576 std::vector<uint8_t> to_bytes() const;
577 static Payload from_bytes(const void* data, size_t size);
578 };
579
580 typedef mdbxc::KeyValueTable<uint64_t, Session> SessionTable;
581 typedef mdbxc::KeyValueTable<std::string, Record> RecordTable;
582 typedef mdbxc::KeyValueTable<uint64_t, Payload> PayloadTable;
583
585 std::shared_ptr<mdbxc::Connection> m_connection;
586 std::unique_ptr<SessionTable> m_sessions;
587 std::unique_ptr<RecordTable> m_records;
588 std::unique_ptr<PayloadTable> m_payloads;
589
590 mutable std::mutex m_db_mutex;
591
592 std::mutex m_mutex;
593 std::condition_variable m_cv;
594 std::condition_variable m_space_cv;
595 std::deque<MdbxLogItem> m_queue;
596 bool m_stopping = false;
597 bool m_idle = true;
598 std::thread m_worker;
599
600 uint64_t m_session_id = 0;
601 std::unordered_map<int64_t, uint32_t> m_next_sequence_by_timestamp;
602
603 std::atomic<int> m_log_level = ATOMIC_VAR_INIT(static_cast<int>(LogLevel::LOG_LVL_TRACE));
604 std::atomic<int64_t> m_last_log_ts = ATOMIC_VAR_INIT(0);
605 std::atomic<int64_t> m_last_log_mono_ts = ATOMIC_VAR_INIT(0);
606 std::atomic<uint64_t> m_dropped = ATOMIC_VAR_INIT(0);
607 std::atomic<uint64_t> m_failed_writes = ATOMIC_VAR_INIT(0);
608 std::atomic<bool> m_shutdown = ATOMIC_VAR_INIT(false);
609
610 mutable std::mutex m_callbacks_mutex;
611 std::map<uint64_t, Callback> m_callbacks;
612 std::atomic<uint64_t> m_next_callback_id{1};
613
614 template <typename T>
615 static LogReadResult<T> make_not_found_result(const std::string& message) {
616 LogReadResult<T> result;
617 result.error = LogReadError::NotFound;
618 result.message = message;
619 return result;
620 }
621
622 void notify_callbacks(const std::vector<LogRecordSnapshot>& snapshots) const {
623 std::vector<Callback> callbacks_copy;
624 {
625 std::lock_guard<std::mutex> lock(m_callbacks_mutex);
626 callbacks_copy.reserve(m_callbacks.size());
627 for (const auto& kv : m_callbacks) {
628 callbacks_copy.push_back(kv.second);
629 }
630 }
631 for (const auto& snapshot : snapshots) {
632 for (const auto& cb : callbacks_copy) {
633 try {
634 cb(snapshot);
635 } catch (const std::exception& e) {
636 if (m_config.on_error) {
637 m_config.on_error(std::string("MdbxLogger callback error: ") + e.what());
638 }
639 } catch (...) {
640 if (m_config.on_error) {
641 m_config.on_error("MdbxLogger callback error");
642 }
643 }
644 }
645 }
646 }
647
649 LogRecordSnapshot snapshot;
650 snapshot.session_id = r.session_id;
651 snapshot.timestamp_ms = r.timestamp_ms;
652 snapshot.sequence = r.sequence;
653 snapshot.level = r.level;
654 snapshot.message = r.message;
655 snapshot.payload_id = r.payload_id;
656 snapshot.file = r.file;
657 snapshot.function = r.function;
658 snapshot.line = r.line;
659 return snapshot;
660 }
661
662 static SessionView to_view(const Session& s) {
663 SessionView v;
664 v.app_name = s.app_name;
669 return v;
670 }
671
672 static PayloadView to_view(const Payload& p) {
673 PayloadView v;
676 v.data = p.data;
677 return v;
678 }
679
680 static std::vector<uint8_t> serialize_session(const Session& s) {
682 out.write_u32(1);
683 out.write_string(s.app_name);
685 out.write_i64(s.end_time_ms);
686 out.write_u64(s.process_id);
688 return out.bytes();
689 }
690
691 static Session deserialize_session(const void* data, size_t size) {
692 detail::MdbxByteReader in(data, size);
693 const uint32_t version = in.read_u32();
694 if (version != 1) {
696 LogReadError::UnsupportedVersion,
697 "MdbxLogger: unsupported session value version");
698 }
699 Session s;
700 s.app_name = in.read_string();
701 s.start_time_ms = in.read_i64();
702 s.end_time_ms = in.read_i64();
703 s.process_id = in.read_u64();
704 s.schema_version = in.read_u32();
705 in.finish();
706 return s;
707 }
708
709 static std::vector<uint8_t> serialize_record(const Record& r) {
711 out.write_u32(1);
712 out.write_u64(r.session_id);
713 out.write_i64(r.timestamp_ms);
714 out.write_u32(r.sequence);
715 out.write_u32(static_cast<uint32_t>(r.level));
716 out.write_string(r.message);
717 out.write_u64(r.payload_id);
718 out.write_string(r.file);
719 out.write_string(r.function);
720 out.write_i64(static_cast<int64_t>(r.line));
721 return out.bytes();
722 }
723
724 static Record deserialize_record(const void* data, size_t size) {
725 detail::MdbxByteReader in(data, size);
726 const uint32_t version = in.read_u32();
727 if (version != 1) {
729 LogReadError::UnsupportedVersion,
730 "MdbxLogger: unsupported record value version");
731 }
732 Record r;
733 r.session_id = in.read_u64();
734 r.timestamp_ms = in.read_i64();
735 r.sequence = in.read_u32();
736 r.level = static_cast<LogLevel>(in.read_u32());
737 r.message = in.read_string();
738 r.payload_id = in.read_u64();
739 r.file = in.read_string();
740 r.function = in.read_string();
741 r.line = static_cast<int>(in.read_i64());
742 in.finish();
743 return r;
744 }
745
746 static std::vector<uint8_t> serialize_payload(const Payload& p) {
748 out.write_u32(1);
749 out.write_u64(p.payload_id);
750 out.write_u8(static_cast<uint8_t>(p.compression));
751 out.write_string(p.data);
752 return out.bytes();
753 }
754
755 static Payload deserialize_payload(const void* data, size_t size) {
756 detail::MdbxByteReader in(data, size);
757 const uint32_t version = in.read_u32();
758 if (version != 1) {
760 LogReadError::UnsupportedVersion,
761 "MdbxLogger: unsupported payload value version");
762 }
763 Payload p;
764 p.payload_id = in.read_u64();
765 const uint8_t compression = in.read_u8();
766 if (compression > static_cast<uint8_t>(MdbxPayloadCompression::Zstd)) {
768 LogReadError::DecodeError,
769 "MdbxLogger: unsupported payload compression");
770 }
771 p.compression = static_cast<MdbxPayloadCompression>(compression);
772 p.data = in.read_string();
773 in.finish();
774 return p;
775 }
776
778 if (m_config.max_batch_size == 0) {
780 }
781 if (m_config.flush_interval_ms <= 0) {
783 }
784 }
785
788#if !defined(LOGIT_HAS_ZLIB)
789 throw std::runtime_error("MdbxLogger: gzip payload compression requested but LOGIT_WITH_GZIP is not enabled");
790#endif
791 }
793#if !defined(LOGIT_HAS_ZSTD)
794 throw std::runtime_error("MdbxLogger: zstd payload compression requested but LOGIT_WITH_ZSTD is not enabled");
795#endif
796 }
797 }
798
800 mdbxc::Config db_config;
801 db_config.pathname = m_config.path;
802 db_config.max_dbs = 4;
803 db_config.no_subdir = true;
804 db_config.sync_durable = true;
805
806 ensure_storage_parent(db_config);
807 m_connection = mdbxc::Connection::create(db_config);
808 m_sessions.reset(new SessionTable(m_connection, "log_sessions"));
809 m_records.reset(new RecordTable(m_connection, "log_records_by_time"));
810 m_payloads.reset(new PayloadTable(m_connection, "log_payloads"));
811 }
812
813 void ensure_storage_parent(const mdbxc::Config& db_config) const {
814 if (!db_config.read_only && db_config.no_subdir) {
815 mdbxc::create_directories(db_config.pathname);
816 }
817 }
818
819 void report_init_error(const std::string& message) const noexcept {
820 if (m_config.on_error) {
821 try {
822 m_config.on_error(message);
823 } catch (...) {
824 }
825 }
826 }
827
828 uint64_t open_session() {
829 std::lock_guard<std::mutex> db_lock(m_db_mutex);
830 auto txn = m_connection->transaction(mdbxc::TransactionMode::WRITABLE);
831 const int64_t now_ms = LOGIT_CURRENT_TIMESTAMP_MS();
832 const uint64_t pid = detail::current_process_id();
833
834 if (m_config.session_id != 0) {
835 Session session;
836 if (find_session_locked(m_config.session_id, session, txn.handle())) {
837 if (!m_config.app_name.empty()) {
838 session.app_name = m_config.app_name;
839 }
840 if (session.start_time_ms == 0) {
841 session.start_time_ms = now_ms;
842 }
843 session.end_time_ms = 0;
844 session.process_id = pid;
845 session.schema_version = 1;
846 } else {
847 session.app_name = m_config.app_name;
848 session.start_time_ms = now_ms;
849 session.end_time_ms = 0;
850 session.process_id = pid;
851 session.schema_version = 1;
852 }
853 m_sessions->insert_or_assign(m_config.session_id, session, txn);
854 txn.commit();
855 return m_config.session_id;
856 }
857
858 for (int attempt = 0; attempt < 1024; ++attempt) {
859 const uint64_t candidate = make_unique_id();
860 Session session;
861 session.app_name = m_config.app_name;
862 session.start_time_ms = now_ms;
863 session.end_time_ms = 0;
864 session.process_id = pid;
865 session.schema_version = 1;
866 if (m_sessions->insert(candidate, session, txn)) {
867 txn.commit();
868 return candidate;
869 }
870 }
871
872 throw std::runtime_error("MdbxLogger: failed to allocate unique session id");
873 }
874
875 bool find_session_locked(uint64_t session_id, Session& out, MDBX_txn* txn) const {
876#if __cplusplus >= 201703L
877 auto value = m_sessions->find(session_id, txn);
878 if (!value) return false;
879 out = *value;
880 return true;
881#else
882 std::pair<bool, Session> value = m_sessions->find(session_id, txn);
883 if (!value.first) return false;
884 out = value.second;
885 return true;
886#endif
887 }
888
890 std::lock_guard<std::mutex> db_lock(m_db_mutex);
891 auto txn = m_connection->transaction(mdbxc::TransactionMode::WRITABLE);
892 Session session;
893 if (!find_session_locked(m_session_id, session, txn.handle())) {
894 session.app_name = m_config.app_name;
897 session.schema_version = 1;
898 }
900 m_sessions->insert_or_assign(m_session_id, session, txn);
901 txn.commit();
902 }
903
904 void worker_loop() {
905 while (true) {
906 std::vector<MdbxLogItem> batch;
907 batch.reserve(m_config.max_batch_size);
908
909 {
910 std::unique_lock<std::mutex> lock(m_mutex);
911 m_cv.wait_for(
912 lock,
913 std::chrono::milliseconds(m_config.flush_interval_ms),
914 [this]() { return m_stopping || !m_queue.empty(); });
915
916 if (m_stopping && m_queue.empty()) {
917 m_idle = true;
918 m_cv.notify_all();
919 return;
920 }
921
922 if (m_queue.empty()) {
923 continue;
924 }
925
926 m_idle = false;
927 while (!m_queue.empty() && batch.size() < m_config.max_batch_size) {
928 batch.push_back(std::move(m_queue.front()));
929 m_queue.pop_front();
930 }
931 m_space_cv.notify_all();
932 }
933
934 write_batch(batch);
935
936 {
937 std::lock_guard<std::mutex> lock(m_mutex);
938 m_idle = true;
939 }
940 m_cv.notify_all();
941 }
942 }
943
944 void write_batch(const std::vector<MdbxLogItem>& batch) {
945 if (batch.empty()) {
946 return;
947 }
948
949 std::vector<LogRecordSnapshot> written_snapshots;
950 try {
951 std::lock_guard<std::mutex> db_lock(m_db_mutex);
952 auto txn = m_connection->transaction(mdbxc::TransactionMode::WRITABLE);
953 for (size_t i = 0; i < batch.size(); ++i) {
954 Record record = write_item_locked(batch[i], txn);
955 written_snapshots.push_back(to_log_record_snapshot(record));
956 }
957 txn.commit();
958 } catch (const std::exception& e) {
959 m_failed_writes.fetch_add(1, std::memory_order_acq_rel);
960 if (m_config.on_error) {
961 m_config.on_error(std::string("MdbxLogger write error: ") + e.what());
962 }
963 return;
964 } catch (...) {
965 m_failed_writes.fetch_add(1, std::memory_order_acq_rel);
966 if (m_config.on_error) {
967 m_config.on_error("MdbxLogger write error");
968 }
969 return;
970 }
971
972 notify_callbacks(written_snapshots);
973 }
974
975 Record write_item_locked(const MdbxLogItem& item, mdbxc::Transaction& txn) {
976 Record record;
977 record.session_id = m_session_id;
978 record.timestamp_ms = item.timestamp_ms;
979 record.level = item.level;
980 record.file = item.file;
981 record.function = item.function;
982 record.line = item.line;
983 record.message = item.message;
984
985 if (should_spill_payload(item.message)) {
986 Payload payload;
988 fill_payload(item.message, payload);
989 m_payloads->insert(payload.payload_id, payload, txn);
990 record.payload_id = payload.payload_id;
991 record.message = make_payload_preview(item.message);
992 }
993
994 for (;;) {
995 const std::string key = next_record_key_locked(record.timestamp_ms, record.sequence, txn);
996 if (m_records->insert(key, record, txn)) {
997 break;
998 }
1000 }
1001 return record;
1002 }
1003
1004 bool should_spill_payload(const std::string& message) const {
1006 message.size() > m_config.large_payload_threshold;
1007 }
1008
1009 std::string make_payload_preview(const std::string& message) const {
1010 const size_t count = std::min(message.size(), m_config.payload_preview_size);
1011 return message.substr(0, count);
1012 }
1013
1014 void fill_payload(const std::string& message, Payload& payload) {
1016 payload.data = message;
1017
1019 return;
1020 }
1021
1022 std::string compressed;
1023 bool ok = false;
1028 }
1029
1030 if (ok) {
1032 payload.data = std::move(compressed);
1033 } else {
1034 m_failed_writes.fetch_add(1, std::memory_order_acq_rel);
1035 }
1036 }
1037
1038 uint64_t allocate_payload_id_locked(mdbxc::Transaction& txn) {
1039 for (int attempt = 0; attempt < 1024; ++attempt) {
1040 const uint64_t candidate = make_unique_id();
1041 if (!m_payloads->contains(candidate, txn)) {
1042 return candidate;
1043 }
1044 }
1045 throw std::runtime_error("MdbxLogger: failed to allocate unique payload id");
1046 }
1047
1049 int64_t timestamp_ms,
1050 uint32_t& sequence,
1051 mdbxc::Transaction& txn) {
1052 std::unordered_map<int64_t, uint32_t>::iterator it =
1053 m_next_sequence_by_timestamp.find(timestamp_ms);
1054 uint32_t candidate = it == m_next_sequence_by_timestamp.end() ? 0 : it->second;
1055
1056 for (;;) {
1057 const std::string key = detail::make_mdbx_record_key(timestamp_ms, candidate);
1058 if (!m_records->contains(key, txn)) {
1059 sequence = candidate;
1060 if (candidate != (std::numeric_limits<uint32_t>::max)()) {
1061 m_next_sequence_by_timestamp[timestamp_ms] = candidate + 1;
1062 } else {
1063 m_next_sequence_by_timestamp[timestamp_ms] = candidate;
1064 }
1065 return key;
1066 }
1067 if (candidate == (std::numeric_limits<uint32_t>::max)()) {
1068 throw std::runtime_error("MdbxLogger: sequence exhausted for timestamp");
1069 }
1070 ++candidate;
1071 }
1072 }
1073
1074 void advance_sequence_after_collision(int64_t timestamp_ms, uint32_t sequence) {
1075 if (sequence == (std::numeric_limits<uint32_t>::max)()) {
1076 throw std::runtime_error("MdbxLogger: sequence exhausted for timestamp");
1077 }
1078 m_next_sequence_by_timestamp[timestamp_ms] = sequence + 1;
1079 }
1080
1081 static uint64_t make_unique_id() {
1082 static std::atomic<uint64_t> counter(0);
1083 const uint64_t now = static_cast<uint64_t>(LOGIT_CURRENT_TIMESTAMP_MS());
1084 const uint64_t n = counter.fetch_add(1, std::memory_order_relaxed) & 0xFFFFu;
1085 const uint64_t id = (now << 16) ^ (detail::current_process_id() << 1) ^ n;
1086 return id == 0 ? (n + 1) : id;
1087 }
1088
1089 void mark_last_log(int64_t timestamp_ms) {
1090 m_last_log_ts.store(timestamp_ms, std::memory_order_release);
1091 m_last_log_mono_ts.store(LOGIT_MONOTONIC_MS(), std::memory_order_release);
1092 }
1093
1094 int64_t get_last_log_ts() const {
1095 return m_last_log_ts.load(std::memory_order_acquire);
1096 }
1097
1098 int64_t get_time_since_last_log() const {
1099 const int64_t last_mono = m_last_log_mono_ts.load(std::memory_order_acquire);
1100 if (last_mono <= 0) {
1101 return 0;
1102 }
1103 const int64_t now = LOGIT_MONOTONIC_MS();
1104 return now > last_mono ? now - last_mono : 0;
1105 }
1106
1107 static int64_t counter_to_int64(uint64_t value) {
1108 const uint64_t max_value = static_cast<uint64_t>((std::numeric_limits<int64_t>::max)());
1109 return value > max_value ? (std::numeric_limits<int64_t>::max)() : static_cast<int64_t>(value);
1110 }
1111 };
1112
1113 inline std::vector<uint8_t> MdbxLogger::Session::to_bytes() const {
1114 return MdbxLogger::serialize_session(*this);
1115 }
1116
1117 inline MdbxLogger::Session MdbxLogger::Session::from_bytes(const void* data, size_t size) {
1118 return MdbxLogger::deserialize_session(data, size);
1119 }
1120
1121 inline std::vector<uint8_t> MdbxLogger::Record::to_bytes() const {
1122 return MdbxLogger::serialize_record(*this);
1123 }
1124
1125 inline MdbxLogger::Record MdbxLogger::Record::from_bytes(const void* data, size_t size) {
1126 return MdbxLogger::deserialize_record(data, size);
1127 }
1128
1129 inline std::vector<uint8_t> MdbxLogger::Payload::to_bytes() const {
1130 return MdbxLogger::serialize_payload(*this);
1131 }
1132
1133 inline MdbxLogger::Payload MdbxLogger::Payload::from_bytes(const void* data, size_t size) {
1134 return MdbxLogger::deserialize_payload(data, size);
1135 }
1136
1137} // namespace logit
1138
1139#endif // LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_LOGGERS_MDBXLOGGER_HPP_INCLUDED
Optional interface for backends that expose stored records.
Optional interface for backends that can notify callers when a record is successfully written.
std::function< void(const LogRecordSnapshot &)> Callback
Optional buffered snapshot access returns empty results by default.
Definition ILogger.hpp:47
Stores formatted logs in MDBX tables with optional async batching.
std::atomic< int > m_log_level
~MdbxLogger() override
void log(const LogRecord &record, const std::string &message) override
Queues or writes a formatted log record.
std::vector< LogRecordSnapshot > read_recent(std::size_t limit, int64_t period_ms=0, LogReadOrder order=LogReadOrder::Ascending) const override
Reads the most recent records.
MdbxLogger(const MdbxLogger &)=delete
std::deque< MdbxLogItem > m_queue
uint64_t session_id() const
Returns this logger session id.
std::mutex m_callbacks_mutex
std::mutex m_db_mutex
static std::vector< uint8_t > serialize_payload(const Payload &p)
int64_t get_int_param(const LoggerParam &param) const override
Retrieves an integer parameter from the logger.
std::string next_record_key_locked(int64_t timestamp_ms, uint32_t &sequence, mdbxc::Transaction &txn)
uint64_t add_log_callback(Callback callback) override
Registers a callback to be invoked after each successfully written record.
void validate_compression_config() const
LogReadResult< std::vector< LogRecordSnapshot > > read_range_result(int64_t from_ms, int64_t to_ms, std::size_t limit=0) const override
Reads records and preserves storage/decode errors.
std::atomic< uint64_t > m_dropped
std::atomic< bool > m_shutdown
MdbxLogger & operator=(const MdbxLogger &)=delete
std::thread m_worker
uint64_t failed_export_count() const
std::optional< SessionView > read_session(uint64_t session_id) const
Reads session metadata by id.
std::unique_ptr< PayloadTable > m_payloads
LogClearResult clear_logs(const LogClearOptions &options=LogClearOptions()) override
Clears logger-owned buffered or persisted records when supported.
MdbxLogger(const Config &config)
static Payload deserialize_payload(const void *data, size_t size)
Record write_item_locked(const MdbxLogItem &item, mdbxc::Transaction &txn)
std::atomic< uint64_t > m_next_callback_id
double get_float_param(const LoggerParam &param) const override
Retrieves a floating-point parameter from the logger.
std::vector< LogRecordSnapshot > read_range(int64_t from_ms, int64_t to_ms, std::size_t limit=0) const override
Reads records in [from_ms, to_ms) ordered by timestamp and sequence.
std::unique_ptr< SessionTable > m_sessions
std::condition_variable m_space_cv
std::unique_ptr< RecordTable > m_records
std::optional< PayloadView > read_payload(uint64_t payload_id) const
Reads a payload by id.
std::unordered_map< int64_t, uint32_t > m_next_sequence_by_timestamp
uint64_t open_session()
std::string make_payload_preview(const std::string &message) const
static std::vector< uint8_t > serialize_session(const Session &s)
uint64_t allocate_payload_id_locked(mdbxc::Transaction &txn)
LogReadResult< SessionView > read_session_result(uint64_t session_id) const
Reads session metadata by id and preserves storage/decode errors.
void report_init_error(const std::string &message) const noexcept
LogReadResult< PayloadView > read_payload_result(uint64_t payload_id) const
Reads a payload by id and preserves storage/decode errors.
std::map< uint64_t, Callback > m_callbacks
LogReadResult< std::vector< LogRecordSnapshot > > read_recent_result(std::size_t limit, int64_t period_ms=0, LogReadOrder order=LogReadOrder::Ascending) const override
Reads the most recent records and preserves storage/decode errors.
static std::vector< uint8_t > serialize_record(const Record &r)
static Record deserialize_record(const void *data, size_t size)
std::shared_ptr< mdbxc::Connection > m_connection
void ensure_storage_parent(const mdbxc::Config &db_config) const
void advance_sequence_after_collision(int64_t timestamp_ms, uint32_t sequence)
static SessionView to_view(const Session &s)
LogReadResult< std::string > read_payload_data_result(uint64_t payload_id) const
Reads and decompresses payload data while preserving failure details.
static Session deserialize_session(const void *data, size_t size)
void notify_callbacks(const std::vector< LogRecordSnapshot > &snapshots) const
void write_batch(const std::vector< MdbxLogItem > &batch)
void wait() override
Waits until accepted async records are written.
void mark_last_log(int64_t timestamp_ms)
bool remove_log_callback(uint64_t callback_id) override
Unregisters a previously added callback.
mdbxc::KeyValueTable< std::string, Record > RecordTable
uint64_t dropped_count() const
mdbxc::KeyValueTable< uint64_t, Session > SessionTable
std::string get_string_param(const LoggerParam &param) const override
Retrieves a string parameter from the logger.
int64_t get_time_since_last_log() const
static int64_t counter_to_int64(uint64_t value)
static LogReadResult< T > make_not_found_result(const std::string &message)
std::atomic< uint64_t > m_failed_writes
bool should_spill_payload(const std::string &message) const
mdbxc::KeyValueTable< uint64_t, Payload > PayloadTable
static LogRecordSnapshot to_log_record_snapshot(const Record &r)
void shutdown() override
Stops the worker after draining accepted records.
std::atomic< int64_t > m_last_log_mono_ts
static PayloadView to_view(const Payload &p)
bool find_session_locked(uint64_t session_id, Session &out, MDBX_txn *txn) const
void fill_payload(const std::string &message, Payload &payload)
LogLevel get_log_level() const override
Gets the minimal log level for this logger.
static uint64_t make_unique_id()
std::condition_variable m_cv
std::optional< std::string > read_payload_data(uint64_t payload_id) const
Reads and decompresses payload data by id.
std::atomic< int64_t > m_last_log_ts
int64_t get_last_log_ts() const
void set_log_level(LogLevel level) override
Sets the minimal log level for this logger.
void write_u64(uint64_t value)
const std::vector< uint8_t > & bytes() const
void write_u32(uint32_t value)
void write_i64(int64_t value)
void write_string(const std::string &value)
void write_u8(uint8_t value)
MdbxReadException(LogReadError error, const std::string &message)
LogReadError error() const noexcept
#define LOGIT_CURRENT_TIMESTAMP_MS()
Macro to get the current timestamp in milliseconds.
Definition config.hpp:84
#define LOGIT_MONOTONIC_MS()
Definition config.hpp:74
std::string make_mdbx_record_key(int64_t timestamp_ms, uint32_t sequence)
bool decompress_string_zstd(const std::string &input, std::string &output)
Decompress zstd-compressed bytes.
bool compress_string_gzip(const std::string &input, std::string &output, int level)
Compress a string with gzip.
bool decompress_string_gzip(const std::string &input, std::string &output)
Decompress gzip-compressed bytes.
uint64_t current_process_id()
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.
LogLevel
Logging levels.
Definition enums.hpp:15
@ LOG_LVL_TRACE
Trace level logging.
MdbxPayloadCompression
Compression algorithm used for payload rows.
@ Zstd
Store zstd-compressed payload bytes.
@ Gzip
Store gzip-compressed payload bytes.
@ None
Store payload bytes as-is.
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.
LogReadOrder
Ordering for query results returned by ILogReader.
@ Ascending
Oldest first.
@ Descending
Newest first.
Selects which data categories a logger cleanup should remove.
Definition ILogger.hpp:19
Result returned by logger cleanup operations.
Definition ILogger.hpp:35
std::string message
Optional diagnostic or unsupported reason.
Definition ILogger.hpp:39
std::size_t cleared_records
Number of primary records/files cleared when available.
Definition ILogger.hpp:38
LogClearStatus status
Detailed cleanup status.
Definition ILogger.hpp:37
bool ok
True when the backend completed cleanup.
Definition ILogger.hpp:36
Common owning snapshot of a stored log record.
std::string message
Formatted message or payload preview.
uint32_t sequence
Per-timestamp sequence, or 0 if unused.
int64_t timestamp_ms
Log timestamp in milliseconds.
int line
Source line number.
uint64_t session_id
Owning session id, or 0 if unused.
uint64_t payload_id
Payload row id, or 0 when absent.
std::string file
Source file path.
std::string function
Source function name.
LogLevel level
Log severity level.
Stores log metadata and content.
Definition LogRecord.hpp:17
const std::string function
Function name.
Definition LogRecord.hpp:22
const int line
Line number in the source file.
Definition LogRecord.hpp:21
const LogLevel log_level
Log level (severity).
Definition LogRecord.hpp:18
const int64_t timestamp_ms
Timestamp in milliseconds.
Definition LogRecord.hpp:19
const std::string file
Source file name.
Definition LogRecord.hpp:20
Configuration for the MDBX logger backend.
std::size_t payload_preview_size
Bytes kept inline when a message spills.
bool store_large_payloads_separately
Store large messages in log_payloads.
std::size_t max_batch_size
Maximum records per write transaction.
uint64_t session_id
Existing session id, or 0 to auto-create.
std::size_t max_queue_size
Maximum async queue size; 0 means unlimited.
std::function< void(const std::string &)> on_error
Optional callback invoked on initialization and write errors instead of stderr.
int payload_compression_level
Compression level for gzip/zstd.
std::string path
MDBX environment path.
bool drop_on_overflow
Drop newest record when the async queue is full.
bool async
Queue records and write them from a worker thread.
std::size_t large_payload_threshold
Spill messages larger than this.
int flush_interval_ms
Worker wake interval.
std::string app_name
Optional application name stored in session metadata.
MdbxPayloadCompression payload_compression
Payload compression.
Public read-only view of a large payload.
uint64_t payload_id
Stable payload id.
std::string data
Stored payload bytes.
MdbxPayloadCompression compression
Stored compression.
static Payload from_bytes(const void *data, size_t size)
MdbxPayloadCompression compression
std::vector< uint8_t > to_bytes() const
std::vector< uint8_t > to_bytes() const
static Record from_bytes(const void *data, size_t size)
Public read-only view of session metadata.
int64_t start_time_ms
Session start timestamp.
int64_t end_time_ms
Session end timestamp, or 0 while active.
uint64_t process_id
Process id that opened the session.
uint32_t schema_version
Storage schema version.
std::string app_name
Optional application name.
static Session from_bytes(const void *data, size_t size)
std::vector< uint8_t > to_bytes() const