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 // Entry is formed like "BootID_timestamp" or "BootID_timestamp_index"
23 inline bool
getTimestampFromID(const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,std::string_view entryIDStrView,sd_id128_t & bootID,uint64_t & timestamp,uint64_t & index)24 getTimestampFromID(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
25 std::string_view entryIDStrView, sd_id128_t& bootID,
26 uint64_t& timestamp, uint64_t& index)
27 {
28 // Convert the unique ID back to a bootID + timestamp to find the entry
29 auto underscore1Pos = entryIDStrView.find('_');
30 if (underscore1Pos == std::string_view::npos)
31 {
32 // EntryID has no bootID or timestamp
33 messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
34 return false;
35 }
36
37 // EntryID has bootID + timestamp
38
39 // Convert entryIDViewString to BootID
40 // NOTE: bootID string which needs to be null-terminated for
41 // sd_id128_from_string()
42 std::string bootIDStr(entryIDStrView.substr(0, underscore1Pos));
43 if (sd_id128_from_string(bootIDStr.c_str(), &bootID) < 0)
44 {
45 messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
46 return false;
47 }
48
49 // Get the timestamp from entryID
50 entryIDStrView.remove_prefix(underscore1Pos + 1);
51
52 auto [timestampEnd, tstampEc] = std::from_chars(
53 entryIDStrView.begin(), entryIDStrView.end(), timestamp);
54 if (tstampEc != std::errc())
55 {
56 messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
57 return false;
58 }
59 entryIDStrView = std::string_view(
60 timestampEnd,
61 static_cast<size_t>(std::distance(timestampEnd, entryIDStrView.end())));
62 if (entryIDStrView.empty())
63 {
64 index = 0U;
65 return true;
66 }
67 // Timestamp might include optional index, if two events happened at the
68 // same "time".
69 if (entryIDStrView[0] != '_')
70 {
71 messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
72 return false;
73 }
74 entryIDStrView.remove_prefix(1);
75 auto [ptr, indexEc] =
76 std::from_chars(entryIDStrView.begin(), entryIDStrView.end(), index);
77 if (indexEc != std::errc() || ptr != entryIDStrView.end())
78 {
79 messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
80 return false;
81 }
82 if (index <= 1)
83 {
84 // Indexes go directly from no postfix (handled above) to _2
85 // so if we ever see _0 or _1, it's incorrect
86 messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView);
87 return false;
88 }
89
90 // URI indexes are one based, journald is zero based
91 index -= 1;
92 return true;
93 }
94
getUniqueEntryID(uint64_t index,uint64_t curTs,sd_id128_t & curBootID)95 inline std::string getUniqueEntryID(uint64_t index, uint64_t curTs,
96 sd_id128_t& curBootID)
97 {
98 // make entryID as <bootID>_<timestamp>[_<index>]
99 std::array<char, SD_ID128_STRING_MAX> bootIDStr{};
100 sd_id128_to_string(curBootID, bootIDStr.data());
101 std::string postfix;
102 if (index > 0)
103 {
104 postfix = std::format("_{}", index + 1);
105 }
106 return std::format("{}_{}{}", bootIDStr.data(), curTs, postfix);
107 }
108
getJournalMetadata(sd_journal * journal,std::string_view field,std::string_view & contents)109 inline int getJournalMetadata(sd_journal* journal, std::string_view field,
110 std::string_view& contents)
111 {
112 const char* data = nullptr;
113 size_t length = 0;
114 int ret = 0;
115 // Get the metadata from the requested field of the journal entry
116 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
117 const void** dataVoid = reinterpret_cast<const void**>(&data);
118
119 ret = sd_journal_get_data(journal, field.data(), dataVoid, &length);
120 if (ret < 0)
121 {
122 return ret;
123 }
124 contents = std::string_view(data, length);
125 // Only use the content after the "=" character.
126 contents.remove_prefix(std::min(contents.find('=') + 1, contents.size()));
127 return ret;
128 }
129
getJournalMetadataInt(sd_journal * journal,std::string_view field,const int & base,long int & contents)130 inline int getJournalMetadataInt(sd_journal* journal, std::string_view field,
131 const int& base, long int& contents)
132 {
133 int ret = 0;
134 std::string_view metadata;
135 // Get the metadata from the requested field of the journal entry
136 ret = getJournalMetadata(journal, field, metadata);
137 if (ret < 0)
138 {
139 return ret;
140 }
141 contents = strtol(metadata.data(), nullptr, base);
142 return ret;
143 }
144
getEntryTimestamp(sd_journal * journal,std::string & entryTimestamp)145 inline bool getEntryTimestamp(sd_journal* journal, std::string& entryTimestamp)
146 {
147 int ret = 0;
148 uint64_t timestamp = 0;
149 ret = sd_journal_get_realtime_usec(journal, ×tamp);
150 if (ret < 0)
151 {
152 BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}", strerror(-ret));
153 return false;
154 }
155 entryTimestamp = redfish::time_utils::getDateTimeUintUs(timestamp);
156 return true;
157 }
158
fillBMCJournalLogEntryJson(const std::string & bmcJournalLogEntryID,sd_journal * journal,nlohmann::json::object_t & bmcJournalLogEntryJson)159 inline bool fillBMCJournalLogEntryJson(
160 const std::string& bmcJournalLogEntryID, sd_journal* journal,
161 nlohmann::json::object_t& bmcJournalLogEntryJson)
162 {
163 // Get the Log Entry contents
164 std::string message;
165 std::string_view syslogID;
166 int ret = getJournalMetadata(journal, "SYSLOG_IDENTIFIER", syslogID);
167 if (ret < 0)
168 {
169 BMCWEB_LOG_DEBUG("Failed to read SYSLOG_IDENTIFIER field: {}",
170 strerror(-ret));
171 }
172 if (!syslogID.empty())
173 {
174 message += std::string(syslogID) + ": ";
175 }
176
177 std::string_view msg;
178 ret = getJournalMetadata(journal, "MESSAGE", msg);
179 if (ret < 0)
180 {
181 BMCWEB_LOG_ERROR("Failed to read MESSAGE field: {}", strerror(-ret));
182 return false;
183 }
184 message += std::string(msg);
185
186 // Get the severity from the PRIORITY field
187 long int severity = 8; // Default to an invalid priority
188 ret = getJournalMetadataInt(journal, "PRIORITY", 10, severity);
189 if (ret < 0)
190 {
191 BMCWEB_LOG_DEBUG("Failed to read PRIORITY field: {}", strerror(-ret));
192 }
193
194 // Get the Created time from the timestamp
195 std::string entryTimeStr;
196 if (!getEntryTimestamp(journal, entryTimeStr))
197 {
198 return false;
199 }
200
201 // Fill in the log entry with the gathered data
202 bmcJournalLogEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry";
203 bmcJournalLogEntryJson["@odata.id"] = boost::urls::format(
204 "/redfish/v1/Managers/{}/LogServices/Journal/Entries/{}",
205 BMCWEB_REDFISH_MANAGER_URI_NAME, bmcJournalLogEntryID);
206 bmcJournalLogEntryJson["Name"] = "BMC Journal Entry";
207 bmcJournalLogEntryJson["Id"] = bmcJournalLogEntryID;
208 bmcJournalLogEntryJson["Message"] = std::move(message);
209 bmcJournalLogEntryJson["EntryType"] = log_entry::LogEntryType::Oem;
210 log_entry::EventSeverity severityEnum = log_entry::EventSeverity::OK;
211 if (severity <= 2)
212 {
213 severityEnum = log_entry::EventSeverity::Critical;
214 }
215 else if (severity <= 4)
216 {
217 severityEnum = log_entry::EventSeverity::Warning;
218 }
219
220 bmcJournalLogEntryJson["Severity"] = severityEnum;
221 bmcJournalLogEntryJson["OemRecordFormat"] = "BMC Journal Entry";
222 bmcJournalLogEntryJson["Created"] = std::move(entryTimeStr);
223 return true;
224 }
225
handleManagersLogServiceJournalGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & managerId)226 inline void handleManagersLogServiceJournalGet(
227 App& app, const crow::Request& req,
228 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
229 const std::string& managerId)
230 {
231 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
232 {
233 return;
234 }
235
236 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
237 {
238 messages::resourceNotFound(asyncResp->res, "Manager", managerId);
239 return;
240 }
241
242 asyncResp->res.jsonValue["@odata.type"] = "#LogService.v1_2_0.LogService";
243 asyncResp->res.jsonValue["@odata.id"] =
244 boost::urls::format("/redfish/v1/Managers/{}/LogServices/Journal",
245 BMCWEB_REDFISH_MANAGER_URI_NAME);
246 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Log Service";
247 asyncResp->res.jsonValue["Description"] = "BMC Journal Log Service";
248 asyncResp->res.jsonValue["Id"] = "Journal";
249 asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull";
250
251 std::pair<std::string, std::string> redfishDateTimeOffset =
252 redfish::time_utils::getDateTimeOffsetNow();
253 asyncResp->res.jsonValue["DateTime"] = redfishDateTimeOffset.first;
254 asyncResp->res.jsonValue["DateTimeLocalOffset"] =
255 redfishDateTimeOffset.second;
256
257 asyncResp->res.jsonValue["Entries"]["@odata.id"] = boost::urls::format(
258 "/redfish/v1/Managers/{}/LogServices/Journal/Entries",
259 BMCWEB_REDFISH_MANAGER_URI_NAME);
260 }
261
262 struct JournalReadState
263 {
264 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal;
265 uint64_t index = 0;
266 sd_id128_t prevBootID{};
267 uint64_t prevTs = 0;
268 };
269
readJournalEntries(uint64_t topEntryCount,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,JournalReadState && readState)270 inline void readJournalEntries(
271 uint64_t topEntryCount, const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
272 JournalReadState&& readState)
273 {
274 nlohmann::json& logEntry = asyncResp->res.jsonValue["Members"];
275 nlohmann::json::array_t* logEntryArray =
276 logEntry.get_ptr<nlohmann::json::array_t*>();
277 if (logEntryArray == nullptr)
278 {
279 messages::internalError(asyncResp->res);
280 return;
281 }
282
283 // The Journal APIs unfortunately do blocking calls to the filesystem, and
284 // can be somewhat expensive. Short of creating our own io_uring based
285 // implementation of sd-journal, which would be difficult, the best thing we
286 // can do is to only parse a certain number of entries at a time. The
287 // current chunk size is selected arbitrarily to ensure that we're not
288 // trying to process thousands of entries at the same time.
289 // The implementation will process the number of entries, then return
290 // control to the io_context to let other operations continue.
291 size_t segmentCountRemaining = 10;
292
293 // Reset the unique ID on the first entry
294 for (uint64_t entryCount = logEntryArray->size();
295 entryCount < topEntryCount; entryCount++)
296 {
297 if (segmentCountRemaining == 0)
298 {
299 boost::asio::post(crow::connections::systemBus->get_io_context(),
300 [asyncResp, topEntryCount,
301 readState = std::move(readState)]() mutable {
302 readJournalEntries(topEntryCount, asyncResp,
303 std::move(readState));
304 });
305 return;
306 }
307
308 // Get the entry timestamp
309 sd_id128_t curBootID{};
310 uint64_t curTs = 0;
311 int ret = sd_journal_get_monotonic_usec(readState.journal.get(), &curTs,
312 &curBootID);
313 if (ret < 0)
314 {
315 BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}",
316 strerror(-ret));
317 messages::internalError(asyncResp->res);
318 return;
319 }
320
321 // If the timestamp isn't unique on the same boot, increment the index
322 bool sameBootIDs = sd_id128_equal(curBootID, readState.prevBootID) != 0;
323 if (sameBootIDs && (curTs == readState.prevTs))
324 {
325 readState.index++;
326 }
327 else
328 {
329 // Otherwise, reset it
330 readState.index = 0;
331 }
332
333 // Save the bootID
334 readState.prevBootID = curBootID;
335
336 // Save the timestamp
337 readState.prevTs = curTs;
338
339 std::string idStr = getUniqueEntryID(readState.index, curTs, curBootID);
340
341 nlohmann::json::object_t bmcJournalLogEntry;
342 if (!fillBMCJournalLogEntryJson(idStr, readState.journal.get(),
343 bmcJournalLogEntry))
344 {
345 messages::internalError(asyncResp->res);
346 return;
347 }
348 logEntryArray->emplace_back(std::move(bmcJournalLogEntry));
349
350 ret = sd_journal_next(readState.journal.get());
351 if (ret < 0)
352 {
353 messages::internalError(asyncResp->res);
354 return;
355 }
356 if (ret == 0)
357 {
358 break;
359 }
360 segmentCountRemaining--;
361 }
362 }
363
handleManagersJournalLogEntryCollectionGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & managerId)364 inline void handleManagersJournalLogEntryCollectionGet(
365 App& app, const crow::Request& req,
366 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
367 const std::string& managerId)
368 {
369 query_param::QueryCapabilities capabilities = {
370 .canDelegateTop = true,
371 .canDelegateSkip = true,
372 };
373 query_param::Query delegatedQuery;
374 if (!redfish::setUpRedfishRouteWithDelegation(app, req, asyncResp,
375 delegatedQuery, capabilities))
376 {
377 return;
378 }
379
380 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
381 {
382 messages::resourceNotFound(asyncResp->res, "Manager", managerId);
383 return;
384 }
385
386 size_t skip = delegatedQuery.skip.value_or(0);
387 size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop);
388
389 // Collections don't include the static data added by SubRoute
390 // because it has a duplicate entry for members
391 asyncResp->res.jsonValue["@odata.type"] =
392 "#LogEntryCollection.LogEntryCollection";
393 asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
394 "/redfish/v1/Managers/{}/LogServices/Journal/Entries",
395 BMCWEB_REDFISH_MANAGER_URI_NAME);
396 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Entries";
397 asyncResp->res.jsonValue["Description"] =
398 "Collection of BMC Journal Entries";
399 asyncResp->res.jsonValue["Members"] = nlohmann::json::array_t();
400
401 // Go through the journal and use the timestamp to create a
402 // unique ID for each entry
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: {}", strerror(-ret));
408 messages::internalError(asyncResp->res);
409 return;
410 }
411
412 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal(
413 journalTmp, sd_journal_close);
414 journalTmp = nullptr;
415
416 // Seek to the end
417 if (sd_journal_seek_tail(journal.get()) < 0)
418 {
419 messages::internalError(asyncResp->res);
420 return;
421 }
422
423 // Get the last entry
424 if (sd_journal_previous(journal.get()) < 0)
425 {
426 messages::internalError(asyncResp->res);
427 return;
428 }
429
430 // Get the last sequence number
431 uint64_t endSeqNum = 0;
432 #if LIBSYSTEMD_VERSION >= 254
433 {
434 if (sd_journal_get_seqnum(journal.get(), &endSeqNum, nullptr) < 0)
435 {
436 messages::internalError(asyncResp->res);
437 return;
438 }
439 }
440 #endif
441
442 // Seek to the beginning
443 if (sd_journal_seek_head(journal.get()) < 0)
444 {
445 messages::internalError(asyncResp->res);
446 return;
447 }
448
449 // Get the first entry
450 if (sd_journal_next(journal.get()) < 0)
451 {
452 messages::internalError(asyncResp->res);
453 return;
454 }
455
456 // Get the first sequence number
457 uint64_t startSeqNum = 0;
458 #if LIBSYSTEMD_VERSION >= 254
459 {
460 if (sd_journal_get_seqnum(journal.get(), &startSeqNum, nullptr) < 0)
461 {
462 messages::internalError(asyncResp->res);
463 return;
464 }
465 }
466 #endif
467
468 BMCWEB_LOG_DEBUG("journal Sequence IDs start:{} end:{}", startSeqNum,
469 endSeqNum);
470
471 // Add 1 to account for the last entry
472 uint64_t totalEntries = endSeqNum - startSeqNum + 1;
473 asyncResp->res.jsonValue["Members@odata.count"] = totalEntries;
474 if (skip + top < totalEntries)
475 {
476 asyncResp->res.jsonValue["Members@odata.nextLink"] =
477 boost::urls::format(
478 "/redfish/v1/Managers/{}/LogServices/Journal/Entries?$skip={}",
479 BMCWEB_REDFISH_MANAGER_URI_NAME, std::to_string(skip + top));
480 }
481 uint64_t index = 0;
482 sd_id128_t curBootID{};
483 uint64_t curTs = 0;
484 if (skip > 0)
485 {
486 if (sd_journal_next_skip(journal.get(), skip) < 0)
487 {
488 messages::internalError(asyncResp->res);
489 return;
490 }
491
492 // Get the entry timestamp
493 ret = sd_journal_get_monotonic_usec(journal.get(), &curTs, &curBootID);
494 if (ret < 0)
495 {
496 BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}",
497 strerror(-ret));
498 messages::internalError(asyncResp->res);
499 return;
500 }
501
502 uint64_t endChunkSeqNum = 0;
503 #if LIBSYSTEMD_VERSION >= 254
504 {
505 if (sd_journal_get_seqnum(journal.get(), &endChunkSeqNum, nullptr) <
506 0)
507 {
508 messages::internalError(asyncResp->res);
509 return;
510 }
511 }
512 #endif
513
514 // Seek to the first entry with the same timestamp and boot
515 ret = sd_journal_seek_monotonic_usec(journal.get(), curBootID, curTs);
516 if (ret < 0)
517 {
518 BMCWEB_LOG_ERROR("Failed to seek: {}", strerror(-ret));
519 messages::internalError(asyncResp->res);
520 return;
521 }
522 if (sd_journal_next(journal.get()) < 0)
523 {
524 messages::internalError(asyncResp->res);
525 return;
526 }
527 uint64_t startChunkSeqNum = 0;
528 #if LIBSYSTEMD_VERSION >= 254
529 {
530 if (sd_journal_get_seqnum(journal.get(), &startChunkSeqNum,
531 nullptr) < 0)
532 {
533 messages::internalError(asyncResp->res);
534 return;
535 }
536 }
537 #endif
538
539 // Get the difference between the start and end. Most of the time this
540 // will be 0
541 BMCWEB_LOG_DEBUG("start={} end={}", startChunkSeqNum, endChunkSeqNum);
542 index = endChunkSeqNum - startChunkSeqNum;
543 if (index > endChunkSeqNum)
544 {
545 // Detect underflows. Should never happen.
546 messages::internalError(asyncResp->res);
547 return;
548 }
549 if (index > 0)
550 {
551 BMCWEB_LOG_DEBUG("index = {}", index);
552 if (sd_journal_next_skip(journal.get(), index) < 0)
553 {
554 messages::internalError(asyncResp->res);
555 return;
556 }
557 }
558 }
559 // If this is the first entry of this series, reset the timestamps so the
560 // Index doesn't increment
561 if (index == 0)
562 {
563 curBootID = {};
564 curTs = 0;
565 }
566 else
567 {
568 index -= 1;
569 }
570 BMCWEB_LOG_DEBUG("Index was {}", index);
571 readJournalEntries(top, asyncResp,
572 {std::move(journal), index, curBootID, curTs});
573 }
574
handleManagersJournalEntriesLogEntryGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & managerId,const std::string & entryID)575 inline void handleManagersJournalEntriesLogEntryGet(
576 App& app, const crow::Request& req,
577 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
578 const std::string& managerId, const std::string& entryID)
579 {
580 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
581 {
582 return;
583 }
584
585 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME)
586 {
587 messages::resourceNotFound(asyncResp->res, "Manager", managerId);
588 return;
589 }
590
591 // Convert the unique ID back to a timestamp to find the entry
592 sd_id128_t bootID{};
593 uint64_t ts = 0;
594 uint64_t index = 0;
595 if (!getTimestampFromID(asyncResp, entryID, bootID, ts, index))
596 {
597 return;
598 }
599
600 sd_journal* journalTmp = nullptr;
601 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY);
602 if (ret < 0)
603 {
604 BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret));
605 messages::internalError(asyncResp->res);
606 return;
607 }
608 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal(
609 journalTmp, sd_journal_close);
610 journalTmp = nullptr;
611 // Go to the timestamp in the log and move to the entry at the
612 // index tracking the unique ID
613 ret = sd_journal_seek_monotonic_usec(journal.get(), bootID, ts);
614 if (ret < 0)
615 {
616 BMCWEB_LOG_ERROR("failed to seek to an entry in journal{}",
617 strerror(-ret));
618 messages::internalError(asyncResp->res);
619 return;
620 }
621
622 if (sd_journal_next_skip(journal.get(), index + 1) < 0)
623 {
624 messages::internalError(asyncResp->res);
625 return;
626 }
627
628 nlohmann::json::object_t bmcJournalLogEntry;
629 if (!fillBMCJournalLogEntryJson(entryID, journal.get(), bmcJournalLogEntry))
630 {
631 messages::internalError(asyncResp->res);
632 return;
633 }
634 asyncResp->res.jsonValue.update(bmcJournalLogEntry);
635 }
636
requestRoutesBMCJournalLogService(App & app)637 inline void requestRoutesBMCJournalLogService(App& app)
638 {
639 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/")
640 .privileges(redfish::privileges::getLogService)
641 .methods(boost::beast::http::verb::get)(
642 std::bind_front(handleManagersLogServiceJournalGet, std::ref(app)));
643
644 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/")
645 .privileges(redfish::privileges::getLogEntryCollection)
646 .methods(boost::beast::http::verb::get)(std::bind_front(
647 handleManagersJournalLogEntryCollectionGet, std::ref(app)));
648
649 BMCWEB_ROUTE(
650 app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/<str>/")
651 .privileges(redfish::privileges::getLogEntry)
652 .methods(boost::beast::http::verb::get)(std::bind_front(
653 handleManagersJournalEntriesLogEntryGet, std::ref(app)));
654 }
655 } // namespace redfish
656