Flow 1.0.1
Flow project: Full implementation reference.
async_file_logger.hpp
Go to the documentation of this file.
1/* Flow
2 * Copyright 2023 Akamai Technologies, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the
5 * "License"); you may not use this file except in
6 * compliance with the License. You may obtain a copy
7 * of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in
12 * writing, software distributed under the License is
13 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
14 * CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing
16 * permissions and limitations under the License. */
17
18/// @file
19#pragma once
20
21#include "flow/log/log.hpp"
25#include <boost/asio.hpp>
26#include <boost/move/unique_ptr.hpp>
27#include <atomic>
28
29namespace flow::log
30{
31
32// Types.
33
34/**
35 * An implementation of Logger that logs messages to a given file-system path but never blocks any logging thread
36 * for file I/O; suitable for heavy-duty file logging. Protects against garbling due to simultaneous logging from
37 * multiple threads.
38 *
39 * For file logging, the two out-of-the-box `Logger`s currently suitable are this Async_file_logger and
40 * Simple_ostream_logger. Vaguely speaking, Simple_ostream_logger is suitable for console (`cout`, `cerr`) output;
41 * and, in a pinch outside of a heavy-duty production/server environment, for file (`ofstream`) output. For heavy-duty
42 * file logging one should use this Async_file_logger. The primary reason is performance; this is discussed
43 * in the Logger class doc header; note Async_file_logger logs asynchronously.
44 * A secondary reason is additional file-logging-specific utilities -- such as
45 * rotation -- are now or in the future going to be in Async_file_logger, as its purpose is heavy-duty file logging
46 * specifically.
47 *
48 * @todo Lacking feature: Compress-as-you-log in Async_file_logger. So, optionally, when characters are actually
49 * written out to file-system, gzip/zip/whatever them instead of writing plain text. (This is possible at least
50 * for gzip.) Background: It is common-place to compress a log file after it has been rotated (e.g., around rotation
51 * time: F.log.1.gz -> F.log.2.gz, F.log -> F.log.1 -> F.log.1.gz). It is more space-efficient (at least), however,
52 * to write to F.log.gz directly already in compressed form; then rotation requires only renaming (e.g.:
53 * F.log.1.gz -> F.log.2.gz, F.log.gz [already gzipped from the start] -> F.log.1.gz).
54 *
55 * ### Throttling ###
56 * By default this feature is disabled, but one can enable/disable/configure it at will via throttling_cfg() mutator.
57 * For example a viable tactic is to call it once right after construction, before any logging via `*this`; then
58 * call it subsequently in case of dynamic config update.
59 *
60 * Throttling deals with the following potential problem which can occur under very heavy do_log() throughput
61 * (lots of calls being made per unit time, probably from many threads); in practice this would typically only happen
62 * if one sets effective verbosity to Sev::S_TRACE or more-verbose -- as `S_INFO`-or-less-verbose means
63 * should_log() should be preventing do_log() from being called frequently. For example one might enable TRACE
64 * logging temporarily in production to see some details, causing a heavy do_log() execution rate. (That said, if
65 * your code does INFO-level logging too often, it could happen then too. The official *convention* is to
66 * log INFO-or-more-severe messages only if this would not meaningfully affect the overall perf and responsiveness
67 * of the system; but sometimes mistakes are made.)
68 *
69 * Internally each do_log() call pushes this *log request* into a central (per `*this`) queue of log requests;
70 * a single (per `*this`) background thread is always popping this queue ASAP, writing the message to the file,
71 * etc., until the queue is emptied; then it sleeps until it becomes non-empty; and so on. If many threads are
72 * pushing messages faster than this thread can get through them, then more and more RAM
73 * is used to store the enqueued messages. If the throughput doesn't decrease in time, letting this central thread
74 * catch up, then the RAM use might cause swapping and eventually out-of-memory (congestive collapse).
75 * To be clear, unless congestive collapse is in fact reached, the system is self-correcting, in that given
76 * a respite from the log-requests (do_log() calls), the queue size *will* get down to zero, as will the corresponding
77 * memory use. However, if this does not occur early enough, congestive collapse occurs.
78 *
79 * Throttling is a safeguard against this. It works as follows. There's a limit H in bytes. We start at
80 * Not-Throttling state. If the memory use M grows beyond H, we enter Throttling state. In this state, we
81 * reject incoming log requests -- thus letting the system deal with just logging-to-file what it has queued up
82 * (in FIFO fashion, naturally) -- until no more such queued-up requests remain. Once the queue is empty,
83 * we re-enter Not-Throttling state. In other words beyond a certain total memory use, we throttle; then to exit
84 * this state total memory use has to go down to 0, before we un-throttle. The idea is to encourage a sawtooth
85 * `/\/\/\` memory-use pattern when subjected to a firehose of log requests.
86 *
87 * @note An earlier functional design contemplated having a limit L (smaller than H; e.g., picture L = 50% of H),
88 * so that mem-use would need to merely get down to L to re-enter Not-Throttling state. However, upon
89 * prototyping this, it became clear this hardly improved the experience, instead making it rather confusing
90 * to understand in action. E.g., if there's a firehose going at full-blast, and you're fighting it by turning
91 * off the fire-hose and letting water drain, but then turn it on again once the container is 50% full, then the
92 * draining will "fight" the filling again, potentially losing quite decisively. Trying to then read resulting
93 * logs is messy and strange, depending on the 2 rates relative to each other. By comparison, the
94 * fill-drain-fill-drain `/\/\/\` pattern is straightforward to understand; and one can cleanly point out
95 * the minima and maxima with log messages. Plus, less configuration/tuning with little to no functional
96 * loss = a good thing.
97 *
98 * The particular mechanic of throttling is as follows. If throttling is enabled during a particular call to
99 * should_log(), and if and only if should_log() would return `true` based on considerations excluding the
100 * throttling feature, then:
101 *
102 * - should_log() returns `true` in Not-Throttling state;
103 * - should_log() returns `false` in Throttling state;
104 *
105 * Since standard `FLOW_LOG_*` macros avoid do_log() (or even the evaluation of the message -- which is itself
106 * quite expensive, possibly quite a bit more expensive than the do_log()) if should_log()
107 * returns `false` for a given log call site, during Throttling state enqueuing of messages is blocked (letting
108 * the logging-to-file thread catch up). `should_log() == false` is how we turn off the firehose.
109 *
110 * M starts at 0. Each do_log() (queued-up log request) increments M based on the memory-use estimate of the
111 * message+metadata passed to do_log(); then it enqueues the log request. Each time the background thread
112 * actually writes out the queued-up message+metadata, it decrements M by the same value by which it was
113 * incremented in do_log(); accordingly the log-request's memory is freed.
114 *
115 * This algorithm (computing M via increments and decrements; setting state to Throttling or Not-Throttling)
116 * is carried out at all times. However, should_log() consults the state (Throttling versus Not-Throttling), if
117 * and only if the throttling feature is enabled at that time. If it is not, that state is simply ignored, and
118 * should_log() only makes the usual Config verbosity check(s); if that results in `true` then the message
119 * is enqueued. Therefore one can enable throttling at any time and count on its having immediate
120 * effect based on actual memory use at that time.
121 *
122 * The limit H can also be reconfigured at any time. Essentially throttling_cfg() mutator takes
123 * 2 orthogonal sets of info: 1, whether throttling is to be possible at all (whether Throttling versus Not-Throttling
124 * affects should_log() from now on); and 2, the limit H which controls the policy about setting
125 * the state (Throttling versus Not-Throttling). (2) affects the algorithm that computes that binary state; whereas
126 * (1) affects whether that binary state actually controls whether to prevent logging to save memory or not.
127 *
128 * If H is modified, the binary state is reinitialized: it is set to Throttling if and only if
129 * memory use M at that time exceeds H; else to Not-Throttling. The state prior to the throttling_cfg() mutator call
130 * does not matter in this situation; it is overwritten. This avoids various annoying corner cases and ambiguities
131 * around config updates.
132 *
133 * Lastly: Initially throttling is disabled, while a certain default value of H is assumed. Hence the above
134 * algorithm is active but has no effect, unless you call `throttling_cfg(true, ...)` to make it have effect
135 * and/or change H. You may use throttling_cfg() accessor and throttling_active() to get a copy of the current
136 * config values.
137 *
138 * ### Thread safety ###
139 * As noted above, simultaneous logging from multiple threads is safe from output corruption, in that
140 * simultaneous do_log() calls for the same Logger targeting the same stream will log serially to each other.
141 * However, if some other code or process writes to the same file, then all bets are off -- so don't.
142 *
143 * See thread safety notes and to-dos regarding #m_config in Simple_ostream_logger doc header. These apply here also.
144 *
145 * throttling_cfg() mutator does not add any thread safety restrictions: it can be called concurrently with any
146 * other method, including should_log(), do_log(), same-named accessor, and throttling_active(). There is one formal
147 * exception: it must not be called concurrently with itself.
148 *
149 * There are no other mutable data (state), so that's that.
150 *
151 * ### Throttling: Functional design rationale notes ###
152 * The throttling feature could have been designed differently (in terms of how it should act, functionally speaking),
153 * and a couple of questions tend to come up, so let's answer here.
154 * - Why have the throttling algorithm always-on, even when `!throttling_active()` -- which is default at that?
155 * Could save cycles otherwise, no? Answer: To begin with, the counting of the memory used (M) should be accurate
156 * in case it is (e.g.) high, and one changes throttling_active() to `true`. Still, couldn't some things be
157 * skipped -- namely perhaps determining whether state is Throttling or Not-Throttling and logging about it -- when
158 * `!throttling_active()`? Answer: Yes, and that might be a decent change in the future, as internally it might
159 * be possible to skip some mutex work in that situation which could be a small optimization. It is basically
160 * simpler to think about and implement the existing way. (More notes on this in internal comments.)
161 * - Why not get rid of throttling_active() knob entirely? E.g., Async_file_logger::Throttling_cfg::m_hi_limit (H)
162 * could just be set to a huge value to have an apparently similar effect to `!throttling_active()`. (The knob
163 * could still exist cosmetically speaking but just have the aforementioned effect.) Answer: I (ygoldfel) first
164 * thought similarly, while others specified otherwise; but I quickly came around to agreeing with them. It is
165 * nice to log about crossing the threshold H even without responding to it by throttling; it could be a signal
166 * for a user to look into enabling the feature. Granted, we could also log at every 500k increment, or
167 * something like that; but the present setup seemed like a nice balance between power and simplicity.
168 *
169 * All in all, these choices are defensible but not necessarily the only good ones.
170 *
171 * @internal
172 *
173 * Implementation
174 * --------------
175 * The basic implementation is straightforward enough to be gleaned from reading the code and other comments.
176 *
177 * ### Throttling impl: The essential algorithm ###
178 * What bears discussion is the implementation of the throttling feature. Read on if you have interest in that
179 * specific topic. If so please carefully read the public section above entitled Throttling; then come back here.
180 *
181 * The impl of throttling writes itself based on that description, if one is allowed to use a mutex. Then it's all
182 * pretty simple: There are 3-4 functions to worry about:
183 *
184 * - should_log(sev, component): First compute `m_serial_logger->should_log(sev, component)`; usually that will
185 * return `false`, so we should too. If it returns `true`, though, then we should apply the added test of
186 * whether throttling should make us return `false` after all. So:
187 * - Lock mutex.
188 * - If #m_throttling_active is `false` (feature disabled) then return `true`. Else:
189 * - If #m_throttling_now is `false` (state is Not-Throttling) then return `true`. Else:
190 * - Return `false`. (Throttling feature enabled, and state is currently Throttling.)
191 * - `do_log(metadata, msg)`:
192 * - Compute `C = mem_cost(msg)` which inlines to essentially `msg.size()` + some compile-time constant.
193 * - Let local `bool throttling_begins = false`.
194 * - Lock mutex.
195 * - Increment `m_pending_logs_sz` by `C`. `m_pending_logs_sz` is called M in the earlier discussion: the
196 * memory use estimate of things do_log() has enqueued but `really_log()` (see just below) has not
197 * yet dequeued and logged to file.
198 * - If #m_throttling_now is `false`, and we just made `m_pending_logs_sz` go from
199 * `< m_throttling_cfg.m_hi_limit` (a/k/a H) to `>= m_throttling_cfg.m_hi_limit`, then
200 * set #m_throttling_now to `true`; and set `throttling_begins = true`.
201 * - Enqueue the log-request:
202 * - Capture `metadata`; a copy of `msg`; and `throttling_begins`.
203 * - `m_async_worker.post()` the lambda which invokes `really_log()` with those 3 items as args.
204 * - `really_log(metadata, msg, throttling_begins)`:
205 * - `m_serial_logger->do_log(metadata, msg)`: write-out the actual message to the file.
206 * - If `throttling_begins == true`: via `m_serial_logger->do_log()` write-out a special message
207 * indicating that `msg` was the message causing state to earlier change from Not-Throttling to
208 * Throttling due to mem-use passing ceiling H at that time.
209 * - Compute `C = mem_cost(msg)` (same as in `do_log()` above).
210 * - Let local `bool throttling_ends = false`.
211 * - Lock mutex.
212 * - Decrement `m_pending_logs_sz` (a/k/a M) by `C`.
213 * - If #m_throttling_now is `true`, and we just made `m_pending_logs_sz` go down to 0, then
214 * set #m_throttling_now to `false`; and set `throttling_ends = true`.
215 * - If `throttling_ends == true`: via `m_serial_logger->do_log()` write-out a special message
216 * indicating that state has changed from Throttling to Not-Throttling due to mem-use reaching 0.
217 * - `throttling_cfg(active, cfg)` mutator:
218 * - Lock mutex.
219 * - Save args into #m_throttling_active and #m_throttling_cfg respectively.
220 * - If the latter's contained value (H) changed:
221 * - Assign `m_throttling_now = (m_pending_logs_sz >= m_throttling_cfg.m_hi_limit)`.
222 * This reinitializes the state machine cleanly as promised in the class doc header public section.
223 *
224 * The mutex makes everything easy. However the resulting perf is potentially unacceptable, at least
225 * because should_log() is called *very* frequently from *many* threads and has a mutex lock now.
226 * We must strive to keep computational overhead in should_log() very low; and avoid extra lock contention
227 * if possible, especially to the extent it would affect should_log(). Onward:
228 *
229 * ### Throttling impl: The algorithm modified to become lock-free in should_log() ###
230 * The easiest way to reduce critical section in should_log() concerns access to #m_throttling_active.
231 * Suppose we make it `atomic<bool>` instead of `bool` with mutex protection. If we store with `relaxed` ordering
232 * and load with `relaxed` ordering, and do both outside any shared mutex-lock section:
233 * throttling_cfg() mutator does the quite-rare storing; should_log() does the possibly-frequent (albeit gated by
234 * `m_serial_logger->should_log() == true` in the first place) loading. The `relaxed` order means
235 * at worst there's a bit of a delay for some threads noticing the config change; so this or that thread might
236 * throttle or not-throttle a tiny bit of time after another: it's absolutely not a problem. Moreover there is
237 * ~zero penalty to a `relaxed` load of `atomic<bool>` compared to simply accessing a `bool`. Adding a `bool` check
238 * to should_log() is not nothing, but it's very close.
239 *
240 * The only now-remaining thing in the mutex-lock section of should_log() (see pseudocode above) is the
241 * Boolean check of #m_throttling_now. There is exactly one consumer of this Boolean: should_log(). Again
242 * let's replace `bool m_throttling_now` with `atomic<bool> m_throttling_now`; and load it with `relaxed`
243 * ordering in should_log(), outside any shared mutex-lock section. There are exactly 3 assigners: do_log
244 * () and `really_log()`; they assign this when M 1st goes up past H (assign `true`) or 1st down to 0
245 * (assign `false`) respectively; and throttling_cfg() mutator (assign depending on where M is compared to
246 * the new H). So let's assume -- and we'll discuss the bejesus out of it below -- we ensure the assigning
247 * algorithm among those 3 places is made to work properly, meaning #m_throttling_now (Throttling versus
248 * Not-Throttling state) algorithm is made to correctly set the flag's value correctly in and of itself. Then
249 * should_log() merely needs to read #m_throttling_now and check it against `true` to return `should_log() ==
250 * false` iff so. Once again, if `relaxed` ordering causes some threads to "see" a new value a little later
251 * than others, that is perfectly fine. (We re-emphasize that the 3 mutating assignment events are hardly
252 * frequent: only when passing H going up for the 1st time since being 0, reaching 0 for the 1st time since
253 * being >= H, and possibly in throttling_cfg() mutator call.)
254 *
255 * Now the critical section in should_log() has been emptied: so no more mutex locking or unlocking needed in it.
256 *
257 * @note Note well! The lock-free, low-overhead nature of should_log() as described in the preceding 3 paragraphs is
258 * **by far** the most important perf achievement of this algorithm. Having achieved that, we've solved what's
259 * almost certainly the only perf objective that really matters. The only other code area that could conceivably
260 * matter perf-wise is do_log() -- and it does conceivably matter but not very much in practice.
261 * Please remember: do_log() is already a heavy-weight operation; before it is
262 * even called, the `FLOW_LOG_*()` macro almost certainly invoking it must perform expensive `ostream` assembly
263 * of `msg`; then do_log() itself needs to make a copy of `msg` and create an `std::function<>` with a number of
264 * captures, and enqueue all that into a boost.asio queue (which internally involves a mutex lock/unlock). That's
265 * why should_log() is a separate call: by being very fast and usually returning `false`, most *potential*
266 * do_log() calls -- *and* the `msg` assembly (and more) *potentially* preceding each -- never happen at all:
267 * `FLOW_LOG_...()` essentially has the form `if (should_log(...)) { ...prep msg and mdt...; do_log(mdt, msg); }`.
268 * So we *should* strive to keep added throttling-algorithm-driven overhead in do_log() low and minimize
269 * mutex-locked critical sections therein; but such striving is a nicety, whereas optimizing should_log()
270 * is a necessity.
271 *
272 * So: We've now reduced the algorithm to:
273 *
274 * - `do_log(metadata, msg)`:
275 * - Compute `C = mem_cost(msg)` (add a few things including `msg.size()`).
276 * - Let local `bool throttling_begins = false`.
277 * - Lock mutex.
278 * - `m_pending_logs_sz += C`.
279 * - If #m_throttling_now is `false`, and we just made `m_pending_logs_sz` go from
280 * `< m_throttling_cfg.m_hi_limit` to `>= m_throttling_cfg.m_hi_limit`, then
281 * set #m_throttling_now to `true`; and set `throttling_begins = true`.
282 * - Enqueue the log-request:
283 * - Capture `metadata`; a copy of `msg`; and `throttling_begins`.
284 * - `m_async_worker.post()` the lambda which invokes `really_log()` with those 3 items as inputs.
285 * - `really_log(metadata, msg, throttling_begins)`:
286 * - `m_serial_logger->do_log(metadata, msg)`.
287 * - If `throttling_begins == true`: via `m_serial_logger->do_log()` write-out a special message
288 * indicating that `msg` was the message causing state to earlier change from Not-Throttling to
289 * Throttling due to mem-use passing ceiling H at that time.
290 * - Compute `C = mem_cost(msg)`.
291 * - Let local `bool throttling_ends = false`.
292 * - Lock mutex.
293 * - `m_pending_logs_sz -= C`.
294 * - If #m_throttling_now is `true`, and we just made `m_pending_logs_sz == 0`,
295 * then set #m_throttling_now to `false`; and set `throttling_ends = true`.
296 * - If `throttling_ends == true`: via `m_serial_logger->do_log()` write-out:
297 * state has changed from Throttling to Not-Throttling due to mem-use use reaching 0.
298 * - `throttling_cfg(active, cfg)` mutator:
299 * - Save `m_throttling_active = active`.
300 * - Lock mutex.
301 * - Save `m_throttling_cfg = cfg`.
302 * - If the latter's contained value (H) changed:
303 * - Assign `m_throttling_now = (m_pending_logs_sz >= m_throttling_cfg.m_hi_limit)`.
304 *
305 * That's acceptable, because the really important (for perf) place, should_log(), now is lock-free
306 * with the added overhead being 2 mere checks against zero; and even then only if the core `should_log()` yielded
307 * `true` -- which usually it doesn't.
308 *
309 * Let's reassert that the overhead and potential lock contention added on account of the throttling logic
310 * in do_log() are minor. (If so, then `really_log()` and throttling_cfg() mutator need not be scrutinized much, as
311 * they matter less and much less respectively.) We've already noted this, but let's make sure.
312 *
313 * - Added cycles (assuming no lock contention): To enumerate this overhead:
314 * - mem_cost(). This adds `msg.size()` (a `size_t` memory value) to a compile-time constant, more or less.
315 * - Increment M by that number (`+=` with saving a `prev_val` and resulting `new_val`).
316 * - Check a `bool`.
317 * - Possibly compare `prev_val < H` and `new_val >= H`.
318 * - Set `bool throttling_begins` to `true` or `false` accordingly.
319 * - Add `throttling_begins` in addition to the existing payload into Log_request (which is
320 * 2 pointers + 1 `size_t`) which is packaged together with the task.
321 * - Mutex lock/unlock. (We're assuming no lock contention; so this is cheap.)
322 * - CONCLUSION: It's 5-ish increments, 2-ish integer comparisons, copying a ~handful of scalars ~1x each, and
323 * change. Compare to the "Enqueue the log-request" step alone: create `function<>`
324 * object, enqueue it to boost.asio task queue -- copying `msg` and other parts of Log_request in the process.
325 * Now throw in the `ostream<<` manipulation needed to assemble `msg`; the time spent heap-allocating + populating
326 * Msg_metadata, such as allocating and copying thread nickname, if it's long enough. And lastly remember
327 * that the should_log() mechanism (even *without* any throttling) is
328 * normally supposed to make do_log() calls so infrequent that the processor cycle cost is small in the
329 * first place. Conclusion: Yes, this overhead is acceptable: it is small as a %; and in absolute terms,
330 * the latter only conceivably not being the case, if it would have been not the case anyway (and not in a
331 * well functioning system).
332 * - Lock contention: The critical section is similar in both potentially contending pieces of code
333 * (do_log() and `really_log()`); so let's take the one in do_log(). It is *tiny*:
334 * Integer add and ~3 assignments; Boolean comparison; possibly 1-2 integer comparisons; and either a short jump
335 * or 2 more Boolean assignments.
336 * - It's tiny in absolute terms.
337 * - It's tiny in % terms, as discussed earlier for do_log(). (In `really_log()` it's even more so, as it is
338 * all in one thread *and* doing synchronous file I/O.)
339 * - do_log() already has a boost.asio task queue push with mutex lock/unlock, contending against `really_log()`
340 * performing mutex lock/unlock + queue pop; plus condition-variable wait/notify. Even under very intense
341 * practical logging scenarios, lock contention from this critical section was never obserbed to be a factor.
342 * - CONCLUSION: It would be very surprising if this added locking ever caused any observable contention.
343 *
344 * @note I (ygoldfel) heavily pursued a completely lock-free solution. I got tantalizingly close. It involved
345 * an added pointer indirection in should_log() (and do_log() and `really_log()`), with a pointer
346 * storing throttling state `struct`, atomically replaced by `throttling_cfg()` mutator; completely removing
347 * mutex; and turning `m_pending_logs_sz` into an `atomic`. Unfortunately there was a very unlikely corner
348 * case that was nevertheless formally possible. Ultimately it came down to the fact that
349 * `A(); if (...based-on-A()...) { B(); }` and `C(); if (...based-on-C()...) { D(); }` executing concurrently,
350 * with `A()` being reached before `C()`, execution order can be A-C-D-B instead of the desired A-C-B-D. In our
351 * case this could, formally speaking, cause `m_throttling_now => true => false` to incorrectly be switched to
352 * `m_throttling_now => false => true` (if, e.g., M=H is reached and then very quickly/near-concurrently M=0 is
353 * reached); or vice versa. Without synchronization of some kind I couldn't make it be bullet-proof.
354 * (There was also the slightly longer computation in should_log(): pointer indirection to account for
355 * config-setting no longer being mutex-protected; but in my view that addition was acceptable still.
356 * Unfortunately I couldn't make the algorithm formally correct.)
357 */
359 public Logger,
360 protected Log_context
361{
362public:
363 // Types.
364
365 /**
366 * Controls behavior of the throttling algorithm as described in Async_file_logger doc header Throttling section.
367 * As noted there, value(s) therein affect the algorithm for computing Throttling versus Not-Throttling state but
368 * *not* whether should_log() actually allows that state to have any effect. That is controlled by a peer
369 * argument to throttling_cfg().
370 *
371 * @internal
372 * ### Rationale ###
373 * Why the `struct` and not just expose `m_hi_limit` by itself? Answer: there is a "note" about it in the
374 * Async_file_logger class doc header. Short answer: maintainability/future-proofing.
375 */
377 {
378 // Data.
379
380 /**
381 * The throttling algorithm will go from Not-Throttling to Throttling state if and only if the current memory
382 * usage changes from `< m_hi_limit` to `>= m_hi_limit`.
383 * Async_file_logger doc header Throttling section calls this value H. It must be positive.
384 */
385 uint64_t m_hi_limit;
386
387 /**
388 * Value of `Async_file_logger(...).throttling_cfg().m_hi_limit`: default/initial value of #m_hi_limit.
389 *
390 * Note that this value is not meant to be some kind of universally correct choice for #m_hi_limit.
391 * Users can and should change `m_hi_limit`.
392 */
393 static constexpr uint64_t S_HI_LIMIT_DEFAULT = 1ull * 1024 * 1024 * 1024;
394 }; // struct Throttling_cfg
395
396 // Constructors/destructor.
397
398 /**
399 * Constructs logger to subsequently log to the given file-system path. It will append.
400 *
401 * @todo Consider adding Async_file_logger constructor option to overwrite the file instead of appending.
402 *
403 * @param config
404 * Controls behavior of this Logger. In particular, it affects should_log() logic (verbosity default and
405 * per-component) and output format (such as time stamp format). See thread safety notes in class doc header.
406 * This is saved in #m_config.
407 * @param log_path
408 * File-system path to which to write subsequently. Note that no writing occurs until the first do_log() call.
409 * @param backup_logger_ptr
410 * The Logger to use for `*this` to log *about* its logging operations to the actual intended file-system path;
411 * or null to not log such things anywhere. If you do not pass in null, but ensure
412 * `backup_logger_ptr->should_log()` lets through nothing more than `Sev::S_INFO` severity messages for
413 * `Flow_log_component::S_LOG`, then you can expect a reasonable amount of useful output that will not
414 * affect performance. Tip: null is a reasonable value. A Simple_ostream_logger logging to `cout` and `cerr`
415 * (or only `cout`) is also a good choice, arguably better than null.
416 * Lastly, setting verbosity to `INFO` for `*backup_logger_ptr` is typically a better choice than
417 * `TRACE` in practice.
418 * @param capture_rotate_signals_internally
419 * If and only if this is `true`, `*this` will detect SIGHUP (or your OS's version thereof);
420 * upon seeing such a signal, it will fire the equivalent of log_flush_and_reopen(), as needed for classic
421 * log rotation. (The idea is: If we are writing to path F, then your outside log rotation tool will rename
422 * F -> F.1 [and F.1 -> F.2, etc.]; even as we continue writing to the underlying file after it has been
423 * renamed; then the tool sends SIGHUP; we flush/close what is really F.1; reopen at the real path F
424 * again, which will create it anew post-rotation.) If `false` then you'd have to do it yourself if desired.
425 * If this is `true`, the user may register their own signal handler(s) (for any purpose whatsoever) using
426 * `boost::asio::signal_set`. However, behavior is undefined if the program registers signal handlers via any
427 * other API, such as `sigaction()` or `signal()`. If you need to set up such a non-`signal_set` signal
428 * handler, AND you require rotation behavior, then (1) set this option to `false`; (2) trap SIGHUP yourself;
429 * (3) in your handlers for the latter, simply call log_flush_and_reopen(). However, if typical, common-sense
430 * behavior is what you're after -- and either don't need additional signal handling or are OK with using
431 * `signal_set` for it -- then setting this to `true` is a good option.
432 */
433 explicit Async_file_logger(Logger* backup_logger_ptr,
434 Config* config, const fs::path& log_path,
435 bool capture_rotate_signals_internally);
436
437 /// Flushes out anything buffered, returns resources/closes output file(s); then returns.
438 ~Async_file_logger() override;
439
440 // Methods.
441
442 /**
443 * Implements interface method by returning `true` if the severity and component (which is allowed to be null)
444 * indicate it should; and if so potentially applies the throttling algorithm's result as well.
445 * As of this writing not thread-safe against changes to `*m_config` (but thread-safe agains throttling_cfg()
446 * mutator).
447 *
448 * Throttling comes into play if and only if: 1, `sev` and `component` indicate
449 * should_log() should return `true` in the first place; and 2, `throttling_active() == true`. In that case
450 * the throttling alogorithm's current output (Throttling versus Not-Throttling state) is consulted to determine
451 * whether to return `true` or `false`. (See Throttling section of class doc header.)
452 *
453 * @param sev
454 * Severity of the message.
455 * @param component
456 * Component of the message. Reminder: `component.empty() == true` is allowed.
457 * @return See above.
458 */
459 bool should_log(Sev sev, const Component& component) const override;
460
461 /**
462 * Implements interface method by returning `true`, indicating that this Logger may need the contents of
463 * `*metadata` and `msg` passed to do_log() even after that method returns.
464 *
465 * @return See above.
466 */
467 bool logs_asynchronously() const override;
468
469 /**
470 * Implements interface method by asynchronously logging the message and some subset of the metadata in a fashion
471 * controlled by #m_config.
472 *
473 * @param metadata
474 * All information to potentially log in addition to `msg`.
475 * @param msg
476 * The message.
477 */
478 void do_log(Msg_metadata* metadata, util::String_view msg) override;
479
480 /**
481 * Causes the log at the file-system path to be flushed/closed (if needed) and
482 * re-opened; this will happen as soon as possible but may occur asynchronously after this method exits, unless
483 * directed otherwise via `async` argument.
484 *
485 * ### Uses ###
486 * Flushing: `log_flush_and_reopen(false)` is a reasonable and safe way to flush anything buffered in memory to the
487 * file. Naturally, for performance, it should not be done frequently. For example this might be useful in the
488 * event of an abnormal termination (`abort()`, etc.), in the signal handler before exiting program.
489 *
490 * Rotation: `log_flush_and_reopen(true)` is useful for rotation purposes; however, you need not do this manually if
491 * you decided to (properly) use the `capture_rotate_signals_internally == true` option in Async_file_logger
492 * constructor; the procedure will occur on receiving the proper signal automatically.
493 *
494 * @todo `Async_file_logger::log_flush_and_reopen(true)` is great for flushing, such as in an abort-signal handler,
495 * but providing just the flushing part without the reopening might be useful. At the moment we've left it
496 * this way, due to the vague feeling that closing the file upon flushing it is somehow more final and thus safer
497 * (in terms of accomplishing its goal) in an abort-signal scenario. Feelings aren't very scientific though.
498 *
499 * @param async
500 * If `true`, the operation will execute ASAP but asynchronously, the method exiting immediately;
501 * else it will complete fully before this method returns.
502 */
503 void log_flush_and_reopen(bool async = true);
504
505 /**
506 * Accessor returning a copy of the current set of throttling knobs. Please see Async_file_logger doc header
507 * Throttling section for description of their meanings in the algorithm.
508 *
509 * @see throttling_active() also.
510 *
511 * @return The current knobs controlling the behavior of the algorithm that determines
512 * Throttling versus Not-Throttling state.
513 * If throttling_cfg() mutator is never called, then the values therein will be some valid defaults.
514 */
516
517 /**
518 * Whether the throttling feature is currently in effect. That is: can the throttling computations actually
519 * affect should_log() output? (It is *not* about whether log lines are actually being rejected due to throttling
520 * right now.) Please see Async_file_logger doc header Throttling section for more info.
521 *
522 * If `true` should_log() will potentially consider Throttling versus Not-Throttling state; else it will ignore it.
523 * If throttling_cfg() mutator is never called, then this shall be `false` (feature inactive by default).
524 *
525 * @see throttling_cfg() accessor also.
526 *
527 * @return See above.
528 */
529 bool throttling_active() const;
530
531 /**
532 * Mutator that sets the throttling knobs. Please see Async_file_logger doc header
533 * Throttling section for description of their meanings in the algorithm.
534 *
535 * ### Thread safety ###
536 * It is okay to call concurrently with any other method on the same `*this`, except it must not be called
537 * concurrently with itself.
538 *
539 * @param active
540 * Whether the feature shall be in effect (if should_log() will
541 * potentially consider Throttling versus Not-Throttling state; else it will ignore it).
542 * @param cfg
543 * The new values for knobs controlling the behavior of the algorithm that determines
544 * Throttling versus Not-Throttling state.
545 */
546 void throttling_cfg(bool active, const Throttling_cfg& cfg);
547
548 // Data. (Public!)
549
550 /**
551 * Reference to the config object passed to constructor. Note that object is mutable; see notes on thread safety.
552 *
553 * @internal
554 * ### Rationale ###
555 * This can be (and is but not exclusively) exclusively stored in `m_serial_logger->m_config`; it is stored here also
556 * for `public` access to the user. It's a pointer in any case.
557 */
559
560private:
561 // Types.
562
563 /// Short-hand for #m_throttling_mutex type.
565
566 /// Short-hand for #Mutex lock.
568
569 /// Short-hand for a signal set.
570 using Signal_set = boost::asio::signal_set;
571
572 /**
573 * In addition to the task object (function) itself, these are the data placed onto the queue of `m_async_worker`
574 * tasks for a particular `do_log()` call, to be used by that task and then freed immediately upon logging of the
575 * message to file.
576 *
577 * @see mem_cost().
578 *
579 * ### Rationale/details ###
580 * The object is movable which is important. It is also copyable but only because, as of this writing, C++17
581 * requires captures by value to be copyable (in order to compile), even though this is *not executed at runtime*,
582 * unless one actually needs to make a copy of the function object (which we avoid like the plague).
583 *
584 * We try hard -- harder than in most situations -- to keep the memory footprint of this thing as small as possible,
585 * right down to even avoiding a `shared_ptr`, when a raw or `unique_ptr` is enough; and not storing the result
586 * of mem_cost() (but rather recomputing it inside the `m_async_worker` task). That is because of the same memory
587 * use potential problem with which the throttling feature (see Async_file_logger class doc header) grapples.
588 *
589 * Ideally each item stored here has RAII semantics, meaning once the object is destroyed, the stuff referred-to
590 * therein is destroyed. However you'll notice this is not the case at least for #m_metadata and
591 * for #m_msg_copy. Therefore the function body (the `m_async_worker` task for this Log_request) must manually
592 * `delete` these objects from the heap. Moreover, if the lambda were to never run (e.g., if we destroyed or
593 * stopped `m_async_worker` while tasks are still enqueued), those objects would get leaked. (As of this writing
594 * we always flush the queue in Async_file_logger dtor for this and another reason.)
595 *
596 * So why not just use `unique_ptr` then? The reason is above: they're not copyable, and we need it to be,
597 * as otherwise C++17 won't let Log_request be value-captured in lambda. One can use `shared_ptr`; this is elegant,
598 * but at this point we're specifically trying to reduce the RAM use to the bare minimum, so we avoid even
599 * the tiny control block size of `shared_ptr`. For #m_msg_copy one could have used `std::string` or util::Basic_blob
600 * or `std::vector`, but they all have some extra members we do not need (size on top of capacity; `Basic_blob`
601 * also has `m_start`). (util::Basic_blob doc header as of this writing has a to-do to implement a
602 * `Tight_blob` class with just the pointer and capacity, no extras; so that would have been useful.) Even
603 * with those options, that would've still left #m_metadata. One could write little wrapper classes for both
604 * the string blob #m_msg_copy and/or Msg_metadata #m_metadata, and that did work. Simply put, however, storing the
605 * rare raw pointers and then explicitly `delete`ing them in one spot is just much less boiler-plate.
606 *
607 * @warning Just be careful with maintenance. Tests should indeed try to force the above leak and use sanitizers
608 * (etc.) to ensure it is avoided.
609 */
611 {
612 // Data.
613
614 /// Pointer to array of characters comprising a copy of `msg` passed to `do_log()`. We must `delete[]` it.
616
617 /// Number of characters in #m_msg_copy pointee string.
619
620 /// Pointer to array of characters comprising a copy of `msg` passed to `do_log()`. We must `delete` it.
622
623 /**
624 * Whether this log request was such that its memory footprint (`mem_cost()`) pushed `m_pending_logs_sz` from
625 * `< m_throttling_cfg.m_hi_limit` to `>= m_throttling_cfg.m_hi_limit` for the first time since it was last
626 * equal to zero.
627 */
629 }; // struct Log_request
630
631 // Methods.
632
633 /**
634 * SIGHUP/equivalent handler for the optional feature `capture_rotate_signals_internally` in constructor.
635 * Assuming no error, executes `m_serial_logger->log_flush_and_reopen()` and then again waits for the next such
636 * signal.
637 *
638 * @param sys_err_code
639 * The code from boost.asio. Anything outside of `operation_aborted` is very strange.
640 * @param sig_number
641 * Should be SIGHUP/equivalent, as we only wait for those signal(s).
642 */
643 void on_rotate_signal(const Error_code& sys_err_code, int sig_number);
644
645 /**
646 * How much do_log() issuing the supplied Log_request shall contribute to #m_pending_logs_sz.
647 * Log_request in this case essentially just describes the `msg` and `metadata` args to do_log().
648 * See discussion of throttling algorithm in Impl section of class doc header.
649 *
650 * @param log_request
651 * See do_log(): essentially filled-out with `msg`- and `metadata`-derived info.
652 * The value of Log_request::m_throttling_begins is ignored in the computation (it would not affect it
653 * anyway), but the other fields must be set.
654 * @return Positive value. (Informally: we've observed roughly 200 plus message size as of this writing.)
655 */
656 static size_t mem_cost(const Log_request& log_request);
657
658 /**
659 * Estimate of memory footprint of the given value, including memory allocated on its behalf -- but
660 * excluding its shallow `sizeof`! -- in bytes.
661 *
662 * @param val
663 * Value.
664 * @return See above.
665 */
666 static size_t deep_size(const Log_request& val);
667
668 // Data.
669
670 /**
671 * Protects throttling algorithm data that require coherence among themselves:
672 * #m_throttling_cfg, #m_pending_logs_sz, #m_throttling_now. The latter is nevertheless `atomic<>`; see
673 * its doc header as to why.
674 *
675 * ### Perf ###
676 * The critical sections locked by this are extremely small, and should_log() does not have one at all.
677 *
678 * @see Class doc header Impl section for discussion of the throttling algorithm and locking in particular.
679 */
681
682 /// See Throttling_cfg. Protected by #m_throttling_mutex.
684
685 /**
686 * Estimate of how much RAM is being used by storing do_log() requests' data (message itself, metadata)
687 * before they've been logged to file via `really_log()` (and therefore freed). Protected by #m_throttling_mutex.
688 *
689 * Each log request's cost is computed via mem_cost(): do_log() increments this by mem_cost(); then
690 * a corresponding `really_log()` decrements it by that same amount.
691 *
692 * ### Brief discussion ###
693 * If one made this `atomic<size_t>`, and one needed merely the correct updating of #m_pending_logs_sz,
694 * the mutex #m_throttling_mutex would not be necessary: `.fetch_add(mem_cost(...), relaxed)`
695 * and `.fetch_sub(mem_cost(...), relaxed)` would have worked perfectly with no corruption or unexpected
696 * reordering; each read/modify/write op is atomic, and that is sufficient. Essentially the mutex was needed
697 * only to synchronize subsequent potential assignment of #m_throttling_now.
698 *
699 * @see Class doc header Impl section for discussion of the throttling algorithm and locking in particular.
700 */
702
703 /**
704 * Contains the output of the always-on throttling algorithm; namely
705 * `true` if currently should_log() shall return `false` due to too-much-RAM-being-used; `false` otherwise.
706 * It starts at `false`; when `m_throttling_cfg.m_hi_limit` is crossed (by #m_pending_logs_sz) going up
707 * in do_log(), it is made equal to `true`; when reaching 0 it is made equal to `false`.
708 *
709 * Protected by #m_throttling_mutex. *Additionally* it is `atomic`, so that should_log() can read it
710 * without locking. should_log() does not care about the other items protected by the mutex, and it for
711 * functional purposes does not care about inter-thread volatility due to `relaxed`-order access to this
712 * flag around the rare occasions when its value actually changes.
713 *
714 * @see Class doc header Impl section for discussion of the throttling algorithm and locking in particular.
715 *
716 * At least for logging purposes we do want to detect when it *changes* from `false` to `true` and vice versa;
717 * this occurs only the 1st time it reaches `hi_limit` since it was last 0; and similarly the 1st time it reaches
718 * 0 since it was last `>= hi_limit`.
719 */
720 std::atomic<bool> m_throttling_now;
721
722 /**
723 * Whether the throttling-based-on-pending-logs-memory-used feature is currently active or not. As explained
724 * in detail in Throttling section in class doc header, this is queried only in should_log() and only as a
725 * gate to access the results of the always-on throttling algorithm: #m_throttling_now. That algorithm,
726 * whose data reside in other `m_throttling_*` and #m_pending_logs_sz, is always active; but this
727 * `m_throttling_active` flag determines whether that algorithm's output #m_throttling_now is used or
728 * ignored by should_log().
729 *
730 * It is atomic, and accessed with `relaxed` order only, due to being potentially frequently accessed in
731 * the very-often-called should_log(). Since independent of the other state, it does not need mutex protection.
732 */
733 std::atomic<bool> m_throttling_active;
734
735 /**
736 * This is the `Logger` doing all the real log-writing work (the one stored in Log_context is the
737 * logger-about-logging, `backup_logger_ptr` from ctor args). In itself it's a fully functional Logger, but
738 * its main limitation is all calls between construction and destruction must be performed non-concurrently --
739 * for example from a single thread or boost.asio "strand." The way we leverage it is we simply only make
740 * log-file-related calls (especially Serial_file_logger::do_log() but also
741 * Serial_file_logger::log_flush_and_reopen()) from within #m_async_worker-posted tasks (in other words from
742 * the thread #m_async_worker starts at construction).
743 *
744 * ### Design rationale ###
745 * What's the point of Serial_file_logger? Why not just have all those data and logic (storing the `ofstream`, etc.)
746 * directly in Async_file_logger? Answer: That's how it was originally. The cumulative amount of state and logic
747 * is the same; and there's no real usefulness for Serial_file_logger as a stand-alone Logger for general users;
748 * so in and of itself splitting it out only adds a bit of coding overhead but is otherwise the same. So why do it?
749 * Answer:
750 *
751 * The reason we split it off was the following: We wanted to sometimes log to the real file from within the
752 * #m_async_worker thread; namely to mark with a pair of log messages that a file has been rotated. It was possible
753 * to do this via direct calls to a would-be `impl_do_log()` (which is do_log() but already from the worker thread),
754 * but then one couldn't use the standard `FLOW_LOG_*()` statements to do it; this was both inconvenient and difficult
755 * to maintain over time (we predicted). At that point it made sense that really Async_file_logger *is*
756 * a would-be Serial_file_logger (one that works from one thread) with the creation of the one thread -- and other
757 * such goodies -- added on top. With that, `FLOW_LOG_SET_LOGGER(m_serial_logger)` + `FLOW_LOG_INFO()` trivially logs
758 * to the file (as long as it's done from the worker thread), accomplishing that goal; and furthermore it makes
759 * logical sense in terms of overall design. The latter fact I (ygoldfel) would say is the best justification; and
760 * the desire for `*this` to log to the file, itself, was the triggering use case.
761 *
762 * @todo Async_file_logger::m_serial_logger (and likely a few other similar `unique_ptr` members of other
763 * classes) would be slightly nicer as an `std::optional<>` instead of `unique_ptr`. `optional` was not
764 * in STL back in the day and either did not exist or did not catch our attention back in the day. `unique_ptr`
765 * in situations like this is fine but uses the heap more.
766 */
767 boost::movelib::unique_ptr<Serial_file_logger> m_serial_logger;
768
769 /**
770 * The thread (1-thread pool, technically) in charge of all #m_serial_logger I/O operations including writes
771 * to file. Thread starts at construction of `*this`; ends (and is synchronously joined) at
772 * destruction of `*this` (by means of delegating to #m_async_worker destructor).
773 */
775
776 /**
777 * Signal set which we may or may not be using to trap SIGHUP in order to auto-fire
778 * `m_serial_logger->log_flush_and_reopen()` in #m_async_worker. `add()` is called on it at init iff [sic]
779 * that feature is enabled by the user via ctor arg `capture_rotate_signals_internally`. Otherwise this object
780 * just does nothing `*this` lifetime.
781 */
783}; // class Async_file_logger
784
785} // namespace flow::log
A Concurrent_task_loop-related adapter-style class that represents a single-thread task loop; essenti...
An implementation of Logger that logs messages to a given file-system path but never blocks any loggi...
flow::util::Lock_guard< Mutex > Lock_guard
Short-hand for Mutex lock.
async::Single_thread_task_loop m_async_worker
The thread (1-thread pool, technically) in charge of all m_serial_logger I/O operations including wri...
flow::util::Mutex_non_recursive Mutex
Short-hand for m_throttling_mutex type.
Config *const m_config
Reference to the config object passed to constructor.
void on_rotate_signal(const Error_code &sys_err_code, int sig_number)
SIGHUP/equivalent handler for the optional feature capture_rotate_signals_internally in constructor.
std::atomic< bool > m_throttling_active
Whether the throttling-based-on-pending-logs-memory-used feature is currently active or not.
boost::movelib::unique_ptr< Serial_file_logger > m_serial_logger
This is the Logger doing all the real log-writing work (the one stored in Log_context is the logger-a...
static size_t mem_cost(const Log_request &log_request)
How much do_log() issuing the supplied Log_request shall contribute to m_pending_logs_sz.
Signal_set m_signal_set
Signal set which we may or may not be using to trap SIGHUP in order to auto-fire m_serial_logger->log...
size_t m_pending_logs_sz
Estimate of how much RAM is being used by storing do_log() requests' data (message itself,...
bool throttling_active() const
Whether the throttling feature is currently in effect.
Mutex m_throttling_mutex
Protects throttling algorithm data that require coherence among themselves: m_throttling_cfg,...
static size_t deep_size(const Log_request &val)
Estimate of memory footprint of the given value, including memory allocated on its behalf – but exclu...
std::atomic< bool > m_throttling_now
Contains the output of the always-on throttling algorithm; namely true if currently should_log() shal...
Throttling_cfg m_throttling_cfg
See Throttling_cfg. Protected by m_throttling_mutex.
boost::asio::signal_set Signal_set
Short-hand for a signal set.
void do_log(Msg_metadata *metadata, util::String_view msg) override
Implements interface method by asynchronously logging the message and some subset of the metadata in ...
void log_flush_and_reopen(bool async=true)
Causes the log at the file-system path to be flushed/closed (if needed) and re-opened; this will happ...
Throttling_cfg throttling_cfg() const
Accessor returning a copy of the current set of throttling knobs.
bool should_log(Sev sev, const Component &component) const override
Implements interface method by returning true if the severity and component (which is allowed to be n...
~Async_file_logger() override
Flushes out anything buffered, returns resources/closes output file(s); then returns.
Async_file_logger(Logger *backup_logger_ptr, Config *config, const fs::path &log_path, bool capture_rotate_signals_internally)
Constructs logger to subsequently log to the given file-system path.
bool logs_asynchronously() const override
Implements interface method by returning true, indicating that this Logger may need the contents of *...
A light-weight class, each object storing a component payload encoding an enum value from enum type o...
Definition: log.hpp:840
Class used to configure the filtering and logging behavior of Loggers; its use in your custom Loggers...
Definition: config.hpp:200
Convenience class that simply stores a Logger and/or Component passed into a constructor; and returns...
Definition: log.hpp:1619
Interface that the user should implement, passing the implementing Logger into logging classes (Flow'...
Definition: log.hpp:1291
Flow module providing logging functionality.
Sev
Enumeration containing one of several message severity levels, ordered from highest to lowest.
Definition: log_fwd.hpp:224
boost::unique_lock< Mutex > Lock_guard
Short-hand for advanced-capability RAII lock guard for any mutex, ensuring exclusive ownership of tha...
Definition: util_fwd.hpp:265
boost::mutex Mutex_non_recursive
Short-hand for non-reentrant, exclusive mutex. ("Reentrant" = one can lock an already-locked-in-that-...
Definition: util_fwd.hpp:215
Basic_string_view< char > String_view
Commonly used char-based Basic_string_view. See its doc header.
boost::system::error_code Error_code
Short-hand for a boost.system error code (which basically encapsulates an integer/enum error code and...
Definition: common.hpp:502
In addition to the task object (function) itself, these are the data placed onto the queue of m_async...
size_t m_msg_size
Number of characters in m_msg_copy pointee string.
Msg_metadata * m_metadata
Pointer to array of characters comprising a copy of msg passed to do_log(). We must delete it.
char * m_msg_copy
Pointer to array of characters comprising a copy of msg passed to do_log(). We must delete[] it.
bool m_throttling_begins
Whether this log request was such that its memory footprint (mem_cost()) pushed m_pending_logs_sz fro...
Controls behavior of the throttling algorithm as described in Async_file_logger doc header Throttling...
static constexpr uint64_t S_HI_LIMIT_DEFAULT
Value of Async_file_logger(...).throttling_cfg().m_hi_limit: default/initial value of m_hi_limit.
uint64_t m_hi_limit
The throttling algorithm will go from Not-Throttling to Throttling state if and only if the current m...
Simple data store containing all of the information generated at every logging call site by flow::log...
Definition: log.hpp:1048