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::NA},
63     {"Storage_TMP", SubType::NA}};
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.windowSize = j.value("Window_size",
77                               HealthMetric::defaults::windowSize);
78     self.hysteresis = j.value("Hysteresis", HealthMetric::defaults::hysteresis);
79     // Path is only valid for storage
80     self.path = j.value("Path", "");
81 
82     auto thresholds = j.find("Threshold");
83     if (thresholds == j.end())
84     {
85         return;
86     }
87 
88     for (auto& [key, value] : thresholds->items())
89     {
90         if (!validThresholdTypesWithBound.contains(key))
91         {
92             warning("Invalid ThresholdType: {TYPE}", "TYPE", key);
93             continue;
94         }
95 
96         auto config = value.template get<Threshold>();
97         if (!std::isfinite(config.value))
98         {
99             throw std::invalid_argument("Invalid threshold value");
100         }
101 
102         static constexpr auto keyDelimiter = "_";
103         std::string typeStr = key.substr(0, key.find_first_of(keyDelimiter));
104         std::string boundStr = key.substr(key.find_last_of(keyDelimiter) + 1,
105                                           key.length());
106 
107         self.thresholds.emplace(
108             std::make_tuple(validThresholdTypes.at(typeStr),
109                             validThresholdBounds.at(boundStr)),
110             config);
111     }
112 }
113 
114 json parseConfigFile(std::string configFile)
115 {
116     std::ifstream jsonFile(configFile);
117     if (!jsonFile.is_open())
118     {
119         info("config JSON file not found: {PATH}", "PATH", configFile);
120         return {};
121     }
122 
123     try
124     {
125         return json::parse(jsonFile, nullptr, true);
126     }
127     catch (const json::parse_error& e)
128     {
129         error("Failed to parse JSON config file {PATH}: {ERROR}", "PATH",
130               configFile, "ERROR", e);
131     }
132 
133     return {};
134 }
135 
136 void printConfig(HealthMetric::map_t& configs)
137 {
138     for (auto& [type, configList] : configs)
139     {
140         for (auto& config : configList)
141         {
142             debug(
143                 "TYPE={TYPE}, NAME={NAME} SUBTYPE={SUBTYPE} PATH={PATH}, WSIZE={WSIZE}, HYSTERESIS={HYSTERESIS}",
144                 "TYPE", type, "NAME", config.name, "SUBTYPE", config.subType,
145                 "PATH", config.path, "WSIZE", config.windowSize, "HYSTERESIS",
146                 config.hysteresis);
147 
148             for (auto& [key, threshold] : config.thresholds)
149             {
150                 debug(
151                     "THRESHOLD TYPE={TYPE} THRESHOLD BOUND={BOUND} VALUE={VALUE} LOG={LOG} TARGET={TARGET}",
152                     "TYPE", get<ThresholdIntf::Type>(key), "BOUND",
153                     get<ThresholdIntf::Bound>(key), "VALUE", threshold.value,
154                     "LOG", threshold.log, "TARGET", threshold.target);
155             }
156         }
157     }
158 }
159 
160 auto getHealthMetricConfigs() -> HealthMetric::map_t
161 {
162     json mergedConfig(defaultHealthMetricConfig);
163 
164     if (auto platformConfig = parseConfigFile(HEALTH_CONFIG_FILE);
165         !platformConfig.empty())
166     {
167         mergedConfig.merge_patch(platformConfig);
168     }
169 
170     HealthMetric::map_t configs = {};
171     for (auto& [name, metric] : mergedConfig.items())
172     {
173         static constexpr auto nameDelimiter = "_";
174         std::string typeStr = name.substr(0, name.find_first_of(nameDelimiter));
175 
176         auto type = validTypes.find(typeStr);
177         if (type == validTypes.end())
178         {
179             warning("Invalid metric type: {TYPE}", "TYPE", typeStr);
180             continue;
181         }
182 
183         auto config = metric.template get<HealthMetric>();
184         config.name = name;
185 
186         auto subType = validSubTypes.find(name);
187         config.subType = (subType != validSubTypes.end() ? subType->second
188                                                          : SubType::NA);
189 
190         configs[type->second].emplace_back(std::move(config));
191     }
192     printConfig(configs);
193     return configs;
194 }
195 
196 json defaultHealthMetricConfig = R"({
197     "CPU": {
198         "Window_size": 120,
199         "Hysteresis": 1.0,
200         "Threshold": {
201             "Critical_Upper": {
202                 "Value": 90.0,
203                 "Log": true,
204                 "Target": ""
205             },
206             "Warning_Upper": {
207                 "Value": 80.0,
208                 "Log": false,
209                 "Target": ""
210             }
211         }
212     },
213     "CPU_User": {
214         "Window_size": 120,
215         "Hysteresis": 1.0
216     },
217     "CPU_Kernel": {
218         "Window_size": 120,
219         "Hysteresis": 1.0
220     },
221     "Memory": {
222         "Window_size": 120,
223         "Hysteresis": 1.0
224     },
225     "Memory_Available": {
226         "Window_size": 120,
227         "Hysteresis": 1.0,
228         "Threshold": {
229             "Critical_Lower": {
230                 "Value": 15.0,
231                 "Log": true,
232                 "Target": ""
233             }
234         }
235     },
236     "Memory_Free": {
237         "Window_size": 120,
238         "Hysteresis": 1.0
239     },
240     "Memory_Shared": {
241         "Window_size": 120,
242         "Hysteresis": 1.0,
243         "Threshold": {
244             "Critical_Upper": {
245                 "Value": 85.0,
246                 "Log": true,
247                 "Target": ""
248             }
249         }
250     },
251     "Memory_Buffered_And_Cached": {
252         "Window_size": 120,
253         "Hysteresis": 1.0
254     },
255     "Storage_RW": {
256         "Path": "/run/initramfs/rw",
257         "Window_size": 120,
258         "Hysteresis": 1.0,
259         "Threshold": {
260             "Critical_Lower": {
261                 "Value": 15.0,
262                 "Log": true,
263                 "Target": ""
264             }
265         }
266     },
267     "Storage_TMP": {
268         "Path": "/tmp",
269         "Window_size": 120,
270         "Hysteresis": 1.0,
271         "Threshold": {
272             "Critical_Lower": {
273                 "Value": 15.0,
274                 "Log": true,
275                 "Target": ""
276             }
277         }
278     }
279 })"_json;
280 
281 } // namespace config
282 
283 namespace details
284 {
285 auto reverse_map_search(const auto& m, auto v)
286 {
287     if (auto match = std::ranges::find_if(
288             m, [=](const auto& p) { return p.second == v; });
289         match != std::end(m))
290     {
291         return match->first;
292     }
293     return std::format("Enum({})", std::to_underlying(v));
294 }
295 } // namespace details
296 
297 // to_string specialization for Type.
298 auto to_string(Type t) -> std::string
299 {
300     return details::reverse_map_search(config::validTypes, t);
301 }
302 
303 // to_string specializaiton for SubType.
304 auto to_string(SubType t) -> std::string
305 {
306     return details::reverse_map_search(config::validSubTypes, t);
307 }
308 
309 } // namespace phosphor::health::metric
310