xref: /openbmc/bmcweb/features/redfish/lib/systems_logservices_hostlogger.hpp (revision 3d66430a19af19df9669ccad106af292eec0cdd7)
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