Support UpdateTimestamp log entry property

The UpdateTimestamp property says when an event log property was last
updated.  On log creation, this field is initialized with the time that
the log was created.

The only other time a log entry property can change is when the Resolved
property changes, so also added support there to update the new
UpdateTimestamp property.

While technically every property of the Entry interface is writeable,
there is no use case nor code for changing those in the field, so
UpdateTimestamp does not support those.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I9b1360b9c5424e130947045cee7b64ede6264829
diff --git a/configure.ac b/configure.ac
index ae9d6e8..0cbc673 100644
--- a/configure.ac
+++ b/configure.ac
@@ -69,6 +69,12 @@
     AC_SUBST([OESDK_TESTCASE_FLAGS], [$testcase_flags])
 )
 
+# The continuous integration tests set this.
+# Use it to know where to store logs.
+AC_ARG_ENABLE([tests],
+    AS_HELP_STRING([--enable-tests], [Specifies running in the CI environment])
+)
+
 AC_DEFINE(BUSNAME_LOGGING, "xyz.openbmc_project.Logging", [The log manager DBus busname to own.])
 AC_DEFINE(OBJ_LOGGING, "/xyz/openbmc_project/logging", [The log manager DBus object path.])
 AC_DEFINE(OBJ_INTERNAL, "/xyz/openbmc_project/logging/internal/manager", [The private log manager DBus object path.])
@@ -80,6 +86,9 @@
 AC_DEFINE(FIRST_CEREAL_CLASS_VERSION_WITH_FWLEVEL, "2",
           [First Cereal class version with the BMC code version persisted])
 
+AC_DEFINE(FIRST_CEREAL_CLASS_VERSION_WITH_UPDATE_TS, "3",
+          [First Cereal class version with the update timestamp persisted])
+
 AC_ARG_VAR(YAML_DIR_TEST, [The path to the test error yaml files.])
 AS_IF([test "x$YAML_DIR_TEST" == "x"], \
     [YAML_DIR_TEST="${srcdir}/tools/"])
@@ -96,6 +105,8 @@
         [ERRLOG_PERSIST_PATH="/tmp/errors"])
     AS_IF([test "x$enable_oe_sdk" == "x"], \
         [ERRLOG_PERSIST_PATH="/var/lib/phosphor-logging/errors"])
+    AS_IF([test "x$enable_tests" == "xyes"], \
+        [ERRLOG_PERSIST_PATH="/tmp/errors"])
     AC_DEFINE_UNQUOTED([ERRLOG_PERSIST_PATH], ["$ERRLOG_PERSIST_PATH"], \
         [Path of directory housing persisted errors])
 )
@@ -116,7 +127,7 @@
     [Cap on informational (and below) severity errors])
 
 AC_ARG_VAR(CLASS_VERSION, [Class version to register with Cereal])
-AS_IF([test "x$CLASS_VERSION" == "x"], [CLASS_VERSION=2])
+AS_IF([test "x$CLASS_VERSION" == "x"], [CLASS_VERSION=3])
 AC_DEFINE_UNQUOTED([CLASS_VERSION], [$CLASS_VERSION], [Class version to register with Cereal])
 
 AC_ARG_VAR(RSYSLOG_SERVER_CONFIG_FILE, \
diff --git a/elog_entry.cpp b/elog_entry.cpp
index 04aa917..36a3fb2 100644
--- a/elog_entry.cpp
+++ b/elog_entry.cpp
@@ -25,6 +25,12 @@
         current =
             sdbusplus::xyz::openbmc_project::Logging::server::Entry::resolved(
                 value);
+
+        uint64_t ms = std::chrono::duration_cast<std::chrono::milliseconds>(
+                          std::chrono::system_clock::now().time_since_epoch())
+                          .count();
+        updateTimestamp(ms);
+
         serialize(*this);
     }
 
diff --git a/elog_entry.hpp b/elog_entry.hpp
index 9c93afe..5bc4882 100644
--- a/elog_entry.hpp
+++ b/elog_entry.hpp
@@ -68,6 +68,7 @@
         id(idErr);
         severity(severityErr);
         timestamp(timestampErr);
+        updateTimestamp(timestampErr);
         message(std::move(msgErr));
         additionalData(std::move(additionalDataErr));
         associations(std::move(objects));
