FastMM documentation
FastMM C++ API 0.6.0
Public headers, tier 1 and tier 2
Loading...
Searching...
No Matches
log.hpp
Go to the documentation of this file.
1#pragma once
2// Asynchronous logging (5.10).
3//
4// FASTMM_LOG_INFO("filled {} @ {} on {}", qty, px, side);
5//
6// The call site captures a pointer to a static LogDescriptor (format string, file, line,
7// level) plus the packed argument values into a 256-byte LogRecord in the calling thread's
8// SpscRing. Nothing is formatted on the caller's thread; the LogSink thread pops records,
9// formats them with fmt and writes them to a FILE*. When a ring is full the record is
10// dropped and counted - the hot path never blocks.
11//
12// The format string is validated at compile time against the argument types (fmt), so a
13// mismatch is a compile error rather than a runtime surprise on the sink thread.
14//
15// Supported argument types: integers, floating point, bool, char, Price/Qty/Notional
16// (formatted as exact decimals), enums (via to_string() when available, else the
17// underlying integer), string_view / const char* / std::string / FixedString (copied,
18// truncated to kLogMaxStrBytes), Timestamp/Duration (ns as integers), Secret<T> ("***").
19#include "fastmm/core/config_macros.hpp"
20#include "fastmm/core/enums.hpp"
22#include "fastmm/core/fixed_string.hpp"
23#include "fastmm/core/spsc_ring.hpp"
25#include "fastmm/core/time.hpp"
26
27#include <fmt/base.h>
28
29#include <atomic>
30#include <concepts>
31#include <cstdint>
32#include <cstdio>
33#include <cstring>
34#include <string>
35#include <string_view>
36#include <type_traits>
37
38namespace fastmm {
39
40struct LogDescriptor {
41 const char* fmt;
42 const char* file;
43 std::uint32_t line;
45};
47enum class LogArgType : std::uint8_t {
48 Int = 1,
49 Uint = 2,
50 Double = 3,
51 Bool = 4,
52 Char = 5,
53 PriceFx = 6,
54 QtyFx = 7,
55 NotionalFx = 8,
56 Str = 9,
57 Secret = 10,
58};
59
60inline constexpr std::size_t kLogRecordBytes = 256;
61inline constexpr std::size_t kLogMaxStrBytes = 64;
62inline constexpr std::size_t kLogRingSize = 8192; // records per thread (2 MiB)
63inline constexpr std::size_t kLogMaxThreads = 64;
65struct LogRecord {
66 const LogDescriptor* desc;
67 std::int64_t ts_ns;
68 std::uint32_t tid;
69 std::uint8_t nargs;
70 std::uint8_t used;
71 std::uint8_t pad_[2];
72 std::uint8_t args[kLogRecordBytes - 24];
73};
74static_assert(sizeof(LogRecord) == kLogRecordBytes && std::is_trivially_copyable_v<LogRecord>);
75
76using LogRing = SpscRing<LogRecord, kLogRingSize>;
77
78// Wrap a value to have it rendered as "***".
79template <class T>
80struct Secret {
81 T value;
82};
83template <class T>
85 return Secret<std::decay_t<T>>{std::forward<T>(v)};
87
88// Global level (runtime); FASTMM_MIN_LOG_LEVEL is the compile-time floor.
89inline std::atomic<std::uint8_t> g_log_level{static_cast<std::uint8_t>(LogLevel::Info)};
90[[nodiscard]] FASTMM_FORCE_INLINE bool log_enabled(LogLevel lvl) noexcept {
91 return static_cast<std::uint8_t>(lvl) >= g_log_level.load(std::memory_order_relaxed);
93
94// Process-wide registry of per-thread rings + the sink thread. Implemented in log_sink.cpp.
95class Logger {
96 public:
97 static Logger& instance() noexcept;
98
99 void set_level(LogLevel lvl) noexcept { g_log_level.store(static_cast<std::uint8_t>(lvl)); }
100 [[nodiscard]] LogLevel level() const noexcept {
101 return static_cast<LogLevel>(g_log_level.load());
103
104 // Gives the calling thread a ring: a drained ring released by an exited thread if one exists,
105 // otherwise a new one (heap). Call during warm-up on every thread that logs; it is otherwise
106 // done lazily on the first log statement. Returns nullptr once kLogMaxThreads rings are in
107 // use at the same time.
109 // Returns the calling thread's ring to the pool. Runs automatically when a thread that logged
110 // exits, so short-lived threads (sweep workers, Python callers) never exhaust the slots.
111 void detach_current_thread() noexcept;
112 // Number of rings allocated so far; reused rings are not counted again.
113 [[nodiscard]] std::size_t thread_slots() const noexcept;
114
115 // Sink control. `out` defaults to stderr. Records at >= mirror_level are also copied to
116 // stderr when `out` is a different stream.
117 void start(std::FILE* out = stderr, LogLevel mirror_level = LogLevel::Warn);
118 void stop(); // drains everything then joins the sink thread
119 // Drains all rings and fflush()es. Works with or without a running sink thread.
120 void flush();
121 [[nodiscard]] bool running() const noexcept { return running_.load(std::memory_order_acquire); }
122 // The sink writes to a stream other than stderr (a log file).
123 [[nodiscard]] bool to_file() const noexcept { return to_file_.load(std::memory_order_acquire); }
124
125 [[nodiscard]] std::uint64_t dropped() const noexcept {
126 return dropped_.load(std::memory_order_relaxed);
128 void note_drop() noexcept { dropped_.fetch_add(1, std::memory_order_relaxed); }
129 [[nodiscard]] std::uint64_t written() const noexcept {
130 return written_.load(std::memory_order_relaxed);
132
133 // Formats one record into `out` (appends). Public for tests/tools.
134 static void format_record(const LogRecord& r, std::string& out);
135
136 private:
137 Logger() = default;
138 ~Logger();
139 std::size_t drain_all(); // returns records processed
140 void sink_loop();
141
142 struct Impl;
143 Impl* impl_ = nullptr;
144 std::atomic<bool> running_{false};
145 std::atomic<bool> to_file_{false};
146 std::atomic<std::uint64_t> dropped_{0};
147 std::atomic<std::uint64_t> written_{0};
148};
149
150namespace detail {
151
152inline thread_local LogRing* t_log_ring = nullptr;
153inline thread_local std::uint32_t t_log_tid = 0;
154inline thread_local std::uint32_t t_log_slot = 0;
155inline thread_local bool t_log_thread_exiting = false; // set once thread-local teardown began
156
157// Compile-time floor check, written as a function so -Wtype-limits stays quiet when the
158// floor is 0.
159constexpr bool log_compiled(LogLevel l) noexcept {
160 const int v = static_cast<int>(l);
161 const int floor = FASTMM_MIN_LOG_LEVEL;
162 return v >= floor;
163}
164
165template <class E>
166concept EnumWithToString = std::is_enum_v<E> && requires(E e) {
167 { to_string(e) } -> std::convertible_to<std::string_view>;
168};
169template <class T>
170concept StringLike = std::convertible_to<const T&, std::string_view>;
171template <class T>
172struct IsSecret : std::false_type {};
173template <class T>
174struct IsSecret<Secret<T>> : std::true_type {};
175template <class T>
176struct IsFixed : std::false_type {};
177template <class Tag>
178struct IsFixed<Fixed<Tag>> : std::true_type {};
179
180// Type the sink hands to fmt for a given argument (used for compile-time format checks).
181template <class T>
182struct LogFmtTypeImpl {
183 using D = std::remove_cvref_t<T>;
184 using type = std::conditional_t<
185 std::is_same_v<D, bool>,
186 bool,
187 std::conditional_t<
188 std::is_same_v<D, char>,
189 char,
190 std::conditional_t<
191 std::is_integral_v<D>,
192 std::conditional_t<std::is_signed_v<D>, std::int64_t, std::uint64_t>,
193 std::conditional_t<
194 std::is_floating_point_v<D>,
195 double,
196 std::conditional_t<
197 IsFixed<D>::value || IsSecret<D>::value || StringLike<D> ||
198 EnumWithToString<D>,
199 std::string_view,
200 std::conditional_t<std::is_enum_v<D>,
201 std::int64_t,
202 std::conditional_t<std::is_same_v<D, Timestamp> ||
203 std::is_same_v<D, Duration>,
204 std::int64_t,
205 void>>>>>>>;
206 static_assert(!std::is_void_v<type>, "unsupported log argument type");
207};
208template <class T>
209using LogFmtType = typename LogFmtTypeImpl<T>::type;
210
211struct ArgPacker {
212 std::uint8_t* p;
213 std::uint8_t* end;
214 std::uint8_t n = 0;
215
216 FASTMM_FORCE_INLINE bool put_scalar(LogArgType t, std::uint64_t bits) noexcept {
217 if (end - p < 9) return false;
218 *p++ = static_cast<std::uint8_t>(t);
219 std::memcpy(p, &bits, 8);
220 p += 8;
221 ++n;
222 return true;
223 }
224 FASTMM_FORCE_INLINE bool put_str(LogArgType t, std::string_view s) noexcept {
225 const std::size_t room = static_cast<std::size_t>(end - p);
226 if (room < 2) return false;
227 std::size_t len = s.size() < kLogMaxStrBytes ? s.size() : kLogMaxStrBytes;
228 if (len > room - 2) len = room - 2;
229 *p++ = static_cast<std::uint8_t>(t);
230 *p++ = static_cast<std::uint8_t>(len);
231 if (len != 0) std::memcpy(p, s.data(), len); // an empty string_view may hold nullptr
232 if (len < s.size() && len >= 3) std::memcpy(p + len - 3, "...", 3); // make the cut visible
233 p += len;
234 ++n;
235 return true;
236 }
237};
238
239template <class T>
240FASTMM_FORCE_INLINE bool pack_arg(ArgPacker& pk, const T& v) noexcept {
241 using D = std::remove_cvref_t<T>;
242 if constexpr (std::is_same_v<D, bool>) {
243 return pk.put_scalar(LogArgType::Bool, v ? 1U : 0U);
244 } else if constexpr (std::is_same_v<D, char>) {
245 return pk.put_scalar(LogArgType::Char, static_cast<std::uint8_t>(v));
246 } else if constexpr (std::is_integral_v<D>) {
247 if constexpr (std::is_signed_v<D>) {
248 return pk.put_scalar(LogArgType::Int,
249 static_cast<std::uint64_t>(static_cast<std::int64_t>(v)));
250 } else {
251 return pk.put_scalar(LogArgType::Uint, static_cast<std::uint64_t>(v));
252 }
253 } else if constexpr (std::is_floating_point_v<D>) {
254 const double d = static_cast<double>(v);
255 std::uint64_t bits = 0;
256 std::memcpy(&bits, &d, 8);
257 return pk.put_scalar(LogArgType::Double, bits);
258 } else if constexpr (std::is_same_v<D, Price>) {
259 return pk.put_scalar(LogArgType::PriceFx, static_cast<std::uint64_t>(v.raw));
260 } else if constexpr (std::is_same_v<D, Qty>) {
261 return pk.put_scalar(LogArgType::QtyFx, static_cast<std::uint64_t>(v.raw));
262 } else if constexpr (std::is_same_v<D, Notional>) {
263 return pk.put_scalar(LogArgType::NotionalFx, static_cast<std::uint64_t>(v.raw));
264 } else if constexpr (IsSecret<D>::value) {
265 return pk.put_str(LogArgType::Secret, "***");
266 } else if constexpr (EnumWithToString<D>) {
267 return pk.put_str(LogArgType::Str, std::string_view(to_string(v)));
268 } else if constexpr (std::is_enum_v<D>) {
269 return pk.put_scalar(LogArgType::Int,
270 static_cast<std::uint64_t>(
271 static_cast<std::int64_t>(static_cast<std::underlying_type_t<D>>(v))));
272 } else if constexpr (std::is_same_v<D, Timestamp> || std::is_same_v<D, Duration>) {
273 return pk.put_scalar(LogArgType::Int, static_cast<std::uint64_t>(v.ns));
274 } else if constexpr (StringLike<D>) {
275 return pk.put_str(LogArgType::Str, std::string_view(v));
276 } else {
277 static_assert(sizeof(D) == 0, "unsupported log argument type");
278 return false;
279 }
280}
281
282template <class... Args>
283FASTMM_FORCE_INLINE void log_emit(const LogDescriptor& d, const Args&... args) noexcept {
284 LogRing* ring = t_log_ring;
285 if (FASTMM_UNLIKELY(ring == nullptr)) {
287 if (ring == nullptr) {
289 return;
290 }
291 }
292 LogRecord* r = ring->try_reserve();
293 if (FASTMM_UNLIKELY(r == nullptr)) {
295 return;
296 }
297 r->desc = &d;
298 r->ts_ns = wall_now().ns;
299 r->tid = t_log_tid;
300 ArgPacker pk{r->args, r->args + sizeof(r->args)};
301 (static_cast<void>(pack_arg(pk, args)), ...);
302 r->nargs = pk.n;
303 r->used = static_cast<std::uint8_t>(pk.p - r->args);
304 ring->commit();
305}
306
307} // namespace detail
308} // namespace fastmm
309
310// fmt_str must be a string literal. The generic lambda lets us spell the packed argument
311// types for the compile-time format check without touching the arguments' values.
312#define FASTMM_LOG_IMPL(lvl, fmt_str, ...) \
313 do { \
314 if constexpr (::fastmm::detail::log_compiled(lvl)) { \
315 if (::fastmm::log_enabled(lvl)) { \
316 static constexpr ::fastmm::LogDescriptor fastmm_log_desc_{ \
317 fmt_str, __FILE__, __LINE__, lvl}; \
318 [&](const auto&... fastmm_a_) { \
319 static_cast<void>( \
320 ::fmt::format_string<::fastmm::detail::LogFmtType<decltype(fastmm_a_)>...>( \
321 fmt_str)); \
322 ::fastmm::detail::log_emit(fastmm_log_desc_, fastmm_a_...); \
323 }(__VA_ARGS__); \
324 } \
325 } \
326 } while (0)
327
328#define FASTMM_LOG_TRACE(fmt_str, ...) \
329 FASTMM_LOG_IMPL(::fastmm::LogLevel::Trace, fmt_str __VA_OPT__(, ) __VA_ARGS__)
330#define FASTMM_LOG_DEBUG(fmt_str, ...) \
331 FASTMM_LOG_IMPL(::fastmm::LogLevel::Debug, fmt_str __VA_OPT__(, ) __VA_ARGS__)
332#define FASTMM_LOG_INFO(fmt_str, ...) \
333 FASTMM_LOG_IMPL(::fastmm::LogLevel::Info, fmt_str __VA_OPT__(, ) __VA_ARGS__)
334#define FASTMM_LOG_WARN(fmt_str, ...) \
335 FASTMM_LOG_IMPL(::fastmm::LogLevel::Warn, fmt_str __VA_OPT__(, ) __VA_ARGS__)
336#define FASTMM_LOG_ERROR(fmt_str, ...) \
337 FASTMM_LOG_IMPL(::fastmm::LogLevel::Error, fmt_str __VA_OPT__(, ) __VA_ARGS__)
Process-wide registry of per-thread rings + the sink thread. Implemented in log_sink....
Definition log.hpp:97
static Logger & instance() noexcept
LogLevel level() const noexcept
Definition log.hpp:102
std::size_t thread_slots() const noexcept
Number of rings allocated so far; reused rings are not counted again.
void start(std::FILE *out=stderr, LogLevel mirror_level=LogLevel::Warn)
Sink control.
void detach_current_thread() noexcept
Returns the calling thread's ring to the pool.
std::uint64_t written() const noexcept
Definition log.hpp:131
void stop()
drains everything then joins the sink thread
std::uint64_t dropped() const noexcept
Definition log.hpp:127
void set_level(LogLevel lvl) noexcept
Definition log.hpp:101
void flush()
Drains all rings and fflush()es. Works with or without a running sink thread.
void note_drop() noexcept
Definition log.hpp:130
bool running() const noexcept
Definition log.hpp:123
LogRing * attach_current_thread()
Gives the calling thread a ring: a drained ring released by an exited thread if one exists,...
bool to_file() const noexcept
The sink writes to a stream other than stderr (a log file).
Definition log.hpp:125
static void format_record(const LogRecord &r, std::string &out)
Formats one record into out (appends). Public for tests/tools.
All engine enumerations.
Fixed<Tag>: int64 fixed point with a global 1e-8 scale (ADR-0001).
Definition strategy.hpp:16
SpscRing< LogRecord, kLogRingSize > LogRing
Definition log.hpp:78
LogLevel
Definition enums.hpp:604
Secret< std::decay_t< T > > secret(T &&v)
Definition log.hpp:86
constexpr std::size_t kLogRecordBytes
Definition log.hpp:62
Timestamp wall_now() noexcept
CLOCK_REALTIME via vDSO (~20 ns). Implemented in src/core/time.cpp.
std::atomic< std::uint8_t > g_log_level
Global level (runtime); FASTMM_MIN_LOG_LEVEL is the compile-time floor.
Definition log.hpp:91
FASTMM_FORCE_INLINE bool log_enabled(LogLevel lvl) noexcept
Definition log.hpp:92
constexpr std::size_t kLogMaxStrBytes
Definition log.hpp:63
LogArgType
Definition log.hpp:49
constexpr std::size_t kLogRingSize
records per thread (2 MiB)
Definition log.hpp:64
constexpr std::string_view to_string(ParamType t) noexcept
Definition params.hpp:63
constexpr std::size_t kLogMaxThreads
Definition log.hpp:65
Strongly typed identifiers.
Definition log.hpp:42
LogLevel level
Definition log.hpp:46
const char * file
Definition log.hpp:44
const char * fmt
Definition log.hpp:43
std::uint32_t line
Definition log.hpp:45
Definition log.hpp:67
std::uint8_t args[kLogRecordBytes - 24]
Definition log.hpp:74
std::int64_t ts_ns
Definition log.hpp:69
std::uint32_t tid
Definition log.hpp:70
std::uint8_t used
Definition log.hpp:72
std::uint8_t pad_[2]
Definition log.hpp:73
const LogDescriptor * desc
Definition log.hpp:68
std::uint8_t nargs
Definition log.hpp:71
Wrap a value to have it rendered as "***".
Definition log.hpp:82
T value
Definition log.hpp:83
std::int64_t ns
Definition time.hpp:46
Time primitives.