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