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