xref: /openbmc/bmcweb/redfish-core/include/utils/eventlog_utils.hpp (revision 3af76e1508cce1272751ede5f4f633a74888950f)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 // SPDX-FileCopyrightText: Copyright 2018 Intel Corporation
4 #pragma once
5 
6 #include "async_resp.hpp"
7 #include "dbus_utility.hpp"
8 #include "error_messages.hpp"
9 #include "http_response.hpp"
10 #include "logging.hpp"
11 #include "registries.hpp"
12 #include "str_utility.hpp"
13 #include "utils/query_param.hpp"
14 
15 #include <boost/beast/http/field.hpp>
16 #include <boost/beast/http/status.hpp>
17 #include <boost/beast/http/verb.hpp>
18 #include <boost/system/linux_error.hpp>
19 #include <boost/url/format.hpp>
20 #include <boost/url/url.hpp>
21 #include <sdbusplus/message.hpp>
22 #include <sdbusplus/message/native_types.hpp>
23 #include <sdbusplus/unpack_properties.hpp>
24 
25 #include <algorithm>
26 #include <cstddef>
27 #include <cstdint>
28 #include <cstdio>
29 #include <ctime>
30 #include <fstream>
31 #include <iomanip>
32 #include <sstream>
33 #include <string>
34 #include <utility>
35 
36 namespace redfish
37 {
38 namespace eventlog_utils
39 {
40 
41 constexpr const char* rfSystemsStr = "Systems";
42 constexpr const char* rfManagersStr = "Managers";
43 
44 enum class LogServiceParent
45 {
46     Systems,
47     Managers
48 };
49 
50 inline std::string logServiceParentToString(LogServiceParent parent)
51 {
52     std::string parentStr;
53     switch (parent)
54     {
55         case LogServiceParent::Managers:
56             parentStr = rfManagersStr;
57             break;
58         case LogServiceParent::Systems:
59             parentStr = rfSystemsStr;
60             break;
61         default:
62             BMCWEB_LOG_ERROR("Unable to stringify bmcweb eventlog location");
63             break;
64     }
65     return parentStr;
66 }
67 
68 inline std::string_view getChildIdFromParent(LogServiceParent parent)
69 {
70     std::string_view childId;
71 
72     switch (parent)
73     {
74         case LogServiceParent::Managers:
75             childId = BMCWEB_REDFISH_MANAGER_URI_NAME;
76             break;
77         case LogServiceParent::Systems:
78             childId = BMCWEB_REDFISH_SYSTEM_URI_NAME;
79             break;
80         default:
81             BMCWEB_LOG_ERROR(
82                 "Unable to stringify bmcweb eventlog location childId");
83             break;
84     }
85     return childId;
86 }
87 
88 inline std::string getLogEntryDescriptor(LogServiceParent parent)
89 {
90     std::string descriptor;
91     switch (parent)
92     {
93         case LogServiceParent::Managers:
94             descriptor = "Manager";
95             break;
96         case LogServiceParent::Systems:
97             descriptor = "System";
98             break;
99         default:
100             BMCWEB_LOG_ERROR("Unable to get Log Entry descriptor");
101             break;
102     }
103     return descriptor;
104 }
105 
106 /*
107  * Journal EventLog utilities
108  * */
109 
110 inline bool getRedfishLogFiles(
111     std::vector<std::filesystem::path>& redfishLogFiles)
112 {
113     static const std::filesystem::path redfishLogDir = "/var/log";
114     static const std::string redfishLogFilename = "redfish";
115 
116     // Loop through the directory looking for redfish log files
117     for (const std::filesystem::directory_entry& dirEnt :
118          std::filesystem::directory_iterator(redfishLogDir))
119     {
120         // If we find a redfish log file, save the path
121         std::string filename = dirEnt.path().filename();
122         if (filename.starts_with(redfishLogFilename))
123         {
124             redfishLogFiles.emplace_back(redfishLogDir / filename);
125         }
126     }
127     // As the log files rotate, they are appended with a ".#" that is higher for
128     // the older logs. Since we don't expect more than 10 log files, we
129     // can just sort the list to get them in order from newest to oldest
130     std::ranges::sort(redfishLogFiles);
131 
132     return !redfishLogFiles.empty();
133 }
134 
135 inline bool getUniqueEntryID(const std::string& logEntry, std::string& entryID,
136                              const bool firstEntry = true)
137 {
138     static time_t prevTs = 0;
139     static int index = 0;
140     if (firstEntry)
141     {
142         prevTs = 0;
143     }
144 
145     // Get the entry timestamp
146     std::time_t curTs = 0;
147     std::tm timeStruct = {};
148     std::istringstream entryStream(logEntry);
149     if (entryStream >> std::get_time(&timeStruct, "%Y-%m-%dT%H:%M:%S"))
150     {
151         curTs = std::mktime(&timeStruct);
152     }
153     // If the timestamp isn't unique, increment the index
154     if (curTs == prevTs)
155     {
156         index++;
157     }
158     else
159     {
160         // Otherwise, reset it
161         index = 0;
162     }
163     // Save the timestamp
164     prevTs = curTs;
165 
166     entryID = std::to_string(curTs);
167     if (index > 0)
168     {
169         entryID += "_" + std::to_string(index);
170     }
171     return true;
172 }
173 
174 enum class LogParseError
175 {
176     success,
177     parseFailed,
178     messageIdNotInRegistry,
179 };
180 
181 static LogParseError fillEventLogEntryJson(
182     const std::string& logEntryID, const std::string& logEntry,
183     nlohmann::json::object_t& logEntryJson, const std::string& parentStr,
184     const std::string_view childId, const std::string& logEntryDescriptor)
185 {
186     // The redfish log format is "<Timestamp> <MessageId>,<MessageArgs>"
187     // First get the Timestamp
188     size_t space = logEntry.find_first_of(' ');
189     if (space == std::string::npos)
190     {
191         return LogParseError::parseFailed;
192     }
193     std::string timestamp = logEntry.substr(0, space);
194     // Then get the log contents
195     size_t entryStart = logEntry.find_first_not_of(' ', space);
196     if (entryStart == std::string::npos)
197     {
198         return LogParseError::parseFailed;
199     }
200     std::string_view entry(logEntry);
201     entry.remove_prefix(entryStart);
202     // Use split to separate the entry into its fields
203     std::vector<std::string> logEntryFields;
204     bmcweb::split(logEntryFields, entry, ',');
205     // We need at least a MessageId to be valid
206     auto logEntryIter = logEntryFields.begin();
207     if (logEntryIter == logEntryFields.end())
208     {
209         return LogParseError::parseFailed;
210     }
211     std::string& messageID = *logEntryIter;
212     // Get the Message from the MessageRegistry
213     const registries::Message* message = registries::getMessage(messageID);
214 
215     logEntryIter++;
216     if (message == nullptr)
217     {
218         BMCWEB_LOG_WARNING("Log entry not found in registry: {}", logEntry);
219         return LogParseError::messageIdNotInRegistry;
220     }
221 
222     std::vector<std::string_view> messageArgs(logEntryIter,
223                                               logEntryFields.end());
224     messageArgs.resize(message->numberOfArgs);
225 
226     std::string msg =
227         redfish::registries::fillMessageArgs(messageArgs, message->message);
228     if (msg.empty())
229     {
230         return LogParseError::parseFailed;
231     }
232 
233     // Get the Created time from the timestamp. The log timestamp is in RFC3339
234     // format which matches the Redfish format except for the fractional seconds
235     // between the '.' and the '+', so just remove them.
236     std::size_t dot = timestamp.find_first_of('.');
237     std::size_t plus = timestamp.find_first_of('+');
238     if (dot != std::string::npos && plus != std::string::npos)
239     {
240         timestamp.erase(dot, plus - dot);
241     }
242 
243     // Fill in the log entry with the gathered data
244     logEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry";
245     logEntryJson["@odata.id"] =
246         boost::urls::format("/redfish/v1/{}/{}/LogServices/EventLog/Entries/{}",
247                             parentStr, childId, logEntryID);
248     logEntryJson["Name"] =
249         std::format("{} Event Log Entry", logEntryDescriptor);
250     logEntryJson["Id"] = logEntryID;
251     logEntryJson["Message"] = std::move(msg);
252     logEntryJson["MessageId"] = std::move(messageID);
253     logEntryJson["MessageArgs"] = messageArgs;
254     logEntryJson["EntryType"] = "Event";
255     logEntryJson["Severity"] = message->messageSeverity;
256     logEntryJson["Created"] = std::move(timestamp);
257     return LogParseError::success;
258 }
259 
260 inline void handleRequestSystemsLogServiceEventLogLogEntryCollection(
261     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
262     query_param::Query& delegatedQuery, LogServiceParent parent)
263 {
264     size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop);
265     size_t skip = delegatedQuery.skip.value_or(0);
266 
267     const std::string parentStr = logServiceParentToString(parent);
268     const std::string_view childId = getChildIdFromParent(parent);
269     const std::string logEntryDescriptor = getLogEntryDescriptor(parent);
270 
271     if (parentStr.empty() || childId.empty() || logEntryDescriptor.empty())
272     {
273         messages::internalError(asyncResp->res);
274         return;
275     }
276 
277     // Collections don't include the static data added by SubRoute
278     // because it has a duplicate entry for members
279     asyncResp->res.jsonValue["@odata.type"] =
280         "#LogEntryCollection.LogEntryCollection";
281     asyncResp->res.jsonValue["@odata.id"] = std::format(
282         "/redfish/v1/{}/{}/LogServices/EventLog/Entries", parentStr, childId);
283     asyncResp->res.jsonValue["Name"] =
284         std::format("{} Event Log Entries", logEntryDescriptor);
285     asyncResp->res.jsonValue["Description"] =
286         std::format("Collection of {} Event Log Entries", logEntryDescriptor);
287 
288     nlohmann::json& logEntryArray = asyncResp->res.jsonValue["Members"];
289     logEntryArray = nlohmann::json::array();
290     // Go through the log files and create a unique ID for each
291     // entry
292     std::vector<std::filesystem::path> redfishLogFiles;
293     getRedfishLogFiles(redfishLogFiles);
294     uint64_t entryCount = 0;
295     std::string logEntry;
296 
297     // Oldest logs are in the last file, so start there and loop
298     // backwards
299     for (auto it = redfishLogFiles.rbegin(); it < redfishLogFiles.rend(); it++)
300     {
301         std::ifstream logStream(*it);
302         if (!logStream.is_open())
303         {
304             continue;
305         }
306 
307         // Reset the unique ID on the first entry
308         bool firstEntry = true;
309         while (std::getline(logStream, logEntry))
310         {
311             std::string idStr;
312             if (!getUniqueEntryID(logEntry, idStr, firstEntry))
313             {
314                 continue;
315             }
316             firstEntry = false;
317 
318             nlohmann::json::object_t bmcLogEntry;
319             LogParseError status =
320                 fillEventLogEntryJson(idStr, logEntry, bmcLogEntry, parentStr,
321                                       childId, logEntryDescriptor);
322             if (status == LogParseError::messageIdNotInRegistry)
323             {
324                 continue;
325             }
326             if (status != LogParseError::success)
327             {
328                 messages::internalError(asyncResp->res);
329                 return;
330             }
331 
332             entryCount++;
333             // Handle paging using skip (number of entries to skip from the
334             // start) and top (number of entries to display)
335             if (entryCount <= skip || entryCount > skip + top)
336             {
337                 continue;
338             }
339 
340             logEntryArray.emplace_back(std::move(bmcLogEntry));
341         }
342     }
343     asyncResp->res.jsonValue["Members@odata.count"] = entryCount;
344     if (skip + top < entryCount)
345     {
346         asyncResp->res.jsonValue["Members@odata.nextLink"] =
347             boost::urls::format(
348                 "/redfish/v1/{}/{}/LogServices/EventLog/Entries?$skip={}",
349                 parentStr, childId, std::to_string(skip + top));
350     }
351 }
352 
353 inline void handleRequestSystemsLogServiceEventLogEntriesGet(
354     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
355     const std::string& param, LogServiceParent parent)
356 {
357     const std::string& targetID = param;
358 
359     const std::string parentStr = logServiceParentToString(parent);
360     const std::string_view childId = getChildIdFromParent(parent);
361     const std::string logEntryDescriptor = getLogEntryDescriptor(parent);
362 
363     if (parentStr.empty() || childId.empty() || logEntryDescriptor.empty())
364     {
365         messages::internalError(asyncResp->res);
366         return;
367     }
368 
369     // Go through the log files and check the unique ID for each
370     // entry to find the target entry
371     std::vector<std::filesystem::path> redfishLogFiles;
372     getRedfishLogFiles(redfishLogFiles);
373     std::string logEntry;
374 
375     // Oldest logs are in the last file, so start there and loop
376     // backwards
377     for (auto it = redfishLogFiles.rbegin(); it < redfishLogFiles.rend(); it++)
378     {
379         std::ifstream logStream(*it);
380         if (!logStream.is_open())
381         {
382             continue;
383         }
384 
385         // Reset the unique ID on the first entry
386         bool firstEntry = true;
387         while (std::getline(logStream, logEntry))
388         {
389             std::string idStr;
390             if (!getUniqueEntryID(logEntry, idStr, firstEntry))
391             {
392                 continue;
393             }
394             firstEntry = false;
395 
396             if (idStr == targetID)
397             {
398                 nlohmann::json::object_t bmcLogEntry;
399                 LogParseError status = fillEventLogEntryJson(
400                     idStr, logEntry, bmcLogEntry, parentStr, childId,
401                     logEntryDescriptor);
402                 if (status != LogParseError::success)
403                 {
404                     messages::internalError(asyncResp->res);
405                     return;
406                 }
407                 asyncResp->res.jsonValue.update(bmcLogEntry);
408                 return;
409             }
410         }
411     }
412     // Requested ID was not found
413     messages::resourceNotFound(asyncResp->res, "LogEntry", targetID);
414 }
415 
416 inline void handleRequestSystemsLogServicesEventLogActionsClearPost(
417     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
418 {
419     // Clear the EventLog by deleting the log files
420     std::vector<std::filesystem::path> redfishLogFiles;
421     if (getRedfishLogFiles(redfishLogFiles))
422     {
423         for (const std::filesystem::path& file : redfishLogFiles)
424         {
425             std::error_code ec;
426             std::filesystem::remove(file, ec);
427         }
428     }
429 
430     // Reload rsyslog so it knows to start new log files
431     dbus::utility::async_method_call(
432         asyncResp,
433         [asyncResp](const boost::system::error_code& ec) {
434             if (ec)
435             {
436                 BMCWEB_LOG_ERROR("Failed to reload rsyslog: {}", ec);
437                 messages::internalError(asyncResp->res);
438                 return;
439             }
440 
441             messages::success(asyncResp->res);
442         },
443         "org.freedesktop.systemd1", "/org/freedesktop/systemd1",
444         "org.freedesktop.systemd1.Manager", "ReloadUnit", "rsyslog.service",
445         "replace");
446 }
447 } // namespace eventlog_utils
448 } // namespace redfish
449