lg2: initial implementation of C++20 structured logging

Signed-off-by: Patrick Williams <patrick@stwcx.xyz>
Change-Id: If37857cc3748df1ea6f767ce535b02c63cde8370
diff --git a/lib/lg2_logger.cpp b/lib/lg2_logger.cpp
new file mode 100644
index 0000000..96e244b
--- /dev/null
+++ b/lib/lg2_logger.cpp
@@ -0,0 +1,232 @@
+#define SD_JOURNAL_SUPPRESS_LOCATION
+
+#include <systemd/sd-journal.h>
+#include <unistd.h>
+
+#include <algorithm>
+#include <bitset>
+#include <cstdarg>
+#include <cstdio>
+#include <iostream>
+#include <phosphor-logging/lg2.hpp>
+#include <vector>
+
+namespace lg2::details
+{
+/** Convert unsigned to string using format flags. */
+static std::string value_to_string(uint64_t f, uint64_t v)
+{
+    switch (f & (hex | bin | dec).value)
+    {
+        // For binary, use bitset<>::to_string.
+        // Treat values without a field-length format flag as 64 bit.
+        case bin.value:
+        {
+            switch (f & (field8 | field16 | field32 | field64).value)
+            {
+                case field8.value:
+                {
+                    return "0b" + std::bitset<8>(v).to_string();
+                }
+                case field16.value:
+                {
+                    return "0b" + std::bitset<16>(v).to_string();
+                }
+                case field32.value:
+                {
+                    return "0b" + std::bitset<32>(v).to_string();
+                }
+                case field64.value:
+                default:
+                {
+                    return "0b" + std::bitset<64>(v).to_string();
+                }
+            }
+        }
+
+        // For hex, use the appropriate sprintf.
+        case hex.value:
+        {
+            char value[19];
+            const char* format = nullptr;
+
+            switch (f & (field8 | field16 | field32 | field64).value)
+            {
+                case field8.value:
+                {
+                    format = "0x%02" PRIx64;
+                    break;
+                }
+
+                case field16.value:
+                {
+                    format = "0x%04" PRIx64;
+                    break;
+                }
+
+                case field32.value:
+                {
+                    format = "0x%08" PRIx64;
+                    break;
+                }
+
+                case field64.value:
+                {
+                    format = "0x%016" PRIx64;
+                    break;
+                }
+
+                default:
+                {
+                    format = "0x%" PRIx64;
+                    break;
+                }
+            }
+
+            snprintf(value, sizeof(value), format, v);
+            return value;
+        }
+
+        // For dec, use the simple to_string.
+        case dec.value:
+        default:
+        {
+            return std::to_string(v);
+        }
+    }
+}
+
+/** Convert signed to string using format flags. */
+static std::string value_to_string(uint64_t f, int64_t v)
+{
+    // If hex or bin was requested just use the unsigned formatting
+    // rules. (What should a negative binary number look like otherwise?)
+    if (f & (hex | bin).value)
+    {
+        return value_to_string(f, static_cast<uint64_t>(v));
+    }
+    return std::to_string(v);
+}
+
+/** Convert float to string using format flags. */
+static std::string value_to_string(uint64_t, double v)
+{
+    // No format flags supported for floats.
+    return std::to_string(v);
+}
+
+// Positions of various strings in an iovec.
+static constexpr size_t pos_msg = 0;
+static constexpr size_t pos_fmtmsg = 1;
+static constexpr size_t pos_prio = 2;
+static constexpr size_t pos_file = 3;
+static constexpr size_t pos_line = 4;
+static constexpr size_t pos_func = 5;
+static constexpr size_t static_locs = pos_func + 1;
+
+/** No-op output of a message. */
+static void noop_extra_output(level, const std::source_location&,
+                              const std::string&)
+{
+}
+
+/** std::cerr output of a message. */
+static void cerr_extra_output(level l, const std::source_location& s,
+                              const std::string& m)
+{
+    std::cerr << s.file_name() << ":" << s.line() << ":" << s.function_name()
+              << "|<" << static_cast<uint64_t>(l) << "> " << m << std::endl;
+}
+
+// Use the cerr output method if we are on a TTY.
+static auto extra_output_method =
+    isatty(fileno(stderr)) ? cerr_extra_output : noop_extra_output;
+
+// Do_log implementation.
+void do_log(level l, const std::source_location& s, const std::string_view& m,
+            size_t count, ...)
+{
+    using namespace std::string_literals;
+
+    const size_t entries = count + static_locs;
+    std::vector<std::string> strings{entries};
+
+    std::string message{m};
+
+    // Assign all the static fields.
+    strings[pos_fmtmsg] = "LOG2_FMTMSG="s + m.data();
+    strings[pos_prio] = "PRIORITY="s + std::to_string(static_cast<uint64_t>(l));
+    strings[pos_file] = "CODE_FILE="s + s.file_name();
+    strings[pos_line] = "CODE_LINE="s + std::to_string(s.line());
+    strings[pos_func] = "CODE_FUNC="s + s.function_name();
+
+    // Handle all the va_list args.
+    std::va_list args;
+    va_start(args, count);
+    for (size_t i = static_locs; i < entries; ++i)
+    {
+        // Get the header out.
+        std::string h = va_arg(args, const char*);
+
+        // Get the format flag.
+        auto f = va_arg(args, uint64_t);
+
+        // Handle the value depending on which type format flag it has.
+        std::string value = {};
+        switch (f & (signed_val | unsigned_val | str | floating).value)
+        {
+            case signed_val.value:
+            {
+                auto v = va_arg(args, int64_t);
+                value = value_to_string(f, v);
+                break;
+            }
+
+            case unsigned_val.value:
+            {
+                auto v = va_arg(args, uint64_t);
+                value = value_to_string(f, v);
+                break;
+            }
+
+            case str.value:
+            {
+                value = va_arg(args, const char*);
+                break;
+            }
+
+            case floating.value:
+            {
+                auto v = va_arg(args, double);
+                value = value_to_string(f, v);
+                break;
+            }
+        }
+
+        // Create the field for this value.
+        strings[i] = h + '=' + value;
+
+        // Check for {HEADER} in the message and replace with value.
+        auto h_brace = '{' + h + '}';
+        if (auto start = message.find(h_brace); start != std::string::npos)
+        {
+            message.replace(start, h_brace.size(), value);
+        }
+    }
+    va_end(args);
+
+    // Add the final message into the strings array.
+    strings[pos_msg] = "MESSAGE="s + message.data();
+
+    // Trasform strings -> iovec.
+    std::vector<iovec> iov{};
+    std::ranges::transform(strings, std::back_inserter(iov), [](auto& s) {
+        return iovec{s.data(), s.length()};
+    });
+
+    // Output the iovec.
+    sd_journal_sendv(iov.data(), entries);
+    extra_output_method(l, s, message);
+}
+
+} // namespace lg2::details