control: Create FlightRecorder class
This class provides a flight recorder feature by providing the following
two interfaces:
1. log(const std::string& id, const std::string& message)
2. dump()
log() saves the passed in message in a map with the previous messages
for that ID along with a timestamp when log() was called. It will
discard the oldest message if necessary to keep a maximum of 20.
dump() will write all messages to the file /tmp/fan_control.txt. It
will sort them all based on the timestamp so messages from different IDs
may be interleaved together.
The ID is used in addition to the message with the idea that there could
be one ID per action or per action instance, so that once active part of
fan control that logged a lot wouldn't force out messages from less
active parts.
Example output is:
Oct 01 04:37:19.123923: pcie_card_cooling-0: Setting parameter to 2
Oct 01 04:37:19.129787: mapped_floor-1: Setting new floor to 4755
The first column is the timestamp down to the microsecond, the second
column is the ID, and the third is the message.
Testing on real hardware showed it took about 21 milliseconds for dump()
to run when there were 30 IDs with 20 messages each.
Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: Ie3630174c8aa54911e56054e9d7c38bb7dfb3f18
diff --git a/control/Makefile.am b/control/Makefile.am
index 33c89d2..3b52e47 100644
--- a/control/Makefile.am
+++ b/control/Makefile.am
@@ -85,6 +85,7 @@
json/actions/timer_based_actions.cpp \
json/actions/mapped_floor.cpp \
json/actions/set_parameter_from_group_max.cpp \
+ json/utils/flight_recorder.cpp \
json/utils/modifier.cpp
else
phosphor_fan_control_SOURCES += \
diff --git a/control/json/utils/flight_recorder.cpp b/control/json/utils/flight_recorder.cpp
new file mode 100644
index 0000000..4cc274f
--- /dev/null
+++ b/control/json/utils/flight_recorder.cpp
@@ -0,0 +1,105 @@
+/**
+ * Copyright © 2021 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 "flight_recorder.hpp"
+
+#include <fmt/format.h>
+
+#include <phosphor-logging/log.hpp>
+
+#include <algorithm>
+#include <ctime>
+#include <fstream>
+#include <iomanip>
+#include <vector>
+
+constexpr auto maxEntriesPerID = 20;
+constexpr auto outputFile = "/tmp/fan_control.txt";
+
+namespace phosphor::fan::control::json
+{
+
+FlightRecorder& FlightRecorder::instance()
+{
+ static FlightRecorder fr;
+ return fr;
+}
+
+void FlightRecorder::log(const std::string& id, const std::string& message)
+{
+ uint64_t ts = std::chrono::duration_cast<std::chrono::microseconds>(
+ std::chrono::system_clock::now().time_since_epoch())
+ .count();
+
+ auto& entry = _entries[id];
+ entry.emplace_back(ts, message);
+ if (entry.size() > maxEntriesPerID)
+ {
+ entry.pop_front();
+ }
+}
+
+void FlightRecorder::dump()
+{
+ using namespace std::chrono;
+ using Timepoint = time_point<system_clock, microseconds>;
+
+ size_t idSize = 0;
+ std::vector<std::tuple<Timepoint, std::string, std::string>> output;
+
+ for (const auto& [id, messages] : _entries)
+ {
+ for (const auto& [ts, msg] : messages)
+ {
+ idSize = std::max(idSize, id.size());
+ Timepoint tp{microseconds{ts}};
+ output.emplace_back(tp, id, msg);
+ }
+ }
+
+ std::sort(output.begin(), output.end(),
+ [](const auto& left, const auto& right) {
+ return std::get<Timepoint>(left) < std::get<Timepoint>(right);
+ });
+
+ std::ofstream file{outputFile};
+
+ if (!file)
+ {
+ phosphor::logging::log<phosphor::logging::level::ERR>(
+ fmt::format("Could not open {} for writing", outputFile).c_str());
+ return;
+ }
+
+ auto formatTime = [](const Timepoint& tp) {
+ std::stringstream ss;
+ std::time_t tt = system_clock::to_time_t(tp);
+ uint64_t us =
+ duration_cast<microseconds>(tp.time_since_epoch()).count();
+
+ // e.g. Oct 04 16:43:45.923555
+ ss << std::put_time(std::localtime(&tt), "%b %d %H:%M:%S.");
+ ss << std::setfill('0') << std::setw(6) << std::to_string(us % 1000000);
+ return ss.str();
+ };
+
+ for (const auto& [ts, id, msg] : output)
+ {
+ file << formatTime(ts) << ": " << std::setw(idSize) << id << ": " << msg
+ << "\n";
+ }
+}
+
+} // namespace phosphor::fan::control::json
diff --git a/control/json/utils/flight_recorder.hpp b/control/json/utils/flight_recorder.hpp
new file mode 100644
index 0000000..154d5ab
--- /dev/null
+++ b/control/json/utils/flight_recorder.hpp
@@ -0,0 +1,80 @@
+/**
+ * Copyright © 2021 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.
+ */
+#pragma once
+#include <chrono>
+#include <cstdint>
+#include <deque>
+#include <string>
+#include <tuple>
+#include <unordered_map>
+
+namespace phosphor::fan::control::json
+{
+
+/**
+ * @class FlightRecorder
+ *
+ * This class stores messages and their timestamps based on an ID.
+ * When an ID accumulates so many messages, the oldest one will
+ * be removed when a new one is added.
+ *
+ * The dump() function interleaves the messages for all IDs together
+ * based on timestamp and then writes them all to /tmp/fan_control.txt.
+ *
+ * For example:
+ * Oct 01 04:37:19.122771: main: Startup
+ * Oct 01 04:37:19.123923: mapped_floor-1: Setting new floor to 4755
+ */
+class FlightRecorder
+{
+ public:
+ ~FlightRecorder() = default;
+ FlightRecorder(const FlightRecorder&) = delete;
+ FlightRecorder& operator=(const FlightRecorder&) = delete;
+ FlightRecorder(FlightRecorder&&) = delete;
+ FlightRecorder& operator=(FlightRecorder&&) = delete;
+
+ /**
+ * @brief Returns a reference to the static instance.
+ */
+ static FlightRecorder& instance();
+
+ /**
+ * @brief Logs an entry to the recorder.
+ *
+ * @param[in] id - The ID of the message owner
+ * @param[in] message - The message to log
+ */
+ void log(const std::string& id, const std::string& message);
+
+ /**
+ * @brief Writes the flight recorder contents to file.
+ *
+ * Sorts all messages by timestamp when doing so.
+ */
+ void dump();
+
+ private:
+ FlightRecorder() = default;
+
+ // tuple<timestamp, message>
+ using Entry = std::tuple<uint64_t, std::string>;
+
+ /* The messages */
+ std::unordered_map<std::string, std::deque<Entry>> _entries;
+};
+
+} // namespace phosphor::fan::control::json