blob: bc8ac943706c74c2ff69c14805a8b83fb14fdde8 [file] [log] [blame]
// Copyright 2021 Google LLC
//
// 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 "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 = readFileThenGrepIntoString("/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 = readFileThenGrepIntoString("/proc/uptime");
double uptime = 0;
double idleProcessTime = 0;
BootTimesMonotonic btm;
if (!parseProcUptime(uptimeBuffer, uptime, idleProcessTime))
{
log<level::ERR>("Error parsing /proc/uptime");
}
else if (!getBootTimesMonotonic(btm))
{
log<level::ERR>("Could not get boot time");
}
else
{
bmcmetrics::metricproto::BmcUptimeMetric m1;
m1.set_uptime(uptime);
m1.set_idle_process_time(idleProcessTime);
if (btm.firmwareTime == 0 && btm.powerOnSecCounterTime != 0)
{
m1.set_firmware_boot_time_sec(
static_cast<double>(btm.powerOnSecCounterTime) - uptime);
}
else
{
m1.set_firmware_boot_time_sec(
static_cast<double>(btm.firmwareTime - btm.loaderTime) / 1e6);
}
m1.set_loader_boot_time_sec(static_cast<double>(btm.loaderTime) / 1e6);
// initrf presents
if (btm.initrdTime != 0)
{
m1.set_kernel_boot_time_sec(static_cast<double>(btm.initrdTime) /
1e6);
m1.set_initrd_boot_time_sec(
static_cast<double>(btm.userspaceTime - btm.initrdTime) / 1e6);
m1.set_userspace_boot_time_sec(
static_cast<double>(btm.finishTime - btm.userspaceTime) / 1e6);
}
else
{
m1.set_kernel_boot_time_sec(static_cast<double>(btm.userspaceTime) /
1e6);
m1.set_initrd_boot_time_sec(0);
m1.set_userspace_boot_time_sec(
static_cast<double>(btm.finishTime - btm.userspaceTime) / 1e6);
}
*(snapshot.mutable_uptime_metric()) = m1;
}
// 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