From 6408b1ded743ecdf29c187e70fff1b3774f154fa Mon Sep 17 00:00:00 2001 From: surtur Date: Thu, 30 Dec 2021 22:30:41 +0100 Subject: [PATCH] add fmtlog library "fmtlog is a performant fmtlib-style logging library with latency in nanoseconds." ref: https://github.com/MengRao/fmtlog --- CMakeLists.txt | 3 + lib/fmtlog/CMakeLists.txt | 21 + lib/fmtlog/include/fmtlog/fmtlog-inl.h | 589 +++++++++++++++++++ lib/fmtlog/include/fmtlog/fmtlog.h | 783 +++++++++++++++++++++++++ 4 files changed, 1396 insertions(+) create mode 100644 lib/fmtlog/CMakeLists.txt create mode 100644 lib/fmtlog/include/fmtlog/fmtlog-inl.h create mode 100644 lib/fmtlog/include/fmtlog/fmtlog.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 31139b9..c6a54fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,8 @@ if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git") endif() add_subdirectory(lib/fmt EXCLUDE_FROM_ALL) +add_subdirectory(lib/fmtlog EXCLUDE_FROM_ALL) + add_subdirectory(lib/da_threading EXCLUDE_FROM_ALL) if(NOT CMAKE_CXX_FLAGS MATCHES "-Wall") @@ -189,5 +191,6 @@ add_executable(fortuna main.cpp generator.cpp generator.h fortuna.cpp fortuna.h target_link_libraries(fortuna PRIVATE cryptopp PRIVATE fmt::fmt-header-only + PRIVATE fmtlog::fmtlog PRIVATE da_threading::da_threading PRIVATE pthread) diff --git a/lib/fmtlog/CMakeLists.txt b/lib/fmtlog/CMakeLists.txt new file mode 100644 index 0000000..b3f15af --- /dev/null +++ b/lib/fmtlog/CMakeLists.txt @@ -0,0 +1,21 @@ +# uses headers from https://github.com/MengRao/fmtlog +# CMakeLists.txt authored by a_mirre (c) 2021 +cmake_minimum_required (VERSION 3.20) + +project (fmtlog LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_library(fmtlog INTERFACE) +add_library(fmtlog::fmtlog ALIAS fmtlog) + +target_compile_definitions(fmtlog INTERFACE FMTLOG_HEADER_ONLY=1) + +set(FMTLOG_INC_DIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE STRING + "Installation directory for include files, a relative path that " + "will be joined with ${CMAKE_INSTALL_PREFIX} or an absolute path.") + +target_include_directories(fmtlog INTERFACE + $ + $) diff --git a/lib/fmtlog/include/fmtlog/fmtlog-inl.h b/lib/fmtlog/include/fmtlog/fmtlog-inl.h new file mode 100644 index 0000000..a0ab2da --- /dev/null +++ b/lib/fmtlog/include/fmtlog/fmtlog-inl.h @@ -0,0 +1,589 @@ +/* +MIT License + +Copyright (c) 2021 Meng Rao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "fmtlog.h" +#include +#include +#include +#include + +#ifdef _WIN32 +#define NOMINMAX +#include +#include +#else +#include +#include +#endif + +template +class fmtlogDetailT +{ +public: + // https://github.com/MengRao/str + template + class Str + { + public: + static const int Size = SIZE; + char s[SIZE]; + + Str() {} + Str(const char* p) { *this = *(const Str*)p; } + + char& operator[](int i) { return s[i]; } + char operator[](int i) const { return s[i]; } + + template + void fromi(T num) { + if constexpr (Size & 1) { + s[Size - 1] = '0' + (num % 10); + num /= 10; + } + switch (Size & -2) { + case 18: *(uint16_t*)(s + 16) = *(uint16_t*)(digit_pairs + ((num % 100) << 1)); num /= 100; + case 16: *(uint16_t*)(s + 14) = *(uint16_t*)(digit_pairs + ((num % 100) << 1)); num /= 100; + case 14: *(uint16_t*)(s + 12) = *(uint16_t*)(digit_pairs + ((num % 100) << 1)); num /= 100; + case 12: *(uint16_t*)(s + 10) = *(uint16_t*)(digit_pairs + ((num % 100) << 1)); num /= 100; + case 10: *(uint16_t*)(s + 8) = *(uint16_t*)(digit_pairs + ((num % 100) << 1)); num /= 100; + case 8: *(uint16_t*)(s + 6) = *(uint16_t*)(digit_pairs + ((num % 100) << 1)); num /= 100; + case 6: *(uint16_t*)(s + 4) = *(uint16_t*)(digit_pairs + ((num % 100) << 1)); num /= 100; + case 4: *(uint16_t*)(s + 2) = *(uint16_t*)(digit_pairs + ((num % 100) << 1)); num /= 100; + case 2: *(uint16_t*)(s + 0) = *(uint16_t*)(digit_pairs + ((num % 100) << 1)); num /= 100; + } + } + + static constexpr const char* digit_pairs = "00010203040506070809" + "10111213141516171819" + "20212223242526272829" + "30313233343536373839" + "40414243444546474849" + "50515253545556575859" + "60616263646566676869" + "70717273747576777879" + "80818283848586878889" + "90919293949596979899"; + }; + + fmtlogDetailT() + : flushDelay(3000000000) { + args.reserve(4096); + args.resize(parttenArgSize); + + fmtlogWrapper<>::impl.init(); + resetDate(); + fmtlog::setLogFile(stdout); + setHeaderPattern("{HMSf} {s:<16} {l}[{t:<6}] "); + logInfos.reserve(32); + bgLogInfos.reserve(128); + bgLogInfos.emplace_back(nullptr, nullptr, fmtlog::DBG, fmt::string_view()); + bgLogInfos.emplace_back(nullptr, nullptr, fmtlog::INF, fmt::string_view()); + bgLogInfos.emplace_back(nullptr, nullptr, fmtlog::WRN, fmt::string_view()); + bgLogInfos.emplace_back(nullptr, nullptr, fmtlog::ERR, fmt::string_view()); + threadBuffers.reserve(8); + bgThreadBuffers.reserve(8); + memset(membuf.data(), 0, membuf.capacity()); + } + + ~fmtlogDetailT() { + stopPollingThread(); + poll(true); + closeLogFile(); + } + + void setHeaderPattern(const char* pattern) { + if (shouldDeallocateHeader) delete[] headerPattern.data(); + using namespace fmt::literals; + for (int i = 0; i < parttenArgSize; i++) { + reorderIdx[i] = parttenArgSize - 1; + } + headerPattern = fmtlog::unNameFormat( + pattern, reorderIdx, "a"_a = "", "b"_a = "", "C"_a = "", "Y"_a = "", "m"_a = "", "d"_a = "", + "t"_a = "thread name", "F"_a = "", "f"_a = "", "e"_a = "", "S"_a = "", "M"_a = "", "H"_a = "", + "l"_a = fmtlog::LogLevel(), "s"_a = "fmtlog.cc:123", "g"_a = "/home/raomeng/fmtlog/fmtlog.cc:123", "Ymd"_a = "", + "HMS"_a = "", "HMSe"_a = "", "HMSf"_a = "", "HMSF"_a = "", "YmdHMS"_a = "", "YmdHMSe"_a = "", "YmdHMSf"_a = "", + "YmdHMSF"_a = ""); + shouldDeallocateHeader = headerPattern.data() != pattern; + + setArg<0>(fmt::string_view(weekdayName.s, 3)); + setArg<1>(fmt::string_view(monthName.s, 3)); + setArg<2>(fmt::string_view(&year[2], 2)); + setArg<3>(fmt::string_view(year.s, 4)); + setArg<4>(fmt::string_view(month.s, 2)); + setArg<5>(fmt::string_view(day.s, 2)); + setArg<6>(fmt::string_view()); + setArg<7>(fmt::string_view(nanosecond.s, 9)); + setArg<8>(fmt::string_view(nanosecond.s, 6)); + setArg<9>(fmt::string_view(nanosecond.s, 3)); + setArg<10>(fmt::string_view(second.s, 2)); + setArg<11>(fmt::string_view(minute.s, 2)); + setArg<12>(fmt::string_view(hour.s, 2)); + setArg<13>(fmt::string_view(logLevel.s, 3)); + setArg<14>(fmt::string_view()); + setArg<15>(fmt::string_view()); + setArg<16>(fmt::string_view(year.s, 10)); // Ymd + setArg<17>(fmt::string_view(hour.s, 8)); // HMS + setArg<18>(fmt::string_view(hour.s, 12)); // HMSe + setArg<19>(fmt::string_view(hour.s, 15)); // HMSf + setArg<20>(fmt::string_view(hour.s, 18)); // HMSF + setArg<21>(fmt::string_view(year.s, 19)); // YmdHMS + setArg<22>(fmt::string_view(year.s, 23)); // YmdHMSe + setArg<23>(fmt::string_view(year.s, 26)); // YmdHMSf + setArg<24>(fmt::string_view(year.s, 29)); // YmdHMSF + } + + class ThreadBufferDestroyer + { + public: + explicit ThreadBufferDestroyer() {} + + void threadBufferCreated() {} + + ~ThreadBufferDestroyer() { + if (fmtlog::threadBuffer != nullptr) { + fmtlog::threadBuffer->shouldDeallocate = true; + fmtlog::threadBuffer = nullptr; + } + } + }; + + struct StaticLogInfo + { + // Constructor + constexpr StaticLogInfo(fmtlog::FormatToFn fn, const char* loc, fmtlog::LogLevel level, fmt::string_view fmtString) + : formatToFn(fn) + , formatString(fmtString) + , location(loc) + , logLevel(level) + , argIdx(-1) {} + + void processLocation() { + size_t size = strlen(location); + const char* p = location + size; + if (size > 255) { + location = p - 255; + } + endPos = p - location; + const char* base = location; + while (p > location) { + char c = *--p; + if (c == '/' || c == '\\') { + base = p + 1; + break; + } + } + basePos = base - location; + } + + inline fmt::string_view getBase() { return fmt::string_view(location + basePos, endPos - basePos); } + + inline fmt::string_view getLocation() { return fmt::string_view(location, endPos); } + + fmtlog::FormatToFn formatToFn; + fmt::string_view formatString; + const char* location; + uint8_t basePos; + uint8_t endPos; + fmtlog::LogLevel logLevel; + int argIdx; + }; + + static thread_local ThreadBufferDestroyer sbc; + int64_t midnightNs; + fmt::string_view headerPattern; + bool shouldDeallocateHeader = false; + FILE* outputFp = nullptr; + bool manageFp = false; + size_t fpos = 0; // file position of membuf, used only when manageFp == true + int64_t flushDelay; + int64_t nextFlushTime = (std::numeric_limits::max)(); + uint32_t flushBufSize = 8 * 1024; + fmtlog::LogLevel flushLogLevel = fmtlog::OFF; + std::mutex bufferMutex; + std::vector threadBuffers; + struct HeapNode + { + HeapNode(fmtlog::ThreadBuffer* buffer) + : tb(buffer) {} + + fmtlog::ThreadBuffer* tb; + const fmtlog::SPSCVarQueueOPT<>::MsgHeader* header = nullptr; + }; + std::vector bgThreadBuffers; + std::mutex logInfoMutex; + std::vector logInfos; + std::vector bgLogInfos; + + fmtlog::LogCBFn logCB = nullptr; + fmtlog::LogLevel minCBLogLevel; + + fmtlog::MemoryBuffer membuf; + + const static int parttenArgSize = 25; + uint32_t reorderIdx[parttenArgSize]; + Str<3> weekdayName; + Str<3> monthName; + Str<4> year; + char dash1 = '-'; + Str<2> month; + char dash2 = '-'; + Str<2> day; + char space = ' '; + Str<2> hour; + char colon1 = ':'; + Str<2> minute; + char colon2 = ':'; + Str<2> second; + char dot1 = '.'; + Str<9> nanosecond; + Str<3> logLevel; + std::vector> args; + + volatile bool threadRunning = false; + std::thread thr; + + void resetDate() { + time_t rawtime = fmtlogWrapper<>::impl.tscns.rdns() / 1000000000; + struct tm* timeinfo = localtime(&rawtime); + timeinfo->tm_sec = timeinfo->tm_min = timeinfo->tm_hour = 0; + midnightNs = mktime(timeinfo) * 1000000000; + year.fromi(1900 + timeinfo->tm_year); + month.fromi(1 + timeinfo->tm_mon); + day.fromi(timeinfo->tm_mday); + const char* weekdays[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + weekdayName = weekdays[timeinfo->tm_wday]; + const char* monthNames[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + monthName = monthNames[timeinfo->tm_mon]; + } + + void preallocate() { + if (fmtlog::threadBuffer) return; + fmtlog::threadBuffer = new fmtlog::ThreadBuffer(); +#ifdef _WIN32 + uint32_t tid = static_cast(::GetCurrentThreadId()); +#else + uint32_t tid = static_cast(::syscall(SYS_gettid)); +#endif + fmtlog::threadBuffer->nameSize = + fmt::format_to_n(fmtlog::threadBuffer->name, sizeof(fmtlog::threadBuffer->name), "{}", tid).size; + sbc.threadBufferCreated(); + + std::unique_lock guard(bufferMutex); + threadBuffers.push_back(fmtlog::threadBuffer); + } + + template + inline void setArg(const T& arg) { + args[reorderIdx[I]] = fmt::detail::make_arg(arg); + } + + template + inline void setArgVal(const T& arg) { + fmt::detail::value& value_ = *(fmt::detail::value*)&args[reorderIdx[I]]; + value_ = fmt::detail::arg_mapper().map(arg); + } + + void flushLogFile() { + if (outputFp) { + fwrite(membuf.data(), 1, membuf.size(), outputFp); + if (!manageFp) fflush(outputFp); + else + fpos += membuf.size(); + } + membuf.clear(); + nextFlushTime = (std::numeric_limits::max)(); + } + + void closeLogFile() { + if (membuf.size()) flushLogFile(); + if (manageFp) fclose(outputFp); + outputFp = nullptr; + manageFp = false; + } + + void startPollingThread(int64_t pollInterval) { + stopPollingThread(); + threadRunning = true; + thr = std::thread([pollInterval, this]() { + while (threadRunning) { + int64_t before = fmtlogWrapper<>::impl.tscns.rdns(); + poll(false); + int64_t delay = fmtlogWrapper<>::impl.tscns.rdns() - before; + if (delay < pollInterval) { + std::this_thread::sleep_for(std::chrono::nanoseconds(pollInterval - delay)); + } + } + poll(true); + }); + } + + void stopPollingThread() { + if (!threadRunning) return; + threadRunning = false; + if (thr.joinable()) thr.join(); + } + + void handleLog(fmt::string_view threadName, const fmtlog::SPSCVarQueueOPT<>::MsgHeader* header) { + setArgVal<6>(threadName); + StaticLogInfo& info = bgLogInfos[header->logId]; + const char* data = (const char*)(header + 1); + const char* end = (const char*)header + header->size; + int64_t tsc = *(int64_t*)data; + data += 8; + if (!info.formatToFn) { // log once + info.location = *(const char**)data; + data += 8; + info.processLocation(); + } + int64_t ts = fmtlogWrapper<>::impl.tscns.tsc2ns(tsc); + // the date could go back when polling different threads + uint64_t t = (ts > midnightNs) ? (ts - midnightNs) : 0; + nanosecond.fromi(t % 1000000000); + t /= 1000000000; + second.fromi(t % 60); + t /= 60; + minute.fromi(t % 60); + t /= 60; + uint32_t h = t; // hour + if (h > 23) { + h %= 24; + resetDate(); + } + hour.fromi(h); + setArgVal<14>(info.getBase()); + setArgVal<15>(info.getLocation()); + logLevel = (const char*)"DBG INF WRN ERR OFF" + (info.logLevel << 2); + + size_t headerPos = membuf.size(); + fmt::detail::vformat_to(membuf, headerPattern, fmt::basic_format_args(args.data(), parttenArgSize)); + size_t bodyPos = membuf.size(); + + if (info.formatToFn) { + info.formatToFn(info.formatString, data, membuf, info.argIdx, args); + } + else { // log once + membuf.append(fmt::string_view(data, end - data)); + } + + if (logCB && info.logLevel >= minCBLogLevel) { + logCB(ts, info.logLevel, info.getLocation(), info.basePos, threadName, + fmt::string_view(membuf.data() + headerPos, membuf.size() - headerPos), bodyPos - headerPos, + fpos + headerPos); + } + membuf.push_back('\n'); + if (membuf.size() >= flushBufSize || info.logLevel >= flushLogLevel) { + flushLogFile(); + } + } + + void adjustHeap(size_t i) { + while (true) { + size_t min_i = i; + for (size_t ch = i * 2 + 1, end = std::min(ch + 2, bgThreadBuffers.size()); ch < end; ch++) { + auto h_ch = bgThreadBuffers[ch].header; + auto h_min = bgThreadBuffers[min_i].header; + if (h_ch && (!h_min || *(int64_t*)(h_ch + 1) < *(int64_t*)(h_min + 1))) min_i = ch; + } + if (min_i == i) break; + std::swap(bgThreadBuffers[i], bgThreadBuffers[min_i]); + i = min_i; + } + } + + void poll(bool forceFlush) { + int64_t tsc = fmtlogWrapper<>::impl.tscns.rdtsc(); + if (logInfos.size()) { + std::unique_lock lock(logInfoMutex); + for (auto& info : logInfos) { + info.processLocation(); + } + bgLogInfos.insert(bgLogInfos.end(), logInfos.begin(), logInfos.end()); + logInfos.clear(); + } + if (threadBuffers.size()) { + std::unique_lock lock(bufferMutex); + for (auto tb : threadBuffers) { + bgThreadBuffers.emplace_back(tb); + } + threadBuffers.clear(); + } + + for (size_t i = 0; i < bgThreadBuffers.size(); i++) { + auto& node = bgThreadBuffers[i]; + if (node.header) continue; + node.header = node.tb->varq.front(); + if (!node.header && node.tb->shouldDeallocate) { + delete node.tb; + node = bgThreadBuffers.back(); + bgThreadBuffers.pop_back(); + i--; + } + } + + if (bgThreadBuffers.empty()) return; + + // build heap + for (int i = bgThreadBuffers.size() / 2; i >= 0; i--) { + adjustHeap(i); + } + + while (true) { + auto h = bgThreadBuffers[0].header; + if (!h || h->logId >= bgLogInfos.size() || *(int64_t*)(h + 1) >= tsc) break; + auto tb = bgThreadBuffers[0].tb; + handleLog(fmt::string_view(tb->name, tb->nameSize), h); + tb->varq.pop(); + bgThreadBuffers[0].header = tb->varq.front(); + adjustHeap(0); + } + + if (membuf.size() == 0) return; + if (!manageFp || forceFlush) { + flushLogFile(); + return; + } + int64_t now = fmtlogWrapper<>::impl.tscns.tsc2ns(tsc); + if (now > nextFlushTime) { + flushLogFile(); + } + else if (nextFlushTime == (std::numeric_limits::max)()) { + nextFlushTime = now + flushDelay; + } + } +}; + +template +thread_local typename fmtlogDetailT<_>::ThreadBufferDestroyer fmtlogDetailT<_>::sbc; + +template +struct fmtlogDetailWrapper +{ static fmtlogDetailT<> impl; }; + +template +fmtlogDetailT<> fmtlogDetailWrapper<_>::impl; + +template +void fmtlogT<_>::registerLogInfo(uint32_t& logId, FormatToFn fn, const char* location, LogLevel level, + fmt::string_view fmtString) { + auto& d = fmtlogDetailWrapper<>::impl; + std::lock_guard lock(d.logInfoMutex); + if (logId) return; + logId = d.logInfos.size() + d.bgLogInfos.size(); + d.logInfos.emplace_back(fn, location, level, fmtString); +} + +template +void fmtlogT<_>::preallocate() { + fmtlogDetailWrapper<>::impl.preallocate(); +} + +template +void fmtlogT<_>::setLogFile(const char* filename, bool truncate) { + auto& d = fmtlogDetailWrapper<>::impl; + FILE* newFp = fopen(filename, truncate ? "w" : "a"); + if (!newFp) { + std::string err = fmt::format("Unable to open file: {}: {}", filename, strerror(errno)); + throw std::ios_base::failure(err); + } + setbuf(newFp, nullptr); + d.fpos = ftell(newFp); + + closeLogFile(); + d.outputFp = newFp; + d.manageFp = true; +} + +template +void fmtlogT<_>::setLogFile(FILE* fp, bool manageFp) { + auto& d = fmtlogDetailWrapper<>::impl; + closeLogFile(); + if (manageFp) { + setbuf(fp, nullptr); + d.fpos = ftell(fp); + } + else + d.fpos = 0; + d.outputFp = fp; + d.manageFp = manageFp; +} + +template +void fmtlogT<_>::setFlushDelay(int64_t ns) { + fmtlogDetailWrapper<>::impl.flushDelay = ns; +} + +template +void fmtlogT<_>::flushOn(LogLevel flushLogLevel) { + fmtlogDetailWrapper<>::impl.flushLogLevel = flushLogLevel; +} + +template +void fmtlogT<_>::setFlushBufSize(uint32_t bytes) { + fmtlogDetailWrapper<>::impl.flushBufSize = bytes; +} + +template +void fmtlogT<_>::closeLogFile() { + fmtlogDetailWrapper<>::impl.closeLogFile(); +} + +template +void fmtlogT<_>::poll(bool forceFlush) { + fmtlogDetailWrapper<>::impl.poll(forceFlush); +} + +template +void fmtlogT<_>::setThreadName(const char* name) { + preallocate(); + threadBuffer->nameSize = fmt::format_to_n(threadBuffer->name, sizeof(fmtlog::threadBuffer->name), "{}", name).size; +} + +template +void fmtlogT<_>::setLogCB(LogCBFn cb, LogLevel minCBLogLevel_) { + auto& d = fmtlogDetailWrapper<>::impl; + d.logCB = cb; + d.minCBLogLevel = minCBLogLevel_; +} + +template +void fmtlogT<_>::setHeaderPattern(const char* pattern) { + fmtlogDetailWrapper<>::impl.setHeaderPattern(pattern); +} + +template +void fmtlogT<_>::startPollingThread(int64_t pollInterval) { + fmtlogDetailWrapper<>::impl.startPollingThread(pollInterval); +} + +template +void fmtlogT<_>::stopPollingThread() { + fmtlogDetailWrapper<>::impl.stopPollingThread(); +} + +template +void fmtlogT<_>::setTscGhz(double tscGhz) { + fmtlogWrapper<>::impl.tscns.init(tscGhz); +} + +template class fmtlogT<0>; + diff --git a/lib/fmtlog/include/fmtlog/fmtlog.h b/lib/fmtlog/include/fmtlog/fmtlog.h new file mode 100644 index 0000000..a719986 --- /dev/null +++ b/lib/fmtlog/include/fmtlog/fmtlog.h @@ -0,0 +1,783 @@ +/* +MIT License + +Copyright (c) 2021 Meng Rao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#pragma once +//#define FMT_HEADER_ONLY +#include "fmt/format.h" +#include +#include +#include +#include + +#ifdef _WIN32 +#define FAST_THREAD_LOCAL thread_local +#else +#define FAST_THREAD_LOCAL __thread +#endif + +// define FMTLOG_BLOCK=1 if log statment should be blocked when queue is full, instead of discarding the msg +#ifndef FMTLOG_BLOCK +#define FMTLOG_BLOCK 0 +#endif + +#define FMTLOG_LEVEL_DBG 0 +#define FMTLOG_LEVEL_INF 1 +#define FMTLOG_LEVEL_WRN 2 +#define FMTLOG_LEVEL_ERR 3 +#define FMTLOG_LEVEL_OFF 4 + +// define FMTLOG_ACTIVE_LEVEL to turn off low log level in compile time +#ifndef FMTLOG_ACTIVE_LEVEL +#define FMTLOG_ACTIVE_LEVEL FMTLOG_LEVEL_INF +#endif + +namespace fmtlogdetail { +template +struct UnrefPtr : std::false_type +{ using type = Arg; }; + +template<> +struct UnrefPtr : std::false_type +{ using type = char*; }; + +template<> +struct UnrefPtr : std::false_type +{ using type = void*; }; + +template +struct UnrefPtr> : std::true_type +{ using type = Arg; }; + +template +struct UnrefPtr> : std::true_type +{ using type = Arg; }; + +template +struct UnrefPtr : std::true_type +{ using type = Arg; }; +}; // namespace fmtlogdetail + +template +class fmtlogT +{ +public: + enum LogLevel : uint8_t + { + DBG = 0, + INF, + WRN, + ERR, + OFF + }; + + // If you know the exact tsc frequency(in ghz) in the os, tell fmtlog! + // But how can I know the frequency? Check below link(for Linux only): + // https://github.com/MengRao/tscns#i-dont-wanna-wait-a-long-time-for-calibration-can-i-cheat + static void setTscGhz(double tscGhz); + + // Preallocate thread queue for current thread + static void preallocate(); + + // Set the file for logging + static void setLogFile(const char* filename, bool truncate = false); + + // Set an existing FILE* for logging, if manageFp is false fmtlog will not buffer log internally and will not close + // the FILE* + static void setLogFile(FILE* fp, bool manageFp = false); + + // Collect log msgs from all threads and write to log file + // If forceFlush = true, internal file buffer is flushed + // User need to call poll() repeatedly if startPollingThread is not used + static void poll(bool forceFlush = false); + + // Set flush delay in nanosecond + // If there's msg older than ns in the buffer, flush will be triggered + static void setFlushDelay(int64_t ns); + + // If current msg has level >= flushLogLevel, flush will be triggered + static void flushOn(LogLevel flushLogLevel); + + // If file buffer has more than specified bytes, flush will be triggered + static void setFlushBufSize(uint32_t bytes); + + // callback signature user can register + // ns: nanosecond timestamp + // level: logLevel + // location: full file path with line num, e.g: /home/raomeng/fmtlog/fmtlog.h:45 + // basePos: file base index in the location + // threadName: thread id or the name user set with setThreadName + // msg: full log msg with header + // bodyPos: log body index in the msg + // logFilePos: log file position of this msg + typedef void (*LogCBFn)(int64_t ns, LogLevel level, fmt::string_view location, size_t basePos, + fmt::string_view threadName, fmt::string_view msg, size_t bodyPos, size_t logFilePos); + + // Set a callback function for all log msgs with a mininum log level + static void setLogCB(LogCBFn cb, LogLevel minCBLogLevel); + + // Close the log file and subsequent msgs will not be written into the file, + // but callback function can still be used + static void closeLogFile(); + + // Set log header pattern with fmt named arguments + static void setHeaderPattern(const char* pattern); + + // Set a name for current thread, it'll be shown in {t} part in header pattern + static void setThreadName(const char* name); + + // Set current log level, lower level log msgs will be discarded + static inline void setLogLevel(LogLevel logLevel); + + // Get current log level + static inline LogLevel getLogLevel(); + + // Run a polling thread in the background with a polling interval + // Note that user must not call poll() himself when the thread is running + static void startPollingThread(int64_t pollInterval = 1000000); + + // Stop the polling thread + static void stopPollingThread(); + +private: + fmtlogT() { init(); } + + void init() { + if (!inited) { + inited = true; + tscns.init(); + currentLogLevel = INF; + } + } + + template + friend class fmtlogDetailT; + template + friend struct fmtlogWrapper; + template + friend void test(const S& format, Args&&...); + + using Context = fmt::format_context; + using MemoryBuffer = fmt::basic_memory_buffer; + typedef const char* (*FormatToFn)(fmt::string_view format, const char* data, MemoryBuffer& out, int& argIdx, + std::vector>& args); + + static void registerLogInfo(uint32_t& logId, FormatToFn fn, const char* location, LogLevel level, + fmt::string_view fmtString); + + // https://github.com/MengRao/SPSC_Queue + template + class SPSCVarQueueOPT + { + public: + struct MsgHeader + { + uint32_t size; + uint32_t logId; + }; + static constexpr uint32_t BLK_CNT = Bytes / sizeof(MsgHeader); + + MsgHeader* alloc(uint32_t size_) { + size = size_ + sizeof(MsgHeader); + uint32_t blk_sz = (size + sizeof(MsgHeader) - 1) / sizeof(MsgHeader); + if (blk_sz >= free_write_cnt) { + uint32_t read_idx_cache = *(volatile uint32_t*)&read_idx; + if (read_idx_cache <= write_idx) { + free_write_cnt = BLK_CNT - write_idx; + if (blk_sz >= free_write_cnt && read_idx_cache != 0) { // wrap around + blk[0].size = 0; + std::atomic_thread_fence(std::memory_order_release); + blk[write_idx].size = 1; + write_idx = 0; + free_write_cnt = read_idx_cache; + } + } + else { + free_write_cnt = read_idx_cache - write_idx; + } + if (free_write_cnt <= blk_sz) { + return nullptr; + } + } + return &blk[write_idx]; + } + + void push() { + uint32_t blk_sz = (size + sizeof(MsgHeader) - 1) / sizeof(MsgHeader); + blk[write_idx + blk_sz].size = 0; + std::atomic_thread_fence(std::memory_order_release); + blk[write_idx].size = size; + write_idx += blk_sz; + free_write_cnt -= blk_sz; + } + + template + bool tryPush(uint32_t size, Writer writer) { + MsgHeader* header = alloc(size); + if (!header) return false; + writer(header); + push(); + return true; + } + + const MsgHeader* front() { + uint32_t size = blk[read_idx].size; + if (size == 1) { // wrap around + read_idx = 0; + size = blk[0].size; + } + if (size == 0) return nullptr; + return &blk[read_idx]; + } + + void pop() { + uint32_t blk_sz = (blk[read_idx].size + sizeof(MsgHeader) - 1) / sizeof(MsgHeader); + *(volatile uint32_t*)&read_idx = read_idx + blk_sz; + } + + template + bool tryPop(Reader reader) { + MsgHeader* header = front(); + if (!header) return false; + reader(header); + pop(); + return true; + } + + private: + alignas(64) MsgHeader blk[BLK_CNT] = {}; + + alignas(128) uint32_t write_idx = 0; + uint32_t free_write_cnt = BLK_CNT; + uint32_t size; + + alignas(128) uint32_t read_idx = 0; + }; + + struct ThreadBuffer + { + SPSCVarQueueOPT<> varq; + bool shouldDeallocate = false; + char name[32]; + size_t nameSize; + }; + + // https://github.com/MengRao/tscns + class TSCNS + { + public: + double init(double tsc_ghz = 0.0) { + syncTime(base_tsc, base_ns); + if (tsc_ghz > 0) { + tsc_ghz_inv = 1.0 / tsc_ghz; + adjustOffset(); + return tsc_ghz; + } + else { +#ifdef _WIN32 + return calibrate(1000000 * 100); // wait more time as Windows' system time is in 100ns precision +#else + return calibrate(1000000 * 10); // +#endif + } + } + + double calibrate(int64_t min_wait_ns) { + int64_t delayed_tsc, delayed_ns; + do { + syncTime(delayed_tsc, delayed_ns); + } while ((delayed_ns - base_ns) < min_wait_ns); + tsc_ghz_inv = (double)(delayed_ns - base_ns) / (delayed_tsc - base_tsc); + adjustOffset(); + return 1.0 / tsc_ghz_inv; + } + + static inline int64_t rdtsc() { +#ifdef _WIN32 + return __rdtsc(); +#else + return __builtin_ia32_rdtsc(); +#endif + } + + inline int64_t tsc2ns(int64_t tsc) const { return ns_offset + (int64_t)(tsc * tsc_ghz_inv); } + + inline int64_t rdns() const { return tsc2ns(rdtsc()); } + + static int64_t rdsysns() { + using namespace std::chrono; + return duration_cast(system_clock::now().time_since_epoch()).count(); + } + + // For checking purposes, see test.cc + int64_t rdoffset() const { return ns_offset; } + + private: + // Linux kernel sync time by finding the first try with tsc diff < 50000 + // We do better: we find the try with the mininum tsc diff + void syncTime(int64_t& tsc, int64_t& ns) { + const int N = 10; + int64_t tscs[N + 1]; + int64_t nses[N + 1]; + + tscs[0] = rdtsc(); + for (int i = 1; i <= N; i++) { + nses[i] = rdsysns(); + tscs[i] = rdtsc(); + } + + int best = 1; + for (int i = 2; i <= N; i++) { + if (tscs[i] - tscs[i - 1] < tscs[best] - tscs[best - 1]) best = i; + } + tsc = (tscs[best] + tscs[best - 1]) >> 1; + ns = nses[best]; + } + + void adjustOffset() { ns_offset = base_ns - (int64_t)(base_tsc * tsc_ghz_inv); } + + alignas(64) double tsc_ghz_inv; // make sure tsc_ghz_inv and ns_offset are on the same cache line + int64_t ns_offset; + int64_t base_tsc; + int64_t base_ns; + }; + + bool inited = false; + +public: + TSCNS tscns; + +private: + volatile LogLevel currentLogLevel; + static FAST_THREAD_LOCAL ThreadBuffer* threadBuffer; + + template + static inline constexpr bool isNamedArg() { + return fmt::detail::is_named_arg>::value; + } + + template + struct unNamedType + { using type = Arg; }; + + template + struct unNamedType> + { using type = Arg; }; + +#if FMT_USE_NONTYPE_TEMPLATE_PARAMETERS + template Str> + struct unNamedType> + { using type = Arg; }; +#endif + + + template + static inline constexpr bool isCstring() { + return fmt::detail::mapped_type_constant::value == fmt::detail::type::cstring_type; + } + + template + static inline constexpr bool isString() { + return fmt::detail::mapped_type_constant::value == fmt::detail::type::string_type; + } + + template + static inline constexpr bool needCallDtor() { + using ArgType = fmt::remove_cvref_t; + if constexpr (isNamedArg()) { + return needCallDtor::type>(); + } + if constexpr (isString()) return false; + return !std::is_trivially_destructible::value; + } + + template + static inline constexpr size_t getArgSizes(size_t* cstringSize) { + return 0; + } + + template + static inline constexpr size_t getArgSizes(size_t* cstringSize, const Arg& arg, const Args&... args) { + if constexpr (isNamedArg()) { + return getArgSizes(cstringSize, arg.value, args...); + } + else if constexpr (isCstring()) { + size_t len = strlen(arg) + 1; + cstringSize[CstringIdx] = len; + return len + getArgSizes(cstringSize, args...); + } + else if constexpr (isString()) { + size_t len = arg.size() + 1; + return len + getArgSizes(cstringSize, args...); + } + else { + return sizeof(Arg) + getArgSizes(cstringSize, args...); + } + } + + template + static inline constexpr char* encodeArgs(size_t* cstringSize, char* out) { + return out; + } + + template + static inline constexpr char* encodeArgs(size_t* cstringSize, char* out, Arg&& arg, Args&&... args) { + if constexpr (isNamedArg()) { + return encodeArgs(cstringSize, out, arg.value, std::forward(args)...); + } + else if constexpr (isCstring()) { + memcpy(out, arg, cstringSize[CstringIdx]); + return encodeArgs(cstringSize, out + cstringSize[CstringIdx], std::forward(args)...); + } + else if constexpr (isString()) { + size_t len = arg.size(); + memcpy(out, arg.data(), len); + out[len] = 0; + return encodeArgs(cstringSize, out + len + 1, std::forward(args)...); + } + else { + new (out) fmt::remove_cvref_t(std::forward(arg)); + return encodeArgs(cstringSize, out + sizeof(Arg), std::forward(args)...); + } + } + + template + static inline constexpr void storeNamedArgs(fmt::detail::named_arg_info* named_args_store) {} + + template + static inline constexpr void storeNamedArgs(fmt::detail::named_arg_info* named_args_store, const Arg& arg, + const Args&... args) { + if constexpr (isNamedArg()) { + named_args_store[NamedIdx] = {arg.name, Idx}; + storeNamedArgs(named_args_store, args...); + } + else { + storeNamedArgs(named_args_store, args...); + } + } + + template + static inline const char* decodeArgs(const char* in, fmt::basic_format_arg* args, + const char** destruct_args) { + return in; + } + + template + static inline const char* decodeArgs(const char* in, fmt::basic_format_arg* args, + const char** destruct_args) { + using namespace fmtlogdetail; + using ArgType = fmt::remove_cvref_t; + if constexpr (isNamedArg()) { + return decodeArgs::type, Args...>(in, args, + destruct_args); + } + else if constexpr (isCstring() || isString()) { + size_t size = strlen(in); + fmt::string_view v(in, size); + if constexpr (ValueOnly) { + fmt::detail::value& value_ = *(fmt::detail::value*)(args + Idx); + value_ = fmt::detail::arg_mapper().map(v); + } + else { + args[Idx] = fmt::detail::make_arg(v); + } + return decodeArgs(in + size + 1, args, destruct_args); + } + else { + if constexpr (ValueOnly) { + fmt::detail::value& value_ = *(fmt::detail::value*)(args + Idx); + if constexpr (UnrefPtr::value) { + value_ = fmt::detail::arg_mapper().map(**(ArgType*)in); + } + else { + value_ = fmt::detail::arg_mapper().map(*(ArgType*)in); + } + } + else { + if constexpr (UnrefPtr::value) { + args[Idx] = fmt::detail::make_arg(**(ArgType*)in); + } + else { + args[Idx] = fmt::detail::make_arg(*(ArgType*)in); + } + } + + if constexpr (needCallDtor()) { + destruct_args[DestructIdx] = in; + return decodeArgs(in + sizeof(ArgType), args, destruct_args); + } + else { + return decodeArgs(in + sizeof(ArgType), args, destruct_args); + } + } + } + + template + static inline void destructArgs(const char** destruct_args) {} + + template + static inline void destructArgs(const char** destruct_args) { + using ArgType = fmt::remove_cvref_t; + if constexpr (isNamedArg()) { + destructArgs::type, Args...>(destruct_args); + } + else if constexpr (needCallDtor()) { + ((ArgType*)destruct_args[DestructIdx])->~ArgType(); + destructArgs(destruct_args); + } + else { + destructArgs(destruct_args); + } + } + + template + static const char* formatTo(fmt::string_view format, const char* data, MemoryBuffer& out, int& argIdx, + std::vector>& args) { + constexpr size_t num_args = sizeof...(Args); + constexpr size_t num_dtors = fmt::detail::count()...>(); + const char* dtor_args[std::max(num_dtors, (size_t)1)]; + const char* ret; + if (argIdx < 0) { + argIdx = args.size(); + args.resize(argIdx + num_args); + ret = decodeArgs(data, args.data() + argIdx, dtor_args); + } + else { + ret = decodeArgs(data, args.data() + argIdx, dtor_args); + } + fmt::detail::vformat_to(out, format, fmt::basic_format_args(args.data() + argIdx, num_args)); + destructArgs<0, Args...>(dtor_args); + + return ret; + } + + template + static fmt::string_view unNameFormat(fmt::string_view in, uint32_t* reorderIdx, const Args&... args) { + constexpr size_t num_named_args = fmt::detail::count()...>(); + if constexpr (num_named_args == 0) { + return in; + } + const char* begin = in.data(); + const char* p = begin; + std::unique_ptr unnamed_str(new char[in.size() + 1 + num_named_args * 5]); + fmt::detail::named_arg_info named_args[std::max(num_named_args, (size_t)1)]; + storeNamedArgs<0, 0>(named_args, args...); + + char* out = (char*)unnamed_str.get(); + uint8_t arg_idx = 0; + while (true) { + auto c = *p++; + if (!c) { + size_t copy_size = p - begin - 1; + memcpy(out, begin, copy_size); + out += copy_size; + break; + } + if (c != '{') continue; + size_t copy_size = p - begin; + memcpy(out, begin, copy_size); + out += copy_size; + begin = p; + c = *p++; + if (!c) throw std::runtime_error("invalid format string"); + if (fmt::detail::is_name_start(c)) { + while ((fmt::detail::is_name_start(c = *p) || ('0' <= c && c <= '9'))) { + ++p; + } + fmt::string_view name(begin, p - begin); + int id = -1; + for (size_t i = 0; i < num_named_args; ++i) { + if (named_args[i].name == name) { + id = named_args[i].id; + break; + } + } + if (id < 0) throw std::runtime_error("invalid format string"); + if constexpr (Reorder) { + reorderIdx[id] = arg_idx++; + } + else { + out = fmt::format_to(out, "{}", id); + } + } + else { + *out++ = c; + } + begin = p; + } + const char* ptr = unnamed_str.release(); + return fmt::string_view(ptr, out - ptr); + } + +public: + template + inline void log(uint32_t& logId, int64_t tsc, const char* location, LogLevel level, const S& format, Args&&... args) { + using namespace fmtlogdetail; + constexpr size_t num_named_args = fmt::detail::count()...>(); + if constexpr (num_named_args == 0) { + fmt::detail::check_format_string>::type...>(format); + } + if (!logId) { + auto unnamed_format = unNameFormat(fmt::to_string_view(format), nullptr, args...); + registerLogInfo(logId, formatTo, location, level, unnamed_format); + } + constexpr size_t num_cstring = fmt::detail::count()...>(); + size_t cstringSizes[std::max(num_cstring, (size_t)1)]; + size_t allocSize = getArgSizes<0>(cstringSizes, args...) + 8; + if (threadBuffer == nullptr) preallocate(); + do { + if (threadBuffer->varq.tryPush(allocSize, [&](typename SPSCVarQueueOPT<>::MsgHeader* header) { + header->logId = logId; + char* writePos = (char*)(header + 1); + *(int64_t*)writePos = tsc; + writePos += 8; + encodeArgs<0>(cstringSizes, writePos, std::forward(args)...); + })) + return; + } while (FMTLOG_BLOCK); + } + + template + inline void logOnce(const char* location, LogLevel level, const S& format, Args&&... args) { + constexpr size_t num_named_args = fmt::detail::count()...>(); + if constexpr (num_named_args == 0) { + fmt::detail::check_format_string(format); + } + fmt::string_view sv(format); + size_t formatted_size = fmt::formatted_size(fmt::runtime(sv), args...); + size_t allocSize = formatted_size + 8 + 8; + if (threadBuffer == nullptr) preallocate(); + do { + if (threadBuffer->varq.tryPush(allocSize, [&](typename SPSCVarQueueOPT<>::MsgHeader* header) { + header->logId = (uint32_t)level; + char* writePos = (char*)(header + 1); + *(int64_t*)writePos = tscns.rdtsc(); + writePos += 8; + *(const char**)writePos = location; + writePos += 8; + fmt::format_to(writePos, fmt::runtime(sv), args...); + })) + return; + } while (FMTLOG_BLOCK); + } +}; + +using fmtlog = fmtlogT<>; + +template +FAST_THREAD_LOCAL typename fmtlogT<_>::ThreadBuffer* fmtlogT<_>::threadBuffer; + +template +struct fmtlogWrapper +{ static fmtlog impl; }; + +template +fmtlog fmtlogWrapper<_>::impl; + +template +inline void fmtlogT<_>::setLogLevel(LogLevel logLevel) { + fmtlogWrapper<>::impl.currentLogLevel = logLevel; +} + +template +inline typename fmtlogT<_>::LogLevel fmtlogT<_>::getLogLevel() { + return fmtlogWrapper<>::impl.currentLogLevel; +} + +#define __FMTLOG_S1(x) #x +#define __FMTLOG_S2(x) __FMTLOG_S1(x) +#define __FMTLOG_LOCATION __FILE__ ":" __FMTLOG_S2(__LINE__) + +#define FMTLOG(level, format, ...) \ + do { \ + static uint32_t logId = 0; \ + \ + if (level < fmtlog::getLogLevel()) break; \ + \ + fmtlogWrapper<>::impl.log(logId, fmtlogWrapper<>::impl.tscns.rdtsc(), __FMTLOG_LOCATION, level, \ + FMT_STRING(format), ##__VA_ARGS__); \ + } while (0) + +#define FMTLOG_LIMIT(min_interval, level, format, ...) \ + do { \ + static uint32_t logId = 0; \ + static int64_t limitNs = 0; \ + \ + if (level < fmtlog::getLogLevel()) break; \ + int64_t tsc = fmtlogWrapper<>::impl.tscns.rdtsc(); \ + int64_t ns = fmtlogWrapper<>::impl.tscns.tsc2ns(tsc); \ + if (ns < limitNs) break; \ + limitNs = ns + min_interval; \ + \ + fmtlogWrapper<>::impl.log(logId, tsc, __FMTLOG_LOCATION, level, FMT_STRING(format), ##__VA_ARGS__); \ + } while (0) + +#define FMTLOG_ONCE(level, format, ...) \ + do { \ + if (level < fmtlog::getLogLevel()) break; \ + \ + fmtlogWrapper<>::impl.logOnce(__FMTLOG_LOCATION, level, FMT_STRING(format), ##__VA_ARGS__); \ + } while (0) + +#if FMTLOG_ACTIVE_LEVEL <= FMTLOG_LEVEL_DBG +#define logd(format, ...) FMTLOG(fmtlog::DBG, format, ##__VA_ARGS__) +#define logdo(format, ...) FMTLOG_ONCE(fmtlog::DBG, format, ##__VA_ARGS__) +#define logdl(min_interval, format, ...) FMTLOG_LIMIT(min_interval, fmtlog::DBG, format, ##__VA_ARGS__) +#else +#define logd(format, ...) (void)0 +#define logdo(format, ...) (void)0 +#define logdl(min_interval, format, ...) (void)0 +#endif + +#if FMTLOG_ACTIVE_LEVEL <= FMTLOG_LEVEL_INF +#define logi(format, ...) FMTLOG(fmtlog::INF, format, ##__VA_ARGS__) +#define logio(format, ...) FMTLOG_ONCE(fmtlog::INF, format, ##__VA_ARGS__) +#define logil(min_interval, format, ...) FMTLOG_LIMIT(min_interval, fmtlog::INF, format, ##__VA_ARGS__) +#else +#define logi(format, ...) (void)0 +#define logio(format, ...) (void)0 +#define logil(min_interval, format, ...) (void)0 +#endif + +#if FMTLOG_ACTIVE_LEVEL <= FMTLOG_LEVEL_WRN +#define logw(format, ...) FMTLOG(fmtlog::WRN, format, ##__VA_ARGS__) +#define logwo(format, ...) FMTLOG_ONCE(fmtlog::WRN, format, ##__VA_ARGS__) +#define logwl(min_interval, format, ...) FMTLOG_LIMIT(min_interval, fmtlog::WRN, format, ##__VA_ARGS__) +#else +#define logw(format, ...) (void)0 +#define logwo(format, ...) (void)0 +#define logwl(min_interval, format, ...) (void)0 +#endif + +#if FMTLOG_ACTIVE_LEVEL <= FMTLOG_LEVEL_ERR +#define loge(format, ...) FMTLOG(fmtlog::ERR, format, ##__VA_ARGS__) +#define logeo(format, ...) FMTLOG_ONCE(fmtlog::ERR, format, ##__VA_ARGS__) +#define logel(min_interval, format, ...) FMTLOG_LIMIT(min_interval, fmtlog::ERR, format, ##__VA_ARGS__) +#else +#define loge(format, ...) (void)0 +#define logeo(format, ...) (void)0 +#define logel(min_interval, format, ...) (void)0 +#endif + +#ifdef FMTLOG_HEADER_ONLY +#include "fmtlog-inl.h" +#endif