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