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