Create a class to store messages for event log

Create a Logger class that can store log messages along with their
timestamps.  These messages will then be added to an event log when they
are created for missing fans, for debug purposes.  Each message is also
logged to the journal.

The maximum number of entries to keep around is specified in the
constructor, and when full the oldest message will be purged when new
ones are added.  This number comes from a configuration option which
defaults to 50.

A standalone getLogger() API was also added to give the new object
singleton behavior, so the same object can be accessed from all classes
in an application.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I8d5ac137acb67bfe78609d02aaf59a01b03c5c8b
diff --git a/Makefile.am b/Makefile.am
index 55d0818..1d4ddec 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,6 +1,6 @@
 AM_DEFAULT_SOURCE_EXT = .cpp
 
-SUBDIRS = .
+SUBDIRS = . test
 
 if WANT_PRESENCE
 SUBDIRS += presence
diff --git a/configure.ac b/configure.ac
index 3d79076..89221ad 100644
--- a/configure.ac
+++ b/configure.ac
@@ -100,6 +100,12 @@
        AC_SUBST([PFPGEN],
                 ["$PYTHON \${top_srcdir}/presence/pfpgen.py generate-cpp -i $PRESENCE_CONFIG"])
     ])
+
+    AC_ARG_VAR(NUM_PRESENCE_LOG_ENTRIES, [Maximum number of entries in the message log])
+    AS_IF([test "x$NUM_PRESENCE_LOG_ENTRIES" == "x"], [NUM_PRESENCE_LOG_ENTRIES=50])
+    AC_DEFINE_UNQUOTED([NUM_PRESENCE_LOG_ENTRIES], [$NUM_PRESENCE_LOG_ENTRIES],
+                       [Maximum number of entries in the message log])
+
     AC_CONFIG_FILES([presence/Makefile])
 ])
 
@@ -191,5 +197,5 @@
 ])
 
 # Create configured output
-AC_CONFIG_FILES([Makefile presence/test/Makefile])
+AC_CONFIG_FILES([Makefile test/Makefile presence/test/Makefile])
 AC_OUTPUT
diff --git a/logger.hpp b/logger.hpp
new file mode 100644
index 0000000..f31ff36
--- /dev/null
+++ b/logger.hpp
@@ -0,0 +1,173 @@
+#pragma once
+
+#include <fmt/format.h>
+#include <unistd.h>
+
+#include <nlohmann/json.hpp>
+#include <phosphor-logging/log.hpp>
+
+#include <cassert>
+#include <ctime>
+#include <filesystem>
+#include <fstream>
+#include <iomanip>
+#include <sstream>
+#include <string>
+#include <vector>
+
+namespace phosphor::fan
+{
+
+/**
+ * @class Logger
+ *
+ * A simple logging class that stores log messages in a vector along
+ * with their timestamp.  When a messaged is logged, it will also be
+ * written to the journal.
+ *
+ * A saveToTempFile() function will write the log entries as JSON to
+ * a temporary file, so they can be added to event logs.
+ *
+ * The maximum number of entries to keep is specified in the
+ * constructor, and after that is hit the oldest entry will be
+ * removed when a new one is added.
+ */
+class Logger
+{
+  public:
+    // timestamp, message
+    using LogEntry = std::tuple<std::string, std::string>;
+
+    enum Priority
+    {
+        error,
+        info
+    };
+
+    Logger() = delete;
+    ~Logger() = default;
+    Logger(const Logger&) = default;
+    Logger& operator=(const Logger&) = default;
+    Logger(Logger&&) = default;
+    Logger& operator=(Logger&&) = default;
+
+    /**
+     * @brief Constructor
+     *
+     * @param[in] maxEntries - The maximum number of log entries
+     *                         to keep.
+     */
+    explicit Logger(size_t maxEntries) : _maxEntries(maxEntries)
+    {
+        assert(maxEntries != 0);
+    }
+
+    /**
+     * @brief Places an entry in the log and writes it to the journal.
+     *
+     * @param[in] message - The log message
+     *
+     * @param[in] priority - The priority for the journal
+     */
+    void log(const std::string& message, Priority priority = Logger::info)
+    {
+        if (priority == Logger::error)
+        {
+            phosphor::logging::log<phosphor::logging::level::ERR>(
+                message.c_str());
+        }
+        else
+        {
+            phosphor::logging::log<phosphor::logging::level::INFO>(
+                message.c_str());
+        }
+
+        if (_entries.size() == _maxEntries)
+        {
+            _entries.erase(_entries.begin());
+        }
+
+        // Generate a timestamp
+        auto t = std::time(nullptr);
+        auto tm = *std::localtime(&t);
+
+        // e.g. Sep 22 19:56:32
+        auto timestamp = std::put_time(&tm, "%b %d %H:%M:%S");
+
+        std::ostringstream stream;
+        stream << timestamp;
+        _entries.emplace_back(stream.str(), message);
+    }
+
+    /**
+     * @brief Returns the entries in a JSON array
+     *
+     * @return JSON
+     */
+    const nlohmann::json getLogs() const
+    {
+        return _entries;
+    }
+
+    /**
+     * @brief Writes the JSON to a temporary file and returns the path
+     *        to it.
+     *
+     * Uses a temp file because the only use case for this is for sending
+     * in to an event log where a temp file makes sense, and frankly it
+     * was just easier to encapsulate everything here.
+     *
+     * @return path - The path to the file.
+     */
+    std::filesystem::path saveToTempFile()
+    {
+        using namespace std::literals::string_literals;
+
+        char tmpFile[] = "/tmp/loggertemp.XXXXXX";
+        int fd = mkstemp(tmpFile);
+        if (fd == -1)
+        {
+            throw std::runtime_error{"mkstemp failed!"};
+        }
+
+        std::filesystem::path path{tmpFile};
+
+        nlohmann::json data;
+        data["Logs"] = _entries;
+        auto jsonString = data.dump();
+
+        auto rc = write(fd, jsonString.c_str(), jsonString.size());
+        auto e = errno;
+        close(fd);
+        if (rc == 0)
+        {
+            log(fmt::format("Could not write to temp file {} errno {}", tmpFile,
+                            e),
+                Logger::error);
+            throw std::runtime_error{"Could not write to "s + path.string()};
+        }
+
+        return std::filesystem::path{tmpFile};
+    }
+
+    /**
+     * @brief Deletes all log entries
+     */
+    void clear()
+    {
+        _entries.clear();
+    }
+
+  private:
+    /**
+     * @brief The maximum number of entries to hold
+     */
+    const size_t _maxEntries;
+
+    /**
+     * @brief The vector of <timestamp, message> entries
+     */
+    std::vector<LogEntry> _entries;
+};
+
+} // namespace phosphor::fan
diff --git a/presence/Makefile.am b/presence/Makefile.am
index a26fac9..af66bbd 100644
--- a/presence/Makefile.am
+++ b/presence/Makefile.am
@@ -10,6 +10,7 @@
 	fallback.cpp \
 	fan.cpp \
 	gpio.cpp \
