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         "Threshold": {
199             "Critical_Upper": {
200                 "Value": 90.0,
201                 "Log": true,
202                 "Target": ""
203             },
204             "Warning_Upper": {
205                 "Value": 80.0,
206                 "Log": false,
207                 "Target": ""
208             }
209         }
210     },
211     "CPU_User": {
212     },
213     "CPU_Kernel": {
214     },
215     "Memory": {
216     },
217     "Memory_Available": {
218         "Threshold": {
219             "Critical_Lower": {
220                 "Value": 15.0,
221                 "Log": true,
222                 "Target": ""
223             }
224         }
225     },
226     "Memory_Free": {
227     },
228     "Memory_Shared": {
229         "Threshold": {
230             "Critical_Upper": {
231                 "Value": 85.0,
232                 "Log": true,
233                 "Target": ""
234             }
235         }
236     },
237     "Memory_Buffered_And_Cached": {
238     },
239     "Storage_RW": {
240         "Path": "/run/initramfs/rw",
241         "Threshold": {
242             "Critical_Lower": {
243                 "Value": 15.0,
244                 "Log": true,
245                 "Target": ""
246             }
247         }
248     },
249     "Storage_TMP": {
250         "Path": "/tmp",
251         "Threshold": {
252             "Critical_Lower": {
253                 "Value": 15.0,
254                 "Log": true,
255                 "Target": ""
256             }
257         }
258     }
259 })"_json;
260 
261 } // namespace config
262 
263 namespace details
264 {
265 auto reverse_map_search(const auto& m, auto v)
266 {
267     if (auto match = std::ranges::find_if(
268             m, [=](const auto& p) { return p.second == v; });
269         match != std::end(m))
270     {
271         return match->first;
272     }
273     return std::format("Enum({})", std::to_underlying(v));
274 }
275 } // namespace details
276 
277 // to_string specialization for Type.
278 auto to_string(Type t) -> std::string
279 {
280     return details::reverse_map_search(config::validTypes, t);
281 }
282 
283 // to_string specialization for SubType.
284 auto to_string(SubType t) -> std::string
285 {
286     return details::reverse_map_search(config::validSubTypes, t);
287 }
288 
289 } // namespace phosphor::health::metric
290