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 = std::unordered_map<std::string, Type>{
47 {"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. */
from_json(const json & j,Threshold & self)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. */
from_json(const json & j,HealthMetric & self)74 void from_json(const json& j, HealthMetric& self)
75 {
76 self.windowSize =
77 j.value("Window_size", 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 =
105 key.substr(key.find_last_of(keyDelimiter) + 1, key.length());
106
107 self.thresholds.emplace(
108 std::make_tuple(validThresholdTypes.at(typeStr),
109 validThresholdBounds.at(boundStr)),
110 config);
111 }
112 }
113
parseConfigFile(std::string configFile)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
printConfig(HealthMetric::map_t & configs)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
getHealthMetricConfigs()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 =
188 (subType != validSubTypes.end() ? subType->second : 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 {
reverse_map_search(const auto & m,auto v)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.
to_string(Type t)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.
to_string(SubType t)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