monitor: add journal entries to fan PELs

Adds a new JSON section to the PEL  that contains the last 25 systemd
journal entries indicating which system services have
started/stopped/etc.

Signed-off-by: Mike Capps <mikepcapps@gmail.com>
Change-Id: I9f8a7ab8bb7c213cde30496327d83e9f3fab7c94
diff --git a/monitor/fan_error.cpp b/monitor/fan_error.cpp
index 15d8ed0..488bab4 100644
--- a/monitor/fan_error.cpp
+++ b/monitor/fan_error.cpp
@@ -1,5 +1,5 @@
 /**
- * Copyright © 2020 IBM Corporation
+ * Copyright © 2022 IBM Corporation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,6 +18,8 @@
 #include "logging.hpp"
 #include "sdbusplus.hpp"
 
+#include <systemd/sd-journal.h>
+
 #include <nlohmann/json.hpp>
 #include <xyz/openbmc_project/Logging/Create/server.hpp>
 
@@ -39,6 +41,33 @@
 namespace fs = std::filesystem;
 using namespace phosphor::fan::util;
 
+/**
+ * @class JournalCloser
+ *
+ * Automatically closes the journal when the object goes out of scope.
+ */
+class JournalCloser
+{
+  public:
+    // Specify which compiler-generated methods we want
+    JournalCloser() = delete;
+    JournalCloser(const JournalCloser&) = delete;
+    JournalCloser(JournalCloser&&) = delete;
+    JournalCloser& operator=(const JournalCloser&) = delete;
+    JournalCloser& operator=(JournalCloser&&) = delete;
+
+    JournalCloser(sd_journal* journal) : journal{journal}
+    {}
+
+    ~JournalCloser()
+    {
+        sd_journal_close(journal);
+    }
+
+  private:
+    sd_journal* journal{nullptr};
+};
+
 FFDCFile::FFDCFile(const fs::path& name) :
     _fd(open(name.c_str(), O_RDONLY)), _name(name)
 {
@@ -62,6 +91,13 @@
         ffdc.emplace_back(FFDCFormat::Text, 0x01, 0x01, logFile->fd());
     }
 
+    // add the previous systemd journal entries as FFDC
+    auto serviceFFDC = makeJsonFFDCFile(getJournalEntries(25));
+    if (serviceFFDC && serviceFFDC->fd() != -1)
+    {
+        ffdc.emplace_back(FFDCFormat::JSON, 0x01, 0x01, serviceFFDC->fd());
+    }
+
     // Add the passed in JSON as FFDC
     auto ffdcFile = makeJsonFFDCFile(jsonFFDC);
     if (ffdcFile && (ffdcFile->fd() != -1))
@@ -165,4 +201,140 @@
     return nullptr;
 }
 
