1 #include "config.h"
2 
3 #include "health_metric_config.hpp"
4 
5 #include <nlohmann/json.hpp>
6 #include <phosphor-logging/lg2.hpp>
7 
8 #include <cmath>
9 #include <fstream>
10 #include <ranges>
11 #include <unordered_map>
12 #include <unordered_set>
13 #include <utility>
14 
15 PHOSPHOR_LOG2_USING;
16 
17 namespace phosphor::health::metric
18 {
19 namespace config
20 {
21 
22 using json = nlohmann::json;
23 
24 // Default health metric config
25 extern json defaultHealthMetricConfig;
26 
27 // Valid thresholds from config
28 static const auto validThresholdTypesWithBound =
29     std::unordered_set<std::string>{"Critical_Lower", "Critical_Upper",
30                                     "Warning_Lower", "Warning_Upper"};
31 
32 static const auto validThresholdBounds =
33     std::unordered_map<std::string, ThresholdIntf::Bound>{
34         {"Lower", ThresholdIntf::Bound::Lower},
35         {"Upper", ThresholdIntf::Bound::Upper}};
36 
37 static const auto validThresholdTypes =
38     std::unordered_map<std::string, ThresholdIntf::Type>{
39         {"HardShutdown", ThresholdIntf::Type::HardShutdown},
40         {"SoftShutdown", ThresholdIntf::Type::SoftShutdown},
41         {"PerformanceLoss", ThresholdIntf::Type::PerformanceLoss},
42         {"Critical", ThresholdIntf::Type::Critical},
43         {"Warning", ThresholdIntf::Type::Warning}};
44 
45 // Valid metrics from config
46 static const auto validTypes =
47     std::unordered_map<std::string, Type>{{"CPU", Type::cpu},
48                                           {"Memory", Type::memory},
49                                           {"Storage", Type::storage},
50                                           {"Inode", Type::inode}};
51 
52 // Valid submetrics from config
53 static const auto validSubTypes = std::unordered_map<std::string, SubType>{
54     {"CPU", SubType::cpuTotal},
55     {"CPU_User", SubType::cpuUser},
56     {"CPU_Kernel", SubType::cpuKernel},
57     {"Memory", SubType::memoryTotal},
58     {"Memory_Free", SubType::memoryFree},
59     {"Memory_Available", SubType::memoryAvailable},
60     {"Memory_Shared", SubType::memoryShared},
61     {"Memory_Buffered_And_Cached", SubType::memoryBufferedAndCached},
62     {"Storage_RW", SubType::storageReadWrite},
63     {"Storage_TMP", SubType::storageTmp}};
64 
65 /** Deserialize a Threshold from JSON. */
66 void from_json(const json& j, Threshold& self)
67 {
68     self.value = j.value("Value", 100.0);
69     self.log = j.value("Log", false);
70     self.target = j.value("Target", Threshold::defaults::target);
71 }
72 
73 /** Deserialize a HealthMetric from JSON. */
74 void from_json(const json& j, HealthMetric& self)
75 {
76     self.collectionFreq = std::chrono::seconds(j.value(
77         "Frequency",
78         std::chrono::seconds(HealthMetric::defaults::frequency).count()));
79 
80     self.windowSize = j.value("Window_size",
81                               HealthMetric::defaults::windowSize);
82     // Path is only valid for storage
83     self.path = j.value("Path", "");
84 
85     auto thresholds = j.find("Threshold");
86     if (thresholds == j.end())
87     {
88         return;
89     }
90 
91     for (auto& [key, value] : thresholds->items())
92     {
93         if (!validThresholdTypesWithBound.contains(key))
94         {
95             warning("Invalid ThresholdType: {TYPE}", "TYPE", key);
96             continue;
97         }
98 
99         auto config = value.template get<Threshold>();
100         if (!std::isfinite(config.value))
101         {
102             throw std::invalid_argument("Invalid threshold value");
103         }
104 
105         static constexpr auto keyDelimiter = "_";
106         std::string typeStr = key.substr(0, key.find_first_of(keyDelimiter));
107         std::string boundStr = key.substr(key.find_last_of(keyDelimiter) + 1,
108                                           key.length());
109 
110         self.thresholds.emplace(
111             std::make_tuple(validThresholdTypes.at(typeStr),
112                             validThresholdBounds.at(boundStr)),
113             config);
114     }
115 }
116 
117 json parseConfigFile(std::string configFile)
118 {
119     std::ifstream jsonFile(configFile);
120     if (!jsonFile.is_open())
121     {
122         info("config JSON file not found: {PATH}", "PATH", configFile);
123         return {};
124     }
125 
126     try
127     {
128         return json::parse(jsonFile, nullptr, true);
129     }
130     catch (const json::parse_error& e)
131     {
132         error("Failed to parse JSON config file {PATH}: {ERROR}", "PATH",
133               configFile, "ERROR", e);
134     }
135 
136     return {};
137 }
138 
139 void printConfig(HealthMetric::map_t& configs)
140 {
141     for (auto& [type, configList] : configs)
142     {
143         for (auto& config : configList)
144         {
145             debug(
146                 "TYPE={TYPE}, NAME={NAME} SUBTYPE={SUBTYPE} PATH={PATH}, FREQ={FREQ}, WSIZE={WSIZE}",
147                 "TYPE", type, "NAME", config.name, "SUBTYPE", config.subType,
148                 "PATH", config.path, "FREQ", config.collectionFreq.count(),
149                 "WSIZE", config.windowSize);
150 
151             for (auto& [key, threshold] : config.thresholds)
152             {
153                 debug(
154                     "THRESHOLD TYPE={TYPE} THRESHOLD BOUND={BOUND} VALUE={VALUE} LOG={LOG} TARGET={TARGET}",
155                     "TYPE", get<ThresholdIntf::Type>(key), "BOUND",
156                     get<ThresholdIntf::Bound>(key), "VALUE", threshold.value,
157                     "LOG", threshold.log, "TARGET", threshold.target);
158             }
159         }
160     }
161 }
162 
163 auto getHealthMetricConfigs() -> HealthMetric::map_t
164 {
165     json mergedConfig(defaultHealthMetricConfig);
166 
167     if (auto platformConfig = parseConfigFile(HEALTH_CONFIG_FILE);
168         !platformConfig.empty())
169     {
170         mergedConfig.merge_patch(platformConfig);
171     }
172 
173     HealthMetric::map_t configs = {};
174     for (auto& [name, metric] : mergedConfig.items())
175     {
176         static constexpr auto nameDelimiter = "_";
177         std::string typeStr = name.substr(0, name.find_first_of(nameDelimiter));
178 
179         auto type = validTypes.find(typeStr);
180         if (type == validTypes.end())
181         {
182             warning("Invalid metric type: {TYPE}", "TYPE", typeStr);
183             continue;
184         }
185 
186         auto config = metric.template get<HealthMetric>();
187         config.name = name;
188 
189         auto subType = validSubTypes.find(name);
190         config.subType = (subType != validSubTypes.end() ? subType->second
191                                                          : SubType::NA);
192 
193         configs[type->second].emplace_back(std::move(config));
194     }
195     printConfig(configs);
196     return configs;
197 }
198 
199 json defaultHealthMetricConfig = R"({
200     "CPU": {
201         "Frequency": 1,
202         "Window_size": 120,
203         "Threshold": {
204             "Critical_Upper": {
205                 "Value": 90.0,
206                 "Log": true,
207                 "Target": ""
208             },
209             "Warning_Upper": {
210                 "Value": 80.0,
211                 "Log": false,
212                 "Target": ""
213             }
214         }
215     },
216     "CPU_User": {
217         "Frequency": 1,
218         "Window_size": 120
219     },
220     "CPU_Kernel": {
221         "Frequency": 1,
222         "Window_size": 120
223     },
224     "Memory": {
225         "Frequency": 1,
226         "Window_size": 120
227     },
228     "Memory_Available": {
229         "Frequency": 1,
230         "Window_size": 120,
231         "Threshold": {
232             "Critical_Lower": {
233                 "Value": 15.0,
234                 "Log": true,
235                 "Target": ""
236             }
237         }
238     },
239     "Memory_Free": {
240         "Frequency": 1,
241         "Window_size": 120
242     },
243     "Memory_Shared": {
244         "Frequency": 1,
245         "Window_size": 120,
246         "Threshold": {
247             "Critical_Upper": {
248                 "Value": 85.0,
249                 "Log": true,
250                 "Target": ""
251             }
252         }
253     },
254     "Memory_Buffered_And_Cached": {
255         "Frequency": 1,
256         "Window_size": 120
257     },
258     "Storage_RW": {
259         "Path": "/run/initramfs/rw",
260         "Frequency": 1,
261         "Window_size": 120,
262         "Threshold": {
263             "Critical_Lower": {
264                 "Value": 15.0,
265                 "Log": true,
266                 "Target": ""
267             }
268         }
269     },
270     "Storage_TMP": {
271         "Path": "/tmp",
272         "Frequency": 1,
273         "Window_size": 120,
274         "Threshold": {
275             "Critical_Lower": {
276                 "Value": 15.0,
277                 "Log": true,
278                 "Target": ""
279             }
280         }
281     }
282 })"_json;
283 
284 } // namespace config
285 
286 namespace details
287 {
288 auto reverse_map_search(const auto& m, auto v)
289 {
290     if (auto match = std::ranges::find_if(
291             m, [=](const auto& p) { return p.second == v; });
292         match != std::end(m))
293     {
294         return match->first;
295     }
296     return std::format("Enum({})", std::to_underlying(v));
297 }
298 } // namespace details
299 
300 // to_string specialization for Type.
301 auto to_string(Type t) -> std::string
302 {
303     return details::reverse_map_search(config::validTypes, t);
304 }
305 
306 // to_string specializaiton for SubType.
307 auto to_string(SubType t) -> std::string
308 {
309     return details::reverse_map_search(config::validSubTypes, t);
310 }
311 
312 } // namespace phosphor::health::metric
313