Revert "Revert "EventService: Add event log support with inotify""

This reverts commit 29d2a95ba12f8b5abed040df7fd59790d6ba2517.
Enable EventService back by fixing issue with
not having '/var/log/redfish' file.

Fix is at: https://gerrit.openbmc-project.xyz/#/c/openbmc/bmcweb/+/33639/

Tested:
 - Along with above mentioned change, removed
   '/var/log/redfish' file and restarted bmcweb. It works.

Change-Id: Ia908bbdf5b9a643afee212a526074f62372208dc
Signed-off-by: AppaRao Puli <apparao.puli@linux.intel.com>
diff --git a/redfish-core/include/event_service_manager.hpp b/redfish-core/include/event_service_manager.hpp
index fb0cfbc..c2d6d38 100644
--- a/redfish-core/include/event_service_manager.hpp
+++ b/redfish-core/include/event_service_manager.hpp
@@ -15,6 +15,11 @@
 */
 #pragma once
 #include "node.hpp"
+#include "registries.hpp"
+#include "registries/base_message_registry.hpp"
+#include "registries/openbmc_message_registry.hpp"
+
+#include <sys/inotify.h>
 
 #include <boost/asio/io_context.hpp>
 #include <boost/container/flat_map.hpp>
@@ -41,6 +46,224 @@
 static constexpr const char* eventServiceFile =
     "/var/lib/bmcweb/eventservice_config.json";
 
