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