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)