+#ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES
+constexpr const char* redfishEventLogFile = "/var/log/redfish";
+constexpr const uint32_t inotifyFileAction = IN_MODIFY;
+std::shared_ptr<boost::asio::posix::stream_descriptor> inotifyConn = nullptr;
+
+// <ID, timestamp, RedfishLogId, registryPrefix, MessageId, MessageArgs>
+using EventLogObjectsType =
+    std::tuple<std::string, std::string, std::string, std::string, std::string,
+               boost::beast::span<std::string>>;
+
+namespace message_registries
+{
+static const Message*
+    getMsgFromRegistry(const std::string& messageKey,
+                       const boost::beast::span<const MessageEntry>& registry)
+{
+    boost::beast::span<const MessageEntry>::const_iterator messageIt =
+        std::find_if(registry.cbegin(), registry.cend(),
+                     [&messageKey](const MessageEntry& messageEntry) {
+                         return !messageKey.compare(messageEntry.first);
+                     });
+    if (messageIt != registry.cend())
+    {
+        return &messageIt->second;
+    }
+
+    return nullptr;
+}
+
+static const Message* formatMessage(const std::string_view& messageID)
+{
+    // Redfish MessageIds are in the form
+    // RegistryName.MajorVersion.MinorVersion.MessageKey, so parse it to find
+    // the right Message
+    std::vector<std::string> fields;
+    fields.reserve(4);
+    boost::split(fields, messageID, boost::is_any_of("."));
+    if (fields.size() != 4)
+    {
+        return nullptr;
+    }
+    std::string& registryName = fields[0];
+    std::string& messageKey = fields[3];
+
+    // Find the right registry and check it for the MessageKey
+    if (std::string(base::header.registryPrefix) == registryName)
+    {
+        return getMsgFromRegistry(
+            messageKey, boost::beast::span<const MessageEntry>(base::registry));
+    }
+    if (std::string(openbmc::header.registryPrefix) == registryName)
+    {
+        return getMsgFromRegistry(
+            messageKey,
+            boost::beast::span<const MessageEntry>(openbmc::registry));
+    }
+    return nullptr;
+}
+} // namespace message_registries
+
+namespace event_log
+{
+bool getUniqueEntryID(const std::string& logEntry, std::string& entryID,
+                      const bool firstEntry = true)
+{
+    static time_t prevTs = 0;
+    static int index = 0;
+    if (firstEntry)
+    {
+        prevTs = 0;
+    }
+
+    // Get the entry timestamp
+    std::time_t curTs = 0;
+    std::tm timeStruct = {};
+    std::istringstream entryStream(logEntry);
+    if (entryStream >> std::get_time(&timeStruct, "%Y-%m-%dT%H:%M:%S"))
+    {
+        curTs = std::mktime(&timeStruct);
+        if (curTs == -1)
+        {
+            return false;
+        }
+    }
+    // If the timestamp isn't unique, increment the index
+    index = (curTs == prevTs) ? index + 1 : 0;
+
+    // Save the timestamp
+    prevTs = curTs;
+
+    entryID = std::to_string(curTs);
+    if (index > 0)
+    {
+        entryID += "_" + std::to_string(index);
+    }
+    return true;
+}
+
+int getEventLogParams(const std::string& logEntry, std::string& timestamp,
+                      std::string& messageID,
+                      boost::beast::span<std::string>& messageArgs)
+{
+    // The redfish log format is "<Timestamp> <MessageId>,<MessageArgs>"
+    // First get the Timestamp
+    size_t space = logEntry.find_first_of(" ");
+    if (space == std::string::npos)
+    {
+        return -EINVAL;
+    }
+    timestamp = logEntry.substr(0, space);
+    // Then get the log contents
+    size_t entryStart = logEntry.find_first_not_of(" ", space);
+    if (entryStart == std::string::npos)
+    {
+        return -EINVAL;
+    }
+    std::string_view entry(logEntry);
+    entry.remove_prefix(entryStart);
+    // Use split to separate the entry into its fields
+    std::vector<std::string> logEntryFields;
+    boost::split(logEntryFields, entry, boost::is_any_of(","),
+                 boost::token_compress_on);
+    // We need at least a MessageId to be valid
+    if (logEntryFields.size() < 1)
+    {
+        return -EINVAL;
+    }
+    messageID = logEntryFields[0];
+
+    // Get the MessageArgs from the log if there are any
+    if (logEntryFields.size() > 1)
+    {
+        std::string& messageArgsStart = logEntryFields[1];
+        // If the first string is empty, assume there are no MessageArgs
+        std::size_t messageArgsSize = 0;
+        if (!messageArgsStart.empty())
+        {
+            messageArgsSize = logEntryFields.size() - 1;
+        }
+
+        messageArgs = boost::beast::span(&messageArgsStart, messageArgsSize);
+    }
+
+    return 0;
+}
+
+void getRegistryAndMessageKey(const std::string& messageID,
+                              std::string& registryName,
+                              std::string& messageKey)
+{
+    // Redfish MessageIds are in the form
+    // RegistryName.MajorVersion.MinorVersion.MessageKey, so parse it to find
+    // the right Message
+    std::vector<std::string> fields;
+    fields.reserve(4);
+    boost::split(fields, messageID, boost::is_any_of("."));
+    if (fields.size() == 4)
+    {
+        registryName = fields[0];
+        messageKey = fields[3];
+    }
+}
+
+int formatEventLogEntry(const std::string& logEntryID,
+                        const std::string& messageID,
+                        const boost::beast::span<std::string>& messageArgs,
+                        std::string timestamp, const std::string customText,
+                        nlohmann::json& logEntryJson)
+{
+    // Get the Message from the MessageRegistry
+    const message_registries::Message* message =
+        message_registries::formatMessage(messageID);
+
+    std::string msg;
+    std::string severity;
+    if (message != nullptr)
+    {
+        msg = message->message;
+        severity = message->severity;
+    }
+
+    // Fill the MessageArgs into the Message
+    int i = 0;
+    for (const std::string& messageArg : messageArgs)
+    {
+        std::string argStr = "%" + std::to_string(++i);
+        size_t argPos = msg.find(argStr);
+        if (argPos != std::string::npos)
+        {
+            msg.replace(argPos, argStr.length(), messageArg);
+        }
+    }
+
+    // Get the Created time from the timestamp. The log timestamp is in
+    // RFC3339 format which matches the Redfish format except for the
+    // fractional seconds between the '.' and the '+', so just remove them.
+    std::size_t dot = timestamp.find_first_of(".");
+    std::size_t plus = timestamp.find_first_of("+");
+    if (dot != std::string::npos && plus != std::string::npos)
+    {
+        timestamp.erase(dot, plus - dot);
+    }
+
+    // Fill in the log entry with the gathered data
+    logEntryJson = {{"EventId", logEntryID},
+                    {"EventType", "Event"},
+                    {"Severity", std::move(severity)},
+                    {"Message", std::move(msg)},
+                    {"MessageId", std::move(messageID)},
+                    {"MessageArgs", std::move(messageArgs)},
+                    {"EventTimestamp", std::move(timestamp)},
+                    {"Context", customText}};
+    return 0;
+}
+
+} // namespace event_log
+#endif
+
 class Subscription
 {
   public:
@@ -112,6 +335,72 @@
         this->eventSeqNum++;
     }
 
