pw_log: Add log proto encode helper for strings

Adds a Log message encode helper for string-based logging where no
information may be practically tokenized.

Change-Id: I68c1124abe87189b9dab3d53b8a3472f9d1aab90
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/77181
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Armando Montanez <amontanez@google.com>
diff --git a/pw_log/proto_utils.cc b/pw_log/proto_utils.cc
index bcd0fbb..ff8eec2 100644
--- a/pw_log/proto_utils.cc
+++ b/pw_log/proto_utils.cc
@@ -15,6 +15,7 @@
 #include "pw_log/proto_utils.h"
 
 #include <span>
+#include <string_view>
 
 #include "pw_bytes/endian.h"
 #include "pw_log/levels.h"
@@ -24,6 +25,40 @@
 
 namespace pw::log {
 
+Result<ConstByteSpan> EncodeLog(int level,
+                                unsigned int flags,
+                                std::string_view module_name,
+                                std::string_view file_name,
+                                int line_number,
+                                int64_t ticks_since_epoch,
+                                std::string_view message,
+                                ByteSpan encode_buffer) {
+  // Encode message to the LogEntry protobuf.
+  LogEntry::MemoryEncoder encoder(encode_buffer);
+
+  if (message.empty()) {
+    return Status::InvalidArgument();
+  }
+
+  // Defer status checks until the end.
+  Status status = encoder.WriteMessage(std::as_bytes(std::span(message)));
+  status = encoder.WriteLineLevel(PackLineLevel(line_number, level));
+  if (flags != 0) {
+    status = encoder.WriteFlags(flags);
+  }
+  status = encoder.WriteTimestamp(ticks_since_epoch);
+
+  // Module name and file name may or may not be present.
+  if (!module_name.empty()) {
+    status = encoder.WriteModule(std::as_bytes(std::span(module_name)));
+  }
+  if (!file_name.empty()) {
+    status = encoder.WriteFile(std::as_bytes(std::span(file_name)));
+  }
+  PW_TRY(encoder.status());
+  return ConstByteSpan(encoder);
+}
+
 Result<ConstByteSpan> EncodeTokenizedLog(pw::log_tokenized::Metadata metadata,
                                          ConstByteSpan tokenized_data,
                                          int64_t ticks_since_epoch,
@@ -34,8 +69,7 @@
   // Defer status checks until the end.
   Status status = encoder.WriteMessage(tokenized_data);
   status = encoder.WriteLineLevel(
-      (metadata.level() & PW_LOG_LEVEL_BITMASK) |
-      ((metadata.line_number() << PW_LOG_LEVEL_BITS) & ~PW_LOG_LEVEL_BITMASK));
+      PackLineLevel(metadata.line_number(), metadata.level()));
   if (metadata.flags() != 0) {
     status = encoder.WriteFlags(metadata.flags());
   }
diff --git a/pw_log/proto_utils_test.cc b/pw_log/proto_utils_test.cc
index 3793a5b..9e3c220 100644
--- a/pw_log/proto_utils_test.cc
+++ b/pw_log/proto_utils_test.cc
@@ -16,15 +16,16 @@
 
 #include "gtest/gtest.h"
 #include "pw_bytes/span.h"
+#include "pw_log/levels.h"
 #include "pw_protobuf/bytes_utils.h"
 #include "pw_protobuf/decoder.h"
 
 namespace pw::log {
 
-void VerifyLogEntry(pw::protobuf::Decoder& entry_decoder,
-                    pw::log_tokenized::Metadata expected_metadata,
-                    ConstByteSpan expected_tokenized_data,
-                    const int64_t expected_timestamp) {
+void VerifyTokenizedLogEntry(pw::protobuf::Decoder& entry_decoder,
+                             pw::log_tokenized::Metadata expected_metadata,
+                             ConstByteSpan expected_tokenized_data,
+                             const int64_t expected_timestamp) {
   ConstByteSpan tokenized_data;
   EXPECT_TRUE(entry_decoder.Next().ok());  // message [tokenized]
   EXPECT_EQ(1U, entry_decoder.FieldNumber());
@@ -37,9 +38,12 @@
   EXPECT_TRUE(entry_decoder.Next().ok());  // line_level
   EXPECT_EQ(2U, entry_decoder.FieldNumber());
   EXPECT_TRUE(entry_decoder.ReadUint32(&line_level).ok());
-  EXPECT_EQ(expected_metadata.level(), line_level & PW_LOG_LEVEL_BITMASK);
-  EXPECT_EQ(expected_metadata.line_number(),
-            (line_level & ~PW_LOG_LEVEL_BITMASK) >> PW_LOG_LEVEL_BITS);
+
+  uint32_t line_number;
+  uint8_t level;
+  std::tie(line_number, level) = UnpackLineLevel(line_level);
+  EXPECT_EQ(expected_metadata.level(), level);
+  EXPECT_EQ(expected_metadata.line_number(), line_number);
 
   if (expected_metadata.flags() != 0) {
     uint32_t flags;
@@ -65,6 +69,108 @@
   }
 }
 
+void VerifyLogEntry(pw::protobuf::Decoder& entry_decoder,
+                    int expected_level,
+                    unsigned int expected_flags,
+                    std::string_view expected_module,
+                    std::string_view expected_file_name,
+                    int expected_line_number,
+                    int64_t expected_ticks_since_epoch,
+                    std::string_view expected_message) {
+  std::string_view message;
+  EXPECT_TRUE(entry_decoder.Next().ok());  // message
+  EXPECT_EQ(1U, entry_decoder.FieldNumber());
+  EXPECT_TRUE(entry_decoder.ReadString(&message).ok());
+  EXPECT_TRUE(std::equal(message.begin(),
+                         message.end(),
+                         expected_message.begin(),
+                         expected_message.end()));
+
+  uint32_t line_level;
+  EXPECT_TRUE(entry_decoder.Next().ok());  // line_level
+  EXPECT_EQ(2U, entry_decoder.FieldNumber());
+  EXPECT_TRUE(entry_decoder.ReadUint32(&line_level).ok());
+  uint32_t line_number;
+  uint8_t level;
+  std::tie(line_number, level) = UnpackLineLevel(line_level);
+  EXPECT_EQ(static_cast<unsigned int>(expected_line_number), line_number);
+  EXPECT_EQ(expected_level, level);
+
+  if (expected_flags != 0) {
+    uint32_t flags;
+    EXPECT_TRUE(entry_decoder.Next().ok());  // flags
+    EXPECT_EQ(3U, entry_decoder.FieldNumber());
+    EXPECT_TRUE(entry_decoder.ReadUint32(&flags).ok());
+    EXPECT_EQ(expected_flags, flags);
+  }
+
+  int64_t timestamp;
+  EXPECT_TRUE(entry_decoder.Next().ok());  // timestamp
+  EXPECT_EQ(4U, entry_decoder.FieldNumber());
+  EXPECT_TRUE(entry_decoder.ReadInt64(&timestamp).ok());
+  EXPECT_EQ(expected_ticks_since_epoch, timestamp);
+
+  if (!expected_module.empty()) {
+    std::string_view module_name;
+    EXPECT_TRUE(entry_decoder.Next().ok());  // module
+    EXPECT_EQ(7U, entry_decoder.FieldNumber());
+    EXPECT_TRUE(entry_decoder.ReadString(&module_name).ok());
+    EXPECT_TRUE(std::equal(module_name.begin(),
+                           module_name.end(),
+                           expected_module.begin(),
+                           expected_module.end()));
+  }
+
+  if (!expected_file_name.empty()) {
+    std::string_view file_name;
+    EXPECT_TRUE(entry_decoder.Next().ok());  // file
+    EXPECT_EQ(8U, entry_decoder.FieldNumber());
+    EXPECT_TRUE(entry_decoder.ReadString(&file_name).ok());
+    EXPECT_TRUE(std::equal(file_name.begin(),
+                           file_name.end(),
+                           expected_file_name.begin(),
+                           expected_file_name.end()));
+  }
+}
+
+TEST(UtilsTest, LineLevelPacking) {
+  constexpr uint8_t kExpectedLevel = PW_LOG_LEVEL_ERROR;
+  constexpr uint32_t kExpectedLine = 1234567;
+  constexpr uint32_t kExpectedLineLevel =
+      (kExpectedLine << PW_LOG_LEVEL_BITS) |
+      (kExpectedLevel & PW_LOG_LEVEL_BITMASK);
+
+  EXPECT_EQ(kExpectedLineLevel, PackLineLevel(kExpectedLine, kExpectedLevel));
+}
+
+TEST(UtilsTest, LineLevelUnpacking) {
+  constexpr uint8_t kExpectedLevel = PW_LOG_LEVEL_ERROR;
+  constexpr uint32_t kExpectedLine = 1234567;
+  constexpr uint32_t kExpectedLineLevel =
+      (kExpectedLine << PW_LOG_LEVEL_BITS) |
+      (kExpectedLevel & PW_LOG_LEVEL_BITMASK);
+
+  uint32_t line_number;
+  uint8_t level;
+  std::tie(line_number, level) = UnpackLineLevel(kExpectedLineLevel);
+
+  EXPECT_EQ(kExpectedLine, line_number);
+  EXPECT_EQ(kExpectedLevel, level);
+}
+
+TEST(UtilsTest, LineLevelPackAndUnpack) {
+  constexpr uint8_t kExpectedLevel = PW_LOG_LEVEL_ERROR;
+  constexpr uint32_t kExpectedLine = 1234567;
+
+  uint32_t line_number;
+  uint8_t level;
+  std::tie(line_number, level) =
+      UnpackLineLevel(PackLineLevel(kExpectedLine, kExpectedLevel));
+
+  EXPECT_EQ(kExpectedLine, line_number);
+  EXPECT_EQ(kExpectedLevel, level);
+}
+
 TEST(UtilsTest, EncodeTokenizedLog) {
   constexpr std::byte kTokenizedData[1] = {(std::byte)0x01};
   constexpr int64_t kExpectedTimestamp = 1;
@@ -78,7 +184,8 @@
   EXPECT_TRUE(result.ok());
 
   pw::protobuf::Decoder log_decoder(result.value());
-  VerifyLogEntry(log_decoder, metadata, kTokenizedData, kExpectedTimestamp);
+  VerifyTokenizedLogEntry(
+      log_decoder, metadata, kTokenizedData, kExpectedTimestamp);
 
   result = EncodeTokenizedLog(metadata,
                               reinterpret_cast<const uint8_t*>(kTokenizedData),
@@ -88,7 +195,8 @@
   EXPECT_TRUE(result.ok());
 
   log_decoder.Reset(result.value());
-  VerifyLogEntry(log_decoder, metadata, kTokenizedData, kExpectedTimestamp);
+  VerifyTokenizedLogEntry(
+      log_decoder, metadata, kTokenizedData, kExpectedTimestamp);
 }
 
 TEST(UtilsTest, EncodeTokenizedLog_EmptyFlags) {
@@ -105,7 +213,8 @@
   EXPECT_TRUE(result.ok());
 
   pw::protobuf::Decoder log_decoder(result.value());
-  VerifyLogEntry(log_decoder, metadata, kTokenizedData, kExpectedTimestamp);
+  VerifyTokenizedLogEntry(
+      log_decoder, metadata, kTokenizedData, kExpectedTimestamp);
 }
 
 TEST(UtilsTest, EncodeTokenizedLog_InsufficientSpace) {
@@ -121,4 +230,172 @@
   EXPECT_TRUE(result.status().IsResourceExhausted());
 }
 
+TEST(UtilsTest, EncodeLog) {
+  constexpr int kExpectedLevel = PW_LOG_LEVEL_INFO;
+  constexpr unsigned int kExpectedFlags = 2;
+  constexpr std::string_view kExpectedModule("TST");
+  constexpr std::string_view kExpectedFile("proto_test.cc");
+  constexpr int kExpectedLine = 14;
+  constexpr int64_t kExpectedTimestamp = 1;
+  constexpr std::string_view kExpectedMessage("msg");
+  std::byte encode_buffer[64];
+
+  Result<ConstByteSpan> result = EncodeLog(kExpectedLevel,
+                                           kExpectedFlags,
+                                           kExpectedModule,
+                                           kExpectedFile,
+                                           kExpectedLine,
+                                           kExpectedTimestamp,
+                                           kExpectedMessage,
+                                           encode_buffer);
+  EXPECT_TRUE(result.ok());
+
+  pw::protobuf::Decoder log_decoder(result.value());
+  VerifyLogEntry(log_decoder,
+                 kExpectedLevel,
+                 kExpectedFlags,
+                 kExpectedModule,
+                 kExpectedFile,
+                 kExpectedLine,
+                 kExpectedTimestamp,
+                 kExpectedMessage);
+}
+
+TEST(UtilsTest, EncodeLog_EmptyFlags) {
+  constexpr int kExpectedLevel = PW_LOG_LEVEL_INFO;
+  constexpr unsigned int kExpectedFlags = 0;
+  constexpr std::string_view kExpectedModule("TST");
+  constexpr std::string_view kExpectedFile("proto_test.cc");
+  constexpr int kExpectedLine = 14;
+  constexpr int64_t kExpectedTimestamp = 1;
+  constexpr std::string_view kExpectedMessage("msg");
+  std::byte encode_buffer[64];
+
+  Result<ConstByteSpan> result = EncodeLog(kExpectedLevel,
+                                           kExpectedFlags,
+                                           kExpectedModule,
+                                           kExpectedFile,
+                                           kExpectedLine,
+                                           kExpectedTimestamp,
+                                           kExpectedMessage,
+                                           encode_buffer);
+  EXPECT_TRUE(result.ok());
+
+  pw::protobuf::Decoder log_decoder(result.value());
+  VerifyLogEntry(log_decoder,
+                 kExpectedLevel,
+                 kExpectedFlags,
+                 kExpectedModule,
+                 kExpectedFile,
+                 kExpectedLine,
+                 kExpectedTimestamp,
+                 kExpectedMessage);
+}
+
+TEST(UtilsTest, EncodeLog_EmptyFile) {
+  constexpr int kExpectedLevel = PW_LOG_LEVEL_INFO;
+  constexpr unsigned int kExpectedFlags = 0;
+  constexpr std::string_view kExpectedModule("TST");
+  constexpr std::string_view kExpectedFile;
+  constexpr int kExpectedLine = 14;
+  constexpr int64_t kExpectedTimestamp = 1;
+  constexpr std::string_view kExpectedMessage("msg");
+  std::byte encode_buffer[64];
+
+  Result<ConstByteSpan> result = EncodeLog(kExpectedLevel,
+                                           kExpectedFlags,
+                                           kExpectedModule,
+                                           kExpectedFile,
+                                           kExpectedLine,
+                                           kExpectedTimestamp,
+                                           kExpectedMessage,
+                                           encode_buffer);
+  EXPECT_TRUE(result.ok());
+
+  pw::protobuf::Decoder log_decoder(result.value());
+  VerifyLogEntry(log_decoder,
+                 kExpectedLevel,
+                 kExpectedFlags,
+                 kExpectedModule,
+                 kExpectedFile,
+                 kExpectedLine,
+                 kExpectedTimestamp,
+                 kExpectedMessage);
+}
+
+TEST(UtilsTest, EncodeLog_EmptyModule) {
+  constexpr int kExpectedLevel = PW_LOG_LEVEL_INFO;
+  constexpr unsigned int kExpectedFlags = 3;
+  constexpr std::string_view kExpectedModule;
+  constexpr std::string_view kExpectedFile("test.cc");
+  constexpr int kExpectedLine = 14;
+  constexpr int64_t kExpectedTimestamp = 1;
+  constexpr std::string_view kExpectedMessage("msg");
+  std::byte encode_buffer[64];
+
+  Result<ConstByteSpan> result = EncodeLog(kExpectedLevel,
+                                           kExpectedFlags,
+                                           kExpectedModule,
+                                           kExpectedFile,
+                                           kExpectedLine,
+                                           kExpectedTimestamp,
+                                           kExpectedMessage,
+                                           encode_buffer);
+  EXPECT_TRUE(result.ok());
+
+  pw::protobuf::Decoder log_decoder(result.value());
+  VerifyLogEntry(log_decoder,
+                 kExpectedLevel,
+                 kExpectedFlags,
+                 kExpectedModule,
+                 kExpectedFile,
+                 kExpectedLine,
+                 kExpectedTimestamp,
+                 kExpectedMessage);
+}
+
+TEST(UtilsTest, EncodeLog_EmptyMessage) {
+  constexpr int kExpectedLevel = PW_LOG_LEVEL_INFO;
+  constexpr unsigned int kExpectedFlags = 0;
+  constexpr std::string_view kExpectedModule;
+  constexpr std::string_view kExpectedFile;
+  constexpr int kExpectedLine = 14;
+  constexpr int64_t kExpectedTimestamp = 1;
+  constexpr std::string_view kExpectedMessage;
+  std::byte encode_buffer[64];
+
+  Result<ConstByteSpan> result = EncodeLog(kExpectedLevel,
+                                           kExpectedFlags,
+                                           kExpectedModule,
+                                           kExpectedFile,
+                                           kExpectedLine,
+                                           kExpectedTimestamp,
+                                           kExpectedMessage,
+                                           encode_buffer);
+
+  EXPECT_TRUE(result.status().IsInvalidArgument());
+}
+
+TEST(UtilsTest, EncodeLog_InsufficientSpace) {
+  constexpr int kExpectedLevel = PW_LOG_LEVEL_INFO;
+  constexpr unsigned int kExpectedFlags = 0;
+  constexpr std::string_view kExpectedModule;
+  constexpr std::string_view kExpectedFile;
+  constexpr int kExpectedLine = 14;
+  constexpr int64_t kExpectedTimestamp = 1;
+  constexpr std::string_view kExpectedMessage("msg");
+  std::byte encode_buffer[1];
+
+  Result<ConstByteSpan> result = EncodeLog(kExpectedLevel,
+                                           kExpectedFlags,
+                                           kExpectedModule,
+                                           kExpectedFile,
+                                           kExpectedLine,
+                                           kExpectedTimestamp,
+                                           kExpectedMessage,
+                                           encode_buffer);
+
+  EXPECT_TRUE(result.status().IsResourceExhausted());
+}
+
 }  // namespace pw::log
diff --git a/pw_log/protobuf.rst b/pw_log/protobuf.rst
index 12c1f62..52f630b 100644
--- a/pw_log/protobuf.rst
+++ b/pw_log/protobuf.rst
@@ -51,11 +51,43 @@
 
 See :ref:`module-pw_tokenizer-proto` for details.
 
-Utility functions
------------------
-Conversion into the ``log.proto`` format from a tokenized log message can be
-performed using the ``pw_log/proto_utils.h`` headers. Given tokenized data and
-a payload, the headers provide a quick way to encode to the LogEntry protobuf.
+Packing and unpacking line_level
+--------------------------------
+As a way to minimize on-the-wire log message size, the log level and the line
+number of a given log statement are packed into a single proto field. There are
+helpers in ``pw_log/proto_utils.h`` for properly packing and unpacking this
+field.
+
+.. code-block:: cpp
+
+   #include "pw_bytes/span.h"
+   #include "pw_log/levels.h"
+   #include "pw_log/proto_utils.h"
+   #include "pw_protobuf/decoder.h"
+
+  bool FilterLog(pw::ConstByteSpan serialized_log) {
+    pw::protobuf::Decoder log_decoder(serialized_log);
+    while (log_decoder.Next().ok()) {
+      if (log_decoder.FieldNumber() == 2) {
+        uint32_t line_and_level;
+        entry_decoder.ReadUint32(&line_and_level);
+        PW_DCHECK(entry_decoder.ok());
+
+        uint8_t level = std::get<1>(pw::log::UnpackLineLevel(line_and_level));
+        if (level < PW_LOG_LEVEL_INFO) {
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+
+Log encoding helpers
+--------------------
+Encoding logs to the ``log.proto`` format can be performed using the helpers
+provided in the ``pw_log/proto_utils.h`` header. Separate helpers are provided
+for encoding tokenized logs and string-based logs.
 
 .. code-block:: cpp
 
@@ -76,4 +108,3 @@
        EmitProtoLogEntry(result.value());
      }
    }
-
diff --git a/pw_log/public/pw_log/proto_utils.h b/pw_log/public/pw_log/proto_utils.h
index fb1b6c3..93a9bdd 100644
--- a/pw_log/public/pw_log/proto_utils.h
+++ b/pw_log/public/pw_log/proto_utils.h
@@ -13,19 +13,59 @@
 // the License.
 #pragma once
 
+#include <string_view>
+
 #include "pw_bytes/span.h"
+#include "pw_log/levels.h"
 #include "pw_log_tokenized/metadata.h"
 #include "pw_result/result.h"
 
 namespace pw::log {
 
+// Packs line number and log level into a single uint32_t as dictated by the
+// line_level field in the Log proto message.
+//
+// Note:
+//   line_number is restricted to 29 bits. Values beyond 536870911 will be lost.
+//   level is restricted to 3 bits. Values beyond 7 will be lost.
+constexpr inline uint32_t PackLineLevel(uint32_t line_number, uint8_t level) {
+  return (level & PW_LOG_LEVEL_BITMASK) |
+         ((line_number << PW_LOG_LEVEL_BITS) & ~PW_LOG_LEVEL_BITMASK);
+}
+
+// Unpacks the line_level field as dictated by the Log proto message into line
+// number (uint32_t) and level (uint8_t).
+constexpr inline std::tuple<uint32_t, uint8_t> UnpackLineLevel(
+    uint32_t line_and_level) {
+  return std::make_tuple(
+      (line_and_level & ~PW_LOG_LEVEL_BITMASK) >> PW_LOG_LEVEL_BITS,
+      line_and_level & PW_LOG_LEVEL_BITMASK);
+}
+
+// Convenience functions to encode multiple log attributes as a log proto
+// message.
+//
+// Returns:
+// OK - A byte span containing the encoded log proto.
+// INVALID_ARGUMENT - `message` argument is zero-length.
+// RESOURCE_EXHAUSTED - The provided buffer was not large enough to encode the
+//   proto.
+Result<ConstByteSpan> EncodeLog(int level,
+                                unsigned int flags,
+                                std::string_view module_name,
+                                std::string_view file_name,
+                                int line_number,
+                                int64_t ticks_since_epoch,
+                                std::string_view message,
+                                ByteSpan encode_buffer);
+
 // Convenience functions to convert from tokenized metadata to the log proto
 // format.
 //
-// Return values:
-// Ok - A byte span containing the encoded log proto.
-// ResourceExhausted - The provided buffer was not large enough to store the
-// proto.
+// Returns:
+// OK - A byte span containing the encoded log proto.
+// RESOURCE_EXHAUSTED - The provided buffer was not large enough to store the
+//   proto.
 Result<ConstByteSpan> EncodeTokenizedLog(log_tokenized::Metadata metadata,
                                          ConstByteSpan tokenized_data,
                                          int64_t ticks_since_epoch,