diff --git a/elog_serialize.cpp b/elog_serialize.cpp
index 72534a4..40c6b74 100644
--- a/elog_serialize.cpp
+++ b/elog_serialize.cpp
@@ -30,7 +30,7 @@
 void save(Archive& a, const Entry& e, const std::uint32_t version)
 {
     a(e.id(), e.severity(), e.timestamp(), e.message(), e.additionalData(),
-      e.associations(), e.resolved(), e.version());
+      e.associations(), e.resolved(), e.version(), e.updateTimestamp());
 }
 
 /** @brief Function required by Cereal to perform deserialization.
@@ -53,16 +53,24 @@
     bool resolved{};
     AssociationList associations{};
     std::string fwVersion{};
+    uint64_t updateTimestamp{};
 
     if (version < std::stoul(FIRST_CEREAL_CLASS_VERSION_WITH_FWLEVEL))
     {
         a(id, severity, timestamp, message, additionalData, associations,
           resolved);
+        updateTimestamp = timestamp;
+    }
+    else if (version < std::stoul(FIRST_CEREAL_CLASS_VERSION_WITH_UPDATE_TS))
+    {
+        a(id, severity, timestamp, message, additionalData, associations,
+          resolved, fwVersion);
+        updateTimestamp = timestamp;
     }
     else
     {
         a(id, severity, timestamp, message, additionalData, associations,
-          resolved, fwVersion);
+          resolved, fwVersion, updateTimestamp);
     }
 
     e.id(id);
@@ -76,6 +84,7 @@
     e.version(fwVersion);
     e.purpose(sdbusplus::xyz::openbmc_project::Software::server::Version::
                   VersionPurpose::BMC);
+    e.updateTimestamp(updateTimestamp);
 }
 
 fs::path serialize(const Entry& e, const fs::path& dir)
diff --git a/test/Makefile.am b/test/Makefile.am
index 64aa6d8..ddd42e6 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -10,7 +10,8 @@
 	remote_logging_test_port \
 	remote_logging_test_config \
 	sdjournal_mock_test \
-	extensions_test
+	extensions_test \
+	elog_update_ts_test
 
 test_cppflags = \
 	-Igtest \
@@ -106,6 +107,13 @@
 	$(top_builddir)/xyz/openbmc_project/Logging/Internal/Manager/server.o
 extensions_test_LDFLAGS = $(test_ldflags)
 
+elog_update_ts_test_CPPFLAGS = $(test_cppflags)
+elog_update_ts_test_CXXFLAGS = $(test_cxxflags)
+elog_update_ts_test_SOURCES = elog_update_ts_test.cpp
+elog_update_ts_test_LDADD = $(test_ldadd)
+elog_update_ts_test_LDFLAGS = \
+	$(test_ldflags)
+
 # TODO Remove once the test-case failure is resolved openbmc/phosphor-logging#11
 XFAIL_TESTS = elog_errorwrap_test
 
diff --git a/test/elog_update_ts_test.cpp b/test/elog_update_ts_test.cpp
new file mode 100644
index 0000000..ece0b80
--- /dev/null
+++ b/test/elog_update_ts_test.cpp
@@ -0,0 +1,73 @@
+#include "config.h"
+
+#include "elog_entry.hpp"
+#include "elog_serialize.hpp"
+#include "log_manager.hpp"
+
+#include <filesystem>
+#include <thread>
+
+#include <gtest/gtest.h>
+
+namespace phosphor
+{
+namespace logging
+{
+namespace test
+{
+
+using namespace std::chrono_literals;
+
+// Test that the update timestamp changes when the resolved property changes
+TEST(TestUpdateTS, testChangeResolved)
+{
+    // Setting resolved will serialize, so need this directory.
+    std::filesystem::create_directory(ERRLOG_PERSIST_PATH);
+
+    auto bus = sdbusplus::bus::new_default();
+    phosphor::logging::internal::Manager manager(bus, OBJ_INTERNAL);
+
+    uint32_t id = 99;
+    uint64_t timestamp{100};
+    std::string message{"test error"};
+    std::string fwLevel{"level42"};
+    std::vector<std::string> testData{"additional", "data"};
+    phosphor::logging::AssociationList associations{};
+
+    Entry elog{bus,
+               std::string(OBJ_ENTRY) + '/' + std::to_string(id),
+               id,
+               timestamp,
+               Entry::Level::Informational,
+               std::move(message),
+               std::move(testData),
+               std::move(associations),
+               fwLevel,
+               manager};
+
+    EXPECT_EQ(elog.timestamp(), elog.updateTimestamp());
+
+    std::this_thread::sleep_for(1ms);
+
+    elog.resolved(true);
+    auto updateTS = elog.updateTimestamp();
+    EXPECT_NE(updateTS, elog.timestamp());
+
+    std::this_thread::sleep_for(1ms);
+
+    elog.resolved(false);
+    EXPECT_NE(updateTS, elog.updateTimestamp());
+    updateTS = elog.updateTimestamp();
+
+    std::this_thread::sleep_for(1ms);
+
+    // No change
+    elog.resolved(false);
+    EXPECT_EQ(updateTS, elog.updateTimestamp());
+
+    std::filesystem::remove_all(ERRLOG_PERSIST_PATH);
+}
+
+} // namespace test
+} // namespace logging
+} // namespace phosphor
diff --git a/test/serialization_test_properties.cpp b/test/serialization_test_properties.cpp
index 8c92024..270f8bc 100644
--- a/test/serialization_test_properties.cpp
+++ b/test/serialization_test_properties.cpp
@@ -38,6 +38,7 @@
     EXPECT_EQ(input->associations(), output->associations());
     EXPECT_EQ(input->version(), output->version());
     EXPECT_EQ(input->purpose(), output->purpose());
+    EXPECT_EQ(input->updateTimestamp(), output->updateTimestamp());
 }
 
 } // namespace test