+#ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES
+    void filterAndSendEventLogs(
+        const std::vector<EventLogObjectsType>& eventRecords)
+    {
+        nlohmann::json logEntryArray;
+        for (const EventLogObjectsType& logEntry : eventRecords)
+        {
+            const std::string& idStr = std::get<0>(logEntry);
+            const std::string& timestamp = std::get<1>(logEntry);
+            const std::string& messageID = std::get<2>(logEntry);
+            const std::string& registryName = std::get<3>(logEntry);
+            const std::string& messageKey = std::get<4>(logEntry);
+            const boost::beast::span<std::string>& messageArgs =
+                std::get<5>(logEntry);
+
+            // If registryPrefixes list is empty, don't filter events
+            // send everything.
+            if (registryPrefixes.size())
+            {
+                auto obj = std::find(registryPrefixes.begin(),
+                                     registryPrefixes.end(), registryName);
+                if (obj == registryPrefixes.end())
+                {
+                    continue;
+                }
+            }
+
+            // If registryMsgIds list is empty, don't filter events
+            // send everything.
+            if (registryMsgIds.size())
+            {
+                auto obj = std::find(registryMsgIds.begin(),
+                                     registryMsgIds.end(), messageKey);
+                if (obj == registryMsgIds.end())
+                {
+                    continue;
+                }
+            }
+
+            logEntryArray.push_back({});
+            nlohmann::json& bmcLogEntry = logEntryArray.back();
+            if (event_log::formatEventLogEntry(idStr, messageID, messageArgs,
+                                               timestamp, customText,
+                                               bmcLogEntry) != 0)
+            {
+                BMCWEB_LOG_DEBUG << "Read eventLog entry failed";
+                continue;
+            }
+        }
+
+        if (logEntryArray.size() < 1)
+        {
+            BMCWEB_LOG_DEBUG << "No log entries available to be transferred.";
+            return;
+        }
+
+        nlohmann::json msg = {{"@odata.type", "#Event.v1_4_0.Event"},
+                              {"Id", std::to_string(eventSeqNum)},
+                              {"Name", "Event Log"},
+                              {"Events", logEntryArray}};
+
+        this->sendEvent(msg.dump());
+        this->eventSeqNum++;
+    }
+#endif
+
     void filterAndSendReports(const std::string& id,
                               const std::string& readingsTs,
                               const ReadingsObjType& readings)
@@ -189,6 +478,7 @@
         initConfig();
     }
 
+    std::string lastEventTStr;
     size_t noOfEventLogSubscribers;
     size_t noOfMetricReportSubscribers;
     std::shared_ptr<sdbusplus::bus::match::match> matchTelemetryMonitor;
@@ -493,6 +783,13 @@
         {
             updateSubscriptionData();
         }
+
+#ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES
+        if (lastEventTStr.empty())
+        {
+            cacheLastEventTimestamp();
+        }
+#endif
         return id;
     }
 
@@ -555,6 +852,165 @@
         }
     }
 
