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_BmcECCMetric getECCMetric(bool& use) noexcept
318 {
319     EccCounts eccCounts;
320     use = getECCErrorCounts(eccCounts);
321     if (!use)
322     {
323         return {};
324     }
325     return bmcmetrics_metricproto_BmcECCMetric{
326         .correctable_error_count = eccCounts.correctableErrCount,
327         .uncorrectable_error_count = eccCounts.uncorrectableErrCount,
328     };
329 }
330 
331 static bmcmetrics_metricproto_BmcMemoryMetric getMemMetric() noexcept
332 {
333     bmcmetrics_metricproto_BmcMemoryMetric ret = {};
334     auto data = readFileThenGrepIntoString("/proc/meminfo");
335     int value;
336     if (parseMeminfoValue(data, "MemAvailable:", value))
337     {
338         ret.mem_available = value;
339     }
340     if (parseMeminfoValue(data, "Slab:", value))
341     {
342         ret.slab = value;
343     }
344 
345     if (parseMeminfoValue(data, "KernelStack:", value))
346     {
347         ret.kernel_stack = value;
348     }
349     return ret;
350 }
351 
352 static bmcmetrics_metricproto_BmcUptimeMetric
353     getUptimeMetric(bool& use) noexcept
354 {
355     bmcmetrics_metricproto_BmcUptimeMetric ret = {};
356 
357     double uptime = 0;
358     {
359         auto data = readFileThenGrepIntoString("/proc/uptime");
360         double idleProcessTime = 0;
361         if (!parseProcUptime(data, uptime, idleProcessTime))
362         {
363             log<level::ERR>("Error parsing /proc/uptime");
364             return ret;
365         }
366         ret.uptime = uptime;
367         ret.idle_process_time = idleProcessTime;
368     }
369 
370     BootTimesMonotonic btm;
371     if (!getBootTimesMonotonic(btm))
372     {
373         log<level::ERR>("Could not get boot time");
374         return ret;
375     }
376     if (btm.firmwareTime == 0 && btm.powerOnSecCounterTime != 0)
377     {
378         ret.firmware_boot_time_sec =
379             static_cast<double>(btm.powerOnSecCounterTime) - uptime;
380     }
381     else
382     {
383         ret.firmware_boot_time_sec =
384             static_cast<double>(btm.firmwareTime - btm.loaderTime) / 1e6;
385     }
386     ret.loader_boot_time_sec = static_cast<double>(btm.loaderTime) / 1e6;
387     if (btm.initrdTime != 0)
388     {
389         ret.kernel_boot_time_sec = static_cast<double>(btm.initrdTime) / 1e6;
390         ret.initrd_boot_time_sec =
391             static_cast<double>(btm.userspaceTime - btm.initrdTime) / 1e6;
392         ret.userspace_boot_time_sec =
393             static_cast<double>(btm.finishTime - btm.userspaceTime) / 1e6;
394     }
395     else
396     {
397         ret.kernel_boot_time_sec = static_cast<double>(btm.userspaceTime) / 1e6;
398         ret.initrd_boot_time_sec = 0;
399         ret.userspace_boot_time_sec =
400             static_cast<double>(btm.finishTime - btm.userspaceTime) / 1e6;
401     }
402 
403     use = true;
404     return ret;
405 }
406 
407 static bmcmetrics_metricproto_BmcDiskSpaceMetric
408     getStorageMetric(bool& use) noexcept
409 {
410     bmcmetrics_metricproto_BmcDiskSpaceMetric ret = {};
411     struct statvfs fiData;
412     if (statvfs("/", &fiData) < 0)
413     {
414         log<level::ERR>("Could not call statvfs");
415     }
416     else
417     {
418         ret.rwfs_kib_available = (fiData.f_bsize * fiData.f_bfree) / 1024;
419         use = true;
420     }
421     return ret;
422 }
423 
424 void BmcHealthSnapshot::doWork()
425 {
426     // The next metrics require a sane ticks_per_sec value, typically 100 on
427     // the BMC. In the very rare circumstance when it's 0, exit early and return
428     // a partially complete snapshot (no process).
429     ticksPerSec = getTicksPerSec();
430 
431     static constexpr auto stcb = [](pb_ostream_t* stream,
432                                     const pb_field_t* field,
433                                     void* const* arg) noexcept {
434         auto& self = *reinterpret_cast<BmcHealthSnapshot*>(*arg);
435         std::vector<std::string_view> strs(self.stringTable.size());
436         for (const auto& [str, i] : self.stringTable)
437         {
438             strs[i] = str;
439         }
440         for (auto& str : strs)
441         {
442             bmcmetrics_metricproto_BmcStringTable_StringEntry msg = {
443                 .value = pbStrEncoder(str),
444             };
445             if (!pb_encode_tag_for_field(stream, field) ||
446                 !pb_encode_submessage(
447                     stream,
448                     bmcmetrics_metricproto_BmcStringTable_StringEntry_fields,
449                     &msg))
450             {
451                 return false;
452             }
453         }
454         return true;
455     };
456     std::vector<bmcmetrics_metricproto_BmcProcStatMetric_BmcProcStat> procs;
457     std::vector<bmcmetrics_metricproto_BmcFdStatMetric_BmcFdStat> fds;
458     bmcmetrics_metricproto_BmcMetricSnapshot snapshot = {
459         .has_string_table = true,
460         .string_table =
461             {
462                 .entries = {{.encode = stcb}, this},
463             },
464         .has_memory_metric = true,
465         .memory_metric = getMemMetric(),
466         .has_uptime_metric = false,
467         .uptime_metric = getUptimeMetric(snapshot.has_uptime_metric),
468         .has_storage_space_metric = false,
469         .storage_space_metric =
470             getStorageMetric(snapshot.has_storage_space_metric),
471         .has_procstat_metric = false,
472         .procstat_metric = getProcStatMetric(*this, ticksPerSec, procs,
473                                              snapshot.has_procstat_metric),
474         .has_fdstat_metric = false,
475         .fdstat_metric = getFdStatMetric(*this, ticksPerSec, fds,
476                                          snapshot.has_fdstat_metric),
477         .has_ecc_metric = false,
478         .ecc_metric = getECCMetric(snapshot.has_ecc_metric),
479     };
480     pb_ostream_t nost = {};
481     if (!pb_encode(&nost, bmcmetrics_metricproto_BmcMetricSnapshot_fields,
482                    &snapshot))
483     {
484         auto msg = std::format("Getting pb size: {}", PB_GET_ERROR(&nost));
485         log<level::ERR>(msg.c_str());
486         return;
487     }
488     pbDump.resize(nost.bytes_written);
489     auto ost = pb_ostream_from_buffer(
490         reinterpret_cast<pb_byte_t*>(pbDump.data()), pbDump.size());
491     if (!pb_encode(&ost, bmcmetrics_metricproto_BmcMetricSnapshot_fields,
492                    &snapshot))
493     {
494         auto msg = std::format("Writing pb msg: {}", PB_GET_ERROR(&ost));
495         log<level::ERR>(msg.c_str());
496         return;
497     }
498     done = true;
499 }
500 
501 // BmcBlobSessionStat (9) but passing meta as reference instead of pointer,
502 // since the metadata must not be null at this point.
503 bool BmcHealthSnapshot::stat(blobs::BlobMeta& meta)
504 {
505     if (!done)
506     {
507         // Bits 8~15 are blob-specific state flags.
508         // For this blob, bit 8 is set when metric collection is still in
509         // progress.
510         meta.blobState |= (1 << 8);
511     }
512     else
513     {
514         meta.blobState = 0;
515         meta.blobState = blobs::StateFlags::open_read;
516         meta.size = pbDump.size();
517     }
518     return true;
519 }
520 
521 std::string_view BmcHealthSnapshot::read(uint32_t offset,
522                                          uint32_t requestedSize)
523 {
524     uint32_t size = static_cast<uint32_t>(pbDump.size());
525     if (offset >= size)
526     {
527         return {};
528     }
529     return std::string_view(pbDump.data() + offset,
530                             std::min(requestedSize, size - offset));
531 }
532 
533 int BmcHealthSnapshot::getStringID(const std::string_view s)
534 {
535     int ret = 0;
536     auto itr = stringTable.find(s.data());
537     if (itr == stringTable.end())
538     {
539         stringTable[s.data()] = stringId;
540         ret = stringId;
541         ++stringId;
542     }
543     else
544     {
545         ret = itr->second;
546     }
547     return ret;
548 }
549 
550 } // namespace metric_blob
551