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,