lg2: initial implementation of C++20 structured logging

Signed-off-by: Patrick Williams <patrick@stwcx.xyz>
Change-Id: If37857cc3748df1ea6f767ce535b02c63cde8370
diff --git a/lib/include/phosphor-logging/lg2.hpp b/lib/include/phosphor-logging/lg2.hpp
new file mode 100644
index 0000000..d6b8407
--- /dev/null
+++ b/lib/include/phosphor-logging/lg2.hpp
@@ -0,0 +1,110 @@
+#pragma once
+
+#if __cplusplus < 202002L
+#error "phosphor-logging lg2 requires C++20"
+#else
+
+#include <phosphor-logging/lg2/concepts.hpp>
+#include <phosphor-logging/lg2/conversion.hpp>
+#include <phosphor-logging/lg2/flags.hpp>
+#include <phosphor-logging/lg2/level.hpp>
+#include <source_location>
+#include <string_view>
+
+namespace lg2
+{
+/** Implementation of the structured logging `lg2::log` interface. */
+template <level S = level::debug, details::any_but<std::source_location>... Ts>
+struct log
+{
+    /** log with a custom source_location.
+     *
+     *  @param[in] s - The custom source location.
+     *  @param[in] msg - The message to log.
+     *  @param[in] ts - The rest of the arguments.
+     */
+    explicit log(const std::source_location& s, const std::string_view& msg,
+                 Ts&&... ts)
+    {
+        details::log_conversion::start(S, s, msg, std::forward<Ts>(ts)...);
+    }
+
+    /** default log (source_location is determined by calling location).
+     *
+     *  @param[in] msg - The message to log.
+     *  @param[in] ts - The rest of the arguments.
+     *  @param[in] s - The derived source_location.
+     */
+    explicit log(
+        const std::string_view& msg, Ts&&... ts,
+        const std::source_location& s = std::source_location::current()) :
+        log(s, msg, std::forward<Ts>(ts)...)
+    {
+    }
+
+    // Give a nicer compile error if someone tries to log without a message.
+    log() = delete;
+};
+
+// Deducation guides to help the compiler out...
+
+template <level S = level::debug, typename... Ts>
+explicit log(const std::string_view&, Ts&&...) -> log<S, Ts...>;
+
+template <level S = level::debug, typename... Ts>
+explicit log(const std::source_location&, const std::string_view&, Ts&&...)
+    -> log<S, Ts...>;
+
+/** Macro to define aliases for lg2::level(...) -> lg2::log<level>(...)
+ *
+ *  Creates a simple inherited structure and corresponding deduction guides.
+ */
+#define PHOSPHOR_LOG2_DECLARE_LEVEL(levelval)                                  \
+    template <typename... Ts>                                                  \
+    struct levelval : public log<level::levelval, Ts...>                       \
+    {                                                                          \
+        using log<level::levelval, Ts...>::log;                                \
+    };                                                                         \
+                                                                               \
+    template <typename... Ts>                                                  \
+    explicit levelval(const std::string_view&, Ts&&...) -> levelval<Ts...>;    \
+                                                                               \
+    template <typename... Ts>                                                  \
+    explicit levelval(const std::source_location&, const std::string_view&,    \
+                      Ts&&...)                                                 \
+        ->levelval<Ts...>
+
+// Enumerate the aliases for each log level.
+PHOSPHOR_LOG2_DECLARE_LEVEL(emergency);
+PHOSPHOR_LOG2_DECLARE_LEVEL(alert);
+PHOSPHOR_LOG2_DECLARE_LEVEL(critical);
+PHOSPHOR_LOG2_DECLARE_LEVEL(error);
+PHOSPHOR_LOG2_DECLARE_LEVEL(warning);
+PHOSPHOR_LOG2_DECLARE_LEVEL(notice);
+PHOSPHOR_LOG2_DECLARE_LEVEL(info);
+PHOSPHOR_LOG2_DECLARE_LEVEL(debug);
+
+#undef PHOSPHOR_LOG2_DECLARE_LEVEL
+
+/** Handy scope-level `using` to get the necessary bits of lg2. */
+#define PHOSPHOR_LOG2_USING                                                    \
+    using lg2::emergency;                                                      \
+    using lg2::alert;                                                          \
+    using lg2::critical;                                                       \
+    using lg2::error;                                                          \
+    using lg2::warning;                                                        \
+    using lg2::notice;                                                         \
+    using lg2::info;                                                           \
+    using lg2::debug
+// We purposefully left out `using lg2::log` above to avoid collisions with
+// the math function `log`.  There is little use for direct calls to `lg2::log`,
+// when the level-aliases are available, since it is just a more awkward syntax.
+
+/** Scope-level `using` to get the everything, incluing format flags. */
+#define PHOSPHOR_LOG2_USING_WITH_FLAGS                                         \
+    PHOSPHOR_LOG2_USING;                                                       \
+    PHOSPHOR_LOG2_USING_FLAGS
+
+} // namespace lg2
+
+#endif
diff --git a/lib/include/phosphor-logging/lg2/concepts.hpp b/lib/include/phosphor-logging/lg2/concepts.hpp
new file mode 100644
index 0000000..7d997ad
--- /dev/null
+++ b/lib/include/phosphor-logging/lg2/concepts.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <concepts>
+
+namespace lg2::details
+{
+
+/** Matches a type T which is anything except one of those in Ss. */
+template <typename T, typename... Ss>
+concept any_but = (... && !std::convertible_to<T, Ss>);
+
+}; // namespace lg2::details
diff --git a/lib/include/phosphor-logging/lg2/conversion.hpp b/lib/include/phosphor-logging/lg2/conversion.hpp
new file mode 100644
index 0000000..beb3cf0
--- /dev/null
+++ b/lib/include/phosphor-logging/lg2/conversion.hpp
@@ -0,0 +1,302 @@
+#pragma once
+
+#include <concepts>
+#include <cstddef>
+#include <phosphor-logging/lg2/flags.hpp>
+#include <phosphor-logging/lg2/level.hpp>
+#include <phosphor-logging/lg2/logger.hpp>
+#include <source_location>
+#include <string_view>
+#include <tuple>
+#include <type_traits>
+
+namespace lg2::details
+{
+
+/** Concept to determine if an item acts like a string.
+ *
+ *  Something acts like a string if we can construct a string_view from it.
+ *  This covers std::string and C-strings.  But, there is subtlety in that
+ *  nullptr_t can be used to construct a string_view until C++23, so we need
+ *  to exempt out nullptr_t's (otherwise `nullptr` ends up acting like a
+ *  string).
+ */
+template <typename T>
+concept string_like_type =
+    std::constructible_from<std::string_view, T> && !std::same_as<nullptr_t, T>;
+
+/** Concept to determine if an item acts like a pointer.
+ *
+ *  Any pointer, which doesn't act like a string, should be treated as a raw
+ *  pointer.
+ */
+template <typename T>
+concept pointer_type = (std::is_pointer_v<T> ||
+                        std::same_as<nullptr_t, T>)&&!string_like_type<T>;
+
+/** Concept to determine if an item acts like an unsigned_integral.
+ *
+ *  Except bool because we want bool to be handled special to create nice
+ *  `True` and `False` strings.
+ */
+template <typename T>
+concept unsigned_integral_except_bool =
+    !std::same_as<T, bool> && std::unsigned_integral<T>;
+
+/** Concept listing all of the types we know how to convert into a format
+ *  for logging.
+ */
+template <typename T>
+concept unsupported_log_convert_types =
+    !(unsigned_integral_except_bool<T> || std::signed_integral<T> ||
+      std::same_as<bool, T> || std::floating_point<T> || string_like_type<T> ||
+      pointer_type<T>);
+
+/** Any type we do not know how to convert for logging gives a nicer
+ *  static_assert message. */
+template <log_flags... Fs, unsupported_log_convert_types T>
+static auto log_convert(const char*, log_flag<Fs...>, T)
+{
+    static_assert(!std::is_same_v<T, T>, "Unsupported type for logging value.");
+    // Having this return of an empty tuple reduces the error messages.
+    return std::tuple<>{};
+}
+
+/** Logging conversion for unsigned. */
+template <log_flags... Fs, unsigned_integral_except_bool V>
+static auto log_convert(const char* h, log_flag<Fs...> f, V v)
+{
+    // Compile-time checks for valid formatting flags.
+    prohibit(f, floating);
+    prohibit(f, signed_val);
+    prohibit(f, str);
+
+    one_from_set(f, dec | hex | bin);
+    one_from_set(f, field8 | field16 | field32 | field64);
+
+    // Add 'unsigned' flag, force to uint64_t for variadic passing.
+    return std::make_tuple(h, (f | unsigned_val).value,
+                           static_cast<uint64_t>(v));
+}
+
+/** Logging conversion for signed. */
+template <log_flags... Fs, std::signed_integral V>
+static auto log_convert(const char* h, log_flag<Fs...> f, V v)
+{
+    // Compile-time checks for valid formatting flags.
+    prohibit(f, floating);
+    prohibit(f, str);
+    prohibit(f, unsigned_val);
+
+    one_from_set(f, dec | hex | bin);
+    one_from_set(f, field8 | field16 | field32 | field64);
+
+    // Add 'signed' flag, force to int64_t for variadic passing.
+    return std::make_tuple(h, (f | signed_val).value, static_cast<int64_t>(v));
+}
+
+/** Logging conversion for bool. */
+template <log_flags... Fs, std::same_as<bool> V>
+static auto log_convert(const char* h, log_flag<Fs...> f, V v)
+{
+    // Compile-time checks for valid formatting flags.
+    prohibit(f, bin);
+    prohibit(f, dec);
+    prohibit(f, field16);
+    prohibit(f, field32);
+    prohibit(f, field64);
+    prohibit(f, field8);
+    prohibit(f, floating);
+    prohibit(f, hex);
+    prohibit(f, signed_val);
+    prohibit(f, unsigned_val);
+
+    // Cast bools to a "True" or "False" string.
+    return std::make_tuple(h, (f | str).value, v ? "True" : "False");
+}
+
+/** Logging conversion for floating points. */
+template <log_flags... Fs, std::floating_point V>
+static auto log_convert(const char* h, log_flag<Fs...> f, V v)
+{
+    // Compile-time checks for valid formatting flags.
+    prohibit(f, bin);
+    prohibit(f, dec);
+    prohibit(f, field16);
+    prohibit(f, field32);
+    prohibit(f, field64);
+    prohibit(f, field8);
+    prohibit(f, hex);
+    prohibit(f, signed_val);
+    prohibit(f, str);
+    prohibit(f, unsigned_val);
+
+    // Add 'floating' flag, force to double for variadic passing.
+    return std::make_tuple(h, (f | floating).value, static_cast<double>(v));
+}
+
+/** Logging conversion for string-likes. */
+template <log_flags... Fs, string_like_type V>
+static auto log_convert(const char* h, log_flag<Fs...> f, V&& v)
+{
+    // Compile-time checks for valid formatting flags.
+    prohibit(f, bin);
+    prohibit(f, dec);
+    prohibit(f, field16);
+    prohibit(f, field32);
+    prohibit(f, field64);
+    prohibit(f, field8);
+    prohibit(f, floating);
+    prohibit(f, hex);
+    prohibit(f, signed_val);
+    prohibit(f, unsigned_val);
+
+    // Add 'str' flag, force to 'const char*' for variadic passing.
+    //
+    // It may appear unsafe that we are using a temporary string_view object
+    // for conversion, but it is a safe and consise way to do this conversion.
+    //  - (const char*) goes through string_view, but data() returns the
+    //    original pointer.
+    //  - (string_view) makes a duplicate temporary pointing to the same
+    //    original data pointer.
+    //  - (string) uses a temporary string_view to return a pointer to the
+    //    original std::string v.data().
+    // Without this pass-through-string_view, we would need some additional
+    // template magic to differentiate between types requiring direct pointer
+    // passing vs v.data() to obtain the underlying pointer.
+    return std::make_tuple(h, (f | str).value, std::string_view{v}.data());
+}
+
+/** Logging conversion for pointer-types. */
+template <log_flags... Fs, pointer_type V>
+static auto log_convert(const char* h, log_flag<Fs...> f, V v)
+{
+    // Compile-time checks for valid formatting flags.
+    prohibit(f, bin);
+    prohibit(f, dec);
+    prohibit(f, field16);
+    prohibit(f, field32);
+    prohibit(f, field64);
+    prohibit(f, field8);
+    prohibit(f, floating);
+    prohibit(f, signed_val);
+    prohibit(f, str);
+
+    // Cast (void*) to a hex-formatted uint64 using the target's pointer-size
+    // to determine field-width.
+    constexpr static auto new_f =
+        sizeof(void*) == 4 ? field32.value : field64.value;
+
+    return std::make_tuple(h, new_f | (hex | unsigned_val).value,
+                           reinterpret_cast<uint64_t>(v));
+}
+
+/** Class to facilitate walking through the arguments of the `lg2::log` function
+ *  and ensuring correct parameter types and conversion operations.
+ */
+class log_conversion
+{
+  private:
+    /** Conversion and validation is complete.  Pass along to the final
+     *  do_log variadic function. */
+    template <typename... Ts>
+    static void done(level l, const std::source_location& s,
+                     const std::string_view& m, Ts&&... ts)
+    {
+        do_log(l, s, m, sizeof...(Ts) / 3, std::forward<Ts>(ts)...);
+    }
+
+    /** Handle conversion of a { Header, Flags, Value } argument set. */
+    template <typename... Ts, std::convertible_to<const char*> H,
+              log_flags... Fs, typename V, typename... Ss>
+    static void step(std::tuple<Ts...> ts, H&& h, log_flag<Fs...> f, V&& v,
+                     Ss&&... ss)
+    {
+        // These two if conditions are similar, except that one calls 'done'
+        // since Ss is empty and the other calls the next 'step'.
+
+        // 1. Call `log_convert` on {h, f, v} for proper conversion.
+        // 2. Append the results of `log_convert` into the already handled
+        //    arguments (in ts).
+        // 3. Call the next step in the chain to handle the remainder Ss.
+
+        if constexpr (sizeof...(Ss) == 0)
+        {
+            std::apply(
+                [](auto... args) { done(args...); },
+                std::tuple_cat(ts, log_convert(std::forward<H>(h), f, v)));
+        }
+        else
+        {
+            step(std::tuple_cat(ts, log_convert(std::forward<H>(h), f, v)),
+                 std::forward<Ss>(ss)...);
+        }
+    }
+
+    /** Handle conversion of a { Header, Value } argument set. */
+    template <typename... Ts, std::convertible_to<const char*> H, typename V,
+              typename... Ss>
+    static void step(std::tuple<Ts...> ts, H&& h, V&& v, Ss&&... ss)
+    {
+        // These two if conditions are similar, except that one calls 'done'
+        // since Ss is empty and the other calls the next 'step'.
+
+        // 1. Call `log_convert` on {h, <empty flags>, v} for proper conversion.
+        // 2. Append the results of `log_convert` into the already handled
+        //    arguments (in ts).
+        // 3. Call the next step in the chain to handle the remainder Ss.
+
+        if constexpr (sizeof...(Ss) == 0)
+        {
+            std::apply([](auto... args) { done(args...); },
+                       std::tuple_cat(ts, log_convert(std::forward<H>(h),
+                                                      log_flag<>{}, v)));
+        }
+        else
+        {
+            step(std::tuple_cat(
+                     ts, log_convert(std::forward<H>(h), log_flag<>{}, v)),
+                 std::forward<Ss>(ss)...);
+        }
+    }
+
+    /** Finding a non-string as the first argument of a 2 or 3 argument set
+     *  is an error (missing HEADER field). */
+    template <typename... Ts, any_but<const char*> H, typename... Ss>
+    static void step(const std::tuple<Ts...>&, H, Ss&&...)
+    {
+        static_assert(std::is_same_v<const char*, H>,
+                      "Found value without expected header field.");
+    }
+
+    /** Finding a free string at the end is an error (found HEADER but no data).
+     */
+    template <typename... Ts, std::convertible_to<const char*> H>
+    static void step(const std::tuple<Ts...>&, H)
+    {
+        static_assert(!std::is_convertible_v<const char*, H>,
+                      "Found header field without expected data.");
+    }
+
+  public:
+    /** Start processing a sequence of arguments to `lg2::log` using `step` or
+     * `done`. */
+    template <typename... Ts>
+    static void start(level l, const std::source_location& s,
+                      const std::string_view& msg, Ts&&... ts)
+    {
+        // If there are no arguments (ie. just a message), then skip processing
+        // and call `done` directly.
+        if constexpr (sizeof...(Ts) == 0)
+        {
+            done(l, s, msg);
+        }
+        // Handle all the Ts by recursively calling 'step'.
+        else
+        {
+            step(std::forward_as_tuple(l, s, msg), std::forward<Ts>(ts)...);
+        }
+    }
+};
+
+} // namespace lg2::details
diff --git a/lib/include/phosphor-logging/lg2/flags.hpp b/lib/include/phosphor-logging/lg2/flags.hpp
new file mode 100644
index 0000000..814e313
--- /dev/null
+++ b/lib/include/phosphor-logging/lg2/flags.hpp
@@ -0,0 +1,105 @@
+#pragma once
+
+#include <bit>
+#include <cstdint>
+#include <type_traits>
+
+namespace lg2
+{
+namespace details
+{
+
+/** Type to hold a set of logging flags. */
+template <typename... Fs>
+struct log_flag
+{
+    /** Combined bit-set value of the held flags. */
+    static constexpr auto value = (0 | ... | Fs::value);
+};
+
+/** Constant for the "zero" flag. */
+static constexpr auto log_flag_seq_start =
+    std::integral_constant<uint64_t, 0>{};
+
+/** Concept to determine if a type is one of the defined flag types. */
+template <typename T>
+concept log_flags = requires
+{
+    T::i_am_a_lg2_flag_type;
+};
+
+/** Operator to combine log_flag sets together. */
+template <log_flags... As, log_flags... Bs>
+constexpr auto operator|(const log_flag<As...>, const log_flag<Bs...>)
+{
+    return details::log_flag<As..., Bs...>{};
+}
+
+/** Static check to determine if a prohibited flag is found in a flag set. */
+template <log_flags... Fs, log_flags F>
+constexpr void prohibit(log_flag<Fs...>, log_flag<F>)
+{
+    static_assert(!(... || std::is_same_v<Fs, F>),
+                  "Prohibited flag found for value type.");
+}
+
+/** Static check to determine if any conflicting flags are found in a flag set.
+ */
+template <log_flags... As, log_flags... Bs>
+constexpr void one_from_set(log_flag<As...> a, log_flag<Bs...> b)
+{
+    static_assert(std::popcount(a.value & b.value) < 2,
+                  "Conflicting flags found for value type.");
+}
+
+} // namespace details
+
+// Macro used to define all of the logging flags as a sequence of bitfields.
+//  - Creates a struct-type where the `value` is 1 bit higher than the previous
+//    so that it can be combined together with other flags using `log_flag`.
+//  - Creates a static instance of the flag in the `lg2` namespace.
+#define PHOSPHOR_LOG2_DECLARE_FLAG(flagname, prev)                             \
+    namespace details                                                          \
+    {                                                                          \
+    struct flag_##flagname                                                     \
+    {                                                                          \
+        static constexpr uint64_t value =                                      \
+            prev.value == log_flag_seq_start.value ? 1 : (prev.value << 1);    \
+                                                                               \
+        static constexpr bool i_am_a_lg2_flag_type = true;                     \
+    };                                                                         \
+    }                                                                          \
+    static constexpr auto flagname =                                           \
+        details::log_flag<details::flag_##flagname>()
+
+// Set of supported logging flags.
+//      Please keep these sorted!
+PHOSPHOR_LOG2_DECLARE_FLAG(bin, log_flag_seq_start);
+PHOSPHOR_LOG2_DECLARE_FLAG(dec, bin);
+PHOSPHOR_LOG2_DECLARE_FLAG(field8, dec);
+PHOSPHOR_LOG2_DECLARE_FLAG(field16, field8);
+PHOSPHOR_LOG2_DECLARE_FLAG(field32, field16);
+PHOSPHOR_LOG2_DECLARE_FLAG(field64, field32);
+PHOSPHOR_LOG2_DECLARE_FLAG(floating, field64);
+PHOSPHOR_LOG2_DECLARE_FLAG(hex, floating);
+PHOSPHOR_LOG2_DECLARE_FLAG(signed_val, hex);
+PHOSPHOR_LOG2_DECLARE_FLAG(str, signed_val);
+PHOSPHOR_LOG2_DECLARE_FLAG(unsigned_val, str);
+
+#undef PHOSPHOR_LOG2_DECLARE_FLAG
+
+/** Handy scope-level `using` to get the format flags. */
+#define PHOSPHOR_LOG2_USING_FLAGS                                              \
+    using lg2::bin;                                                            \
+    using lg2::dec;                                                            \
+    using lg2::field8;                                                         \
+    using lg2::field16;                                                        \
+    using lg2::field32;                                                        \
+    using lg2::field64;                                                        \
+    using lg2::floating;                                                       \
+    using lg2::hex;                                                            \
+    using lg2::signed_val;                                                     \
+    using lg2::str;                                                            \
+    using lg2::unsigned_val
+
+} // namespace lg2
diff --git a/lib/include/phosphor-logging/lg2/level.hpp b/lib/include/phosphor-logging/lg2/level.hpp
new file mode 100644
index 0000000..7d96c0c
--- /dev/null
+++ b/lib/include/phosphor-logging/lg2/level.hpp
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <syslog.h>
+
+namespace lg2
+{
+
+enum class level
+{
+    emergency = LOG_EMERG,
+    alert = LOG_ALERT,
+    critical = LOG_CRIT,
+    error = LOG_ERR,
+    warning = LOG_WARNING,
+    notice = LOG_NOTICE,
+    info = LOG_INFO,
+    debug = LOG_DEBUG,
+};
+
+}
diff --git a/lib/include/phosphor-logging/lg2/logger.hpp b/lib/include/phosphor-logging/lg2/logger.hpp
new file mode 100644
index 0000000..52ee9ed
--- /dev/null
+++ b/lib/include/phosphor-logging/lg2/logger.hpp
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <cstddef>
+#include <phosphor-logging/lg2/level.hpp>
+#include <source_location>
+#include <string_view>
+
+namespace lg2::details
+{
+
+/** Low-level function that actually performs the logging.
+ *
+ *  This is a variadic argument function (std::va_list) where the variadic
+ *  arguments are a set of 3: { header, flags, value }.
+ *
+ *  @param[in] level - The logging level to use.
+ *  @param[in] source_location - The original source location of the upper-level
+ *                               log call.
+ *  @param[in] string_view - The primary message to log.
+ *  @param[in] size_t - The number of sets in the variadic arguments.
+ */
+void do_log(level, const std::source_location&, const std::string_view&, size_t,
+            ...);
+
+} // namespace lg2::details
diff --git a/lib/include/phosphor-logging/meson.build b/lib/include/phosphor-logging/meson.build
index 490fad3..ebe52ac 100644
--- a/lib/include/phosphor-logging/meson.build
+++ b/lib/include/phosphor-logging/meson.build
@@ -16,12 +16,22 @@
 
 install_headers(
     'elog.hpp',
+    'lg2.hpp',
     'log.hpp',
     'sdjournal.hpp',
     subdir: 'phosphor-logging',
 )
 
 install_headers(
+    'lg2/concepts.hpp',
+    'lg2/conversion.hpp',
+    'lg2/flags.hpp',
+    'lg2/level.hpp',
+    'lg2/logger.hpp',
+    subdir: 'phosphor-logging/lg2',
+)
+
+install_headers(
     'test/sdjournal_mock.hpp',
     subdir: 'phosphor-logging/test',
 )
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
diff --git a/lib/meson.build b/lib/meson.build
index c2cf87f..1de91ec 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -13,6 +13,7 @@
 phosphor_logging_lib = library(
     'phosphor_logging',
     'elog.cpp',
+    'lg2_logger.cpp',
     'sdjournal.cpp',
     phosphor_logging_gen,
     implicit_include_directories: false,