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, and pointers. 112 113The APIs also perform compile-time analysis of the arguments to give descriptive 114error messages for incorrect parameters or format flags. Some examples are: 115 116- `(dec | hex)` yields: 117 - `error: static assertion failed: Conflicting flags found for value type.` 118- `dec` applied to a string yields: 119 - `error: static assertion failed: Prohibited flag found for value type.` 120- Missing a key yields: 121 - `error: static assertion failed: Found value without expected header 122 field.` 123- Missing data yields: 124 - `error: static assertion failed: Found header field without expected 125 data.` 126- Missing a message yields: 127 - `error: use of deleted function ‘lg2::debug<>::debug()’` 128 129### LOG2_FMTMSG key 130 131The API adds an extra journald key to represent the original message prior 132to `{KEY}` replacement, which is saved with the `LOG2_FMTMSG` key. This 133is done to facilitate searching the journal with a known fixed version of 134the message (prior to the dynamic replacement). 135 136### Key format checking 137 138The journald APIs require that keys (also called data 'headers') may only 139contain underscore, uppercase letters, or numbers (`[_A-Z0-9]`) and may not 140start with underscores. If these requirements are ignored, the journal API 141silently drops journal requests. In order to prevent silent bugs, the code 142performs compile-time checking of these requirements. 143 144The code that enables compile-time header checking imposes two constraints: 1451. Keys / headers must be passed as constant C-string values. 146 - `"KEY"` is valid; `"KEY"_s` or `variable_key` is not. 1472. Any constant C-string may be interpreted as a key and give non-obvious 148 compile warnings about format violations. 149 - Constant C-strings (`"a string"`) should be passed as a C++ literal 150 (`"a string"_s`) instead. 151 152### stderr output 153 154When running an application or daemon on a console or SSH session, it might 155not be obvious that the application is writing to the journal. The `lg2` APIs 156detect if the application is running on a TTY and additionally mirror the 157information sent to the journal to the TTY. 158 159### Why a new API? 160 161There were a number of issues raised by `logging::log` which are not easily 162fixed with the existing API. 163 1641. The mixture of template and temporary-constructed `entry` parameters is 165 verbose and clumsy. `lg2` is far more succinct in this regard. 1662. The printf format-strings were error prone and potentially missed in code 167 reviews. `lg2` eliminates the need for printf strings by generating the 168 formatting internally. 1693. `logging::log` generates incorrect code location information (see 170 openbmc/openbmc#2297). `lg2` uses C++20's `source_location` to, by default, 171 generate correct code location info and can optionally be passed a 172 non-defaulted `source_location` for rare but appropriate cases. 1734. The previous APIs had no mechanism to generate dynamic user-friendly strings 174 which caused some developers to rely on external formatting libraries such 175 as `{fmt}`. `{KEY}` replacement is now a core feature of the new API. 1765. When running on a TTY, `logging::log` sent data to journal and the TTY was 177 silent. This resulted in some applications creating custom code to write 178 some data to `stdout` and some to `logging::log` APIs. `lg2` automatically 179 detects if it is running on a TTY and duplicates logging data to the console 180 and the journal. 181 182It is possible #3 and #5 could be fixed with the existing APIs, but the 183remainder are only possible to be resolved with changes to the API syntax. 184 185### Why C++20? 186 187Solving issue openbmc/openbmc#2297 requires C++20's `source_location` feature. 188It is possible that this could be solved in the `logging::log` APIs by utilizing 189`#ifdef` detection of the `source_location` feature so that C++20 applications 190gain this support. 191 192Implementing much of the syntactic improvements of the `lg2` API is made 193possible by leveraging C++20's Concepts feature. Experts in C++ may argue that 194this could be implemented in earlier revisions of C++ using complex SFINAE 195techniques with templated-class partial-specialization and overloading. Those 196experts are more than welcome to implement the `lg2` API in C++17 on their own. 197 198### Why didn't you do ...? 199 200> Why didn't you just use `{fmt}`? 201 202`{fmt}` is a great API for creating unstructured logging strings, but we are 203trying to create structured logging. `{fmt}` doesn't address that problem 204domain. 205 206> Why invent your own formatting and not use `{fmt}`? 207 208The formatting performed by this API is purposefully minimal. `{fmt}` is very 209capable and especially apt for human-facing string formatting. That is not the 210typical use-case for our logging. Instead we prioritized the following: 211 2121. Reasonable syntactic ergonomics so that the API can easily be adopted. 2132. Simple, consistent, machine parse-able data contents. 2143. Sufficient human-facing messages for developer-level debug. 2154. Very tight code generation at logging call sites and reasonably performant 216 code. 217 218(1) The lg2 API is roughly equivalent to `printf`, `{fmt}`, `cout` in terms of 219 ergonomics, but significantly better compile-time error proofing than the 220 others (except on par with `{fmt}` for errors). 221 222(2) Adding robust formatting would lead to less consistent structured data with 223 essentially no additional benefit. Detailed field specifiers like `{.4f}` 224 do not serve any purpose when the consumer is another computer program, and 225 only minimal enhancement for developers. The typical utility formatting 226 for hardware-facing usage is implemented (hex, binary, field-size). 227 228(3) The `{HEADER}` placeholders allow data to be placed in a human-friendly 229 manner on par with `{fmt}`. 230 231(4) The call-site code generated by this API is almost identical to a `printf` 232 and the journal-facing code is on similar performance footing to the 233 journal_send APIs. We save some processing by using `iovec` interfaces 234 and providing the source-code information, compared to the older `logging` 235 APIs and have similar formatting performance to the printf-style formatting 236 that journal_send used. The only difference is that this is done in our 237 library rather than in `libsystemd`. 238 239 Utilizing `{fmt}` for each structured data element would impose much greater 240 overheads. Either we insert the `{fmt}` calls at the call-site (N calls 241 plus N string objects for N structured data elements), or we do them in the 242 library side where we lose the compile-time format checking. Also, the 243 performance of the more robust formatting would almost certainly be worse, 244 especially if we do the formatting library-side. 245 246 Logging is done often. Shifting a few values onto the stack for a 247 printf-type call compared to a kilobyte+ of generated code for inline 248 `{fmt}` calls is a significant trade-off. And one with the only major 249 advantage being more universally standardized API. The `lg2` API seems 250 obvious enough in ergonomics such that this should not be an impediment to 251 our community of developers. 252 253If it is later decided that we need more robust formatting or the `lg2::format` 254flags were a bad idea they could be deprecated and replaced. The format flags 255are a unique C++ type, which makes syntax parsing easier. As long as they are 256replaced with a similar unique C++ type both syntaxes could be supported for a 257time. Thus enhancing to support something like `fmt::arg` in the future could 258be done without impacting existing code usage. Also, an ambitious developer 259could write a Clang-Tidy code upgrader to transition from format flags to 260something else, like Abseil provides for API changes. 261 262> Doesn't duplicating the structured data in the message decrease the available 263> journal space? 264 265Far less than you might imagine. Journald keeps the messages in a compressed 266binary format. Since the data embedded in the message and the structured data 267are identical in content, and very near each other in the on-disk format, they 268compress exceptionally well. Likely on the order of 1-2 bytes per structured 269data element. 270 271The lack of data in the default `journalctl` output was a serious impediment to 272adoption of the `logging` API by some members of the development community. 273Unless we dispense with structured logging entirely, this duplication seems like 274a reasonable compromise. 275 276> Doesn't the `{HEADER}` syntax needlessly lengthen the message strings? 277 278Lengthen, yes. Needlessly, no? 279 280An earlier `lg2` proposal had a format flag that appended data to the message 281string instead of putting it in-place. The consensus was that this did not 282create as human-friendly messages as developers desired so the placeholder 283syntax was implemented instead. 284 285`{fmt}` can use shorter placeholders of `{}` or `{3}`. The non-indexed syntax 286would require structured data elements be in specific order and could be error 287prone with code maintenance. The indexed syntax is similarly error prone and 288harder to review. Both of them are more work for string formatting on the 289library. 290 291The `{HEADER}` syntax is identical to `{fmt}`'s "Named Argument" syntax but 292actually with better parameter ergonomics as `{fmt}` would require wrapping the 293named argument with a `fmt::arg` call, which is similar to `logging`'s `entry`. 294