xref: /openbmc/bmcweb/redfish-core/include/utils/eventlog_utils.hpp (revision d9495964cd857f8775677f3f5c1f35f0cf00c0a6)
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 
257     std::optional<registries::MessageId> msgComponents =
258         registries::getMessageComponents(messageID);
259     if (!msgComponents)
260     {
261         return LogParseError::parseFailed;
262     }
263 
264     std::optional<registries::RegistryEntryRef> registry =
265         registries::getRegistryFromPrefix(msgComponents->registryName);
266     if (!registry)
267     {
268         return LogParseError::messageIdNotInRegistry;
269     }
270 
271     // Get the Message from the MessageKey and RegistryEntries
272     const registries::Message* message = registries::getMessageFromRegistry(
273         msgComponents->messageKey, registry->get().entries);
274 
275     logEntryIter++;
276     if (message == nullptr)
277     {
278         BMCWEB_LOG_WARNING("Log entry not found in registry: {}", logEntry);
279         return LogParseError::messageIdNotInRegistry;
280     }
281 
282     const unsigned int& versionMajor = registry->get().header.versionMajor;
283     const unsigned int& versionMinor = registry->get().header.versionMinor;
284 
285     std::vector<std::string_view> messageArgs(logEntryIter,
286                                               logEntryFields.end());
287     messageArgs.resize(message->numberOfArgs);
288 
289     std::string msg =
290         redfish::registries::fillMessageArgs(messageArgs, message->message);
291     if (msg.empty())
292     {
293         return LogParseError::parseFailed;
294     }
295 
296     // Get the Created time from the timestamp. The log timestamp is in RFC3339
297     // format which matches the Redfish format except for the
298     // fractional seconds between the '.' and the '+', so just remove them.
299     std::size_t dot = timestamp.find_first_of('.');
300     std::size_t plus = timestamp.find_first_of('+');
301     if (dot != std::string::npos && plus != std::string::npos)
302     {
303         timestamp.erase(dot, plus - dot);
304     }
305 
306     // Fill in the log entry with the gathered data
307     logEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry";
308     logEntryJson["@odata.id"] =
309         boost::urls::format("/redfish/v1/{}/{}/LogServices/EventLog/Entries/{}",
310                             parentStr, childId, logEntryID);
311     logEntryJson["Name"] =
312         std::format("{} Event Log Entry", logEntryDescriptor);
313     logEntryJson["Id"] = logEntryID;
314     logEntryJson["Message"] = std::move(msg);
315     logEntryJson["MessageId"] =
316         std::format("{}.{}.{}.{}", msgComponents->registryName, versionMajor,
317                     versionMinor, msgComponents->messageKey);
318     logEntryJson["MessageArgs"] = messageArgs;
319     logEntryJson["EntryType"] = "Event";
320     logEntryJson["Severity"] = message->messageSeverity;
321     logEntryJson["Created"] = std::move(timestamp);
322     return LogParseError::success;
323 }
324 
handleSystemsAndManagersLogServiceEventLogLogEntryCollection(const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,query_param::Query & delegatedQuery,LogServiceParent parent)325 inline void handleSystemsAndManagersLogServiceEventLogLogEntryCollection(
326     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
327     query_param::Query& delegatedQuery, LogServiceParent parent)
328 {
329     size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop);
330     size_t skip = delegatedQuery.skip.value_or(0);
331 
332     const std::string parentStr = logServiceParentToString(parent);
333     const std::string_view childId = getChildIdFromParent(parent);
334     const std::string logEntryDescriptor = getLogEntryDescriptor(parent);
335 
336     if (parentStr.empty() || childId.empty() || logEntryDescriptor.empty())
337     {
338         messages::internalError(asyncResp->res);
339         return;
340     }
341 
342     // Collections don't include the static data added by SubRoute
343     // because it has a duplicate entry for members
344     asyncResp->res.jsonValue["@odata.type"] =
345         "#LogEntryCollection.LogEntryCollection";
346     asyncResp->res.jsonValue["@odata.id"] = std::format(
347         "/redfish/v1/{}/{}/LogServices/EventLog/Entries", parentStr, childId);
348     asyncResp->res.jsonValue["Name"] =
349         std::format("{} Event Log Entries", logEntryDescriptor);
350     asyncResp->res.jsonValue["Description"] =
351         std::format("Collection of {} Event Log Entries", logEntryDescriptor);
352 
353     nlohmann::json& logEntryArray = asyncResp->res.jsonValue["Members"];
354     logEntryArray = nlohmann::json::array();
355     // Go through the log files and create a unique ID for each
356     // entry
357     std::vector<std::filesystem::path> redfishLogFiles;
358     getRedfishLogFiles(redfishLogFiles);
359     uint64_t entryCount = 0;
360     std::string logEntry;
361 
362     // Oldest logs are in the last file, so start there and loop
363     // backwards
364     for (auto it = redfishLogFiles.rbegin(); it < redfishLogFiles.rend(); it++)
365     {
366         std::ifstream logStream(*it);
367         if (!logStream.is_open())
368         {
369             continue;
370         }
371 
372         // Reset the unique ID on the first entry
373         bool firstEntry = true;
374         while (std::getline(logStream, logEntry))
375         {
376             std::string idStr;
377             if (!getUniqueEntryID(logEntry, idStr, firstEntry))
378             {
379                 continue;
380             }
381             firstEntry = false;
382 
383             nlohmann::json::object_t bmcLogEntry;
384             LogParseError status =
385                 fillEventLogEntryJson(idStr, logEntry, bmcLogEntry, parentStr,
386                                       childId, logEntryDescriptor);
387             if (status == LogParseError::messageIdNotInRegistry)
388             {
389                 continue;
390             }
391             if (status != LogParseError::success)
392             {
393                 messages::internalError(asyncResp->res);
394                 return;
395             }
396 
397             entryCount++;
398             // Handle paging using skip (number of entries to skip from the
399             // start) and top (number of entries to display)
400             if (entryCount <= skip || entryCount > skip + top)
401             {
402                 continue;
403             }
404 
405             logEntryArray.emplace_back(std::move(bmcLogEntry));
406         }
407     }
408     asyncResp->res.jsonValue["Members@odata.count"] = entryCount;
409     if (skip + top < entryCount)
410     {
411         asyncResp->res.jsonValue["Members@odata.nextLink"] =
412             boost::urls::format(
413                 "/redfish/v1/{}/{}/LogServices/EventLog/Entries?$skip={}",
414                 parentStr, childId, std::to_string(skip + top));
415     }
416 }
417 
handleSystemsAndManagersLogServiceEventLogEntriesGet(const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & param,LogServiceParent parent)418 inline void handleSystemsAndManagersLogServiceEventLogEntriesGet(
419     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
420     const std::string& param, LogServiceParent parent)
421 {
422     const std::string& targetID = param;
423 
424     const std::string parentStr = logServiceParentToString(parent);
425     const std::string_view childId = getChildIdFromParent(parent);
426     const std::string logEntryDescriptor = getLogEntryDescriptor(parent);
427 
428     if (parentStr.empty() || childId.empty() || logEntryDescriptor.empty())
429     {
430         messages::internalError(asyncResp->res);
431         return;
432     }
433 
434     // Go through the log files and check the unique ID for each
435     // entry to find the target entry
436     std::vector<std::filesystem::path> redfishLogFiles;
437     getRedfishLogFiles(redfishLogFiles);
438     std::string logEntry;
439 
440     // Oldest logs are in the last file, so start there and loop
441     // backwards
442     for (auto it = redfishLogFiles.rbegin(); it < redfishLogFiles.rend(); it++)
443     {
444         std::ifstream logStream(*it);
445         if (!logStream.is_open())
446         {
447             continue;
448         }
449 
450         // Reset the unique ID on the first entry
451         bool firstEntry = true;
452         while (std::getline(logStream, logEntry))
453         {
454             std::string idStr;
455             if (!getUniqueEntryID(logEntry, idStr, firstEntry))
456             {
457                 continue;
458             }
459             firstEntry = false;
460 
461             if (idStr == targetID)
462             {
463                 nlohmann::json::object_t bmcLogEntry;
464                 LogParseError status = fillEventLogEntryJson(
465                     idStr, logEntry, bmcLogEntry, parentStr, childId,
466                     logEntryDescriptor);
467                 if (status != LogParseError::success)
468                 {
469                     messages::internalError(asyncResp->res);
470                     return;
471                 }
472                 asyncResp->res.jsonValue.update(bmcLogEntry);
473                 return;
474             }
475         }
476     }
477     // Requested ID was not found
478     messages::resourceNotFound(asyncResp->res, "LogEntry", targetID);
479 }
480 
handleSystemsAndManagersLogServicesEventLogActionsClearPost(const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)481 inline void handleSystemsAndManagersLogServicesEventLogActionsClearPost(
482     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
483 {
484     // Clear the EventLog by deleting the log files
485     std::vector<std::filesystem::path> redfishLogFiles;
486     if (getRedfishLogFiles(redfishLogFiles))
487     {
488         for (const std::filesystem::path& file : redfishLogFiles)
489         {
490             std::error_code ec;
491             std::filesystem::remove(file, ec);
492         }
493     }
494 
495     // Reload rsyslog so it knows to start new log files
496     dbus::utility::async_method_call(
497         asyncResp,
498         [asyncResp](const boost::system::error_code& ec) {
499             if (ec)
500             {
501                 BMCWEB_LOG_ERROR("Failed to reload rsyslog: {}", ec);
502                 messages::internalError(asyncResp->res);
503                 return;
504             }
505 
506             messages::success(asyncResp->res);
507         },
508         "org.freedesktop.systemd1", "/org/freedesktop/systemd1",
509         "org.freedesktop.systemd1.Manager", "ReloadUnit", "rsyslog.service",
510         "replace");
511 }
512 } // namespace eventlog_utils
513 } // namespace redfish
514