+nlohmann::json FanError::getJournalEntries(int numLines) const
+{
+    // Sleep 100ms; otherwise recent journal entries sometimes not available
+    using namespace std::chrono_literals;
+    std::this_thread::sleep_for(100ms);
+
+    std::vector<std::string> entries;
+
+    // Open the journal
+    sd_journal* journal;
+    int rc = sd_journal_open(&journal, SD_JOURNAL_LOCAL_ONLY);
+    if (rc < 0)
+    {
+        // Build one line string containing field values
+        entries.push_back("[Internal error: sd_journal_open(), rc=" +
+                          std::string(strerror(rc)) + "]");
+        return json(entries);
+    }
+
+    // Create object to automatically close journal
+    JournalCloser closer{journal};
+
+    std::string field{"SYSLOG_IDENTIFIER"};
+    std::vector<std::string> executables{"systemd"};
+
+    entries.reserve(2 * numLines);
+
+    for (const auto& executable : executables)
+    {
+        // Add match so we only loop over entries with specified field value
+        std::string match{field + '=' + executable};
+        rc = sd_journal_add_match(journal, match.c_str(), 0);
+        if (rc < 0)
+        {
+            // Build one line string containing field values
+            entries.push_back("[Internal error: sd_journal_add_match(), rc=" +
+                              std::string(strerror(rc)) + "]");
+
+            break;
+        }
+
+        int count{1};
+
+        std::string syslogID, pid, message, timeStamp;
+
+        // Loop through journal entries from newest to oldest
+        SD_JOURNAL_FOREACH_BACKWARDS(journal)
+        {
+            // Get relevant journal entry fields
+            timeStamp = getTimeStamp(journal);
+            syslogID = getFieldValue(journal, "SYSLOG_IDENTIFIER");
+            pid = getFieldValue(journal, "_PID");
+            message = getFieldValue(journal, "MESSAGE");
+
+            // Build one line string containing field values
+            entries.push_back(timeStamp + " " + syslogID + "[" + pid +
+                              "]: " + message);
+
+            // Stop after number of lines was read
+            if (count++ >= numLines)
+            {
+                break;
+            }
+        }
+    }
+
+    // put the journal entries in chronological order
+    std::reverse(entries.begin(), entries.end());
+
+    return json(entries);
+}
+
+std::string FanError::getTimeStamp(sd_journal* journal) const
+{
+    // Get realtime (wallclock) timestamp of current journal entry.  The
+    // timestamp is in microseconds since the epoch.
+    uint64_t usec{0};
+    int rc = sd_journal_get_realtime_usec(journal, &usec);
+    if (rc < 0)
+    {
+        return "[Internal error: sd_journal_get_realtime_usec(), rc=" +
+               std::string(strerror(rc)) + "]";
+    }
+
+    // Convert to number of seconds since the epoch
+    time_t secs = usec / 1000000;
+
+    // Convert seconds to tm struct required by strftime()
+    struct tm* timeStruct = localtime(&secs);
+    if (timeStruct == nullptr)
+    {
+        return "[Internal error: localtime() returned nullptr]";
+    }
+
+    // Convert tm struct into a date/time string
+    char timeStamp[80];
+    strftime(timeStamp, sizeof(timeStamp), "%b %d %H:%M:%S", timeStruct);
+
+    return timeStamp;
+}
+
+std::string FanError::getFieldValue(sd_journal* journal,
+                                    const std::string& field) const
+{
+    std::string value{};
+
+    // Get field data from current journal entry
+    const void* data{nullptr};
+    size_t length{0};
+    int rc = sd_journal_get_data(journal, field.c_str(), &data, &length);
+    if (rc < 0)
+    {
+        if (-rc == ENOENT)
+        {
+            // Current entry does not include this field; return empty value
+            return value;
+        }
+        else
+        {
+            return "[Internal error: sd_journal_get_data() rc=" +
+                   std::string(strerror(rc)) + "]";
+        }
+    }
+
+    // Get value from field data.  Field data in format "FIELD=value".
+    std::string dataString{static_cast<const char*>(data), length};
+    std::string::size_type pos = dataString.find('=');
+    if ((pos != std::string::npos) && ((pos + 1) < dataString.size()))
+    {
+        // Value is substring after the '='
+        value = dataString.substr(pos + 1);
+    }
+
+    return value;
+}
+
 } // namespace phosphor::fan::monitor
diff --git a/monitor/fan_error.hpp b/monitor/fan_error.hpp
index 79573e1..1f36c2f 100644
--- a/monitor/fan_error.hpp
+++ b/monitor/fan_error.hpp
@@ -138,6 +138,35 @@
 
   private:
     /**
+     * @brief returns a JSON structure containing the previous N journal
+     * entries.
+     *
+     * @param[in] numLines - Number of lines of journal to retrieve
+     */
+    nlohmann::json getJournalEntries(int numLines) const;
+
+    /**
+     * Gets the realtime (wallclock) timestamp for the current journal entry.
+     *
+     * @param journal current journal entry
+     * @return timestamp as a date/time string
+     */
+    std::string getTimeStamp(sd_journal* journal) const;
+
+    /**
+     * Gets the value of the specified field for the current journal entry.
+     *
+     * Returns an empty string if the current journal entry does not have the
+     * specified field.
+     *
+     * @param journal current journal entry
+     * @param field journal field name
+     * @return field value
+     */
+    std::string getFieldValue(sd_journal* journal,
+                              const std::string& field) const;
+
+    /**
      * @brief Returns an FFDCFile holding the Logger contents
      *
      * @return std::unique_ptr<FFDCFile> - The file object