#pragma once #include "app.hpp" #include "error_messages.hpp" #include "generated/enums/log_entry.hpp" #include "query.hpp" #include "registries/base_message_registry.hpp" #include "registries/privilege_registry.hpp" #include "utils/time_utils.hpp" #include #include #include #include #include #include namespace redfish { // Entry is formed like "BootID_timestamp" or "BootID_timestamp_index" inline bool getTimestampFromID(const std::shared_ptr& asyncResp, std::string_view entryIDStrView, sd_id128_t& bootID, uint64_t& timestamp, uint64_t& index) { // Convert the unique ID back to a bootID + timestamp to find the entry auto underscore1Pos = entryIDStrView.find('_'); if (underscore1Pos == std::string_view::npos) { // EntryID has no bootID or timestamp messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView); return false; } // EntryID has bootID + timestamp // Convert entryIDViewString to BootID // NOTE: bootID string which needs to be null-terminated for // sd_id128_from_string() std::string bootIDStr(entryIDStrView.substr(0, underscore1Pos)); if (sd_id128_from_string(bootIDStr.c_str(), &bootID) < 0) { messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView); return false; } // Get the timestamp from entryID entryIDStrView.remove_prefix(underscore1Pos + 1); auto [timestampEnd, tstampEc] = std::from_chars( entryIDStrView.begin(), entryIDStrView.end(), timestamp); if (tstampEc != std::errc()) { messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView); return false; } entryIDStrView = std::string_view( timestampEnd, static_cast(std::distance(timestampEnd, entryIDStrView.end()))); if (entryIDStrView.empty()) { index = 0U; return true; } // Timestamp might include optional index, if two events happened at the // same "time". if (entryIDStrView[0] != '_') { messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView); return false; } entryIDStrView.remove_prefix(1); auto [ptr, indexEc] = std::from_chars(entryIDStrView.begin(), entryIDStrView.end(), index); if (indexEc != std::errc() || ptr != entryIDStrView.end()) { messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView); return false; } return true; } inline bool getUniqueEntryID(sd_journal* journal, std::string& entryID, const bool firstEntry = true) { int ret = 0; static sd_id128_t prevBootID{}; static uint64_t prevTs = 0; static int index = 0; if (firstEntry) { prevBootID = {}; prevTs = 0; } // Get the entry timestamp uint64_t curTs = 0; sd_id128_t curBootID{}; ret = sd_journal_get_monotonic_usec(journal, &curTs, &curBootID); if (ret < 0) { BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}", strerror(-ret)); return false; } // If the timestamp isn't unique on the same boot, increment the index bool sameBootIDs = sd_id128_equal(curBootID, prevBootID) != 0; if (sameBootIDs && (curTs == prevTs)) { index++; } else { // Otherwise, reset it index = 0; } if (!sameBootIDs) { // Save the bootID prevBootID = curBootID; } // Save the timestamp prevTs = curTs; // make entryID as _[_] std::array bootIDStr{}; sd_id128_to_string(curBootID, bootIDStr.data()); entryID = std::format("{}_{}", bootIDStr.data(), curTs); if (index > 0) { entryID += "_" + std::to_string(index); } return true; } inline int getJournalMetadata(sd_journal* journal, std::string_view field, std::string_view& contents) { const char* data = nullptr; size_t length = 0; int ret = 0; // Get the metadata from the requested field of the journal entry // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) const void** dataVoid = reinterpret_cast(&data); ret = sd_journal_get_data(journal, field.data(), dataVoid, &length); if (ret < 0) { return ret; } contents = std::string_view(data, length); // Only use the content after the "=" character. contents.remove_prefix(std::min(contents.find('=') + 1, contents.size())); return ret; } inline int getJournalMetadataInt(sd_journal* journal, std::string_view field, const int& base, long int& contents) { int ret = 0; std::string_view metadata; // Get the metadata from the requested field of the journal entry ret = getJournalMetadata(journal, field, metadata); if (ret < 0) { return ret; } contents = strtol(metadata.data(), nullptr, base); return ret; } inline bool getEntryTimestamp(sd_journal* journal, std::string& entryTimestamp) { int ret = 0; uint64_t timestamp = 0; ret = sd_journal_get_realtime_usec(journal, ×tamp); if (ret < 0) { BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}", strerror(-ret)); return false; } entryTimestamp = redfish::time_utils::getDateTimeUintUs(timestamp); return true; } inline int fillBMCJournalLogEntryJson(const std::string& bmcJournalLogEntryID, sd_journal* journal, nlohmann::json::object_t& bmcJournalLogEntryJson) { // Get the Log Entry contents int ret = 0; std::string message; std::string_view syslogID; ret = getJournalMetadata(journal, "SYSLOG_IDENTIFIER", syslogID); if (ret < 0) { BMCWEB_LOG_DEBUG("Failed to read SYSLOG_IDENTIFIER field: {}", strerror(-ret)); } if (!syslogID.empty()) { message += std::string(syslogID) + ": "; } std::string_view msg; ret = getJournalMetadata(journal, "MESSAGE", msg); if (ret < 0) { BMCWEB_LOG_ERROR("Failed to read MESSAGE field: {}", strerror(-ret)); return 1; } message += std::string(msg); // Get the severity from the PRIORITY field long int severity = 8; // Default to an invalid priority ret = getJournalMetadataInt(journal, "PRIORITY", 10, severity); if (ret < 0) { BMCWEB_LOG_DEBUG("Failed to read PRIORITY field: {}", strerror(-ret)); } // Get the Created time from the timestamp std::string entryTimeStr; if (!getEntryTimestamp(journal, entryTimeStr)) { return 1; } // Fill in the log entry with the gathered data bmcJournalLogEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry"; bmcJournalLogEntryJson["@odata.id"] = boost::urls::format( "/redfish/v1/Managers/{}/LogServices/Journal/Entries/{}", BMCWEB_REDFISH_MANAGER_URI_NAME, bmcJournalLogEntryID); bmcJournalLogEntryJson["Name"] = "BMC Journal Entry"; bmcJournalLogEntryJson["Id"] = bmcJournalLogEntryID; bmcJournalLogEntryJson["Message"] = std::move(message); bmcJournalLogEntryJson["EntryType"] = "Oem"; log_entry::EventSeverity severityEnum = log_entry::EventSeverity::OK; if (severity <= 2) { severityEnum = log_entry::EventSeverity::Critical; } else if (severity <= 4) { severityEnum = log_entry::EventSeverity::Warning; } bmcJournalLogEntryJson["Severity"] = severityEnum; bmcJournalLogEntryJson["OemRecordFormat"] = "BMC Journal Entry"; bmcJournalLogEntryJson["Created"] = std::move(entryTimeStr); return 0; } inline void requestRoutesBMCJournalLogService(App& app) { BMCWEB_ROUTE(app, "/redfish/v1/Managers//LogServices/Journal/") .privileges(redfish::privileges::getLogService) .methods(boost::beast::http::verb::get)( [&app](const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& managerId) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) { messages::resourceNotFound(asyncResp->res, "Manager", managerId); return; } asyncResp->res.jsonValue["@odata.type"] = "#LogService.v1_2_0.LogService"; asyncResp->res.jsonValue["@odata.id"] = boost::urls::format("/redfish/v1/Managers/{}/LogServices/Journal", BMCWEB_REDFISH_MANAGER_URI_NAME); asyncResp->res.jsonValue["Name"] = "Open BMC Journal Log Service"; asyncResp->res.jsonValue["Description"] = "BMC Journal Log Service"; asyncResp->res.jsonValue["Id"] = "Journal"; asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull"; std::pair redfishDateTimeOffset = redfish::time_utils::getDateTimeOffsetNow(); asyncResp->res.jsonValue["DateTime"] = redfishDateTimeOffset.first; asyncResp->res.jsonValue["DateTimeLocalOffset"] = redfishDateTimeOffset.second; asyncResp->res.jsonValue["Entries"]["@odata.id"] = boost::urls::format( "/redfish/v1/Managers/{}/LogServices/Journal/Entries", BMCWEB_REDFISH_MANAGER_URI_NAME); }); } inline void requestRoutesBMCJournalLogEntryCollection(App& app) { BMCWEB_ROUTE(app, "/redfish/v1/Managers//LogServices/Journal/Entries/") .privileges(redfish::privileges::getLogEntryCollection) .methods(boost::beast::http::verb::get)( [&app](const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& managerId) { query_param::QueryCapabilities capabilities = { .canDelegateTop = true, .canDelegateSkip = true, }; query_param::Query delegatedQuery; if (!redfish::setUpRedfishRouteWithDelegation( app, req, asyncResp, delegatedQuery, capabilities)) { return; } if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) { messages::resourceNotFound(asyncResp->res, "Manager", managerId); return; } size_t skip = delegatedQuery.skip.value_or(0); size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop); // Collections don't include the static data added by SubRoute // because it has a duplicate entry for members asyncResp->res.jsonValue["@odata.type"] = "#LogEntryCollection.LogEntryCollection"; asyncResp->res.jsonValue["@odata.id"] = boost::urls::format( "/redfish/v1/Managers/{}/LogServices/Journal/Entries", BMCWEB_REDFISH_MANAGER_URI_NAME); asyncResp->res.jsonValue["Name"] = "Open BMC Journal Entries"; asyncResp->res.jsonValue["Description"] = "Collection of BMC Journal Entries"; nlohmann::json& logEntryArray = asyncResp->res.jsonValue["Members"]; logEntryArray = nlohmann::json::array(); // Go through the journal and use the timestamp to create a // unique ID for each entry sd_journal* journalTmp = nullptr; int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); if (ret < 0) { BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret)); messages::internalError(asyncResp->res); return; } std::unique_ptr journal( journalTmp, sd_journal_close); journalTmp = nullptr; uint64_t entryCount = 0; // Reset the unique ID on the first entry bool firstEntry = true; SD_JOURNAL_FOREACH(journal.get()) { entryCount++; // Handle paging using skip (number of entries to skip from // the start) and top (number of entries to display) if (entryCount <= skip || entryCount > skip + top) { continue; } std::string idStr; if (!getUniqueEntryID(journal.get(), idStr, firstEntry)) { continue; } firstEntry = false; nlohmann::json::object_t bmcJournalLogEntry; if (fillBMCJournalLogEntryJson(idStr, journal.get(), bmcJournalLogEntry) != 0) { messages::internalError(asyncResp->res); return; } logEntryArray.emplace_back(std::move(bmcJournalLogEntry)); } asyncResp->res.jsonValue["Members@odata.count"] = entryCount; if (skip + top < entryCount) { asyncResp->res .jsonValue["Members@odata.nextLink"] = boost::urls::format( "/redfish/v1/Managers/{}/LogServices/Journal/Entries?$skip={}", BMCWEB_REDFISH_MANAGER_URI_NAME, std::to_string(skip + top)); } }); } inline void requestRoutesBMCJournalLogEntry(App& app) { BMCWEB_ROUTE( app, "/redfish/v1/Managers//LogServices/Journal/Entries//") .privileges(redfish::privileges::getLogEntry) .methods(boost::beast::http::verb::get)( [&app](const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& managerId, const std::string& entryID) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) { messages::resourceNotFound(asyncResp->res, "Manager", managerId); return; } // Convert the unique ID back to a timestamp to find the entry sd_id128_t bootID{}; uint64_t ts = 0; uint64_t index = 0; if (!getTimestampFromID(asyncResp, entryID, bootID, ts, index)) { return; } sd_journal* journalTmp = nullptr; int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); if (ret < 0) { BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret)); messages::internalError(asyncResp->res); return; } std::unique_ptr journal( journalTmp, sd_journal_close); journalTmp = nullptr; // Go to the timestamp in the log and move to the entry at the // index tracking the unique ID std::string idStr; bool firstEntry = true; ret = sd_journal_seek_monotonic_usec(journal.get(), bootID, ts); if (ret < 0) { BMCWEB_LOG_ERROR("failed to seek to an entry in journal{}", strerror(-ret)); messages::internalError(asyncResp->res); return; } for (uint64_t i = 0; i <= index; i++) { sd_journal_next(journal.get()); if (!getUniqueEntryID(journal.get(), idStr, firstEntry)) { messages::internalError(asyncResp->res); return; } firstEntry = false; } // Confirm that the entry ID matches what was requested if (idStr != entryID) { messages::resourceNotFound(asyncResp->res, "LogEntry", entryID); return; } nlohmann::json::object_t bmcJournalLogEntry; if (fillBMCJournalLogEntryJson(entryID, journal.get(), bmcJournalLogEntry) != 0) { messages::internalError(asyncResp->res); return; } asyncResp->res.jsonValue.update(bmcJournalLogEntry); }); } } // namespace redfish