Flow 2.0.0
Flow project: Full implementation reference.
config.cpp
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#include "flow/log/config.hpp"
20#include <boost/algorithm/string.hpp>
21
22namespace flow::log
23{
24// Static initializations.
25
26// By definition INFO is the most-verbose severity meant to avoid affecting performance.
28
29// Implementations.
30
31Config::Config(Sev most_verbose_sev_default) :
32 m_use_human_friendly_time_stamps(true),
33 m_verbosity_default(static_cast<raw_sev_t>(most_verbose_sev_default))
34{
35 // Nothing.
36}
37
38Config::Config(const Config&) = default;
39
41{
42 /* Caution! This is called by output_whether_should_log() which is potentially called in *every* log call site
43 * including ones for which output_whether_should_log() == Logger::should_log() will yields false.
44 * Keep this code -- including .lookup() insides -- tight and performant, more-so than almost anyplace else! */
45
46 // Use sentinel -1 to track lookup() failure instead of lookup() retval. Might speed things up a tiny bit.
47 Component_config component_cfg{component_union_idx_t(-1)};
48
49 m_component_cfgs_by_payload_type.lookup(component.payload_type(), &component_cfg);
50 // BTW, as of this writing, that asserts !component.empty() (non-null Component), as advertised (undefined behavior).
51
52 if (component_cfg.m_enum_to_num_offset != component_union_idx_t(-1))
53 {
54 component_cfg.m_enum_to_num_offset += component.payload_enum_raw_value();
55 // Note: Result is not -1 (both addends are non-negative).
56 }
57 // else if (m_enum_to_num_offset == -1) { As advertised, this is an allowed eventuality: return -1. }
58
59 return component_cfg.m_enum_to_num_offset;
60}
61
62void Config::store_severity_by_component(component_union_idx_t component_union_idx, raw_sev_t most_verbose_sev_or_none)
63{
64 using std::memory_order_relaxed;
65
66 /* This is explicitly disallowed in our contract. See doc header for m_verbosities_by_component for brief
67 * discussion of why this is disallowed, but it's allowed in output_whether_should_log(). */
68 assert(component_union_idx != component_union_idx_t(-1));
69 assert(component_union_idx < m_verbosities_by_component.size());
70
71 /* Store the severity, encoded as a byte for compactness and such. Please read doc header for
72 * Component_union_idx_to_sev_map at this point then come back here.
73 * As noted there, the lookup by index is safe against concurrent output_whether_should_log() doing the same,
74 * because by the time either we or output_whether_should_log() are called, init_component_to_union_idx_mapping()
75 * calls are required to have been all completed for perpetuity.
76 * That brings us to the actual assignment to the atomic. Firstly, we could have used just a regular-looking
77 * =assignment. However that actually means basically .store(..., memory_order_seq_cst) which has spectacular
78 * thread safety (total coherence across cores, and of course atomicity) -- but it is expensive. Granted, we don't
79 * care about *our* performance that much, but we highly care about output_whether_should_log()'s; and unless that
80 * one also uses memory_order_seq_cst (via = or otherwise) we don't really reap the benefit. So, we use
81 * memory_order_relaxed in both places. Its performance is the highest possible for an atomic<>, which in fact for
82 * x86 means it is no worse than an assignment of a *non*-atomic (in fact the more stringent memory_order_release
83 * would, on x86, been perf-equivalent to a non-atomic assignment -- and this is faster or the same by definition).
84 * Basically memory_order_relaxed guarantees atomicity (so it is not corrupted, formally) and not much more.
85 * So the only question remains: is the multi-thread behavior of this acceptable for our purposes?
86 *
87 * To tackle this, consider the actual scenario. What's at stake? Simple: The Sev is set to X before our call
88 * and Y after; the change from X to Y would cause message M to pass the output_whether_should_log() filter
89 * before but not after the change, or vice versa. So by executing this store() op, do we get this desired effect?
90 * Consider that we are called in thread 1, and there are other threads; take thread 2 as an example.
91 * - Thread 1: If M is logged in this thread, then either that occurs strictly before this call or strictly after.
92 * Even a regular assignment, no atomics involved, works in that scenario. So this store() certainly does also.
93 * No problem.
94 * - Thread 2: If M is logged in thread 2, then there is potential controversy. The store() is atomic, but there
95 * is no guarantee that it will reflect immediately (for some definition of immediately) in thread 2; thread 2
96 * could have loaded X into a register already and may use it strictly after in memory it has changed to Y,
97 * so to thread 2's C++ code the change to Y is not "visible" yet. However, this does not matter in practice.
98 * It just means it will act as if M were logged a split second earlier and thus with verbosity X and not Y
99 * anyway. Very shortly the side effect of the assignment will propagate to be visible on every core, registers
100 * and caching notwithstanding. Now thread 2 will operate according to severity Y. No problem.
101 *
102 * As you can see in atomic<> docs, memory_order_relaxed guarantees atomicity and read-write coherence (a/k/a sanity)
103 * but performs no synchronization, meaning other memory writes that happen before may be reordered to after and
104 * vice versa; but we absolutely do not care about that. Even just conceivably we could only care about other
105 * components' severities, and we make no promise that output_whether_should_log() will expect to work with some sort
106 * of batch-atomic multi-component verbosity update. So we are fine.
107 *
108 * What about `configure_component_verbosity*()` in thread 1 vs. same method in thread 2, concurrently?
109 * Well, that's just a race. Both are atomic, so either one or the other "wins" and will be propagated to be
110 * visible by all threads soon enough. I add that we explicitly say concurrent such calls are allowed and are safe,
111 * but in practical terms it's ill-advised to have your program change config concurrently from multiple threads;
112 * there is unlikely to be a good design that would do this. */
113 m_verbosities_by_component[size_t{component_union_idx}].store(most_verbose_sev_or_none, memory_order_relaxed);
114} // Config::store_severity_by_component()
115
116void Config::configure_default_verbosity(Sev most_verbose_sev, bool reset)
117{
118 using std::memory_order_relaxed;
119
120 // Straightforward (except see discussion on atomic assignment in store_severity_by_component(); applies here).
121 m_verbosity_default.store(static_cast<raw_sev_t>(most_verbose_sev), memory_order_relaxed);
122
123 if (reset)
124 {
125 for (component_union_idx_t component_union_idx = 0;
126 size_t{component_union_idx} != m_verbosities_by_component.size();
127 ++component_union_idx)
128 {
129 store_severity_by_component(component_union_idx, raw_sev_t(-1));
130 }
131 }
132}
133
135 util::String_view component_name_unnormalized)
136{
137 const auto component_name = normalized_component_name(component_name_unnormalized);
138 const auto component_union_idx_it = m_component_union_idxs_by_name.find(component_name);
139 if (component_union_idx_it == m_component_union_idxs_by_name.end())
140 {
141 return false;
142 }
143 // else
144
145 const component_union_idx_t component_union_idx = component_union_idx_it->second;
146 assert(component_union_idx != component_union_idx_t(-1)); // We don't save `-1`s. This is our bug if it happens.
147
148 store_severity_by_component(component_union_idx, raw_sev_t(most_verbose_sev));
149 return true;
150} // Config::configure_component_verbosity_by_name()
151
152bool Config::output_component_to_ostream(std::ostream* os_ptr, const Component& component) const
153{
154 assert(os_ptr);
155 auto& os = *os_ptr;
156
157 if (component.empty()) // Unspecified component at log call site (or wherever).
158 {
159 // Same as if unregistered component: do not show one.
160 return false;
161 }
162
163 const auto component_union_idx = component_to_union_idx(component);
164 if (component_union_idx == component_union_idx_t(-1))
165 {
166 return false; // Unregistered component.payload_type().
167 }
168 // else
169
170 const auto component_name_it = m_component_names_by_union_idx.find(component_union_idx);
171 if (component_name_it == m_component_names_by_union_idx.end())
172 {
173 os << component_union_idx; // Unknown name; or intentionally not saved, b/c they preferred numeric output.
174 }
175 else
176 {
177 auto& name = component_name_it->second;
178 assert(!name.empty());
179 os << name;
180 }
181 return true;
182} // Config::output_component_to_ostream()
183
184bool Config::output_whether_should_log(Sev sev, const Component& component) const
185{
186 using std::memory_order_relaxed;
187
188 // See doc header for contract please; then come back here.
189
190 // Check possible thread-local override in which case it's a very quick filter check.
191 const auto sev_override = *(this_thread_verbosity_override());
192 if (sev_override != Sev::S_END_SENTINEL)
193 {
194 return sev <= sev_override;
195 }
196 // else: Must actually check the config in `*this`. Concurrency is a factor from this point forward.
197
198 // See doc header for Component_union_idx_to_sev_map; then come back here.
199
200 if (!component.empty())
201 {
202 const auto component_union_idx = component_to_union_idx(component);
203 if (component_union_idx != component_union_idx_t(-1))
204 {
205 /* There are a few things going on here.
206 * - If m_verbosities_by_component[component_union_idx] == -1, then by m_verbosities_by_component semantics
207 * (see its doc header) that means it's not a key in the conceptual map, so no severity configured for
208 * component: fall through to check of m_verbosity_default.
209 * - If component_union_idx is out of bounds, we promised in our contract and in doc header for
210 * m_verbosities_by_component to treat it the same (in this context) as if it were -1 (not in the "map").
211 * The rationale for not crashing instead is briefly discussed in m_verbosities_by_component doc header,
212 * including why in store_severity_by_component() it is disallowed (crashes).
213 * - If m_verbosities_by_component[component_union_idx] != -1, then yay, that's the severity configured for
214 * component. Read it from the atomic with memory_order_relaxed; the full reasoning is discussed in the
215 * write site, store_severity_by_component(); we just follow that strategy here. */
216
217 const auto most_verbose_sev_raw
218 = (component_union_idx < m_verbosities_by_component.size())
219 ? m_verbosities_by_component[component_union_idx].load(memory_order_relaxed) // Might be -1 still.
220 : raw_sev_t(-1);
221
222 if (most_verbose_sev_raw != raw_sev_t(-1))
223 {
224 return sev <= Sev{most_verbose_sev_raw};
225 }
226 // else { Fall through. Out of bounds, or -1. }
227 }
228 // else { component.payload()'s enum wasn't registered. Fall through. }
229 }
230 // else if (component.empty()) { No component, so cannot look it up. Fall through. }
231
232 /* Fell through here: no component specified; or enum wasn't registered, so none
233 * of the values therein is in the verbosity table; or the particular enum value doesn't have an individual
234 * verbosty in the table. In all cases the default catch-all verbosity therefore applies. */
235
236 // Use the same atomic read technique as in the verbosity table for the identical reasons.
237 return sev <= Sev{m_verbosity_default.load(memory_order_relaxed)};
238} // Config::output_whether_should_log()
239
241{
242 using boost::algorithm::to_upper_copy;
243 using std::locale;
244 using std::string;
245
246 return to_upper_copy(string{name}, locale::classic());
247}
248
249void Config::normalize_component_name(std::string* name) // Static.
250{
251 using boost::algorithm::to_upper;
252 using std::locale;
253
254 to_upper(*name, locale::classic());
255}
256
258{
259 thread_local Sev verbosity_override = Sev::S_END_SENTINEL; // Initialized 1st time through this line in this thread.
260 return &verbosity_override;
261
262 /* Also considered using boost::thread_specific_ptr<Sev> and/or making it a class-static member/file-static variable.
263 * But this seems like it has the highest chance to be optimized to the max, being as local as possible and skipping
264 * any library (like Boost) that could add overhead with user-space associative lookups. Details omitted. */
265}
266
268{
269 /* Return this RAII thingie that'll immediately set *P and in dtor restore *P, where P is the pointer to thread-local
270 * storage as returned by this_thread_verbosity_override(). */
271 return util::Scoped_setter<Sev>{this_thread_verbosity_override(), std::move(most_verbose_sev_or_none)};
272}
273
275 std::atomic<raw_sev_t>(init_val)
276{
277 // Nothing more.
278}
279
281 std::atomic<raw_sev_t>(src.load(std::memory_order_relaxed))
282{
283 /* Note the way we implement the copy construction via load(relaxed); this is why surprising stuff can technically
284 * happen if src's contents haven't propagated to this thread yet. As of this writing the 2 call sites are
285 * in the Config::Config(Config)=default (copy ctor); and in init_component_to_union_idx_mapping() when the resize()
286 * makes copies of a default-constructed Atomic_raw_sev(-1). In the latter scenario we're guaranteed to call
287 * this in the same thread as the default-construction. In the former scenario we warn the user in the doc header
288 * about the possibility. If the call sites change after this writing, the spirit should stand fast, I think. */
289}
290
291} // namespace flow::log
bool lookup(const std::type_info &type, Cfg *cfg) const
Looks up a value previously added via insert(), or indicates no such value has yet been added.
A light-weight class, each object storing a component payload encoding an enum value from enum type o...
Definition: log.hpp:840
const std::type_info & payload_type() const
Returns typeid(Payload), where Payload was the template param used when calling the originating one-a...
Definition: log.cpp:174
bool empty() const
Returns true if *this is as if default-constructed (a null Component); false if as if constructed via...
Definition: log.cpp:169
enum_raw_t payload_enum_raw_value() const
Returns the numeric value of the enum payload stored by this Component, originating in the one-arg co...
Definition: log.cpp:180
Trivial wrapper of atomic<raw_sev_t> which adds a couple of things to make it possible to construct,...
Definition: config.hpp:883
Atomic_raw_sev(raw_sev_t init_val=raw_sev_t(-1))
Constructs the atomic to the given value, guaranteed to be load()ed as such in all subsequent accesse...
Definition: config.cpp:274
Class used to configure the filtering and logging behavior of Loggers; its use in your custom Loggers...
Definition: config.hpp:317
Config(Sev most_verbose_sev_default=S_MOST_VERBOSE_SEV_DEFAULT)
Constructs a conceptually blank but functional set of Config.
Definition: config.cpp:31
static void normalize_component_name(std::string *name)
Normalizes given component name in place.
Definition: config.cpp:249
bool configure_component_verbosity_by_name(Sev most_verbose_sev, util::String_view component_name)
Like configure_component_verbosity(), but the component is to be specified by its registered string n...
Definition: config.cpp:134
Component_union_idx_to_name_map m_component_names_by_union_idx
Maps each flat union component index to its output component name as registered in init_component_nam...
Definition: config.hpp:1088
Atomic_raw_sev m_verbosity_default
Most verbose (highest) log::Sev for which output_whether_should_log() will return true,...
Definition: config.hpp:1049
static std::string normalized_component_name(util::String_view name)
Normalized version of given component name.
Definition: config.cpp:240
bool output_component_to_ostream(std::ostream *os, const Component &component) const
An output of Config, this writes a string representation of the given component value to the given os...
Definition: config.cpp:152
Component_union_idx_to_sev_map m_verbosities_by_component
Maps from flat union component index to most verbose (highest) log::Sev for which output_whether_shou...
Definition: config.hpp:1078
static Sev * this_thread_verbosity_override()
Returns pointer to this thread's mutable verbosity override, for querying or assignment alike.
Definition: config.cpp:257
static const Sev S_MOST_VERBOSE_SEV_DEFAULT
Recommended default/catch-all most-verbose-severity value if no specific config is given.
Definition: config.hpp:330
Component::enum_raw_t component_union_idx_t
Unsigned index into the flat union of component tables maintained by a Config, combining potentially ...
Definition: config.hpp:325
static util::Scoped_setter< Sev > this_thread_verbosity_override_auto(Sev most_verbose_sev_or_none)
Sets *(this_thread_verbosity_override()) = most_verbose_sev_or_none; and returns an object that shall...
Definition: config.cpp:267
void configure_default_verbosity(Sev most_verbose_sev_default, bool reset)
Sets the default verbosity to the given value, to be used by subsequent output_whether_should_log() c...
Definition: config.cpp:116
uint8_t raw_sev_t
How we store a log::Sev (a mere enum itself) in a certain data structure.
Definition: config.hpp:859
Component_name_to_union_idx_map m_component_union_idxs_by_name
Maps each distinct component name as registered in init_component_names() to its flat union component...
Definition: config.hpp:1095
bool output_whether_should_log(Sev sev, const Component &component) const
A key output of Config, this computes the verbosity-filtering answer to Logger::should_log() based on...
Definition: config.cpp:184
Component_payload_type_to_cfg_map m_component_cfgs_by_payload_type
Fast-lookup map from distinct Component_payload type to the config for that component enum.
Definition: config.hpp:1034
void store_severity_by_component(component_union_idx_t component_union_idx, raw_sev_t most_verbose_sev_or_none)
Helper that for the given flat-union-component-index saves the given per-component verbosity,...
Definition: config.cpp:62
component_union_idx_t component_to_union_idx(const Component &component) const
Given a component in the form user provides it at log call sites, returns its index in the flat compo...
Definition: config.cpp:40
A simple RAII-pattern class template that, at construction, sets the specified location in memory to ...
Definition: util.hpp:145
Flow module providing logging functionality.
Sev
Enumeration containing one of several message severity levels, ordered from highest to lowest.
Definition: log_fwd.hpp:224
@ S_END_SENTINEL
Not an actual value but rather stores the highest numerical payload, useful for validity checks.
@ S_INFO
Message indicates a not-"bad" condition that is not frequent enough to be of severity Sev::S_TRACE.
Basic_string_view< char > String_view
Commonly used char-based Basic_string_view. See its doc header.
The set of config stored for each distinct (as determined by Component::payload_type(),...
Definition: config.hpp:806