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