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