1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 #pragma once 4 5 #include "app.hpp" 6 #include "generated/enums/log_entry.hpp" 7 #include "gzfile.hpp" 8 #include "human_sort.hpp"" 9 #include "query.hpp" 10 #include "registries/openbmc_message_registry.hpp" 11 #include "registries/privilege_registry.hpp" 12 #include "utils/time_utils.hpp" 13 14 #include <cstdint> 15 #include <memory> 16 #include <string_view> 17 #include <utility> 18 #include <vector> 19 20 namespace redfish 21 { 22 constexpr const char* hostLoggerFolderPath = "/var/log/console"; 23 24 inline bool 25 getHostLoggerFiles(const std::string& hostLoggerFilePath, 26 std::vector<std::filesystem::path>& hostLoggerFiles) 27 { 28 std::error_code ec; 29 std::filesystem::directory_iterator logPath(hostLoggerFilePath, ec); 30 if (ec) 31 { 32 BMCWEB_LOG_WARNING("{}", ec.message()); 33 return false; 34 } 35 for (const std::filesystem::directory_entry& it : logPath) 36 { 37 std::string filename = it.path().filename(); 38 // Prefix of each log files is "log". Find the file and save the 39 // path 40 if (filename.starts_with("log")) 41 { 42 hostLoggerFiles.emplace_back(it.path()); 43 } 44 } 45 // As the log files rotate, they are appended with a ".#" that is higher for 46 // the older logs. Since we start from oldest logs, sort the name in 47 // descending order. 48 std::sort(hostLoggerFiles.rbegin(), hostLoggerFiles.rend(), 49 AlphanumLess<std::string>()); 50 51 return true; 52 } 53 54 inline bool getHostLoggerEntries( 55 const std::vector<std::filesystem::path>& hostLoggerFiles, uint64_t skip, 56 uint64_t top, std::vector<std::string>& logEntries, size_t& logCount) 57 { 58 GzFileReader logFile; 59 60 // Go though all log files and expose host logs. 61 for (const std::filesystem::path& it : hostLoggerFiles) 62 { 63 if (!logFile.gzGetLines(it.string(), skip, top, logEntries, logCount)) 64 { 65 BMCWEB_LOG_ERROR("fail to expose host logs"); 66 return false; 67 } 68 } 69 // Get lastMessage from constructor by getter 70 std::string lastMessage = logFile.getLastMessage(); 71 if (!lastMessage.empty()) 72 { 73 logCount++; 74 if (logCount > skip && logCount <= (skip + top)) 75 { 76 logEntries.push_back(lastMessage); 77 } 78 } 79 return true; 80 } 81 82 inline void fillHostLoggerEntryJson(std::string_view logEntryID, 83 std::string_view msg, 84 nlohmann::json::object_t& logEntryJson) 85 { 86 // Fill in the log entry with the gathered data. 87 logEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry"; 88 logEntryJson["@odata.id"] = boost::urls::format( 89 "/redfish/v1/Systems/{}/LogServices/HostLogger/Entries/{}", 90 BMCWEB_REDFISH_SYSTEM_URI_NAME, logEntryID); 91 logEntryJson["Name"] = "Host Logger Entry"; 92 logEntryJson["Id"] = logEntryID; 93 logEntryJson["Message"] = msg; 94 logEntryJson["EntryType"] = log_entry::LogEntryType::Oem; 95 logEntryJson["Severity"] = log_entry::EventSeverity::OK; 96 logEntryJson["OemRecordFormat"] = "Host Logger Entry"; 97 } 98 99 inline void handleSystemsLogServicesHostloggerGet( 100 App& app, const crow::Request& req, 101 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 102 const std::string& systemName) 103 { 104 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 105 { 106 return; 107 } 108 if constexpr (BMCWEB_EXPERIMENTAL_REDFISH_MULTI_COMPUTER_SYSTEM) 109 { 110 // Option currently returns no systems. TBD 111 messages::resourceNotFound(asyncResp->res, "ComputerSystem", 112 systemName); 113 return; 114 } 115 if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME) 116 { 117 messages::resourceNotFound(asyncResp->res, "ComputerSystem", 118 systemName); 119 return; 120 } 121 asyncResp->res.jsonValue["@odata.id"] = 122 std::format("/redfish/v1/Systems/{}/LogServices/HostLogger", 123 BMCWEB_REDFISH_SYSTEM_URI_NAME); 124 asyncResp->res.jsonValue["@odata.type"] = "#LogService.v1_2_0.LogService"; 125 asyncResp->res.jsonValue["Name"] = "Host Logger Service"; 126 asyncResp->res.jsonValue["Description"] = "Host Logger Service"; 127 asyncResp->res.jsonValue["Id"] = "HostLogger"; 128 asyncResp->res.jsonValue["Entries"]["@odata.id"] = 129 std::format("/redfish/v1/Systems/{}/LogServices/HostLogger/Entries", 130 BMCWEB_REDFISH_SYSTEM_URI_NAME); 131 } 132 133 inline void handleSystemsLogServicesHostloggerEntriesGet( 134 App& app, const crow::Request& req, 135 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 136 const std::string& systemName) 137 { 138 query_param::QueryCapabilities capabilities = { 139 .canDelegateTop = true, 140 .canDelegateSkip = true, 141 }; 142 query_param::Query delegatedQuery; 143 if (!redfish::setUpRedfishRouteWithDelegation(app, req, asyncResp, 144 delegatedQuery, capabilities)) 145 { 146 return; 147 } 148 if constexpr (BMCWEB_EXPERIMENTAL_REDFISH_MULTI_COMPUTER_SYSTEM) 149 { 150 // Option currently returns no systems. TBD 151 messages::resourceNotFound(asyncResp->res, "ComputerSystem", 152 systemName); 153 return; 154 } 155 if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME) 156 { 157 messages::resourceNotFound(asyncResp->res, "ComputerSystem", 158 systemName); 159 return; 160 } 161 asyncResp->res.jsonValue["@odata.id"] = 162 std::format("/redfish/v1/Systems/{}/LogServices/HostLogger/Entries", 163 BMCWEB_REDFISH_SYSTEM_URI_NAME); 164 asyncResp->res.jsonValue["@odata.type"] = 165 "#LogEntryCollection.LogEntryCollection"; 166 asyncResp->res.jsonValue["Name"] = "HostLogger Entries"; 167 asyncResp->res.jsonValue["Description"] = 168 "Collection of HostLogger Entries"; 169 nlohmann::json& logEntryArray = asyncResp->res.jsonValue["Members"]; 170 logEntryArray = nlohmann::json::array(); 171 asyncResp->res.jsonValue["Members@odata.count"] = 0; 172 173 std::vector<std::filesystem::path> hostLoggerFiles; 174 if (!getHostLoggerFiles(hostLoggerFolderPath, hostLoggerFiles)) 175 { 176 BMCWEB_LOG_DEBUG("Failed to get host log file path"); 177 return; 178 } 179 // If we weren't provided top and skip limits, use the defaults. 180 size_t skip = delegatedQuery.skip.value_or(0); 181 size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop); 182 size_t logCount = 0; 183 // This vector only store the entries we want to expose that 184 // control by skip and top. 185 std::vector<std::string> logEntries; 186 if (!getHostLoggerEntries(hostLoggerFiles, skip, top, logEntries, logCount)) 187 { 188 messages::internalError(asyncResp->res); 189 return; 190 } 191 // If vector is empty, that means skip value larger than total 192 // log count 193 if (logEntries.empty()) 194 { 195 asyncResp->res.jsonValue["Members@odata.count"] = logCount; 196 return; 197 } 198 if (!logEntries.empty()) 199 { 200 for (size_t i = 0; i < logEntries.size(); i++) 201 { 202 nlohmann::json::object_t hostLogEntry; 203 fillHostLoggerEntryJson(std::to_string(skip + i), logEntries[i], 204 hostLogEntry); 205 logEntryArray.emplace_back(std::move(hostLogEntry)); 206 } 207 208 asyncResp->res.jsonValue["Members@odata.count"] = logCount; 209 if (skip + top < logCount) 210 { 211 asyncResp->res.jsonValue["Members@odata.nextLink"] = 212 std::format( 213 "/redfish/v1/Systems/{}/LogServices/HostLogger/Entries?$skip=", 214 BMCWEB_REDFISH_SYSTEM_URI_NAME) + 215 std::to_string(skip + top); 216 } 217 } 218 } 219 220 inline void handleSystemsLogServicesHostloggerEntriesEntryGet( 221 App& app, const crow::Request& req, 222 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 223 const std::string& systemName, const std::string& param) 224 { 225 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 226 { 227 return; 228 } 229 if constexpr (BMCWEB_EXPERIMENTAL_REDFISH_MULTI_COMPUTER_SYSTEM) 230 { 231 // Option currently returns no systems. TBD 232 messages::resourceNotFound(asyncResp->res, "ComputerSystem", 233 systemName); 234 return; 235 } 236 if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME) 237 { 238 messages::resourceNotFound(asyncResp->res, "ComputerSystem", 239 systemName); 240 return; 241 } 242 std::string_view targetID = param; 243 244 uint64_t idInt = 0; 245 246 auto [ptr, ec] = std::from_chars(targetID.begin(), targetID.end(), idInt); 247 if (ec != std::errc{} || ptr != targetID.end()) 248 { 249 messages::resourceNotFound(asyncResp->res, "LogEntry", param); 250 return; 251 } 252 253 std::vector<std::filesystem::path> hostLoggerFiles; 254 if (!getHostLoggerFiles(hostLoggerFolderPath, hostLoggerFiles)) 255 { 256 BMCWEB_LOG_DEBUG("Failed to get host log file path"); 257 return; 258 } 259 260 size_t logCount = 0; 261 size_t top = 1; 262 std::vector<std::string> logEntries; 263 // We can get specific entry by skip and top. For example, if we 264 // want to get nth entry, we can set skip = n-1 and top = 1 to 265 // get that entry 266 if (!getHostLoggerEntries(hostLoggerFiles, idInt, top, logEntries, 267 logCount)) 268 { 269 messages::internalError(asyncResp->res); 270 return; 271 } 272 273 if (!logEntries.empty()) 274 { 275 nlohmann::json::object_t hostLogEntry; 276 fillHostLoggerEntryJson(targetID, logEntries[0], hostLogEntry); 277 asyncResp->res.jsonValue.update(hostLogEntry); 278 return; 279 } 280 281 // Requested ID was not found 282 messages::resourceNotFound(asyncResp->res, "LogEntry", param); 283 } 284 285 inline void requestRoutesSystemsLogServiceHostlogger(App& app) 286 { 287 BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/LogServices/HostLogger/") 288 .privileges(redfish::privileges::getLogService) 289 .methods(boost::beast::http::verb::get)(std::bind_front( 290 handleSystemsLogServicesHostloggerGet, std::ref(app))); 291 BMCWEB_ROUTE(app, 292 "/redfish/v1/Systems/<str>/LogServices/HostLogger/Entries/") 293 .privileges(redfish::privileges::getLogEntryCollection) 294 .methods(boost::beast::http::verb::get)(std::bind_front( 295 handleSystemsLogServicesHostloggerEntriesGet, std::ref(app))); 296 297 BMCWEB_ROUTE( 298 app, "/redfish/v1/Systems/<str>/LogServices/HostLogger/Entries/<str>/") 299 .privileges(redfish::privileges::getLogEntry) 300 .methods(boost::beast::http::verb::get)(std::bind_front( 301 handleSystemsLogServicesHostloggerEntriesEntryGet, std::ref(app))); 302 } 303 304 } // namespace redfish 305