diff --git a/doc/05-service-monitoring.md b/doc/05-service-monitoring.md index 49343616e..19aa196e1 100644 --- a/doc/05-service-monitoring.md +++ b/doc/05-service-monitoring.md @@ -646,22 +646,46 @@ Icinga sets `LC_NUMERIC=C` to enforce this locale on plugin execution. ##### Unit of Measurement (UOM) -Unit | Description ----------|--------------------------------- -None | Integer or floating point number for any type (processes, users, etc.). -`s` | Seconds, can be `s`, `ms`, `us`. -`%` | Percentage. -`B` | Bytes, can be `KB`, `MB`, `GB`, `TB`. Lowercase is also possible. -`c` | A continuous counter (e.g. interface traffic counters). - -Icinga metric writers normalize these values to the lowest common base, e.g. seconds and bytes. -Bad plugins change the UOM for different sizing, e.g. returning the disk usage in MB and later GB -for the same performance data label. This is to ensure that graphs always look the same. - ``` 'rta'=12.445000ms 'pl'=0% ``` +Icinga interprets the plugins' UoMs as follows: + +* If the UoM is "c", the value is a continuous counter (e.g. interface traffic counters). +* Otherwise if the UoM is listed in the table below (case-insensitive if possible), + Icinga normalizes the value (and warn, crit, min, max) to the respective common base. + That common base is also used by the metric writers (if any). + +Common base | UoMs +-------------------|--------------------------------------- +bytes | B, KB, MB, ..., YB, KiB, MiB, ..., YiB +bits | b, kb, mb, ..., yb, kib, mib, ..., yib +packets | packets +seconds | ns, us, ms, s, m, h, d +percent | % +amperes | nA, uA, mA, A, kA, MA, GA, ..., YA +ohms | nO, uO, mO, O, kO, MO, GO, ..., YO +volts | nV, uV, mV, V, kV, MV, GV, ..., YV +watts | nW, uW, mW, W, kW, MW, GW, ..., YW +ampere-seconds | nAs, uAs, mAs, As, kAs, MAs, GAs, ..., YAs, all of these also for Am and Ah +watt-hours | nWh, uWh, mWh, Wh, kWh, MWh, GWh, ..., YWh, all of these also for Wm and Ws +lumens | lm +decibel-milliwatts | dBm +grams | ng, ug, mg, g, kg, t +degrees-celsius | C +degrees-fahrenheit | F +degrees-kelvin | K +liters | ml, l, hl + +Some plugins change the UoM for different sizing, e.g. returning the disk usage in MB and later GB +for the same performance data label. This is to ensure that graphs always look the same. + +* Otherwise the UoM is discarted (as if none was given). + +A value without any UoM may be an integer or floating point number +for any type (processes, users, etc.). + ##### Thresholds and Min/Max Next to the performance data value, warn, crit, min, max can optionally be provided. They must be separated diff --git a/lib/base/perfdatavalue.cpp b/lib/base/perfdatavalue.cpp index 422988d80..7f1612ce1 100644 --- a/lib/base/perfdatavalue.cpp +++ b/lib/base/perfdatavalue.cpp @@ -6,12 +6,211 @@ #include "base/exception.hpp" #include "base/logger.hpp" #include "base/function.hpp" +#include +#include +#include +#include +#include +#include using namespace icinga; REGISTER_TYPE(PerfdataValue); REGISTER_FUNCTION(System, parse_performance_data, PerfdataValue::Parse, "perfdata"); +struct UoM +{ + double Factor; + const char* Out; +}; + +typedef std::unordered_map UoMs; +typedef std::unordered_multimap DupUoMs; + +static const UoMs l_CsUoMs (([]() -> UoMs { + DupUoMs uoms ({ + // Misc: + { "", { 1, "" } }, + { "%", { 1, "percent" } }, + { "c", { 1, "" } }, + { "C", { 1, "degrees-celsius" } } + }); + + { + // Data (rate): + + struct { const char* Char; int Power; } prefixes[] = { + { "k", 1 }, { "K", 1 }, + { "m", 2 }, { "M", 2 }, + { "g", 3 }, { "G", 3 }, + { "t", 4 }, { "T", 4 }, + { "p", 5 }, { "P", 5 }, + { "e", 6 }, { "E", 6 }, + { "z", 7 }, { "Z", 7 }, + { "y", 8 }, { "Y", 8 } + }; + + struct { const char* Char; double Factor; } siIecs[] = { + { "", 1000 }, + { "i", 1024 }, { "I", 1024 } + }; + + struct { const char *In, *Out; } bases[] = { + { "b", "bits" }, + { "B", "bytes" } + }; + + for (auto base : bases) { + uoms.emplace(base.In, UoM{1, base.Out}); + } + + for (auto prefix : prefixes) { + for (auto siIec : siIecs) { + auto factor (pow(siIec.Factor, prefix.Power)); + + for (auto base : bases) { + uoms.emplace( + std::string(prefix.Char) + siIec.Char + base.In, + UoM{factor, base.Out} + ); + } + } + } + } + + { + // Energy: + + struct { const char* Char; int Power; } prefixes[] = { + { "n", -3 }, { "N", -3 }, + { "u", -2 }, { "U", -2 }, + { "m", -1 }, + { "", 0 }, + { "k", 1 }, { "K", 1 }, + { "M", 2 }, + { "g", 3 }, { "G", 3 }, + { "t", 4 }, { "T", 4 }, + { "p", 5 }, { "P", 5 }, + { "e", 6 }, { "E", 6 }, + { "z", 7 }, { "Z", 7 }, + { "y", 8 }, { "Y", 8 } + }; + + { + struct { const char* Ins[2]; const char* Out; } bases[] = { + { { "a", "A" }, "amperes" }, + { { "o", "O" }, "ohms" }, + { { "v", "V" }, "volts" }, + { { "w", "W" }, "watts" } + }; + + for (auto prefix : prefixes) { + auto factor (pow(1000.0, prefix.Power)); + + for (auto base : bases) { + for (auto b : base.Ins) { + uoms.emplace(std::string(prefix.Char) + b, UoM{factor, base.Out}); + } + } + } + } + + struct { const char* Char; double Factor; } suffixes[] = { + { "s", 1 }, { "S", 1 }, + { "m", 60 }, { "M", 60 }, + { "h", 60 * 60 }, { "H", 60 * 60 } + }; + + struct { const char* Ins[2]; double Factor; const char* Out; } bases[] = { + { { "a", "A" }, 1, "ampere-seconds" }, + { { "w", "W" }, 60 * 60, "watt-hours" } + }; + + for (auto prefix : prefixes) { + auto factor (pow(1000.0, prefix.Power)); + + for (auto suffix : suffixes) { + auto timeFactor (factor * suffix.Factor); + + for (auto& base : bases) { + auto baseFactor (timeFactor / base.Factor); + + for (auto b : base.Ins) { + uoms.emplace( + std::string(prefix.Char) + b + suffix.Char, + UoM{baseFactor, base.Out} + ); + } + } + } + } + } + + UoMs uniqUoms; + + for (auto& uom : uoms) { + if (!uniqUoms.emplace(uom).second) { + throw std::logic_error("Duplicate case-sensitive UoM detected: " + uom.first); + } + } + + return std::move(uniqUoms); +})()); + +static const UoMs l_CiUoMs (([]() -> UoMs { + DupUoMs uoms ({ + // Time: + { "ns", { 1.0 / 1000 / 1000 / 1000, "seconds" } }, + { "us", { 1.0 / 1000 / 1000, "seconds" } }, + { "ms", { 1.0 / 1000, "seconds" } }, + { "s", { 1, "seconds" } }, + { "m", { 60, "seconds" } }, + { "h", { 60 * 60, "seconds" } }, + { "d", { 60 * 60 * 24, "seconds" } }, + + // Mass: + { "ng", { 1.0 / 1000 / 1000 / 1000, "grams" } }, + { "ug", { 1.0 / 1000 / 1000, "grams" } }, + { "mg", { 1.0 / 1000, "grams" } }, + { "g", { 1, "grams" } }, + { "kg", { 1000, "grams" } }, + { "t", { 1000 * 1000, "grams" } }, + + // Volume: + { "ml", { 1.0 / 1000, "liters" } }, + { "l", { 1, "liters" } }, + { "hl", { 100, "liters" } }, + + // Misc: + { "packets", { 1, "packets" } }, + { "lm", { 1, "lumens" } }, + { "dbm", { 1, "decibel-milliwatts" } }, + { "f", { 1, "degrees-fahrenheit" } }, + { "k", { 1, "degrees-kelvin" } } + }); + + UoMs uniqUoms; + + for (auto& uom : uoms) { + if (!uniqUoms.emplace(uom).second) { + throw std::logic_error("Duplicate case-insensitive UoM detected: " + uom.first); + } + } + + for (auto& uom : l_CsUoMs) { + auto input (uom.first); + boost::algorithm::to_lower(input); + + auto pos (uoms.find(input)); + + if (pos != uoms.end()) { + throw std::logic_error("Duplicate case-sensitive/case-insensitive UoM detected: " + pos->first); + } + } + + return std::move(uniqUoms); +})()); + PerfdataValue::PerfdataValue(const String& label, double value, bool counter, const String& unit, const Value& warn, const Value& crit, const Value& min, const Value& max) @@ -47,6 +246,10 @@ PerfdataValue::Ptr PerfdataValue::Parse(const String& perfdata) size_t pos = valueStr.FindFirstNotOf("+-0123456789.e"); + if (pos != String::NPos && valueStr[pos] == ',') { + BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid performance data value: " + perfdata)); + } + double value = Convert::ToDouble(valueStr.SubStr(0, pos)); std::vector tokens = valueStr.Split(";"); @@ -58,39 +261,33 @@ PerfdataValue::Ptr PerfdataValue::Parse(const String& perfdata) if (pos != String::NPos) unit = valueStr.SubStr(pos, tokens[0].GetLength() - pos); - unit = unit.ToLower(); + double base; - double base = 1.0; + { + auto uom (l_CsUoMs.find(unit.GetData())); - if (unit == "us") { - base /= 1000.0 * 1000.0; - unit = "seconds"; - } else if (unit == "ms") { - base /= 1000.0; - unit = "seconds"; - } else if (unit == "s") { - unit = "seconds"; - } else if (unit == "tb") { - base *= 1024.0 * 1024.0 * 1024.0 * 1024.0; - unit = "bytes"; - } else if (unit == "gb") { - base *= 1024.0 * 1024.0 * 1024.0; - unit = "bytes"; - } else if (unit == "mb") { - base *= 1024.0 * 1024.0; - unit = "bytes"; - } else if (unit == "kb") { - base *= 1024.0; - unit = "bytes"; - } else if (unit == "b") { - unit = "bytes"; - } else if (unit == "%") { - unit = "percent"; - } else if (unit == "c") { + if (uom == l_CsUoMs.end()) { + auto ciUnit (unit.ToLower()); + auto uom (l_CiUoMs.find(ciUnit.GetData())); + + if (uom == l_CiUoMs.end()) { + Log(LogDebug, "PerfdataValue") + << "Invalid performance data unit: " << unit; + + unit = ""; + base = 1.0; + } else { + unit = uom->second.Out; + base = uom->second.Factor; + } + } else { + unit = uom->second.Out; + base = uom->second.Factor; + } + } + + if (unit == "c") { counter = true; - unit = ""; - } else if (unit != "") { - BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid performance data unit: " + unit)); } warn = ParseWarnCritMinMaxToken(tokens, 1, "warning"); @@ -115,6 +312,26 @@ PerfdataValue::Ptr PerfdataValue::Parse(const String& perfdata) return new PerfdataValue(label, value, counter, unit, warn, crit, min, max); } +static const std::unordered_map l_FormatUoMs ({ + { "ampere-seconds", "As" }, + { "amperes", "A" }, + { "bits", "b" }, + { "bytes", "B" }, + { "decibel-milliwatts", "dBm" }, + { "degrees-celsius", "C" }, + { "degrees-fahrenheit", "F" }, + { "degrees-kelvin", "K" }, + { "grams", "g" }, + { "liters", "l" }, + { "lumens", "lm" }, + { "ohms", "O" }, + { "percent", "%" }, + { "seconds", "s" }, + { "volts", "V" }, + { "watt-hours", "Wh" }, + { "watts", "W" } +}); + String PerfdataValue::Format() const { std::ostringstream result; @@ -128,14 +345,16 @@ String PerfdataValue::Format() const String unit; - if (GetCounter()) + if (GetCounter()) { unit = "c"; - else if (GetUnit() == "seconds") - unit = "s"; - else if (GetUnit() == "percent") - unit = "%"; - else if (GetUnit() == "bytes") - unit = "B"; + } else { + auto myUnit (GetUnit()); + auto uom (l_FormatUoMs.find(myUnit.GetData())); + + if (uom != l_FormatUoMs.end()) { + unit = uom->second; + } + } result << unit; diff --git a/test/icinga-perfdata.cpp b/test/icinga-perfdata.cpp index b0fa1ee98..726345fe2 100644 --- a/test/icinga-perfdata.cpp +++ b/test/icinga-perfdata.cpp @@ -79,6 +79,188 @@ BOOST_AUTO_TEST_CASE(uom) str = pv->Format(); BOOST_CHECK(str == "test=1s"); + + pv = PerfdataValue::Parse("test=1kAm"); + BOOST_CHECK(pv); + + BOOST_CHECK(pv->GetValue() == 60 * 1000); + BOOST_CHECK(pv->GetUnit() == "ampere-seconds"); + BOOST_CHECK(pv->GetCrit() == Empty); + BOOST_CHECK(pv->GetWarn() == Empty); + BOOST_CHECK(pv->GetMin() == Empty); + BOOST_CHECK(pv->GetMax() == Empty); + + str = pv->Format(); + BOOST_CHECK(str == "test=60000As"); + + pv = PerfdataValue::Parse("test=1MA"); + BOOST_CHECK(pv); + + BOOST_CHECK(pv->GetValue() == 1000 * 1000); + BOOST_CHECK(pv->GetUnit() == "amperes"); + BOOST_CHECK(pv->GetCrit() == Empty); + BOOST_CHECK(pv->GetWarn() == Empty); + BOOST_CHECK(pv->GetMin() == Empty); + BOOST_CHECK(pv->GetMax() == Empty); + + str = pv->Format(); + BOOST_CHECK(str == "test=1000000A"); + + pv = PerfdataValue::Parse("test=1gib"); + BOOST_CHECK(pv); + + BOOST_CHECK(pv->GetValue() == 1024 * 1024 * 1024); + BOOST_CHECK(pv->GetUnit() == "bits"); + BOOST_CHECK(pv->GetCrit() == Empty); + BOOST_CHECK(pv->GetWarn() == Empty); + BOOST_CHECK(pv->GetMin() == Empty); + BOOST_CHECK(pv->GetMax() == Empty); + + str = pv->Format(); + BOOST_CHECK(str == "test=1073741824b"); + + pv = PerfdataValue::Parse("test=1dBm"); + BOOST_CHECK(pv); + + BOOST_CHECK(pv->GetValue() == 1); + BOOST_CHECK(pv->GetUnit() == "decibel-milliwatts"); + BOOST_CHECK(pv->GetCrit() == Empty); + BOOST_CHECK(pv->GetWarn() == Empty); + BOOST_CHECK(pv->GetMin() == Empty); + BOOST_CHECK(pv->GetMax() == Empty); + + str = pv->Format(); + BOOST_CHECK(str == "test=1dBm"); + + pv = PerfdataValue::Parse("test=1C"); + BOOST_CHECK(pv); + + BOOST_CHECK(pv->GetValue() == 1); + BOOST_CHECK(pv->GetUnit() == "degrees-celsius"); + BOOST_CHECK(pv->GetCrit() == Empty); + BOOST_CHECK(pv->GetWarn() == Empty); + BOOST_CHECK(pv->GetMin() == Empty); + BOOST_CHECK(pv->GetMax() == Empty); + + str = pv->Format(); + BOOST_CHECK(str == "test=1C"); + + pv = PerfdataValue::Parse("test=1F"); + BOOST_CHECK(pv); + + BOOST_CHECK(pv->GetValue() == 1); + BOOST_CHECK(pv->GetUnit() == "degrees-fahrenheit"); + BOOST_CHECK(pv->GetCrit() == Empty); + BOOST_CHECK(pv->GetWarn() == Empty); + BOOST_CHECK(pv->GetMin() == Empty); + BOOST_CHECK(pv->GetMax() == Empty); + + str = pv->Format(); + BOOST_CHECK(str == "test=1F"); + + pv = PerfdataValue::Parse("test=1K"); + BOOST_CHECK(pv); + + BOOST_CHECK(pv->GetValue() == 1); + BOOST_CHECK(pv->GetUnit() == "degrees-kelvin"); + BOOST_CHECK(pv->GetCrit() == Empty); + BOOST_CHECK(pv->GetWarn() == Empty); + BOOST_CHECK(pv->GetMin() == Empty); + BOOST_CHECK(pv->GetMax() == Empty); + + str = pv->Format(); + BOOST_CHECK(str == "test=1K"); + + pv = PerfdataValue::Parse("test=1t"); + BOOST_CHECK(pv); + + BOOST_CHECK(pv->GetValue() == 1000 * 1000); + BOOST_CHECK(pv->GetUnit() == "grams"); + BOOST_CHECK(pv->GetCrit() == Empty); + BOOST_CHECK(pv->GetWarn() == Empty); + BOOST_CHECK(pv->GetMin() == Empty); + BOOST_CHECK(pv->GetMax() == Empty); + + str = pv->Format(); + BOOST_CHECK(str == "test=1000000g"); + + pv = PerfdataValue::Parse("test=1hl"); + BOOST_CHECK(pv); + + BOOST_CHECK(pv->GetValue() == 100); + BOOST_CHECK(pv->GetUnit() == "liters"); + BOOST_CHECK(pv->GetCrit() == Empty); + BOOST_CHECK(pv->GetWarn() == Empty); + BOOST_CHECK(pv->GetMin() == Empty); + BOOST_CHECK(pv->GetMax() == Empty); + + str = pv->Format(); + BOOST_CHECK(str == "test=100l"); + + pv = PerfdataValue::Parse("test=1lm"); + BOOST_CHECK(pv); + + BOOST_CHECK(pv->GetValue() == 1); + BOOST_CHECK(pv->GetUnit() == "lumens"); + BOOST_CHECK(pv->GetCrit() == Empty); + BOOST_CHECK(pv->GetWarn() == Empty); + BOOST_CHECK(pv->GetMin() == Empty); + BOOST_CHECK(pv->GetMax() == Empty); + + str = pv->Format(); + BOOST_CHECK(str == "test=1lm"); + + pv = PerfdataValue::Parse("test=1TO"); + BOOST_CHECK(pv); + + BOOST_CHECK(pv->GetValue() == 1000.0 * 1000 * 1000 * 1000); + BOOST_CHECK(pv->GetUnit() == "ohms"); + BOOST_CHECK(pv->GetCrit() == Empty); + BOOST_CHECK(pv->GetWarn() == Empty); + BOOST_CHECK(pv->GetMin() == Empty); + BOOST_CHECK(pv->GetMax() == Empty); + + str = pv->Format(); + BOOST_CHECK(str == "test=1000000000000O"); + + pv = PerfdataValue::Parse("test=1PV"); + BOOST_CHECK(pv); + + BOOST_CHECK(pv->GetValue() == 1000.0 * 1000 * 1000 * 1000 * 1000); + BOOST_CHECK(pv->GetUnit() == "volts"); + BOOST_CHECK(pv->GetCrit() == Empty); + BOOST_CHECK(pv->GetWarn() == Empty); + BOOST_CHECK(pv->GetMin() == Empty); + BOOST_CHECK(pv->GetMax() == Empty); + + str = pv->Format(); + BOOST_CHECK(str == "test=1000000000000000V"); + + pv = PerfdataValue::Parse("test=1EWh"); + BOOST_CHECK(pv); + + BOOST_CHECK(pv->GetValue() == 1000.0 * 1000 * 1000 * 1000 * 1000 * 1000); + BOOST_CHECK(pv->GetUnit() == "watt-hours"); + BOOST_CHECK(pv->GetCrit() == Empty); + BOOST_CHECK(pv->GetWarn() == Empty); + BOOST_CHECK(pv->GetMin() == Empty); + BOOST_CHECK(pv->GetMax() == Empty); + + str = pv->Format(); + BOOST_CHECK(str == "test=1000000000000000000Wh"); + + pv = PerfdataValue::Parse("test=1000mW"); + BOOST_CHECK(pv); + + BOOST_CHECK(pv->GetValue() == 1); + BOOST_CHECK(pv->GetUnit() == "watts"); + BOOST_CHECK(pv->GetCrit() == Empty); + BOOST_CHECK(pv->GetWarn() == Empty); + BOOST_CHECK(pv->GetMin() == Empty); + BOOST_CHECK(pv->GetMax() == Empty); + + str = pv->Format(); + BOOST_CHECK(str == "test=1W"); } BOOST_AUTO_TEST_CASE(warncritminmax)