metrics-ipmi-blobs: Add from gBMC
Tested: ran CI locally.
1/5 ncsid / iface_test OK 0.01s
2/5 ncsid / sock_test OK 0.02s
3/5 ncsid / normalize_ip_test OK 0.02s
4/5 ncsid / normalize_mac_test OK 0.03s
5/5 metrics-ipmi-blobs / gtest test OK 0.01s
Signed-off-by: Sui Chen <suichen@google.com>
Change-Id: Icaf266b170f96b062774c3ef90e59ddec9ea15c6
diff --git a/meson.build b/meson.build
index 3ca0866..e235b7b 100644
--- a/meson.build
+++ b/meson.build
@@ -21,3 +21,4 @@
endif
subproject('ncsid', default_options: 'tests=' + tests_str)
+subproject('metrics-ipmi-blobs', default_options: 'tests=' + tests_str)
diff --git a/metrics-ipmi-blobs/README.md b/metrics-ipmi-blobs/README.md
new file mode 100644
index 0000000..c9302eb
--- /dev/null
+++ b/metrics-ipmi-blobs/README.md
@@ -0,0 +1,13 @@
+IPMI BLOBs handler to export BMC metrics snapshot
+
+This BLOB handler registers one blob with the name "/metric/snapshot".
+
+The contents of the BLOB is a protocol buffer containing an instantaneous snapshot of the BMC's health metrics, which includes the following categories:
+
+1. BMC memory metric: mem_available, slab, kernel_stack
+2. Uptime: uptime in wall clock time, idle process across all cores
+3. Disk space: free space in RWFS in KiB
+4. Status of the top 10 processes: cmdline, utime, stime
+5. File descriptor of top 10 processes: cmdline, file descriptor count
+
+The size of the metrics are usually around 1KB to 1.5KB.
diff --git a/metrics-ipmi-blobs/handler.cpp b/metrics-ipmi-blobs/handler.cpp
new file mode 100644
index 0000000..46ac9cf
--- /dev/null
+++ b/metrics-ipmi-blobs/handler.cpp
@@ -0,0 +1,139 @@
+#include "handler.hpp"
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace blobs
+{
+
+namespace
+{
+constexpr std::string_view metricPath("/metric/snapshot");
+} // namespace
+
+bool MetricBlobHandler::canHandleBlob(const std::string& path)
+{
+ return path == metricPath;
+}
+
+// A blob handler may have multiple Blobs. For this blob handler, there is only
+// one blob.
+std::vector<std::string> MetricBlobHandler::getBlobIds()
+{
+ return {std::string(metricPath)};
+}
+
+// BmcBlobDelete (7) is not supported.
+bool MetricBlobHandler::deleteBlob(const std::string& path)
+{
+ return false;
+}
+
+// BmcBlobStat (8) (global stat) is not supported.
+bool MetricBlobHandler::stat(const std::string& path, BlobMeta* meta)
+{
+ return false;
+}
+
+// Checks for a read-only flag.
+bool MetricBlobHandler::isReadOnlyOpenFlags(const uint16_t flags)
+{
+ if (((flags & blobs::OpenFlags::read) == blobs::OpenFlags::read) &&
+ ((flags & blobs::OpenFlags::write) == 0))
+ {
+ return true;
+ }
+ return false;
+}
+
+// BmcBlobOpen(2) handler.
+bool MetricBlobHandler::open(uint16_t session, uint16_t flags,
+ const std::string& path)
+{
+ if (!isReadOnlyOpenFlags(flags))
+ {
+ return false;
+ }
+ if (!canHandleBlob(path))
+ {
+ return false;
+ }
+ if (path == metricPath)
+ {
+ std::unique_ptr<metric_blob::BmcHealthSnapshot> bhs =
+ std::make_unique<metric_blob::BmcHealthSnapshot>();
+ bhs.get()->doWork();
+ sessions[session] = nullptr;
+ sessions[session] = std::move(bhs);
+ return true;
+ }
+ return false;
+}
+
+// BmcBlobRead(3) handler.
+std::vector<uint8_t> MetricBlobHandler::read(uint16_t session, uint32_t offset,
+ uint32_t requestedSize)
+{
+ auto it = sessions.find(session);
+ if (it == sessions.end())
+ {
+ return {};
+ }
+
+ std::string_view result = it->second->read(offset, requestedSize);
+ return std::vector<uint8_t>(result.begin(), result.end());
+}
+
+// BmcBlobWrite(4) is not supported.
+bool MetricBlobHandler::write(uint16_t session, uint32_t offset,
+ const std::vector<uint8_t>& data)
+{
+ return false;
+}
+
+// BmcBlobWriteMeta(10) is not supported.
+bool MetricBlobHandler::writeMeta(uint16_t session, uint32_t offset,
+ const std::vector<uint8_t>& data)
+{
+ return false;
+}
+
+// BmcBlobCommit(5) is not supported.
+bool MetricBlobHandler::commit(uint16_t session,
+ const std::vector<uint8_t>& data)
+{
+ return false;
+}
+
+// BmcBlobClose(6) handler.
+bool MetricBlobHandler::close(uint16_t session)
+{
+ auto itr = sessions.find(session);
+ if (itr == sessions.end())
+ {
+ return false;
+ }
+ sessions.erase(itr);
+ return true;
+}
+
+// BmcBlobSessionStat(9) handler
+bool MetricBlobHandler::stat(uint16_t session, BlobMeta* meta)
+{
+ auto it = sessions.find(session);
+ if (it == sessions.end())
+ {
+ return false;
+ }
+ return it->second->stat(*meta);
+}
+
+bool MetricBlobHandler::expire(uint16_t session)
+{
+ return close(session);
+}
+
+} // namespace blobs
diff --git a/metrics-ipmi-blobs/handler.hpp b/metrics-ipmi-blobs/handler.hpp
new file mode 100644
index 0000000..d811333
--- /dev/null
+++ b/metrics-ipmi-blobs/handler.hpp
@@ -0,0 +1,48 @@
+#pragma once
+
+#include <blobs-ipmid/blobs.hpp>
+#include <metric.hpp>
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace blobs
+{
+
+class MetricBlobHandler : public GenericBlobInterface
+{
+ public:
+ MetricBlobHandler() = default;
+ ~MetricBlobHandler() = default;
+ MetricBlobHandler(const MetricBlobHandler&) = delete;
+ MetricBlobHandler& operator=(const MetricBlobHandler&) = delete;
+ MetricBlobHandler(MetricBlobHandler&&) = default;
+ MetricBlobHandler& operator=(MetricBlobHandler&&) = default;
+
+ bool canHandleBlob(const std::string& path) override;
+ std::vector<std::string> getBlobIds() override;
+ bool deleteBlob(const std::string& path) override;
+ bool stat(const std::string& path, BlobMeta* meta) override;
+ bool open(uint16_t session, uint16_t flags,
+ const std::string& path) override;
+ std::vector<uint8_t> read(uint16_t session, uint32_t offset,
+ uint32_t requestedSize) override;
+ bool write(uint16_t session, uint32_t offset,
+ const std::vector<uint8_t>& data) override;
+ bool writeMeta(uint16_t session, uint32_t offset,
+ const std::vector<uint8_t>& data) override;
+ bool commit(uint16_t session, const std::vector<uint8_t>& data) override;
+ bool close(uint16_t session) override;
+ bool stat(uint16_t session, BlobMeta* meta) override;
+ bool expire(uint16_t session) override;
+
+ private:
+ bool isReadOnlyOpenFlags(const uint16_t flag);
+ /* Every session gets its own BmcHealthSnapshot instance. */
+ std::unordered_map<uint16_t,
+ std::unique_ptr<metric_blob::BmcHealthSnapshot>>
+ sessions;
+};
+
+} // namespace blobs
diff --git a/metrics-ipmi-blobs/main.cpp b/metrics-ipmi-blobs/main.cpp
new file mode 100644
index 0000000..ef09c46
--- /dev/null
+++ b/metrics-ipmi-blobs/main.cpp
@@ -0,0 +1,21 @@
+#include "handler.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <memory>
+
+// Extern "C" is used due to the usage of dlopen() for loading IPMI handlers
+// and IPMI blob handlers. This happens in two steps:
+//
+// 1) ipmid loads all libraries in /usr/lib/ipmid-providers from its
+// loadLibraries() function using dlopen().
+// 2) The blobs library, libblobcmds.so, loads all libraries in
+// /usr/lib/blobs-ipmid from its loadLibraries() function. It uses dlsym()
+// to locate the createHandler function by its un-mangled name
+// "createHandler". Using extern "C" prevents its name from being mangled
+// into, for example, "_Z13createHandlerv".
+
+extern "C" std::unique_ptr<blobs::GenericBlobInterface> createHandler()
+{
+ return std::make_unique<blobs::MetricBlobHandler>();
+}
diff --git a/metrics-ipmi-blobs/meson.build b/metrics-ipmi-blobs/meson.build
new file mode 100644
index 0000000..b3de2d8
--- /dev/null
+++ b/metrics-ipmi-blobs/meson.build
@@ -0,0 +1,44 @@
+project(
+ 'metrics-ipmi-blobs',
+ 'cpp',
+ version: '0.1',
+ default_options: [
+ 'cpp_std=c++17',
+ ],
+)
+
+add_project_arguments(
+ '-Wno-unused-parameter',
+ language:'cpp')
+
+protoc = find_program('protoc', required: true)
+
+gen = generator(protoc,
+ output: [
+ '@BASENAME@.pb.cc', '@BASENAME@.pb.h'
+ ],
+ arguments : ['--proto_path=@CURRENT_SOURCE_DIR@', '--cpp_out=@BUILD_DIR@', '@INPUT@'])
+generated = gen.process(['metricblob.proto'])
+
+shared_library(
+ 'metrics',
+ 'main.cpp',
+ 'handler.cpp',
+ 'metric.cpp',
+ 'util.cpp',
+ generated,
+ install: true,
+ install_dir: '/usr/lib/blob-ipmid/',
+ dependencies: [
+ dependency('phosphor-logging'),
+ dependency('phosphor-ipmi-blobs'),
+ dependency('protobuf'),
+ ],
+ version: '0',
+)
+
+gtest_dep = dependency('gtest')
+text_executable = executable('testprog', [
+ 'test/util_test.cpp', 'util.cpp'],
+ dependencies: [ gtest_dep, dependency('phosphor-logging') ])
+test('gtest test', text_executable)
diff --git a/metrics-ipmi-blobs/metric.cpp b/metrics-ipmi-blobs/metric.cpp
new file mode 100644
index 0000000..afccc62
--- /dev/null
+++ b/metrics-ipmi-blobs/metric.cpp
@@ -0,0 +1,405 @@
+#include "metric.hpp"
+
+#include "metricblob.pb.h"
+
+#include "util.hpp"
+
+#include <sys/statvfs.h>
+
+#include <phosphor-logging/log.hpp>
+
+#include <cstdint>
+#include <filesystem>
+#include <sstream>
+#include <string>
+#include <string_view>
+
+namespace metric_blob
+{
+
+using phosphor::logging::entry;
+using phosphor::logging::log;
+using level = phosphor::logging::level;
+
+BmcHealthSnapshot::BmcHealthSnapshot() :
+ done(false), stringId(0), ticksPerSec(0)
+{}
+
+struct ProcStatEntry
+{
+ std::string cmdline;
+ std::string tcomm;
+ float utime;
+ float stime;
+
+ // Processes with the longest utime + stime are ranked first.
+ // Tie breaking is done with cmdline then tcomm.
+ bool operator<(const ProcStatEntry& other) const
+ {
+ const float negTime = -(utime + stime);
+ const float negOtherTime = -(other.utime + other.stime);
+ return std::tie(negTime, cmdline, tcomm) <
+ std::tie(negOtherTime, other.cmdline, other.tcomm);
+ }
+};
+
+bmcmetrics::metricproto::BmcProcStatMetric BmcHealthSnapshot::getProcStatList()
+{
+ constexpr std::string_view procPath = "/proc/";
+
+ bmcmetrics::metricproto::BmcProcStatMetric ret;
+ std::vector<ProcStatEntry> entries;
+
+ for (const auto& procEntry : std::filesystem::directory_iterator(procPath))
+ {
+ const std::string& path = procEntry.path();
+ int pid = -1;
+ if (isNumericPath(path, pid))
+ {
+ ProcStatEntry entry;
+
+ try
+ {
+ entry.cmdline = getCmdLine(pid);
+ TcommUtimeStime t = getTcommUtimeStime(pid, ticksPerSec);
+ entry.tcomm = t.tcomm;
+ entry.utime = t.utime;
+ entry.stime = t.stime;
+
+ entries.push_back(entry);
+ }
+ catch (const std::exception& e)
+ {
+ log<level::ERR>("Could not obtain process stats");
+ }
+ }
+ }
+
+ std::sort(entries.begin(), entries.end());
+
+ bool isOthers = false;
+ ProcStatEntry others;
+ others.cmdline = "(Others)";
+ others.utime = others.stime = 0;
+
+ // Only show this many processes and aggregate all remaining ones into
+ // "others" in order to keep the size of the snapshot reasonably small.
+ // With 10 process stat entries and 10 FD count entries, the size of the
+ // snapshot reaches around 1.5KiB. This is non-trivial, and we have to set
+ // the collection interval long enough so as not to over-stress the IPMI
+ // interface and the data collection service. The value of 10 is chosen
+ // empirically, it might be subject to adjustments when the system is
+ // launched later.
+ constexpr int topN = 10;
+
+ for (size_t i = 0; i < entries.size(); ++i)
+ {
+ if (i >= topN)
+ {
+ isOthers = true;
+ }
+
+ ProcStatEntry& entry = entries[i];
+
+ if (isOthers)
+ {
+ others.utime += entry.utime;
+ others.stime += entry.stime;
+ }
+ else
+ {
+ bmcmetrics::metricproto::BmcProcStatMetric::BmcProcStat s;
+ std::string fullCmdline = entry.cmdline;
+ if (entry.tcomm.size() > 0)
+ {
+ fullCmdline += " " + entry.tcomm;
+ }
+ s.set_sidx_cmdline(getStringID(fullCmdline));
+ s.set_utime(entry.utime);
+ s.set_stime(entry.stime);
+ *(ret.add_stats()) = s;
+ }
+ }
+
+ if (isOthers)
+ {
+ bmcmetrics::metricproto::BmcProcStatMetric::BmcProcStat s;
+ s.set_sidx_cmdline(getStringID(others.cmdline));
+ s.set_utime(others.utime);
+ s.set_stime(others.stime);
+ *(ret.add_stats()) = s;
+ }
+
+ return ret;
+}
+
+int getFdCount(int pid)
+{
+ const std::string& fdPath = "/proc/" + std::to_string(pid) + "/fd";
+ return std::distance(std::filesystem::directory_iterator(fdPath),
+ std::filesystem::directory_iterator{});
+}
+
+struct FdStatEntry
+{
+ int fdCount;
+ std::string cmdline;
+ std::string tcomm;
+
+ // Processes with the largest fdCount goes first.
+ // Tie-breaking using cmdline then tcomm.
+ bool operator<(const FdStatEntry& other) const
+ {
+ const int negFdCount = -fdCount;
+ const int negOtherFdCount = -other.fdCount;
+ return std::tie(negFdCount, cmdline, tcomm) <
+ std::tie(negOtherFdCount, other.cmdline, other.tcomm);
+ }
+};
+
+bmcmetrics::metricproto::BmcFdStatMetric BmcHealthSnapshot::getFdStatList()
+{
+ bmcmetrics::metricproto::BmcFdStatMetric ret;
+
+ // Sort by fd count, no tie-breaking
+ std::vector<FdStatEntry> entries;
+
+ const std::string_view procPath = "/proc/";
+ for (const auto& procEntry : std::filesystem::directory_iterator(procPath))
+ {
+ const std::string& path = procEntry.path();
+ int pid = 0;
+ FdStatEntry entry;
+ if (isNumericPath(path, pid))
+ {
+ try
+ {
+ entry.fdCount = getFdCount(pid);
+ TcommUtimeStime t = getTcommUtimeStime(pid, ticksPerSec);
+ entry.cmdline = getCmdLine(pid);
+ entry.tcomm = t.tcomm;
+ entries.push_back(entry);
+ }
+ catch (const std::exception& e)
+ {
+ log<level::ERR>("Could not get file descriptor stats");
+ }
+ }
+ }
+
+ std::sort(entries.begin(), entries.end());
+
+ bool isOthers = false;
+
+ // Only report the detailed fd count and cmdline for the top 10 entries,
+ // and collapse all others into "others".
+ constexpr int topN = 10;
+
+ FdStatEntry others;
+ others.cmdline = "(Others)";
+ others.fdCount = 0;
+
+ for (size_t i = 0; i < entries.size(); ++i)
+ {
+ if (i >= topN)
+ {
+ isOthers = true;
+ }
+
+ const FdStatEntry& entry = entries[i];
+ if (isOthers)
+ {
+ others.fdCount += entry.fdCount;
+ }
+ else
+ {
+ bmcmetrics::metricproto::BmcFdStatMetric::BmcFdStat s;
+ std::string fullCmdline = entry.cmdline;
+ if (entry.tcomm.size() > 0)
+ {
+ fullCmdline += " " + entry.tcomm;
+ }
+ s.set_sidx_cmdline(getStringID(fullCmdline));
+ s.set_fd_count(entry.fdCount);
+ *(ret.add_stats()) = s;
+ }
+ }
+
+ if (isOthers)
+ {
+ bmcmetrics::metricproto::BmcFdStatMetric::BmcFdStat s;
+ s.set_sidx_cmdline(getStringID(others.cmdline));
+ s.set_fd_count(others.fdCount);
+ *(ret.add_stats()) = s;
+ }
+
+ return ret;
+}
+
+void BmcHealthSnapshot::serializeSnapshotToArray(
+ const bmcmetrics::metricproto::BmcMetricSnapshot& snapshot)
+{
+ size_t size = snapshot.ByteSizeLong();
+ if (size > 0)
+ {
+ pbDump.resize(size);
+ if (!snapshot.SerializeToArray(pbDump.data(), size))
+ {
+ log<level::ERR>("Could not serialize protobuf to array");
+ }
+ }
+}
+
+void BmcHealthSnapshot::doWork()
+{
+ bmcmetrics::metricproto::BmcMetricSnapshot snapshot;
+
+ // Memory info
+ std::string meminfoBuffer = readFileIntoString("/proc/meminfo");
+
+ {
+ bmcmetrics::metricproto::BmcMemoryMetric m;
+
+ std::string_view sv(meminfoBuffer.data());
+ // MemAvailable
+ int value;
+ bool ok = parseMeminfoValue(sv, "MemAvailable:", value);
+ if (ok)
+ {
+ m.set_mem_available(value);
+ }
+
+ ok = parseMeminfoValue(sv, "Slab:", value);
+ if (ok)
+ {
+ m.set_slab(value);
+ }
+
+ ok = parseMeminfoValue(sv, "KernelStack:", value);
+ if (ok)
+ {
+ m.set_kernel_stack(value);
+ }
+
+ *(snapshot.mutable_memory_metric()) = m;
+ }
+
+ // Uptime
+ std::string uptimeBuffer = readFileIntoString("/proc/uptime");
+ double uptime = 0, idleProcessTime = 0;
+ if (parseProcUptime(uptimeBuffer, uptime, idleProcessTime))
+ {
+ bmcmetrics::metricproto::BmcUptimeMetric m1;
+ m1.set_uptime(uptime);
+ m1.set_idle_process_time(idleProcessTime);
+ *(snapshot.mutable_uptime_metric()) = m1;
+ }
+ else
+ {
+ log<level::ERR>("Error parsing /proc/uptime");
+ }
+
+ // Storage space
+ struct statvfs fiData;
+ if ((statvfs("/", &fiData)) < 0)
+ {
+ log<level::ERR>("Could not call statvfs");
+ }
+ else
+ {
+ uint64_t kib = (fiData.f_bsize * fiData.f_bfree) / 1024;
+ bmcmetrics::metricproto::BmcDiskSpaceMetric m2;
+ m2.set_rwfs_kib_available(static_cast<int>(kib));
+ *(snapshot.mutable_storage_space_metric()) = m2;
+ }
+
+ // The next metrics require a sane ticks_per_sec value, typically 100 on
+ // the BMC. In the very rare circumstance when it's 0, exit early and return
+ // a partially complete snapshot (no process).
+ ticksPerSec = getTicksPerSec();
+
+ // FD stat
+ *(snapshot.mutable_fdstat_metric()) = getFdStatList();
+
+ if (ticksPerSec == 0)
+ {
+ log<level::ERR>("ticksPerSec is 0, skipping the process list metric");
+ serializeSnapshotToArray(snapshot);
+ done = true;
+ return;
+ }
+
+ // Proc stat
+ *(snapshot.mutable_procstat_metric()) = getProcStatList();
+
+ // String table
+ std::vector<std::string_view> strings(stringTable.size());
+ for (const auto& [s, i] : stringTable)
+ {
+ strings[i] = s;
+ }
+
+ bmcmetrics::metricproto::BmcStringTable st;
+ for (size_t i = 0; i < strings.size(); ++i)
+ {
+ bmcmetrics::metricproto::BmcStringTable::StringEntry entry;
+ entry.set_value(strings[i].data());
+ *(st.add_entries()) = entry;
+ }
+ *(snapshot.mutable_string_table()) = st;
+
+ // Save to buffer
+ serializeSnapshotToArray(snapshot);
+ done = true;
+}
+
+// BmcBlobSessionStat (9) but passing meta as reference instead of pointer,
+// since the metadata must not be null at this point.
+bool BmcHealthSnapshot::stat(blobs::BlobMeta& meta)
+{
+ if (!done)
+ {
+ // Bits 8~15 are blob-specific state flags.
+ // For this blob, bit 8 is set when metric collection is still in
+ // progress.
+ meta.blobState |= (1 << 8);
+ }
+ else
+ {
+ meta.blobState = 0;
+ meta.blobState = blobs::StateFlags::open_read;
+ meta.size = pbDump.size();
+ }
+ return true;
+}
+
+std::string_view BmcHealthSnapshot::read(uint32_t offset,
+ uint32_t requestedSize)
+{
+ uint32_t size = static_cast<uint32_t>(pbDump.size());
+ if (offset >= size)
+ {
+ return {};
+ }
+ return std::string_view(pbDump.data() + offset,
+ std::min(requestedSize, size - offset));
+}
+
+int BmcHealthSnapshot::getStringID(const std::string_view s)
+{
+ int ret = 0;
+ auto itr = stringTable.find(s.data());
+ if (itr == stringTable.end())
+ {
+ stringTable[s.data()] = stringId;
+ ret = stringId;
+ ++stringId;
+ }
+ else
+ {
+ ret = itr->second;
+ }
+ return ret;
+}
+
+} // namespace metric_blob
\ No newline at end of file
diff --git a/metrics-ipmi-blobs/metric.hpp b/metrics-ipmi-blobs/metric.hpp
new file mode 100644
index 0000000..5d0d49e
--- /dev/null
+++ b/metrics-ipmi-blobs/metric.hpp
@@ -0,0 +1,69 @@
+#pragma once
+
+#include "metricblob.pb.h"
+
+#include <unistd.h>
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <atomic>
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+namespace metric_blob
+{
+
+class BmcHealthSnapshot
+{
+ public:
+ BmcHealthSnapshot();
+
+ /**
+ * Reads data from this metric
+ * @param offset: offset into the data to read
+ * @param requestedSize: how many bytes to read
+ * @returns Bytes able to read. Returns empty if nothing can be read.
+ */
+ std::string_view read(uint32_t offset, uint32_t requestedSize);
+
+ /**
+ * Returns information about the amount of readable data and whether the
+ * metric has finished populating.
+ * @param meta: Struct to fill with the metadata info
+ */
+ bool stat(blobs::BlobMeta& meta);
+
+ /**
+ * Start the metric collection process
+ */
+ void doWork();
+
+ /**
+ * The size of the content string.
+ */
+ uint32_t size();
+
+ private:
+ /**
+ * Serialize to the pb_dump_ array.
+ */
+ void serializeSnapshotToArray(
+ const bmcmetrics::metricproto::BmcMetricSnapshot& snapshot);
+
+ // The two following functions access the snapshot's string table so they
+ // have to be member functions.
+ bmcmetrics::metricproto::BmcProcStatMetric getProcStatList();
+ bmcmetrics::metricproto::BmcFdStatMetric getFdStatList();
+
+ int getStringID(const std::string_view s);
+ std::atomic<bool> done;
+ std::vector<char> pbDump;
+ std::unordered_map<std::string, int> stringTable;
+ int stringId;
+ long ticksPerSec;
+};
+
+} // namespace metric_blob
diff --git a/metrics-ipmi-blobs/metricblob.proto b/metrics-ipmi-blobs/metricblob.proto
new file mode 100644
index 0000000..6cf6223
--- /dev/null
+++ b/metrics-ipmi-blobs/metricblob.proto
@@ -0,0 +1,52 @@
+syntax = "proto3";
+
+package bmcmetrics.metricproto;
+
+message BmcMemoryMetric {
+ int32 mem_available = 1;
+ int32 slab = 2;
+ int32 kernel_stack = 3;
+}
+
+message BmcUptimeMetric {
+ float uptime = 1; // Uptime (wall clock time)
+ float idle_process_time = 2; // Idle process time across all cores
+}
+
+message BmcDiskSpaceMetric {
+ int32 rwfs_kib_available = 1; // Free space in RWFS in KiB
+}
+
+// The following messages use string tables to save space
+message BmcProcStatMetric {
+ message BmcProcStat {
+ int32 sidx_cmdline = 1; // complete command line
+ float utime = 2; // Time (seconds) in user mode
+ float stime = 3; // Time (seconds) in kernel mode
+ }
+ repeated BmcProcStat stats = 10;
+}
+
+message BmcFdStatMetric {
+ message BmcFdStat {
+ int32 sidx_cmdline = 1; // complete command line
+ int32 fd_count = 2; // count of open FD's
+ }
+ repeated BmcFdStat stats = 10;
+}
+
+message BmcStringTable {
+ message StringEntry {
+ string value = 1;
+ }
+ repeated StringEntry entries = 10;
+}
+
+message BmcMetricSnapshot {
+ BmcStringTable string_table = 1;
+ BmcMemoryMetric memory_metric = 2;
+ BmcUptimeMetric uptime_metric = 3;
+ BmcDiskSpaceMetric storage_space_metric = 4;
+ BmcProcStatMetric procstat_metric = 5;
+ BmcFdStatMetric fdstat_metric = 6;
+}
diff --git a/metrics-ipmi-blobs/subprojects b/metrics-ipmi-blobs/subprojects
new file mode 120000
index 0000000..15858bc
--- /dev/null
+++ b/metrics-ipmi-blobs/subprojects
@@ -0,0 +1 @@
+../subprojects
\ No newline at end of file
diff --git a/metrics-ipmi-blobs/test/util_test.cpp b/metrics-ipmi-blobs/test/util_test.cpp
new file mode 100644
index 0000000..9b45502
--- /dev/null
+++ b/metrics-ipmi-blobs/test/util_test.cpp
@@ -0,0 +1,143 @@
+#include "util.hpp"
+
+#include <filesystem>
+#include <fstream>
+
+#include "gtest/gtest.h"
+
+TEST(IsNumericPath, invalidNumericPath)
+{
+ std::string badPath{"badNumericPath"};
+ int id = -1;
+ EXPECT_FALSE(metric_blob::isNumericPath(badPath, id));
+ EXPECT_EQ(id, -1);
+}
+
+TEST(IsNumericPath, validNumericPath)
+{
+ std::string goodPath{"proc/10000"};
+ int id = -1;
+ EXPECT_TRUE(metric_blob::isNumericPath(goodPath, id));
+ EXPECT_EQ(id, 10000);
+}
+
+TEST(ReadFileIntoString, goodFile)
+{
+ const std::string& fileName = "./test_file";
+ std::ofstream ofs(fileName, std::ios::trunc);
+ std::string_view content = "This is\ntest\tcontentt\n\n\n\n.\n\n##$#$";
+ ofs << content;
+ ofs.close();
+ std::string readContent = metric_blob::readFileIntoString(fileName);
+ std::filesystem::remove(fileName);
+ EXPECT_EQ(readContent, content);
+}
+
+TEST(ReadFileIntoString, inexistentFile)
+{
+ const std::string& fileName = "./inexistent_file";
+ std::string readContent = metric_blob::readFileIntoString(fileName);
+ EXPECT_EQ(readContent, "");
+}
+
+TEST(GetTcommUtimeStime, validInput)
+{
+ // ticks_per_sec is usually 100 on the BMC
+ const long ticksPerSec = 100;
+
+ const std::string_view content = "2596 (dbus-broker) R 2577 2577 2577 0 -1 "
+ "4194560 299 0 1 0 333037 246110 0 0 20 0 "
+ "1 0 1545 3411968 530 4294967295 65536 "
+ "246512 2930531712 0 0 0 81923 4";
+
+ metric_blob::TcommUtimeStime t =
+ metric_blob::parseTcommUtimeStimeString(content, ticksPerSec);
+ const float EPS = 0.01; // The difference was 0.000117188
+ EXPECT_LT(std::abs(t.utime - 3330.37), EPS);
+ EXPECT_LT(std::abs(t.stime - 2461.10), EPS);
+ EXPECT_EQ(t.tcomm, "(dbus-broker)");
+}
+
+TEST(GetTcommUtimeStime, invalidInput)
+{
+ // ticks_per_sec is usually 100 on the BMC
+ const long ticksPerSec = 100;
+
+ const std::string_view content =
+ "x invalid x x x x x x x x x x x x x x x x x x x x x x x x x x x";
+
+ metric_blob::TcommUtimeStime t =
+ metric_blob::parseTcommUtimeStimeString(content, ticksPerSec);
+
+ EXPECT_EQ(t.utime, 0);
+ EXPECT_EQ(t.stime, 0);
+ EXPECT_EQ(t.tcomm, "invalid");
+}
+
+TEST(ParseMeminfoValue, validInput)
+{
+ const std::string_view content = "MemTotal: 1027040 kB\n"
+ "MemFree: 868144 kB\n"
+ "MemAvailable: 919308 kB\n"
+ "Buffers: 13008 kB\n"
+ "Cached: 82840 kB\n"
+ "SwapCached: 0 kB\n"
+ "Active: 62076 kB\n";
+ int value;
+ EXPECT_TRUE(metric_blob::parseMeminfoValue(content, "MemTotal:", value));
+ EXPECT_EQ(value, 1027040);
+ EXPECT_TRUE(metric_blob::parseMeminfoValue(content, "MemFree:", value));
+ EXPECT_EQ(value, 868144);
+ EXPECT_TRUE(
+ metric_blob::parseMeminfoValue(content, "MemAvailable:", value));
+ EXPECT_EQ(value, 919308);
+ EXPECT_TRUE(metric_blob::parseMeminfoValue(content, "Buffers:", value));
+ EXPECT_EQ(value, 13008);
+ EXPECT_TRUE(metric_blob::parseMeminfoValue(content, "Cached:", value));
+ EXPECT_EQ(value, 82840);
+ EXPECT_TRUE(metric_blob::parseMeminfoValue(content, "SwapCached:", value));
+ EXPECT_EQ(value, 0);
+ EXPECT_TRUE(metric_blob::parseMeminfoValue(content, "Active:", value));
+ EXPECT_EQ(value, 62076);
+}
+
+TEST(ParseMeminfoValue, invalidInput)
+{
+ const std::string_view invalid = "MemTotal: 1";
+ int value = -999;
+ EXPECT_FALSE(metric_blob::parseMeminfoValue(invalid, "MemTotal:", value));
+ EXPECT_EQ(value, -999);
+ EXPECT_FALSE(metric_blob::parseMeminfoValue(invalid, "x", value));
+ EXPECT_EQ(value, -999);
+}
+
+TEST(ParseProcUptime, validInput)
+{
+ const std::string_view content = "266923.67 512184.95";
+ const double eps =
+ 1e-4; // Empirical threshold for floating point number compare
+ double uptime, idleProcessTime;
+ EXPECT_EQ(metric_blob::parseProcUptime(content, uptime, idleProcessTime),
+ true);
+ EXPECT_LT(abs(uptime - 266923.67), eps);
+ EXPECT_LT(abs(idleProcessTime - 512184.95), eps);
+}
+
+TEST(TrimStringRight, nonEmptyResult)
+{
+ EXPECT_EQ(
+ metric_blob::trimStringRight("\n\nabc\n\t\r\x00\x01\x02\x03").size(),
+ 5); // "\n\nabc" is left
+}
+
+TEST(TrimStringRight, trimToEmpty)
+{
+ EXPECT_TRUE(metric_blob::trimStringRight(" ").empty());
+ EXPECT_TRUE(metric_blob::trimStringRight("").empty());
+}
+
+int main(int argc, char** argv)
+{
+ ::testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
\ No newline at end of file
diff --git a/metrics-ipmi-blobs/util.cpp b/metrics-ipmi-blobs/util.cpp
new file mode 100644
index 0000000..6096d51
--- /dev/null
+++ b/metrics-ipmi-blobs/util.cpp
@@ -0,0 +1,209 @@
+#include "util.hpp"
+
+#include <unistd.h>
+
+#include <phosphor-logging/log.hpp>
+
+#include <cmath>
+#include <cstdlib>
+#include <fstream>
+#include <sstream>
+#include <string>
+#include <string_view>
+
+namespace metric_blob
+{
+
+using phosphor::logging::log;
+using level = phosphor::logging::level;
+
+char controlCharsToSpace(char c)
+{
+ if (c < 32)
+ {
+ c = ' ';
+ }
+ return c;
+}
+
+long getTicksPerSec()
+{
+ return sysconf(_SC_CLK_TCK);
+}
+
+std::string readFileIntoString(const std::string_view fileName)
+{
+ std::stringstream ss;
+ std::ifstream ifs(fileName.data());
+ while (ifs.good())
+ {
+ std::string line;
+ std::getline(ifs, line);
+ ss << line;
+ if (ifs.good())
+ ss << std::endl;
+ }
+ return ss.str();
+}
+
+bool isNumericPath(const std::string_view path, int& value)
+{
+ size_t p = path.rfind('/');
+ if (p == std::string::npos)
+ {
+ return false;
+ }
+ int id = 0;
+ for (size_t i = p + 1; i < path.size(); ++i)
+ {
+ const char ch = path[i];
+ if (ch < '0' || ch > '9')
+ return false;
+ else
+ {
+ id = id * 10 + (ch - '0');
+ }
+ }
+ value = id;
+ return true;
+}
+
+// Trims all control characters at the end of a string.
+std::string trimStringRight(std::string_view s)
+{
+ std::string ret(s.data());
+ while (!ret.empty())
+ {
+ if (ret.back() <= 32)
+ ret.pop_back();
+ else
+ break;
+ }
+ return ret;
+}
+
+std::string getCmdLine(const int pid)
+{
+ const std::string& cmdlinePath =
+ "/proc/" + std::to_string(pid) + "/cmdline";
+
+ std::string cmdline = readFileIntoString(cmdlinePath);
+ for (size_t i = 0; i < cmdline.size(); ++i)
+ {
+ cmdline[i] = controlCharsToSpace(cmdline[i]);
+ }
+
+ // Trim empty strings
+ cmdline = trimStringRight(cmdline);
+
+ return cmdline;
+}
+
+// strtok is used in this function in order to avoid usage of <sstream>.
+// However, that would require us to create a temporary std::string.
+TcommUtimeStime parseTcommUtimeStimeString(std::string_view content,
+ const long ticksPerSec)
+{
+ TcommUtimeStime ret;
+ ret.tcomm = "";
+ ret.utime = ret.stime = 0;
+
+ const float invTicksPerSec = 1.0f / static_cast<float>(ticksPerSec);
+
+ // pCol now points to the first part in content after content is split by
+ // space.
+ // This is not ideal,
+ std::string temp(content);
+ char* pCol = strtok(temp.data(), " ");
+
+ if (pCol != nullptr)
+ {
+ const int fields[] = {1, 13, 14}; // tcomm, utime, stime
+ int fieldIdx = 0;
+ for (int colIdx = 0; colIdx < 15; ++colIdx)
+ {
+ if (fieldIdx < 3 && colIdx == fields[fieldIdx])
+ {
+ switch (fieldIdx)
+ {
+ case 0:
+ {
+ ret.tcomm = std::string(pCol);
+ break;
+ }
+ case 1:
+ [[fallthrough]];
+ case 2:
+ {
+ int ticks = std::atoi(pCol);
+ float t = static_cast<float>(ticks) * invTicksPerSec;
+
+ if (fieldIdx == 1)
+ {
+ ret.utime = t;
+ }
+ else if (fieldIdx == 2)
+ {
+ ret.stime = t;
+ }
+ break;
+ }
+ }
+ ++fieldIdx;
+ }
+ pCol = strtok(nullptr, " ");
+ }
+ }
+
+ if (ticksPerSec <= 0)
+ {
+ log<level::ERR>("ticksPerSec is equal or less than zero");
+ }
+
+ return ret;
+}
+
+TcommUtimeStime getTcommUtimeStime(const int pid, const long ticksPerSec)
+{
+ const std::string& statPath = "/proc/" + std::to_string(pid) + "/stat";
+ return parseTcommUtimeStimeString(readFileIntoString(statPath),
+ ticksPerSec);
+}
+
+// Returns true if successfully parsed and false otherwise. If parsing was
+// successful, value is set accordingly.
+// Input: "MemAvailable: 1234 kB"
+// Returns true, value set to 1234
+bool parseMeminfoValue(const std::string_view content,
+ const std::string_view keyword, int& value)
+{
+ size_t p = content.find(keyword);
+ if (p != std::string::npos)
+ {
+ std::string_view v = content.substr(p + keyword.size());
+ p = v.find("kB");
+ if (p != std::string::npos)
+ {
+ v = v.substr(0, p);
+ value = std::atoi(v.data());
+ return true;
+ }
+ }
+ return false;
+}
+
+bool parseProcUptime(const std::string_view content, double& uptime,
+ double& idleProcessTime)
+{
+ double t0, t1; // Attempts to parse uptime and idleProcessTime
+ int ret = sscanf(content.data(), "%lf %lf", &t0, &t1);
+ if (ret == 2 && std::isfinite(t0) && std::isfinite(t1))
+ {
+ uptime = t0;
+ idleProcessTime = t1;
+ return true;
+ }
+ return false;
+}
+
+} // namespace metric_blob
\ No newline at end of file
diff --git a/metrics-ipmi-blobs/util.hpp b/metrics-ipmi-blobs/util.hpp
new file mode 100644
index 0000000..b74b589
--- /dev/null
+++ b/metrics-ipmi-blobs/util.hpp
@@ -0,0 +1,33 @@
+#ifndef _UTIL_HPP_
+#define _UTIL_HPP_
+
+#include <string>
+#include <string_view>
+
+namespace metric_blob
+{
+
+struct TcommUtimeStime
+{
+ std::string tcomm;
+ float utime;
+ float stime;
+};
+
+TcommUtimeStime parseTcommUtimeStimeString(std::string_view content,
+ long ticksPerSec);
+std::string readFileIntoString(std::string_view fileName);
+bool isNumericPath(std::string_view path, int& value);
+TcommUtimeStime getTcommUtimeStime(int pid, long ticksPerSec);
+std::string getCmdLine(int pid);
+bool parseMeminfoValue(std::string_view content, std::string_view keyword,
+ int& value);
+bool parseProcUptime(const std::string_view content, double& uptime,
+ double& idleProcessTime);
+long getTicksPerSec();
+char controlCharsToSpace(char c);
+std::string trimStringRight(std::string_view s);
+
+} // namespace metric_blob
+
+#endif
\ No newline at end of file
diff --git a/subprojects/metrics-ipmi-blobs b/subprojects/metrics-ipmi-blobs
new file mode 120000
index 0000000..360cb5a
--- /dev/null
+++ b/subprojects/metrics-ipmi-blobs
@@ -0,0 +1 @@
+../metrics-ipmi-blobs
\ No newline at end of file