xref: /openbmc/bmcweb/redfish-core/include/utils/eventlog_utils.hpp (revision 7681b8a16fd75dc3424a6d70a76a906d128b8de9)
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 /*
42  * Journal EventLog utilities
43  * */
44 
45 inline bool getRedfishLogFiles(
46     std::vector<std::filesystem::path>& redfishLogFiles)
47 {
48     static const std::filesystem::path redfishLogDir = "/var/log";
49     static const std::string redfishLogFilename = "redfish";
50 
51     // Loop through the directory looking for redfish log files
52     for (const std::filesystem::directory_entry& dirEnt :
53          std::filesystem::directory_iterator(redfishLogDir))
54     {
55         // If we find a redfish log file, save the path
56         std::string filename = dirEnt.path().filename();
57         if (filename.starts_with(redfishLogFilename))
58         {
59             redfishLogFiles.emplace_back(redfishLogDir / filename);
60         }
61     }
62     // As the log files rotate, they are appended with a ".#" that is higher for
63     // the older logs. Since we don't expect more than 10 log files, we
64     // can just sort the list to get them in order from newest to oldest
65     std::ranges::sort(redfishLogFiles);
66 
67     return !redfishLogFiles.empty();
68 }
69 
70 inline bool getUniqueEntryID(const std::string& logEntry, std::string& entryID,
71                              const bool firstEntry = true)
72 {
73     static time_t prevTs = 0;
74     static int index = 0;
75     if (firstEntry)
76     {
77         prevTs = 0;
78     }
79 
80     // Get the entry timestamp
81     std::time_t curTs = 0;
82     std::tm timeStruct = {};
83     std::istringstream entryStream(logEntry);
84     if (entryStream >> std::get_time(&timeStruct, "%Y-%m-%dT%H:%M:%S"))
85     {
86         curTs = std::mktime(&timeStruct);
87     }
88     // If the timestamp isn't unique, increment the index
89     if (curTs == prevTs)
90     {
91         index++;
92     }
93     else
94     {
95         // Otherwise, reset it
96         index = 0;
97     }
98     // Save the timestamp
99     prevTs = curTs;
100 
101     entryID = std::to_string(curTs);
102     if (index > 0)
103     {
104         entryID += "_" + std::to_string(index);
105     }
106     return true;
107 }
108 
109 enum class LogParseError
110 {
111     success,
112     parseFailed,
113     messageIdNotInRegistry,
114 };
115 
116 static LogParseError fillEventLogEntryJson(
117     const std::string& logEntryID, const std::string& logEntry,
118     nlohmann::json::object_t& logEntryJson)
119 {
120     // The redfish log format is "<Timestamp> <MessageId>,<MessageArgs>"
121     // First get the Timestamp
122     size_t space = logEntry.find_first_of(' ');
123     if (space == std::string::npos)
124     {
125         return LogParseError::parseFailed;
126     }
127     std::string timestamp = logEntry.substr(0, space);
128     // Then get the log contents
129     size_t entryStart = logEntry.find_first_not_of(' ', space);
130     if (entryStart == std::string::npos)
131     {
132         return LogParseError::parseFailed;
133     }
134     std::string_view entry(logEntry);
135     entry.remove_prefix(entryStart);
136     // Use split to separate the entry into its fields
137     std::vector<std::string> logEntryFields;
138     bmcweb::split(logEntryFields, entry, ',');
139     // We need at least a MessageId to be valid
140     auto logEntryIter = logEntryFields.begin();
141     if (logEntryIter == logEntryFields.end())
142     {
143         return LogParseError::parseFailed;
144     }
145     std::string& messageID = *logEntryIter;
146     // Get the Message from the MessageRegistry
147     const registries::Message* message = registries::getMessage(messageID);
148 
149     logEntryIter++;
150     if (message == nullptr)
151     {
152         BMCWEB_LOG_WARNING("Log entry not found in registry: {}", logEntry);
153         return LogParseError::messageIdNotInRegistry;
154     }
155 
156     std::vector<std::string_view> messageArgs(logEntryIter,
157                                               logEntryFields.end());
158     messageArgs.resize(message->numberOfArgs);
159 
160     std::string msg =
161         redfish::registries::fillMessageArgs(messageArgs, message->message);
162     if (msg.empty())
163     {
164         return LogParseError::parseFailed;
165     }
166 
167     // Get the Created time from the timestamp. The log timestamp is in RFC3339
168     // format which matches the Redfish format except for the fractional seconds
169     // between the '.' and the '+', so just remove them.
170     std::size_t dot = timestamp.find_first_of('.');
171     std::size_t plus = timestamp.find_first_of('+');
172     if (dot != std::string::npos && plus != std::string::npos)
173     {
174         timestamp.erase(dot, plus - dot);
175     }
176 
177     // Fill in the log entry with the gathered data
178     logEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry";
179     logEntryJson["@odata.id"] = boost::urls::format(
180         "/redfish/v1/Systems/{}/LogServices/EventLog/Entries/{}",
181         BMCWEB_REDFISH_SYSTEM_URI_NAME, logEntryID);
182     logEntryJson["Name"] = "System Event Log Entry";
183     logEntryJson["Id"] = logEntryID;
184     logEntryJson["Message"] = std::move(msg);
185     logEntryJson["MessageId"] = std::move(messageID);
186     logEntryJson["MessageArgs"] = messageArgs;
187     logEntryJson["EntryType"] = "Event";
188     logEntryJson["Severity"] = message->messageSeverity;
189     logEntryJson["Created"] = std::move(timestamp);
190     return LogParseError::success;
191 }
192 
193 inline void handleRequestSystemsLogServiceEventLogLogEntryCollection(
194     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
195     query_param::Query& delegatedQuery)
196 {
197     size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop);
198     size_t skip = delegatedQuery.skip.value_or(0);
199 
200     // Collections don't include the static data added by SubRoute
201     // because it has a duplicate entry for members
202     asyncResp->res.jsonValue["@odata.type"] =
203         "#LogEntryCollection.LogEntryCollection";
204     asyncResp->res.jsonValue["@odata.id"] =
205         std::format("/redfish/v1/Systems/{}/LogServices/EventLog/Entries",
206                     BMCWEB_REDFISH_SYSTEM_URI_NAME);
207     asyncResp->res.jsonValue["Name"] = "System Event Log Entries";
208     asyncResp->res.jsonValue["Description"] =
209         "Collection of System Event Log Entries";
210 
211     nlohmann::json& logEntryArray = asyncResp->res.jsonValue["Members"];
212     logEntryArray = nlohmann::json::array();
213     // Go through the log files and create a unique ID for each
214     // entry
215     std::vector<std::filesystem::path> redfishLogFiles;
216     getRedfishLogFiles(redfishLogFiles);
217     uint64_t entryCount = 0;
218     std::string logEntry;
219 
220     // Oldest logs are in the last file, so start there and loop
221     // backwards
222     for (auto it = redfishLogFiles.rbegin(); it < redfishLogFiles.rend(); it++)
223     {
224         std::ifstream logStream(*it);
225         if (!logStream.is_open())
226         {
227             continue;
228         }
229 
230         // Reset the unique ID on the first entry
231         bool firstEntry = true;
232         while (std::getline(logStream, logEntry))
233         {
234             std::string idStr;
235             if (!getUniqueEntryID(logEntry, idStr, firstEntry))
236             {
237                 continue;
238             }
239             firstEntry = false;
240 
241             nlohmann::json::object_t bmcLogEntry;
242             LogParseError status =
243                 fillEventLogEntryJson(idStr, logEntry, bmcLogEntry);
244             if (status == LogParseError::messageIdNotInRegistry)
245             {
246                 continue;
247             }
248             if (status != LogParseError::success)
249             {
250                 messages::internalError(asyncResp->res);
251                 return;
252             }
253 
254             entryCount++;
255             // Handle paging using skip (number of entries to skip from the
256             // start) and top (number of entries to display)
257             if (entryCount <= skip || entryCount > skip + top)
258             {
259                 continue;
260             }
261 
262             logEntryArray.emplace_back(std::move(bmcLogEntry));
263         }
264     }
265     asyncResp->res.jsonValue["Members@odata.count"] = entryCount;
266     if (skip + top < entryCount)
267     {
268         asyncResp->res.jsonValue["Members@odata.nextLink"] =
269             boost::urls::format(
270                 "/redfish/v1/Systems/{}/LogServices/EventLog/Entries?$skip={}",
271                 BMCWEB_REDFISH_SYSTEM_URI_NAME, std::to_string(skip + top));
272     }
273 }
274 
275 inline void handleRequestSystemsLogServiceEventLogEntriesGet(
276     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
277     const std::string& param)
278 {
279     const std::string& targetID = param;
280 
281     // Go through the log files and check the unique ID for each
282     // entry to find the target entry
283     std::vector<std::filesystem::path> redfishLogFiles;
284     getRedfishLogFiles(redfishLogFiles);
285     std::string logEntry;
286 
287     // Oldest logs are in the last file, so start there and loop
288     // backwards
289     for (auto it = redfishLogFiles.rbegin(); it < redfishLogFiles.rend(); it++)
290     {
291         std::ifstream logStream(*it);
292         if (!logStream.is_open())
293         {
294             continue;
295         }
296 
297         // Reset the unique ID on the first entry
298         bool firstEntry = true;
299         while (std::getline(logStream, logEntry))
300         {
301             std::string idStr;
302             if (!getUniqueEntryID(logEntry, idStr, firstEntry))
303             {
304                 continue;
305             }
306             firstEntry = false;
307 
308             if (idStr == targetID)
309             {
310                 nlohmann::json::object_t bmcLogEntry;
311                 LogParseError status =
312                     fillEventLogEntryJson(idStr, logEntry, bmcLogEntry);
313                 if (status != LogParseError::success)
314                 {
315                     messages::internalError(asyncResp->res);
316                     return;
317                 }
318                 asyncResp->res.jsonValue.update(bmcLogEntry);
319                 return;
320             }
321         }
322     }
323     // Requested ID was not found
324     messages::resourceNotFound(asyncResp->res, "LogEntry", targetID);
325 }
326 
327 inline void handleRequestSystemsLogServicesEventLogActionsClearPost(
328     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
329 {
330     // Clear the EventLog by deleting the log files
331     std::vector<std::filesystem::path> redfishLogFiles;
332     if (getRedfishLogFiles(redfishLogFiles))
333     {
334         for (const std::filesystem::path& file : redfishLogFiles)
335         {
336             std::error_code ec;
337             std::filesystem::remove(file, ec);
338         }
339     }
340 
341     // Reload rsyslog so it knows to start new log files
342     dbus::utility::async_method_call(
343         asyncResp,
344         [asyncResp](const boost::system::error_code& ec) {
345             if (ec)
346             {
347                 BMCWEB_LOG_ERROR("Failed to reload rsyslog: {}", ec);
348                 messages::internalError(asyncResp->res);
349                 return;
350             }
351 
352             messages::success(asyncResp->res);
353         },
354         "org.freedesktop.systemd1", "/org/freedesktop/systemd1",
355         "org.freedesktop.systemd1.Manager", "ReloadUnit", "rsyslog.service",
356         "replace");
357 }
358 } // namespace eventlog_utils
359 } // namespace redfish
360