PEL: Capture the journal in UserData sections

If a PEL message registry entry has a 'JournalCapture' section, capture
the listed portions of the journal in UserData sections for that error.

If the JSON looks like:

"JournalCapture": {
    "NumLines": 30
}

Then the code will capture the previous 30 lines from the journal into a
single UserData section.

If the JSON looks like:

"JournalCapture":
{
    "Sections": [
        {
            "SyslogID": "phosphor-bmc-state-manager",
            "NumLines": 20
        },
        {
            "SyslogID": "phosphor-log-manager",
            "NumLines": 15
        }
    ]
}

Then the code will create two UserData sections, the first with the most
recent 20 lines from phosphor-bmc-state-manager, and the second with 15
lines from phosphor-log-manager.

If a section would cause the PEL to exceed its maximum size of 16KB, it
will be dropped.  While the UserData class does have a shrink() method,
it prunes data from the end, which would cause the most recent journal
entries to be removed, which could be misleading.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I2ecbd8002b0e7087eb166a1219c6ab9da14a122a
diff --git a/extensions/openpower-pels/pel.cpp b/extensions/openpower-pels/pel.cpp
index f74eaf3..9fa1747 100644
--- a/extensions/openpower-pels/pel.cpp
+++ b/extensions/openpower-pels/pel.cpp
@@ -56,7 +56,7 @@
 PEL::PEL(const message::Entry& regEntry, uint32_t obmcLogID, uint64_t timestamp,
          phosphor::logging::Entry::Level severity,
          const AdditionalData& additionalData, const PelFFDC& ffdcFilesIn,
-         const DataInterfaceBase& dataIface)
+         const DataInterfaceBase& dataIface, const JournalBase& journal)
 {
     // No changes in input, for non SBE error related requests
     PelFFDC ffdcFiles = ffdcFilesIn;
@@ -195,6 +195,8 @@
         }
     }
 
+    addJournalSections(regEntry, journal);
+
     _ph->setSectionCount(2 + _optionalSections.size());
 
     checkRulesAndFix();
@@ -596,6 +598,111 @@
     }
 }
 
+void PEL::addJournalSections(const message::Entry& regEntry,
+                             const JournalBase& journal)
+{
+    if (!regEntry.journalCapture)
+    {
+        return;
+    }
+
+    // Write all unwritten journal data to disk.
+    journal.sync();
+
+    const auto& jc = regEntry.journalCapture.value();
+    std::vector<std::vector<std::string>> allMessages;
+
+    if (std::holds_alternative<size_t>(jc))
+    {
+        // Get the previous numLines journal entries
+        const auto& numLines = std::get<size_t>(jc);
+        try
+        {
+            auto messages = journal.getMessages("", numLines);
+            if (!messages.empty())
+            {
+                allMessages.push_back(std::move(messages));
+            }
+        }
+        catch (const std::exception& e)
+        {
+            log<level::ERR>(
+                fmt::format("Failed during journal collection: {}", e.what())
+                    .c_str());
+        }
+    }
+    else if (std::holds_alternative<message::AppCaptureList>(jc))
+    {
+        // Get journal entries based on the syslog id field.
+        const auto& sections = std::get<message::AppCaptureList>(jc);
+        for (const auto& [syslogID, numLines] : sections)
+        {
+            try
+            {
+                auto messages = journal.getMessages(syslogID, numLines);
+                if (!messages.empty())
+                {
+                    allMessages.push_back(std::move(messages));
+                }
+            }
+            catch (const std::exception& e)
+            {
+                log<level::ERR>(
+                    fmt::format("Failed during journal collection: {}",
+                                e.what())
+                        .c_str());
+            }
+        }
+    }
+
+    // Create the UserData sections
+    for (const auto& messages : allMessages)
+    {
+        auto buffer = util::flattenLines(messages);
+
+        // If the buffer is way too big, it can overflow the uint16_t
+        // PEL section size field that is checked below so do a cursory
+        // check here.
+        if (buffer.size() > _maxPELSize)
+        {
+            log<level::WARNING>(
+                "Journal UserData section does not fit in PEL, dropping");
+            log<level::WARNING>(fmt::format("PEL size = {}, data size = {}",
+                                            size(), buffer.size())
+                                    .c_str());
+            continue;
+        }
+
+        // Sections must be 4 byte aligned.
+        while (buffer.size() % 4 != 0)
+        {
+            buffer.push_back(0);
+        }
+
+        auto ud = std::make_unique<UserData>(
+            static_cast<uint16_t>(ComponentID::phosphorLogging),
+            static_cast<uint8_t>(UserDataFormat::text),
+            static_cast<uint8_t>(UserDataFormatVersion::text), buffer);
+
+        if (size() + ud->header().size <= _maxPELSize)
+        {
+            _optionalSections.push_back(std::move(ud));
+        }
+        else
+        {
+            // Don't attempt to shrink here since we'd be dropping the
+            // most recent journal entries which would be confusing.
+            log<level::WARNING>(
+                "Journal UserData section does not fit in PEL, dropping");
+            log<level::WARNING>(fmt::format("PEL size = {}, UserData size = {}",
+                                            size(), ud->header().size)
+                                    .c_str());
+            ud.reset();
+            continue;
+        }
+    }
+}
+
 namespace util
 {
 
@@ -852,6 +959,23 @@
     return std::make_unique<UserData>(compID, subType, version, data);
 }
 
+std::vector<uint8_t> flattenLines(const std::vector<std::string>& lines)
+{
+    std::vector<uint8_t> out;
+
+    for (const auto& line : lines)
+    {
+        out.insert(out.end(), line.begin(), line.end());
+
+        if (out.back() != '\n')
+        {
+            out.push_back('\n');
+        }
+    }
+
+    return out;
+}
+
 } // namespace util
 
 } // namespace pels