+	logging.cpp \
 	psensor.cpp \
 	tach.cpp \
 	tach_detect.cpp \
diff --git a/presence/logging.cpp b/presence/logging.cpp
new file mode 100644
index 0000000..3945ac9
--- /dev/null
+++ b/presence/logging.cpp
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2020 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "config.h"
+
+#include "logging.hpp"
+
+namespace phosphor::fan
+{
+
+Logger& getLogger()
+{
+    static Logger logger{NUM_PRESENCE_LOG_ENTRIES};
+
+    return logger;
+}
+
+} // namespace phosphor::fan
diff --git a/presence/logging.hpp b/presence/logging.hpp
new file mode 100644
index 0000000..dd75a96
--- /dev/null
+++ b/presence/logging.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "logger.hpp"
+
+namespace phosphor::fan
+{
+/**
+ * @brief Returns the singleton Logger class
+ *
+ * @return Logger& - The logger
+ */
+Logger& getLogger();
+} // namespace phosphor::fan
diff --git a/test/Makefile.am b/test/Makefile.am
new file mode 100644
index 0000000..ac61577
--- /dev/null
+++ b/test/Makefile.am
@@ -0,0 +1,21 @@
+AM_CPPFLAGS = -iquote$(top_srcdir)
+gtest_cflags = $(PTHREAD_CFLAGS)
+gtest_ldadd = -lgtest -lgtest_main -lgmock $(PTHREAD_LIBS)
+
+check_PROGRAMS = logger_test
+
+TESTS = $(check_PROGRAMS)
+
+logger_test_SOURCES = \
+	logger_test.cpp
+logger_test_CXXFLAGS = \
+	$(gtest_cflags) \
+	${PHOSPHOR_DBUS_INTERFACES_CFLAGS} \
+	$(SDBUSPLUS_CFLAGS)
+logger_test_LDFLAGS = \
+	$(OESDK_TESTCASE_FLAGS)
+logger_test_LDADD = \
+	$(gtest_ldadd) \
+	${PHOSPHOR_DBUS_INTERFACES_LIBS} \
+	$(SDBUSPLUS_LIBS) \
+	$(FMT_LIBS)
diff --git a/test/logger_test.cpp b/test/logger_test.cpp
new file mode 100644
index 0000000..c234b4f
--- /dev/null
+++ b/test/logger_test.cpp
@@ -0,0 +1,59 @@
+#include "logger.hpp"
+
+#include <gtest/gtest.h>
+
+using namespace phosphor::fan;
+using namespace std::literals::string_literals;
+
+TEST(LoggerTest, Test)
+{
+    const auto logSize = 5;
+
+    Logger logger{logSize};
+
+    for (int i = 0; i < logSize; i++)
+    {
+        logger.log("Test Message "s + std::to_string(i));
+    }
+
+    auto messages = logger.getLogs();
+
+    ASSERT_EQ(messages.size(), logSize);
+
+    EXPECT_EQ((messages[0][1].get<std::string>()), "Test Message 0");
+    EXPECT_EQ((messages[1][1].get<std::string>()), "Test Message 1");
+    EXPECT_EQ((messages[2][1].get<std::string>()), "Test Message 2");
+    EXPECT_EQ((messages[3][1].get<std::string>()), "Test Message 3");
+    EXPECT_EQ((messages[4][1].get<std::string>()), "Test Message 4");
+
+    // There isn't really a way to verify the timestamp, but
+    // it can at least be printed.
+    for (const auto& msg : messages)
+    {
+        std::cout << "Timestamp: " << msg[0] << "\n";
+    }
+
+    // Add another message, it should purge the first one
+    logger.log("New Message");
+
+    messages = logger.getLogs();
+    ASSERT_EQ(messages.size(), logSize);
+
+    // Check the first and last
+    EXPECT_EQ((messages[0][1].get<std::string>()), "Test Message 1");
+    EXPECT_EQ((messages[4][1].get<std::string>()), "New Message");
+
+    auto path = logger.saveToTempFile();
+    ASSERT_TRUE(std::filesystem::exists(path));
+
+    std::ifstream file{path};
+
+    // check that valid JSON was written
+    auto newJSON = nlohmann::json::parse(file);
+    EXPECT_EQ(newJSON["Logs"].size(), logSize);
+    std::filesystem::remove(path);
+
+    logger.clear();
+    messages = logger.getLogs();
+    EXPECT_TRUE(messages.empty());
+}