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