1 // Copyright 2021 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 #include "metric.hpp" 16 17 #include "metricblob.pb.n.h" 18 19 #include "util.hpp" 20 21 #include <pb_encode.h> 22 #include <sys/statvfs.h> 23 24 #include <phosphor-logging/log.hpp> 25 26 #include <cstdint> 27 #include <filesystem> 28 #include <sstream> 29 #include <string> 30 #include <string_view> 31 32 namespace metric_blob 33 { 34 35 using phosphor::logging::entry; 36 using phosphor::logging::log; 37 using level = phosphor::logging::level; 38 39 BmcHealthSnapshot::BmcHealthSnapshot() : 40 done(false), stringId(0), ticksPerSec(0) 41 {} 42 43 template <typename T> 44 static constexpr auto pbEncodeStr = [](pb_ostream_t* stream, 45 const pb_field_iter_t* field, 46 void* const* arg) noexcept { 47 static_assert(sizeof(*std::declval<T>().data()) == sizeof(pb_byte_t)); 48 const auto& s = *reinterpret_cast<const T*>(*arg); 49 return pb_encode_tag_for_field(stream, field) && 50 pb_encode_string( 51 stream, reinterpret_cast<const pb_byte_t*>(s.data()), s.size()); 52 }; 53 54 template <typename T> 55 static pb_callback_t pbStrEncoder(const T& t) noexcept 56 { 57 return {{.encode = pbEncodeStr<T>}, const_cast<T*>(&t)}; 58 } 59 60 template <auto fields, typename T> 61 static constexpr auto pbEncodeSubs = [](pb_ostream_t* stream, 62 const pb_field_iter_t* field, 63 void* const* arg) noexcept { 64 for (const auto& sub : *reinterpret_cast<const std::vector<T>*>(*arg)) 65 { 66 if (!pb_encode_tag_for_field(stream, field) || 67 !pb_encode_submessage(stream, fields, &sub)) 68 { 69 return false; 70 } 71 } 72 return true; 73 }; 74 75 template <auto fields, typename T> 76 static pb_callback_t pbSubsEncoder(const std::vector<T>& t) 77 { 78 return {{.encode = pbEncodeSubs<fields, T>}, 79 const_cast<std::vector<T>*>(&t)}; 80 } 81 82 struct ProcStatEntry 83 { 84 std::string cmdline; 85 std::string tcomm; 86 float utime; 87 float stime; 88 89 // Processes with the longest utime + stime are ranked first. 90 // Tie breaking is done with cmdline then tcomm. 91 bool operator<(const ProcStatEntry& other) const 92 { 93 const float negTime = -(utime + stime); 94 const float negOtherTime = -(other.utime + other.stime); 95 return std::tie(negTime, cmdline, tcomm) < 96 std::tie(negOtherTime, other.cmdline, other.tcomm); 97 } 98 }; 99 100 static bmcmetrics_metricproto_BmcProcStatMetric getProcStatMetric( 101 BmcHealthSnapshot& obj, long ticksPerSec, 102 std::vector<bmcmetrics_metricproto_BmcProcStatMetric_BmcProcStat>& procs, 103 bool& use) noexcept 104 { 105 if (ticksPerSec == 0) 106 { 107 return {}; 108 } 109 constexpr std::string_view procPath = "/proc/"; 110 111 std::vector<ProcStatEntry> entries; 112 113 for (const auto& procEntry : std::filesystem::directory_iterator(procPath)) 114 { 115 const std::string& path = procEntry.path(); 116 int pid = -1; 117 if (isNumericPath(path, pid)) 118 { 119 ProcStatEntry entry; 120 121 try 122 { 123 entry.cmdline = getCmdLine(pid); 124 TcommUtimeStime t = getTcommUtimeStime(pid, ticksPerSec); 125 entry.tcomm = t.tcomm; 126 entry.utime = t.utime; 127 entry.stime = t.stime; 128 129 entries.push_back(entry); 130 } 131 catch (const std::exception& e) 132 { 133 log<level::ERR>("Could not obtain process stats"); 134 } 135 } 136 } 137 138 std::sort(entries.begin(), entries.end()); 139 140 bool isOthers = false; 141 ProcStatEntry others; 142 others.cmdline = "(Others)"; 143 others.utime = others.stime = 0; 144 145 // Only show this many processes and aggregate all remaining ones into 146 // "others" in order to keep the size of the snapshot reasonably small. 147 // With 10 process stat entries and 10 FD count entries, the size of the 148 // snapshot reaches around 1.5KiB. This is non-trivial, and we have to set 149 // the collection interval long enough so as not to over-stress the IPMI 150 // interface and the data collection service. The value of 10 is chosen 151 // empirically, it might be subject to adjustments when the system is 152 // launched later. 153 constexpr int topN = 10; 154 155 for (size_t i = 0; i < entries.size(); ++i) 156 { 157 if (i >= topN) 158 { 159 isOthers = true; 160 } 161 162 const ProcStatEntry& entry = entries[i]; 163 164 if (isOthers) 165 { 166 others.utime += entry.utime; 167 others.stime += entry.stime; 168 } 169 else 170 { 171 std::string fullCmdline = entry.cmdline; 172 if (entry.tcomm.size() > 0) 173 { 174 fullCmdline += " "; 175 fullCmdline += entry.tcomm; 176 } 177 procs.emplace_back( 178 bmcmetrics_metricproto_BmcProcStatMetric_BmcProcStat{ 179 .sidx_cmdline = obj.getStringID(fullCmdline), 180 .utime = entry.utime, 181 .stime = entry.stime, 182 }); 183 } 184 } 185 186 if (isOthers) 187 { 188 procs.emplace_back(bmcmetrics_metricproto_BmcProcStatMetric_BmcProcStat{ 189 .sidx_cmdline = obj.getStringID(others.cmdline), 190 .utime = others.utime, 191 .stime = others.stime, 192 193 }); 194 } 195 196 use = true; 197 return bmcmetrics_metricproto_BmcProcStatMetric{ 198 .stats = pbSubsEncoder< 199 bmcmetrics_metricproto_BmcProcStatMetric_BmcProcStat_fields>(procs), 200 }; 201 } 202 203 int getFdCount(int pid) 204 { 205 const std::string& fdPath = "/proc/" + std::to_string(pid) + "/fd"; 206 return std::distance(std::filesystem::directory_iterator(fdPath), 207 std::filesystem::directory_iterator{}); 208 } 209 210 struct FdStatEntry 211 { 212 int fdCount; 213 std::string cmdline; 214 std::string tcomm; 215 216 // Processes with the largest fdCount goes first. 217 // Tie-breaking using cmdline then tcomm. 218 bool operator<(const FdStatEntry& other) const 219 { 220 const int negFdCount = -fdCount; 221 const int negOtherFdCount = -other.fdCount; 222 return std::tie(negFdCount, cmdline, tcomm) < 223 std::tie(negOtherFdCount, other.cmdline, other.tcomm); 224 } 225 }; 226 227 static bmcmetrics_metricproto_BmcFdStatMetric getFdStatMetric( 228 BmcHealthSnapshot& obj, long ticksPerSec, 229 std::vector<bmcmetrics_metricproto_BmcFdStatMetric_BmcFdStat>& fds, 230 bool& use) noexcept 231 { 232 if (ticksPerSec == 0) 233 { 234 return {}; 235 } 236 237 // Sort by fd count, no tie-breaking 238 std::vector<FdStatEntry> entries; 239 240 const std::string_view procPath = "/proc/"; 241 for (const auto& procEntry : std::filesystem::directory_iterator(procPath)) 242 { 243 const std::string& path = procEntry.path(); 244 int pid = 0; 245 FdStatEntry entry; 246 if (isNumericPath(path, pid)) 247 { 248 try 249 { 250 entry.fdCount = getFdCount(pid); 251 TcommUtimeStime t = getTcommUtimeStime(pid, ticksPerSec); 252 entry.cmdline = getCmdLine(pid); 253 entry.tcomm = t.tcomm; 254 entries.push_back(entry); 255 } 256 catch (const std::exception& e) 257 { 258 log<level::ERR>("Could not get file descriptor stats"); 259 } 260 } 261 } 262 263 std::sort(entries.begin(), entries.end()); 264 265 bool isOthers = false; 266 267 // Only report the detailed fd count and cmdline for the top 10 entries, 268 // and collapse all others into "others". 269 constexpr int topN = 10; 270 271 FdStatEntry others; 272 others.cmdline = "(Others)"; 273 others.fdCount = 0; 274 275 for (size_t i = 0; i < entries.size(); ++i) 276 { 277 if (i >= topN) 278 { 279 isOthers = true; 280 } 281 282 const FdStatEntry& entry = entries[i]; 283 if (isOthers) 284 { 285 others.fdCount += entry.fdCount; 286 } 287 else 288 { 289 std::string fullCmdline = entry.cmdline; 290 if (entry.tcomm.size() > 0) 291 { 292 fullCmdline += " "; 293 fullCmdline += entry.tcomm; 294 } 295 fds.emplace_back(bmcmetrics_metricproto_BmcFdStatMetric_BmcFdStat{ 296 .sidx_cmdline = obj.getStringID(fullCmdline), 297 .fd_count = entry.fdCount, 298 }); 299 } 300 } 301 302 if (isOthers) 303 { 304 fds.emplace_back(bmcmetrics_metricproto_BmcFdStatMetric_BmcFdStat{ 305 .sidx_cmdline = obj.getStringID(others.cmdline), 306 .fd_count = others.fdCount, 307 }); 308 } 309 310 use = true; 311 return bmcmetrics_metricproto_BmcFdStatMetric{ 312 .stats = pbSubsEncoder< 313 bmcmetrics_metricproto_BmcFdStatMetric_BmcFdStat_fields>(fds), 314 }; 315 } 316 317 static bmcmetrics_metricproto_BmcMemoryMetric getMemMetric() noexcept 318 { 319 bmcmetrics_metricproto_BmcMemoryMetric ret = {}; 320 auto data = readFileThenGrepIntoString("/proc/meminfo"); 321 int value; 322 if (parseMeminfoValue(data, "MemAvailable:", value)) 323 { 324 ret.mem_available = value; 325 } 326 if (parseMeminfoValue(data, "Slab:", value)) 327 { 328 ret.slab = value; 329 } 330 331 if (parseMeminfoValue(data, "KernelStack:", value)) 332 { 333 ret.kernel_stack = value; 334 } 335 return ret; 336 } 337 338 static bmcmetrics_metricproto_BmcUptimeMetric 339 getUptimeMetric(bool& use) noexcept 340 { 341 bmcmetrics_metricproto_BmcUptimeMetric ret = {}; 342 343 double uptime = 0; 344 { 345 auto data = readFileThenGrepIntoString("/proc/uptime"); 346 double idleProcessTime = 0; 347 if (!parseProcUptime(data, uptime, idleProcessTime)) 348 { 349 log<level::ERR>("Error parsing /proc/uptime"); 350 return ret; 351 } 352 ret.uptime = uptime; 353 ret.idle_process_time = idleProcessTime; 354 } 355 356 BootTimesMonotonic btm; 357 if (!getBootTimesMonotonic(btm)) 358 { 359 log<level::ERR>("Could not get boot time"); 360 return ret; 361 } 362 if (btm.firmwareTime == 0 && btm.powerOnSecCounterTime != 0) 363 { 364 ret.firmware_boot_time_sec = 365 static_cast<double>(btm.powerOnSecCounterTime) - uptime; 366 } 367 else 368 { 369 ret.firmware_boot_time_sec = 370 static_cast<double>(btm.firmwareTime - btm.loaderTime) / 1e6; 371 } 372 ret.loader_boot_time_sec = static_cast<double>(btm.loaderTime) / 1e6; 373 if (btm.initrdTime != 0) 374 { 375 ret.kernel_boot_time_sec = static_cast<double>(btm.initrdTime) / 1e6; 376 ret.initrd_boot_time_sec = 377 static_cast<double>(btm.userspaceTime - btm.initrdTime) / 1e6; 378 ret.userspace_boot_time_sec = 379 static_cast<double>(btm.finishTime - btm.userspaceTime) / 1e6; 380 } 381 else 382 { 383 ret.kernel_boot_time_sec = static_cast<double>(btm.userspaceTime) / 1e6; 384 ret.initrd_boot_time_sec = 0; 385 ret.userspace_boot_time_sec = 386 static_cast<double>(btm.finishTime - btm.userspaceTime) / 1e6; 387 } 388 389 use = true; 390 return ret; 391 } 392 393 static bmcmetrics_metricproto_BmcDiskSpaceMetric 394 getStorageMetric(bool& use) noexcept 395 { 396 bmcmetrics_metricproto_BmcDiskSpaceMetric ret = {}; 397 struct statvfs fiData; 398 if (statvfs("/", &fiData) < 0) 399 { 400 log<level::ERR>("Could not call statvfs"); 401 } 402 else 403 { 404 ret.rwfs_kib_available = (fiData.f_bsize * fiData.f_bfree) / 1024; 405 use = true; 406 } 407 return ret; 408 } 409 410 void BmcHealthSnapshot::doWork() 411 { 412 // The next metrics require a sane ticks_per_sec value, typically 100 on 413 // the BMC. In the very rare circumstance when it's 0, exit early and return 414 // a partially complete snapshot (no process). 415 ticksPerSec = getTicksPerSec(); 416 417 static constexpr auto stcb = [](pb_ostream_t* stream, 418 const pb_field_t* field, 419 void* const* arg) noexcept { 420 auto& self = *reinterpret_cast<BmcHealthSnapshot*>(*arg); 421 std::vector<std::string_view> strs(self.stringTable.size()); 422 for (const auto& [str, i] : self.stringTable) 423 { 424 strs[i] = str; 425 } 426 for (auto& str : strs) 427 { 428 bmcmetrics_metricproto_BmcStringTable_StringEntry msg = { 429 .value = pbStrEncoder(str), 430 }; 431 if (!pb_encode_tag_for_field(stream, field) || 432 !pb_encode_submessage( 433 stream, 434 bmcmetrics_metricproto_BmcStringTable_StringEntry_fields, 435 &msg)) 436 { 437 return false; 438 } 439 } 440 return true; 441 }; 442 std::vector<bmcmetrics_metricproto_BmcProcStatMetric_BmcProcStat> procs; 443 std::vector<bmcmetrics_metricproto_BmcFdStatMetric_BmcFdStat> fds; 444 bmcmetrics_metricproto_BmcMetricSnapshot snapshot = { 445 .has_string_table = true, 446 .string_table = 447 { 448 .entries = {{.encode = stcb}, this}, 449 }, 450 .has_memory_metric = true, 451 .memory_metric = getMemMetric(), 452 .has_uptime_metric = false, 453 .uptime_metric = getUptimeMetric(snapshot.has_uptime_metric), 454 .has_storage_space_metric = false, 455 .storage_space_metric = 456 getStorageMetric(snapshot.has_storage_space_metric), 457 .has_procstat_metric = false, 458 .procstat_metric = getProcStatMetric(*this, ticksPerSec, procs, 459 snapshot.has_procstat_metric), 460 .has_fdstat_metric = false, 461 .fdstat_metric = getFdStatMetric(*this, ticksPerSec, fds, 462 snapshot.has_fdstat_metric), 463 }; 464 pb_ostream_t nost = {}; 465 if (!pb_encode(&nost, bmcmetrics_metricproto_BmcMetricSnapshot_fields, 466 &snapshot)) 467 { 468 auto msg = std::format("Getting pb size: {}", PB_GET_ERROR(&nost)); 469 log<level::ERR>(msg.c_str()); 470 return; 471 } 472 pbDump.resize(nost.bytes_written); 473 auto ost = pb_ostream_from_buffer( 474 reinterpret_cast<pb_byte_t*>(pbDump.data()), pbDump.size()); 475 if (!pb_encode(&ost, bmcmetrics_metricproto_BmcMetricSnapshot_fields, 476 &snapshot)) 477 { 478 auto msg = std::format("Writing pb msg: {}", PB_GET_ERROR(&ost)); 479 log<level::ERR>(msg.c_str()); 480 return; 481 } 482 done = true; 483 } 484 485 // BmcBlobSessionStat (9) but passing meta as reference instead of pointer, 486 // since the metadata must not be null at this point. 487 bool BmcHealthSnapshot::stat(blobs::BlobMeta& meta) 488 { 489 if (!done) 490 { 491 // Bits 8~15 are blob-specific state flags. 492 // For this blob, bit 8 is set when metric collection is still in 493 // progress. 494 meta.blobState |= (1 << 8); 495 } 496 else 497 { 498 meta.blobState = 0; 499 meta.blobState = blobs::StateFlags::open_read; 500 meta.size = pbDump.size(); 501 } 502 return true; 503 } 504 505 std::string_view BmcHealthSnapshot::read(uint32_t offset, 506 uint32_t requestedSize) 507 { 508 uint32_t size = static_cast<uint32_t>(pbDump.size()); 509 if (offset >= size) 510 { 511 return {}; 512 } 513 return std::string_view(pbDump.data() + offset, 514 std::min(requestedSize, size - offset)); 515 } 516 517 int BmcHealthSnapshot::getStringID(const std::string_view s) 518 { 519 int ret = 0; 520 auto itr = stringTable.find(s.data()); 521 if (itr == stringTable.end()) 522 { 523 stringTable[s.data()] = stringId; 524 ret = stringId; 525 ++stringId; 526 } 527 else 528 { 529 ret = itr->second; 530 } 531 return ret; 532 } 533 534 } // namespace metric_blob 535