xref: /openbmc/bmcweb/redfish-core/lib/manager_logservices_journal.hpp (revision 56b81992ba8a8e644f2e75251a94df4f4d0d0880)
1 #pragma once
2 
3 #include "app.hpp"
4 #include "error_messages.hpp"
5 #include "generated/enums/log_entry.hpp"
6 #include "query.hpp"
7 #include "registries/base_message_registry.hpp"
8 #include "registries/privilege_registry.hpp"
9 #include "utils/time_utils.hpp"
10 
11 #include <systemd/sd-journal.h>
12 
13 #include <boost/beast/http/verb.hpp>
14 
15 #include <array>
16 #include <memory>
17 #include <string>
18 #include <string_view>
19 
20 namespace redfish
21 {
22 
23 inline int getJournalMetadata(sd_journal* journal, std::string_view field,
24                               std::string_view& contents)
25 {
26     const char* data = nullptr;
27     size_t length = 0;
28     int ret = 0;
29     // Get the metadata from the requested field of the journal entry
30     // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
31     const void** dataVoid = reinterpret_cast<const void**>(&data);
32 
33     ret = sd_journal_get_data(journal, field.data(), dataVoid, &length);
34     if (ret < 0)
35     {
36         return ret;
37     }
38     contents = std::string_view(data, length);
39     // Only use the content after the "=" character.
40     contents.remove_prefix(std::min(contents.find('=') + 1, contents.size()));
41     return ret;
42 }
43 
44 inline int getJournalMetadataInt(sd_journal* journal, std::string_view field,
45                                  const int& base, long int& contents)
46 {
47     int ret = 0;
48     std::string_view metadata;
49     // Get the metadata from the requested field of the journal entry
50     ret = getJournalMetadata(journal, field, metadata);
51     if (ret < 0)
52     {
53         return ret;
54     }
55     contents = strtol(metadata.data(), nullptr, base);
56     return ret;
57 }
58 
59 inline bool getEntryTimestamp(sd_journal* journal, std::string& entryTimestamp)
60 {
61     int ret = 0;
62     uint64_t timestamp = 0;
63     ret = sd_journal_get_realtime_usec(journal, &timestamp);
64     if (ret < 0)
65     {
66         BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}", strerror(-ret));
67         return false;
68     }
69     entryTimestamp = redfish::time_utils::getDateTimeUintUs(timestamp);
70     return true;
71 }
72 
73 inline bool fillBMCJournalLogEntryJson(
74     sd_journal* journal, nlohmann::json::object_t& bmcJournalLogEntryJson)
75 {
76     char* cursor = nullptr;
77     int ret = sd_journal_get_cursor(journal, &cursor);
78     if (ret < 0)
79     {
80         return false;
81     }
82     std::unique_ptr<char*> cursorptr = std::make_unique<char*>(cursor);
83     std::string bmcJournalLogEntryID(cursor);
84 
85     // Get the Log Entry contents
86     std::string message;
87     std::string_view syslogID;
88     ret = getJournalMetadata(journal, "SYSLOG_IDENTIFIER", syslogID);
89     if (ret < 0)
90     {
91         BMCWEB_LOG_DEBUG("Failed to read SYSLOG_IDENTIFIER field: {}",
92                          strerror(-ret));
93     }
94     if (!syslogID.empty())
95     {
96         message += std::string(syslogID) + ": ";
97     }
98 
99     std::string_view msg;
100     ret = getJournalMetadata(journal, "MESSAGE", msg);
101     if (ret < 0)
102     {
103         BMCWEB_LOG_ERROR("Failed to read MESSAGE field: {}", strerror(-ret));
104         return false;
105     }
106     message += std::string(msg);
107 
108     // Get the severity from the PRIORITY field
109     long int severity = 8; // Default to an invalid priority
110     ret = getJournalMetadataInt(journal, "PRIORITY", 10, severity);
111     if (ret < 0)
112     {
113         BMCWEB_LOG_DEBUG("Failed to read PRIORITY field: {}", strerror(-ret));
114     }
115 
116     // Get the Created time from the timestamp
117     std::string entryTimeStr;
118     if (!getEntryTimestamp(journal, entryTimeStr))
119     {
120         return false;
121     }
122 
123     // Fill in the log entry with the gathered data
124     bmcJournalLogEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry";
125 
126     std::string entryIdBase64 =
127         crow::utility::base64encode(bmcJournalLogEntryID);
128 
129     bmcJournalLogEntryJson["@odata.id"] = boost::urls::format(
130         "/redfish/v1/Managers/{}/LogServices/Journal/Entries/{}",
131         BMCWEB_REDFISH_MANAGER_URI_NAME, entryIdBase64);
132     bmcJournalLogEntryJson["Name"] = "BMC Journal Entry";
133     bmcJournalLogEntryJson["Id"] = entryIdBase64;
134     bmcJournalLogEntryJson["Message"] = std::move(message);
135     bmcJournalLogEntryJson["EntryType"] = log_entry::LogEntryType::Oem;
136     log_entry::EventSeverity severityEnum = log_entry::EventSeverity::OK;
137     if (severity <= 2)
138     {
139         severityEnum = log_entry::EventSeverity::Critical;
140     }
141     else if (severity <= 4)
142     {
143         severityEnum = log_entry::EventSeverity::Warning;
144     }
145 
146     bmcJournalLogEntryJson["Severity"] = severityEnum;
147     bmcJournalLogEntryJson["OemRecordFormat"] = "BMC Journal Entry";
148     bmcJournalLogEntryJson["Created"] = std::move(entryTimeStr);
149     return true;
150 }
151 
152 inline void handleManagersLogServiceJournalGet(
153     App& app, const crow::Request& req,
154     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
155     const std::string& managerId)
156 {
157     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
158     {
159         return;
160     }
161 
162     if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
163     {
164         messages::resourceNotFound(asyncResp->res, "Manager", managerId);
165         return;
166     }
167 
168     asyncResp->res.jsonValue["@odata.type"] = "#LogService.v1_2_0.LogService";
169     asyncResp->res.jsonValue["@odata.id"] =
170         boost::urls::format("/redfish/v1/Managers/{}/LogServices/Journal",
171                             BMCWEB_REDFISH_MANAGER_URI_NAME);
172     asyncResp->res.jsonValue["Name"] = "Open BMC Journal Log Service";
173     asyncResp->res.jsonValue["Description"] = "BMC Journal Log Service";
174     asyncResp->res.jsonValue["Id"] = "Journal";
175     asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull";
176 
177     std::pair<std::string, std::string> redfishDateTimeOffset =
178         redfish::time_utils::getDateTimeOffsetNow();
179     asyncResp->res.jsonValue["DateTime"] = redfishDateTimeOffset.first;
180     asyncResp->res.jsonValue["DateTimeLocalOffset"] =
181         redfishDateTimeOffset.second;
182 
183     asyncResp->res.jsonValue["Entries"]["@odata.id"] = boost::urls::format(
184         "/redfish/v1/Managers/{}/LogServices/Journal/Entries",
185         BMCWEB_REDFISH_MANAGER_URI_NAME);
186 }
187 
188 struct JournalReadState
189 {
190     std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal;
191 };
192 
193 inline void readJournalEntries(
194     uint64_t topEntryCount, const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
195     JournalReadState&& readState)
196 {
197     nlohmann::json& logEntry = asyncResp->res.jsonValue["Members"];
198     nlohmann::json::array_t* logEntryArray =
199         logEntry.get_ptr<nlohmann::json::array_t*>();
200     if (logEntryArray == nullptr)
201     {
202         messages::internalError(asyncResp->res);
203         return;
204     }
205 
206     // The Journal APIs unfortunately do blocking calls to the filesystem, and
207     // can be somewhat expensive.  Short of creating our own io_uring based
208     // implementation of sd-journal, which would be difficult, the best thing we
209     // can do is to only parse a certain number of entries at a time.  The
210     // current chunk size is selected arbitrarily to ensure that we're not
211     // trying to process thousands of entries at the same time.
212     // The implementation will process the number of entries, then return
213     // control to the io_context to let other operations continue.
214     size_t segmentCountRemaining = 10;
215 
216     // Reset the unique ID on the first entry
217     for (uint64_t entryCount = logEntryArray->size();
218          entryCount < topEntryCount; entryCount++)
219     {
220         if (segmentCountRemaining == 0)
221         {
222             boost::asio::post(crow::connections::systemBus->get_io_context(),
223                               [asyncResp, topEntryCount,
224                                readState = std::move(readState)]() mutable {
225                                   readJournalEntries(topEntryCount, asyncResp,
226                                                      std::move(readState));
227                               });
228             return;
229         }
230 
231         nlohmann::json::object_t bmcJournalLogEntry;
232         if (!fillBMCJournalLogEntryJson(readState.journal.get(),
233                                         bmcJournalLogEntry))
234         {
235             messages::internalError(asyncResp->res);
236             return;
237         }
238         logEntryArray->emplace_back(std::move(bmcJournalLogEntry));
239 
240         int ret = sd_journal_next(readState.journal.get());
241         if (ret < 0)
242         {
243             messages::internalError(asyncResp->res);
244             return;
245         }
246         if (ret == 0)
247         {
248             break;
249         }
250         segmentCountRemaining--;
251     }
252 }
253 
254 inline void handleManagersJournalLogEntryCollectionGet(
255     App& app, const crow::Request& req,
256     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
257     const std::string& managerId)
258 {
259     query_param::QueryCapabilities capabilities = {
260         .canDelegateTop = true,
261         .canDelegateSkip = true,
262     };
263     query_param::Query delegatedQuery;
264     if (!redfish::setUpRedfishRouteWithDelegation(app, req, asyncResp,
265                                                   delegatedQuery, capabilities))
266     {
267         return;
268     }
269 
270     if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
271     {
272         messages::resourceNotFound(asyncResp->res, "Manager", managerId);
273         return;
274     }
275 
276     size_t skip = delegatedQuery.skip.value_or(0);
277     size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop);
278 
279     // Collections don't include the static data added by SubRoute
280     // because it has a duplicate entry for members
281     asyncResp->res.jsonValue["@odata.type"] =
282         "#LogEntryCollection.LogEntryCollection";
283     asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
284         "/redfish/v1/Managers/{}/LogServices/Journal/Entries",
285         BMCWEB_REDFISH_MANAGER_URI_NAME);
286     asyncResp->res.jsonValue["Name"] = "Open BMC Journal Entries";
287     asyncResp->res.jsonValue["Description"] =
288         "Collection of BMC Journal Entries";
289     asyncResp->res.jsonValue["Members"] = nlohmann::json::array_t();
290 
291     // Go through the journal and use the timestamp to create a
292     // unique ID for each entry
293     sd_journal* journalTmp = nullptr;
294     int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY);
295     if (ret < 0)
296     {
297         BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret));
298         messages::internalError(asyncResp->res);
299         return;
300     }
301 
302     std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal(
303         journalTmp, sd_journal_close);
304     journalTmp = nullptr;
305 
306     // Seek to the end
307     if (sd_journal_seek_tail(journal.get()) < 0)
308     {
309         messages::internalError(asyncResp->res);
310         return;
311     }
312 
313     // Get the last entry
314     if (sd_journal_previous(journal.get()) < 0)
315     {
316         messages::internalError(asyncResp->res);
317         return;
318     }
319 
320     // Get the last sequence number
321     uint64_t endSeqNum = 0;
322 #if LIBSYSTEMD_VERSION >= 254
323     {
324         if (sd_journal_get_seqnum(journal.get(), &endSeqNum, nullptr) < 0)
325         {
326             messages::internalError(asyncResp->res);
327             return;
328         }
329     }
330 #endif
331 
332     // Seek to the beginning
333     if (sd_journal_seek_head(journal.get()) < 0)
334     {
335         messages::internalError(asyncResp->res);
336         return;
337     }
338 
339     // Get the first entry
340     if (sd_journal_next(journal.get()) < 0)
341     {
342         messages::internalError(asyncResp->res);
343         return;
344     }
345 
346     // Get the first sequence number
347     uint64_t startSeqNum = 0;
348 #if LIBSYSTEMD_VERSION >= 254
349     {
350         if (sd_journal_get_seqnum(journal.get(), &startSeqNum, nullptr) < 0)
351         {
352             messages::internalError(asyncResp->res);
353             return;
354         }
355     }
356 #endif
357 
358     BMCWEB_LOG_DEBUG("journal Sequence IDs start:{} end:{}", startSeqNum,
359                      endSeqNum);
360 
361     // Add 1 to account for the last entry
362     uint64_t totalEntries = endSeqNum - startSeqNum + 1;
363     asyncResp->res.jsonValue["Members@odata.count"] = totalEntries;
364     if (skip + top < totalEntries)
365     {
366         asyncResp->res.jsonValue["Members@odata.nextLink"] =
367             boost::urls::format(
368                 "/redfish/v1/Managers/{}/LogServices/Journal/Entries?$skip={}",
369                 BMCWEB_REDFISH_MANAGER_URI_NAME, std::to_string(skip + top));
370     }
371     uint64_t index = 0;
372     if (skip > 0)
373     {
374         if (sd_journal_next_skip(journal.get(), skip) < 0)
375         {
376             messages::internalError(asyncResp->res);
377             return;
378         }
379     }
380     BMCWEB_LOG_DEBUG("Index was {}", index);
381     readJournalEntries(top, asyncResp, {std::move(journal)});
382 }
383 
384 inline void handleManagersJournalEntriesLogEntryGet(
385     App& app, const crow::Request& req,
386     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
387     const std::string& managerId, const std::string& entryID)
388 {
389     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
390     {
391         return;
392     }
393 
394     if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
395     {
396         messages::resourceNotFound(asyncResp->res, "Manager", managerId);
397         return;
398     }
399 
400     sd_journal* journalTmp = nullptr;
401     int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY);
402     if (ret < 0)
403     {
404         BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret));
405         messages::internalError(asyncResp->res);
406         return;
407     }
408     std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal(
409         journalTmp, sd_journal_close);
410     journalTmp = nullptr;
411 
412     std::string cursor;
413     if (!crow::utility::base64Decode(entryID, cursor))
414     {
415         messages::resourceNotFound(asyncResp->res, "LogEntry", entryID);
416         return;
417     }
418 
419     // Go to the cursor in the log
420     ret = sd_journal_seek_cursor(journal.get(), cursor.c_str());
421     if (ret < 0)
422     {
423         messages::resourceNotFound(asyncResp->res, "LogEntry", entryID);
424         return;
425     }
426 
427     if (sd_journal_next(journal.get()) < 0)
428     {
429         messages::internalError(asyncResp->res);
430         return;
431     }
432 
433     ret = sd_journal_test_cursor(journal.get(), cursor.c_str());
434     if (ret == 0)
435     {
436         messages::resourceNotFound(asyncResp->res, "LogEntry", entryID);
437         return;
438     }
439     if (ret < 0)
440     {
441         messages::internalError(asyncResp->res);
442         return;
443     }
444 
445     nlohmann::json::object_t bmcJournalLogEntry;
446     if (!fillBMCJournalLogEntryJson(journal.get(), bmcJournalLogEntry))
447     {
448         messages::internalError(asyncResp->res);
449         return;
450     }
451     asyncResp->res.jsonValue.update(bmcJournalLogEntry);
452 }
453 
454 inline void requestRoutesBMCJournalLogService(App& app)
455 {
456     BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/")
457         .privileges(redfish::privileges::getLogService)
458         .methods(boost::beast::http::verb::get)(
459             std::bind_front(handleManagersLogServiceJournalGet, std::ref(app)));
460 
461     BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/")
462         .privileges(redfish::privileges::getLogEntryCollection)
463         .methods(boost::beast::http::verb::get)(std::bind_front(
464             handleManagersJournalLogEntryCollectionGet, std::ref(app)));
465 
466     BMCWEB_ROUTE(
467         app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/<str>/")
468         .privileges(redfish::privileges::getLogEntry)
469         .methods(boost::beast::http::verb::get)(std::bind_front(
470             handleManagersJournalEntriesLogEntryGet, std::ref(app)));
471 }
472 } // namespace redfish
473