+#ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES
+    void cacheLastEventTimestamp()
+    {
+        std::ifstream logStream(redfishEventLogFile);
+        if (!logStream.good())
+        {
+            BMCWEB_LOG_ERROR << " Redfish log file open failed \n";
+            return;
+        }
+        std::string logEntry;
+        while (std::getline(logStream, logEntry))
+        {
+            size_t space = logEntry.find_first_of(" ");
+            if (space == std::string::npos)
+            {
+                // Shouldn't enter here but lets skip it.
+                BMCWEB_LOG_DEBUG << "Invalid log entry found.";
+                continue;
+            }
+            lastEventTStr = logEntry.substr(0, space);
+        }
+        BMCWEB_LOG_DEBUG << "Last Event time stamp set: " << lastEventTStr;
+    }
+
+    void readEventLogsFromFile()
+    {
+        if (!serviceEnabled || !noOfEventLogSubscribers)
+        {
+            BMCWEB_LOG_DEBUG << "EventService disabled or no Subscriptions.";
+            return;
+        }
+        std::ifstream logStream(redfishEventLogFile);
+        if (!logStream.good())
+        {
+            BMCWEB_LOG_ERROR << " Redfish log file open failed";
+            return;
+        }
+
+        std::vector<EventLogObjectsType> eventRecords;
+
+        bool startLogCollection = false;
+        bool firstEntry = true;
+
+        std::string logEntry;
+        while (std::getline(logStream, logEntry))
+        {
+            if (!startLogCollection)
+            {
+                if (boost::starts_with(logEntry, lastEventTStr))
+                {
+                    startLogCollection = true;
+                }
+                continue;
+            }
+
+            std::string idStr;
+            if (!event_log::getUniqueEntryID(logEntry, idStr, firstEntry))
+            {
+                continue;
+            }
+            firstEntry = false;
+
+            std::string timestamp;
+            std::string messageID;
+            boost::beast::span<std::string> messageArgs;
+            if (event_log::getEventLogParams(logEntry, timestamp, messageID,
+                                             messageArgs) != 0)
+            {
+                BMCWEB_LOG_DEBUG << "Read eventLog entry params failed";
+                continue;
+            }
+
+            std::string registryName;
+            std::string messageKey;
+            event_log::getRegistryAndMessageKey(messageID, registryName,
+                                                messageKey);
+            if (registryName.empty() || messageKey.empty())
+            {
+                continue;
+            }
+
+            lastEventTStr = timestamp;
+            eventRecords.emplace_back(idStr, timestamp, messageID, registryName,
+                                      messageKey, messageArgs);
+        }
+
+        for (const auto& it : this->subscriptionsMap)
+        {
+            std::shared_ptr<Subscription> entry = it.second;
+            if (entry->eventFormatType == "Event")
+            {
+                entry->filterAndSendEventLogs(eventRecords);
+            }
+        }
+    }
+
+    static void watchRedfishEventLogFile()
+    {
+        if (inotifyConn == nullptr)
+        {
+            return;
+        }
+
+        static std::array<char, 1024> readBuffer;
+
+        inotifyConn->async_read_some(
+            boost::asio::buffer(readBuffer),
+            [&](const boost::system::error_code& ec,
+                const std::size_t& bytesTransferred) {
+                if (ec)
+                {
+                    BMCWEB_LOG_ERROR << "Callback Error: " << ec.message();
+                    return;
+                }
+                std::size_t index = 0;
+                while ((index + sizeof(inotify_event)) <= bytesTransferred)
+                {
+                    struct inotify_event event;
+                    std::memcpy(&event, &readBuffer[index],
+                                sizeof(inotify_event));
+                    if (event.mask == inotifyFileAction)
+                    {
+                        EventServiceManager::getInstance()
+                            .readEventLogsFromFile();
+                    }
+                    index += (sizeof(inotify_event) + event.len);
+                }
+
+                watchRedfishEventLogFile();
+            });
+    }
+
+    static int startEventLogMonitor(boost::asio::io_context& ioc)
+    {
+        inotifyConn =
+            std::make_shared<boost::asio::posix::stream_descriptor>(ioc);
+        int fd = inotify_init1(IN_NONBLOCK);
+        if (fd == -1)
+        {
+            BMCWEB_LOG_ERROR << "inotify_init1 failed.";
+            return -1;
+        }
+        auto wd = inotify_add_watch(fd, redfishEventLogFile, inotifyFileAction);
+        if (wd == -1)
+        {
+            BMCWEB_LOG_ERROR
+                << "inotify_add_watch failed for redfish log file.";
+            return -1;
+        }
+
+        // monitor redfish event log file
+        inotifyConn->assign(fd);
+        watchRedfishEventLogFile();
+
+        return 0;
+    }
+
+#endif
+
     void getMetricReading(const std::string& service,
                           const std::string& objPath, const std::string& intf)
     {
@@ -706,6 +1162,6 @@
         }
         return true;
     }
-};
+}; // namespace redfish
 
 } // namespace redfish
diff --git a/src/webserver_main.cpp b/src/webserver_main.cpp
index 738fb28..11e8e92 100644
--- a/src/webserver_main.cpp
+++ b/src/webserver_main.cpp
@@ -118,6 +118,15 @@
 
     redfish::RedfishService redfish(app);
 
+#ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES
+    int rc = redfish::EventServiceManager::startEventLogMonitor(*io);
+    if (rc)
+    {
+        BMCWEB_LOG_ERROR << "Redfish event handler setup failed...";
+        return rc;
+    }
+#endif
+
     app.run();
     io->run();