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.h"
18 
19 #include "util.hpp"
20 
21 #include <sys/statvfs.h>
22 
23 #include <phosphor-logging/log.hpp>
24 
25 #include <cstdint>
26 #include <filesystem>
27 #include <sstream>
28 #include <string>
29 #include <string_view>
30 
31 namespace metric_blob
32 {
33 
34 using phosphor::logging::entry;
35 using phosphor::logging::log;
36 using level = phosphor::logging::level;
37 
38 BmcHealthSnapshot::BmcHealthSnapshot() :
39     done(false), stringId(0), ticksPerSec(0)
40 {}
41 
42 struct ProcStatEntry
43 {
44     std::string cmdline;
45     std::string tcomm;
46     float utime;
47     float stime;
48 
49     // Processes with the longest utime + stime are ranked first.
50     // Tie breaking is done with cmdline then tcomm.
51     bool operator<(const ProcStatEntry& other) const
52     {
53         const float negTime = -(utime + stime);
54         const float negOtherTime = -(other.utime + other.stime);
55         return std::tie(negTime, cmdline, tcomm) <
56                std::tie(negOtherTime, other.cmdline, other.tcomm);
57     }
58 };
59 
60 bmcmetrics::metricproto::BmcProcStatMetric BmcHealthSnapshot::getProcStatList()
61 {
62     constexpr std::string_view procPath = "/proc/";
63 
64     bmcmetrics::metricproto::BmcProcStatMetric ret;
65     std::vector<ProcStatEntry> entries;
66 
67     for (const auto& procEntry : std::filesystem::directory_iterator(procPath))
68     {
69         const std::string& path = procEntry.path();
70         int pid = -1;
71         if (isNumericPath(path, pid))
72         {
73             ProcStatEntry entry;
74 
75             try
76             {
77                 entry.cmdline = getCmdLine(pid);
78                 TcommUtimeStime t = getTcommUtimeStime(pid, ticksPerSec);
79                 entry.tcomm = t.tcomm;
80                 entry.utime = t.utime;
81                 entry.stime = t.stime;
82 
83                 entries.push_back(entry);
84             }
85             catch (const std::exception& e)
86             {
87                 log<level::ERR>("Could not obtain process stats");
88             }
89         }
90     }
91 
92     std::sort(entries.begin(), entries.end());
93 
94     bool isOthers = false;
95     ProcStatEntry others;
96     others.cmdline = "(Others)";
97     others.utime = others.stime = 0;
98 
99     // Only show this many processes and aggregate all remaining ones into
100     // "others" in order to keep the size of the snapshot reasonably small.
101     // With 10 process stat entries and 10 FD count entries, the size of the
102     // snapshot reaches around 1.5KiB. This is non-trivial, and we have to set
103     // the collection interval long enough so as not to over-stress the IPMI
104     // interface and the data collection service. The value of 10 is chosen
105     // empirically, it might be subject to adjustments when the system is
106     // launched later.
107     constexpr int topN = 10;
108 
109     for (size_t i = 0; i < entries.size(); ++i)
110     {
111         if (i >= topN)
112         {
113             isOthers = true;
114         }
115 
116         ProcStatEntry& entry = entries[i];
117 
118         if (isOthers)
119         {
120             others.utime += entry.utime;
121             others.stime += entry.stime;
122         }
123         else
124         {
125             bmcmetrics::metricproto::BmcProcStatMetric::BmcProcStat s;
126             std::string fullCmdline = entry.cmdline;
127             if (entry.tcomm.size() > 0)
128             {
129                 fullCmdline += " " + entry.tcomm;
130             }
131             s.set_sidx_cmdline(getStringID(fullCmdline));
132             s.set_utime(entry.utime);
133             s.set_stime(entry.stime);
134             *(ret.add_stats()) = s;
135         }
136     }
137 
138     if (isOthers)
139     {
140         bmcmetrics::metricproto::BmcProcStatMetric::BmcProcStat s;
141         s.set_sidx_cmdline(getStringID(others.cmdline));
142         s.set_utime(others.utime);
143         s.set_stime(others.stime);
144         *(ret.add_stats()) = s;
145     }
146 
147     return ret;
148 }
149 
150 int getFdCount(int pid)
151 {
152     const std::string& fdPath = "/proc/" + std::to_string(pid) + "/fd";
153     return std::distance(std::filesystem::directory_iterator(fdPath),
154                          std::filesystem::directory_iterator{});
155 }
156 
157 struct FdStatEntry
158 {
159     int fdCount;
160     std::string cmdline;
161     std::string tcomm;
162 
163     // Processes with the largest fdCount goes first.
164     // Tie-breaking using cmdline then tcomm.
165     bool operator<(const FdStatEntry& other) const
166     {
167         const int negFdCount = -fdCount;
168         const int negOtherFdCount = -other.fdCount;
169         return std::tie(negFdCount, cmdline, tcomm) <
170                std::tie(negOtherFdCount, other.cmdline, other.tcomm);
171     }
172 };
173 
174 bmcmetrics::metricproto::BmcFdStatMetric BmcHealthSnapshot::getFdStatList()
175 {
176     bmcmetrics::metricproto::BmcFdStatMetric ret;
177 
178     // Sort by fd count, no tie-breaking
179     std::vector<FdStatEntry> entries;
180 
181     const std::string_view procPath = "/proc/";
182     for (const auto& procEntry : std::filesystem::directory_iterator(procPath))
183     {
184         const std::string& path = procEntry.path();
185         int pid = 0;
186         FdStatEntry entry;
187         if (isNumericPath(path, pid))
188         {
189             try
190             {
191                 entry.fdCount = getFdCount(pid);
192                 TcommUtimeStime t = getTcommUtimeStime(pid, ticksPerSec);
193                 entry.cmdline = getCmdLine(pid);
194                 entry.tcomm = t.tcomm;
195                 entries.push_back(entry);
196             }
197             catch (const std::exception& e)
198             {
199                 log<level::ERR>("Could not get file descriptor stats");
200             }
201         }
202     }
203 
204     std::sort(entries.begin(), entries.end());
205 
206     bool isOthers = false;
207 
208     // Only report the detailed fd count and cmdline for the top 10 entries,
209     // and collapse all others into "others".
210     constexpr int topN = 10;
211 
212     FdStatEntry others;
213     others.cmdline = "(Others)";
214     others.fdCount = 0;
215 
216     for (size_t i = 0; i < entries.size(); ++i)
217     {
218         if (i >= topN)
219         {
220             isOthers = true;
221         }
222 
223         const FdStatEntry& entry = entries[i];
224         if (isOthers)
225         {
226             others.fdCount += entry.fdCount;
227         }
228         else
229         {
230             bmcmetrics::metricproto::BmcFdStatMetric::BmcFdStat s;
231             std::string fullCmdline = entry.cmdline;
232             if (entry.tcomm.size() > 0)
233             {
234                 fullCmdline += " " + entry.tcomm;
235             }
236             s.set_sidx_cmdline(getStringID(fullCmdline));
237             s.set_fd_count(entry.fdCount);
238             *(ret.add_stats()) = s;
239         }
240     }
241 
242     if (isOthers)
243     {
244         bmcmetrics::metricproto::BmcFdStatMetric::BmcFdStat s;
245         s.set_sidx_cmdline(getStringID(others.cmdline));
246         s.set_fd_count(others.fdCount);
247         *(ret.add_stats()) = s;
248     }
249 
250     return ret;
251 }
252 
253 void BmcHealthSnapshot::serializeSnapshotToArray(
254     const bmcmetrics::metricproto::BmcMetricSnapshot& snapshot)
255 {
256     size_t size = snapshot.ByteSizeLong();
257     if (size > 0)
258     {
259         pbDump.resize(size);
260         if (!snapshot.SerializeToArray(pbDump.data(), size))
261         {
262             log<level::ERR>("Could not serialize protobuf to array");
263         }
264     }
265 }
266 
267 void BmcHealthSnapshot::doWork()
268 {
269     bmcmetrics::metricproto::BmcMetricSnapshot snapshot;
270 
271     // Memory info
272     std::string meminfoBuffer = readFileThenGrepIntoString("/proc/meminfo");
273 
274     {
275         bmcmetrics::metricproto::BmcMemoryMetric m;
276 
277         std::string_view sv(meminfoBuffer.data());
278         // MemAvailable
279         int value;
280         bool ok = parseMeminfoValue(sv, "MemAvailable:", value);
281         if (ok)
282         {
283             m.set_mem_available(value);
284         }
285 
286         ok = parseMeminfoValue(sv, "Slab:", value);
287         if (ok)
288         {
289             m.set_slab(value);
290         }
291 
292         ok = parseMeminfoValue(sv, "KernelStack:", value);
293         if (ok)
294         {
295             m.set_kernel_stack(value);
296         }
297 
298         *(snapshot.mutable_memory_metric()) = m;
299     }
300 
301     // Uptime
302     std::string uptimeBuffer = readFileThenGrepIntoString("/proc/uptime");
303     double uptime = 0;
304     double idleProcessTime = 0;
305     BootTimesMonotonic btm;
306     if (!parseProcUptime(uptimeBuffer, uptime, idleProcessTime))
307     {
308         log<level::ERR>("Error parsing /proc/uptime");
309     }
310     else if (!getBootTimesMonotonic(btm))
311     {
312         log<level::ERR>("Could not get boot time");
313     }
314     else
315     {
316         bmcmetrics::metricproto::BmcUptimeMetric m1;
317         m1.set_uptime(uptime);
318         m1.set_idle_process_time(idleProcessTime);
319         if (btm.firmwareTime == 0 && btm.powerOnSecCounterTime != 0)
320         {
321             m1.set_firmware_boot_time_sec(
322                 static_cast<double>(btm.powerOnSecCounterTime) - uptime);
323         }
324         else
325         {
326             m1.set_firmware_boot_time_sec(
327                 static_cast<double>(btm.firmwareTime - btm.loaderTime) / 1e6);
328         }
329         m1.set_loader_boot_time_sec(static_cast<double>(btm.loaderTime) / 1e6);
330         // initrf presents
331         if (btm.initrdTime != 0)
332         {
333             m1.set_kernel_boot_time_sec(static_cast<double>(btm.initrdTime) /
334                                         1e6);
335             m1.set_initrd_boot_time_sec(
336                 static_cast<double>(btm.userspaceTime - btm.initrdTime) / 1e6);
337             m1.set_userspace_boot_time_sec(
338                 static_cast<double>(btm.finishTime - btm.userspaceTime) / 1e6);
339         }
340         else
341         {
342             m1.set_kernel_boot_time_sec(static_cast<double>(btm.userspaceTime) /
343                                         1e6);
344             m1.set_initrd_boot_time_sec(0);
345             m1.set_userspace_boot_time_sec(
346                 static_cast<double>(btm.finishTime - btm.userspaceTime) / 1e6);
347         }
348         *(snapshot.mutable_uptime_metric()) = m1;
349     }
350 
351     // Storage space
352     struct statvfs fiData;
353     if ((statvfs("/", &fiData)) < 0)
354     {
355         log<level::ERR>("Could not call statvfs");
356     }
357     else
358     {
359         uint64_t kib = (fiData.f_bsize * fiData.f_bfree) / 1024;
360         bmcmetrics::metricproto::BmcDiskSpaceMetric m2;
361         m2.set_rwfs_kib_available(static_cast<int>(kib));
362         *(snapshot.mutable_storage_space_metric()) = m2;
363     }
364 
365     // The next metrics require a sane ticks_per_sec value, typically 100 on
366     // the BMC. In the very rare circumstance when it's 0, exit early and return
367     // a partially complete snapshot (no process).
368     ticksPerSec = getTicksPerSec();
369 
370     // FD stat
371     *(snapshot.mutable_fdstat_metric()) = getFdStatList();
372 
373     if (ticksPerSec == 0)
374     {
375         log<level::ERR>("ticksPerSec is 0, skipping the process list metric");
376         serializeSnapshotToArray(snapshot);
377         done = true;
378         return;
379     }
380 
381     // Proc stat
382     *(snapshot.mutable_procstat_metric()) = getProcStatList();
383 
384     // String table
385     std::vector<std::string_view> strings(stringTable.size());
386     for (const auto& [s, i] : stringTable)
387     {
388         strings[i] = s;
389     }
390 
391     bmcmetrics::metricproto::BmcStringTable st;
392     for (size_t i = 0; i < strings.size(); ++i)
393     {
394         bmcmetrics::metricproto::BmcStringTable::StringEntry entry;
395         entry.set_value(strings[i].data());
396         *(st.add_entries()) = entry;
397     }
398     *(snapshot.mutable_string_table()) = st;
399 
400     // Save to buffer
401     serializeSnapshotToArray(snapshot);
402     done = true;
403 }
404 
405 // BmcBlobSessionStat (9) but passing meta as reference instead of pointer,
406 // since the metadata must not be null at this point.
407 bool BmcHealthSnapshot::stat(blobs::BlobMeta& meta)
408 {
409     if (!done)
410     {
411         // Bits 8~15 are blob-specific state flags.
412         // For this blob, bit 8 is set when metric collection is still in
413         // progress.
414         meta.blobState |= (1 << 8);
415     }
416     else
417     {
418         meta.blobState = 0;
419         meta.blobState = blobs::StateFlags::open_read;
420         meta.size = pbDump.size();
421     }
422     return true;
423 }
424 
425 std::string_view BmcHealthSnapshot::read(uint32_t offset,
426                                          uint32_t requestedSize)
427 {
428     uint32_t size = static_cast<uint32_t>(pbDump.size());
429     if (offset >= size)
430     {
431         return {};
432     }
433     return std::string_view(pbDump.data() + offset,
434                             std::min(requestedSize, size - offset));
435 }
436 
437 int BmcHealthSnapshot::getStringID(const std::string_view s)
438 {
439     int ret = 0;
440     auto itr = stringTable.find(s.data());
441     if (itr == stringTable.end())
442     {
443         stringTable[s.data()] = stringId;
444         ret = stringId;
445         ++stringId;
446     }
447     else
448     {
449         ret = itr->second;
450     }
451     return ret;
452 }
453 
454 } // namespace metric_blob