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