1# Structured Logging
2
3There are currently two APIs for structured logging:
4[log](../lib/include/phosphor-logging/log.hpp) and
5[lg2](../lib/include/phosphor-logging/lg2.hpp).  If your code is C++20 (or
6later) it is preferred to use `lg2`.
7
8## Why structured logging?
9
10Structured logging is a method of logging where every variable piece of data is
11tagged with some identifier for the data.  This is opposite of unstructured
12logging where logged events are free-form strings.
13
14The principal logging daemon in OpenBMC (systemd-journald) natively supports
15structured logging.  As a result, there are some designs in place where
16specific structured events are added to the journal and downstream these events
17can be consumed.  For example, one implementation of the IPMI SEL utilizes
18specific journal structured data to stored and later retrieve SEL events.
19
20Even if an argument might be made against the merits of using the journal as a
21form of IPC, the value of structured logging persists.  It is very common as
22part of various failure-analysis operations, either on the part of a system
23manufacturer or an end-user, to need to interrogate the system logs to determine
24when/where/why a situation degraded.  With unstructured logging, the
25implementation is left chasing message format and data changes, where as with
26structured logging the format is somewhat static and easily parsed.
27
28A specific example of where structured logging is beneficial is a service which
29gathers `error` or higher log reports and creates issues when a new or unknown
30signature is discovered.  If the only information available is an unstructured
31string, any kind of signature identification requires creating a regular
32expression (likely).  With structured log, specific identifiers can be used as
33the error signature while others are ignored.  For instance, maybe a specific
34`ERROR_RC` is critical to identifying the scenario but `FILE_PATH` is variable
35and ignored.
36
37For deeper understanding of the OpenBMC logging subsystem, it may be useful
38to read the manpages for `man 1 journalctl` and `man 3 sd_journal_send`.
39Generally accepted log-levels and their definition is historically documented
40in `man 3 syslog`.
41
42## log
43
44The pre-C++20 logging APIs presented by phosphor-logging are
45`phosphor::logging::log`.  The basic format of a log call is:
46
47```
48    log<level>("A message", entry("TAG0=%s", "value"), entry("TAG1=%x", 2));
49```
50
51Each log call has a level or priority, which corresponds to syslog priorities,
52such as 'debug', 'info', 'emergency', a free-form message string, and any number
53of entries, which are key-value pairs of data.
54
55The 'key' of an entry is an upper-case tag for the data along with a
56printf-style format string for the data.  Journald imposes some restrictions on
57the tag: it must be all capital letters, numbers, or underscores and must not
58start with an underscore.  Unfortunately, if these restrictions are not followed
59or the printf string is invalid for the data, the code will compile but journald
60may silently drop the log request (or pieces of it).
61
62It is highly discouraged to dynamically create the free-form message string
63because the contents are then, effectively, unstructured.
64
65## lg2
66
67The post-C++20 logging APIs presented by phosphor-logging are
68`lg2::log`.  The basic format of a log call is:
69
70```
71    lg2::level("A {TAG0} occured.", "TAG0", "foo"_s, "TAG1", lg2::hex, 2);
72```
73
74Each log call has a level or priority, but the level is indicated by the
75function call name (such as `lg2::debug(...)`).  The log call also has a
76free-form message string and any number of entries indicated by 2 or 3
77argument sets:
78
79- key name (with the same `[_A-Z0-9]` requirements imposed by journald).
80- [optional] set of format flags
81- data value
82
83The free-form message may also optionally contain brace-wrapped key names,
84for which the message will be dynamically modified to contain the formatted
85value in place of the `{KEY}`.  This enables human-friendly message strings
86to be formed without relying on verbose journald output modes.
87
88Note: Since a free-form message with data can be created using the `{KEY}`
89mechanism, no other string formatting libraries are necessary or should be
90used.  Avoiding the `{KEY}` feature causes the journal messages to become
91unstructured.  Do not use `sstream` or `{fmt}` to format the message!
92
93The supported format flags are:
94- `bin`, `dec`, `hex`
95    - The [integer] data should be formatted in the requested manner.
96    - Decimal is the default.
97    - Examples:
98        + `(bin, 0xabcd)` -> `0b1010101111001101`
99        + `(hex, 1234)` -> `0x4d2`
100- `field8`, `field16`, `field32`, `field64`
101    - The [integer] data should be padded as if it were a field of
102      specified bit-length (useful only for `bin` or `hex` data).
103    - Examples:
104        + `(bin | field8, 0xff)` -> `0b11111111`
105        + `(hex | field16, 10)` -> `0x000a`
106
107Format flags can be OR'd together as necessary: `hex | field32`.
108
109The APIs can handle (and format appropriately) any data of the following
110types: signed or unsigned integers, floating point numbers, booleans, strings
111(C-strings, std::strings, or std::string_views), sdbusplus enums, exceptions,
112and pointers.
113
114The APIs also perform compile-time analysis of the arguments to give descriptive
115error messages for incorrect parameters or format flags.  Some examples are:
116
117- `(dec | hex)` yields:
118    - `error: static assertion failed: Conflicting flags found for value type.`
119- `dec` applied to a string yields:
120    - `error: static assertion failed: Prohibited flag found for value type.`
121- Missing a key yields:
122    - `error: static assertion failed: Found value without expected header
123       field.`
124- Missing data yields:
125    - `error: static assertion failed: Found header field without expected
126       data.`
127- Missing a message yields:
128    - `error: use of deleted function ‘lg2::debug<>::debug()’`
129
130### LOG2_FMTMSG key
131
132The API adds an extra journald key to represent the original message prior
133to `{KEY}` replacement, which is saved with the `LOG2_FMTMSG` key.  This
134is done to facilitate searching the journal with a known fixed version of
135the message (prior to the dynamic replacement).
136
137### Key format checking
138
139The journald APIs require that keys (also called data 'headers') may only
140contain underscore, uppercase letters, or numbers (`[_A-Z0-9]`) and may not
141start with underscores.  If these requirements are ignored, the journal API
142silently drops journal requests.  In order to prevent silent bugs, the code
143performs compile-time checking of these requirements.
144
145The code that enables compile-time header checking imposes two constraints:
1461. Keys / headers must be passed as constant C-string values.
147    - `"KEY"` is valid; `"KEY"_s` or `variable_key` is not.
1482. Any constant C-string may be interpreted as a key and give non-obvious
149   compile warnings about format violations.
150    - Constant C-strings (`"a string"`) should be passed as a C++ literal
151      (`"a string"_s`) instead.
152
153### stderr output
154
155When running an application or daemon on a console or SSH session, it might
156not be obvious that the application is writing to the journal.  The `lg2` APIs
157detect if the application is running on a TTY and additionally mirror the
158information sent to the journal to the TTY.
159
160### Why a new API?
161
162There were a number of issues raised by `logging::log` which are not easily
163fixed with the existing API.
164
1651. The mixture of template and temporary-constructed `entry` parameters is
166   verbose and clumsy.  `lg2` is far more succinct in this regard.
1672. The printf format-strings were error prone and potentially missed in code
168   reviews.  `lg2` eliminates the need for printf strings by generating the
169   formatting internally.
1703. `logging::log` generates incorrect code location information (see
171   openbmc/openbmc#2297).  `lg2` uses C++20's `source_location` to, by default,
172   generate correct code location info and can optionally be passed a
173   non-defaulted `source_location` for rare but appropriate cases.
1744. The previous APIs had no mechanism to generate dynamic user-friendly strings
175   which caused some developers to rely on external formatting libraries such
176   as `{fmt}`.  `{KEY}` replacement is now a core feature of the new API.
1775. When running on a TTY, `logging::log` sent data to journal and the TTY was
178   silent.  This resulted in some applications creating custom code to write
179   some data to `stdout` and some to `logging::log` APIs.  `lg2` automatically
180   detects if it is running on a TTY and duplicates logging data to the console
181   and the journal.
182
183It is possible #3 and #5 could be fixed with the existing APIs, but the
184remainder are only possible to be resolved with changes to the API syntax.
185
186### Why C++20?
187
188Solving issue openbmc/openbmc#2297 requires C++20's `source_location` feature.
189It is possible that this could be solved in the `logging::log` APIs by utilizing
190`#ifdef` detection of the `source_location` feature so that C++20 applications
191gain this support.
192
193Implementing much of the syntactic improvements of the `lg2` API is made
194possible by leveraging C++20's Concepts feature.  Experts in C++ may argue that
195this could be implemented in earlier revisions of C++ using complex SFINAE
196techniques with templated-class partial-specialization and overloading.  Those
197experts are more than welcome to implement the `lg2` API in C++17 on their own.
198
199### Why didn't you do ...?
200
201> Why didn't you just use `{fmt}`?
202
203`{fmt}` is a great API for creating unstructured logging strings, but we are
204trying to create structured logging.  `{fmt}` doesn't address that problem
205domain.
206
207> Why invent your own formatting and not use `{fmt}`?
208
209The formatting performed by this API is purposefully minimal.  `{fmt}` is very
210capable and especially apt for human-facing string formatting.  That is not the
211typical use-case for our logging.  Instead we prioritized the following:
212
2131. Reasonable syntactic ergonomics so that the API can easily be adopted.
2142. Simple, consistent, machine parse-able data contents.
2153. Sufficient human-facing messages for developer-level debug.
2164. Very tight code generation at logging call sites and reasonably performant
217   code.
218
219(1) The lg2 API is roughly equivalent to `printf`, `{fmt}`, `cout` in terms of
220    ergonomics, but significantly better compile-time error proofing than the
221    others (except on par with `{fmt}` for errors).
222
223(2) Adding robust formatting would lead to less consistent structured data with
224    essentially no additional benefit.  Detailed field specifiers like `{.4f}`
225    do not serve any purpose when the consumer is another computer program, and
226    only minimal enhancement for developers.  The typical utility formatting
227    for hardware-facing usage is implemented (hex, binary, field-size).
228
229(3) The `{HEADER}` placeholders allow data to be placed in a human-friendly
230    manner on par with `{fmt}`.
231
232(4) The call-site code generated by this API is almost identical to a `printf`
233    and the journal-facing code is on similar performance footing to the
234    journal_send APIs.  We save some processing by using `iovec` interfaces
235    and providing the source-code information, compared to the older `logging`
236    APIs and have similar formatting performance to the printf-style formatting
237    that journal_send used.  The only difference is that this is done in our
238    library rather than in `libsystemd`.
239
240    Utilizing `{fmt}` for each structured data element would impose much greater
241    overheads.  Either we insert the `{fmt}` calls at the call-site (N calls
242    plus N string objects for N structured data elements), or we do them in the
243    library side where we lose the compile-time format checking.  Also, the
244    performance of the more robust formatting would almost certainly be worse,
245    especially if we do the formatting library-side.
246
247    Logging is done often.  Shifting a few values onto the stack for a
248    printf-type call compared to a kilobyte+ of generated code for inline
249    `{fmt}` calls is a significant trade-off.  And one with the only major
250    advantage being more universally standardized API.  The `lg2` API seems
251    obvious enough in ergonomics such that this should not be an impediment to
252    our community of developers.
253
254If it is later decided that we need more robust formatting or the `lg2::format`
255flags were a bad idea they could be deprecated and replaced.  The format flags
256are a unique C++ type, which makes syntax parsing easier.  As long as they are
257replaced with a similar unique C++ type both syntaxes could be supported for a
258time.  Thus enhancing to support something like `fmt::arg` in the future could
259be done without impacting existing code usage.  Also, an ambitious developer
260could write a Clang-Tidy code upgrader to transition from format flags to
261something else, like Abseil provides for API changes.
262
263> Doesn't duplicating the structured data in the message decrease the available
264> journal space?
265
266Far less than you might imagine.  Journald keeps the messages in a compressed
267binary format.  Since the data embedded in the message and the structured data
268are identical in content, and very near each other in the on-disk format, they
269compress exceptionally well.  Likely on the order of 1-2 bytes per structured
270data element.
271
272The lack of data in the default `journalctl` output was a serious impediment to
273adoption of the `logging` API by some members of the development community.
274Unless we dispense with structured logging entirely, this duplication seems like
275a reasonable compromise.
276
277> Doesn't the `{HEADER}` syntax needlessly lengthen the message strings?
278
279Lengthen, yes.  Needlessly, no?
280
281An earlier `lg2` proposal had a format flag that appended data to the message
282string instead of putting it in-place.  The consensus was that this did not
283create as human-friendly messages as developers desired so the placeholder
284syntax was implemented instead.
285
286`{fmt}` can use shorter placeholders of `{}` or `{3}`.  The non-indexed syntax
287would require structured data elements be in specific order and could be error
288prone with code maintenance.  The indexed syntax is similarly error prone and
289harder to review.  Both of them are more work for string formatting on the
290library.
291
292The `{HEADER}` syntax is identical to `{fmt}`'s "Named Argument" syntax but
293actually with better parameter ergonomics as `{fmt}` would require wrapping the
294named argument with a `fmt::arg` call, which is similar to `logging`'s `entry`.
295