diff --git a/doc/6-object-types.md b/doc/6-object-types.md index 9921c0608..3dc32601e 100644 --- a/doc/6-object-types.md +++ b/doc/6-object-types.md @@ -882,10 +882,10 @@ Example: } Measurement names and tags are fully configurable by the end user. The InfluxdbWriter -object will automatically add a `metric` and `type` tag to each data point. These -correlate to perfdata label and perfdata field (value, warn, crit, min, max) respectively. -If a value associated with a tag is not able to be resolved, it will be dropped and not -sent to the target host. +object will automatically add a `metric` tag to each data point. This correlates to the +perfdata label. Fields (value, warn, crit, min, max) are created from data if available +and the configuration allows it. If a value associated with a tag is not able to be +resolved, it will be dropped and not sent to the target host. The database is assumed to exist so this object will make no attempt to create it currently. @@ -908,6 +908,45 @@ Configuration Attributes: flush_interval | **Optional.** How long to buffer data points before transfering to InfluxDB. Defaults to `10s`. flush_threshold | **Optional.** How many data points to buffer before forcing a transfer to InfluxDB. Defaults to `1024`. +### Instance Tagging + +Consider the following service check: + + apply Service "disk" for (disk => attributes in host.vars.disks) { + import "generic-service" + check_command = "disk" + display_name = "Disk " + disk + vars.disk_partitions = disk + assign where host.vars.disks + } + +This is a typical pattern for checking individual disks, NICs, SSL certificates etc associated +with a host. What would be useful is to have the data points tagged with the specific instance +for that check. This would allow you to query time series data for a check on a host and for a +specific instance e.g. /dev/sda. To do this quite simply add the instance to the service variables: + + apply Service "disk" for (disk => attributes in host.vars.disks) { + ... + vars.instance = disk + ... + } + +Then modify your writer configuration to add this tag to your data points if the instance variable +is associated with the service: + + object InfluxdbWriter "influxdb" { + ... + service_template = { + measurement = "$service.check_command$" + tags = { + hostname = "$host.name$" + service = "$service.name$" + instance = "$service.vars.instance$" + } + } + ... + } + ## LiveStatusListener Livestatus API interface available as TCP or UNIX socket. Historical table queries diff --git a/lib/perfdata/influxdbwriter.cpp b/lib/perfdata/influxdbwriter.cpp index a091c31fb..caf9e1dfa 100644 --- a/lib/perfdata/influxdbwriter.cpp +++ b/lib/perfdata/influxdbwriter.cpp @@ -45,6 +45,7 @@ #include #include #include +#include using namespace icinga; @@ -140,8 +141,6 @@ void InfluxdbWriter::CheckResultHandler(const Checkable::Ptr& checkable, const C double ts = cr->GetExecutionEnd(); // Clone the template and perform an in-place macro expansion of measurement and tag values - // Work Needed: Escape ' ', ',' and '=' in field keys, tag keys and tag values - // Quote field values when the type is string Dictionary::Ptr tmpl_clean = service ? GetServiceTemplate() : GetHostTemplate(); Dictionary::Ptr tmpl = static_pointer_cast(tmpl_clean->Clone()); tmpl->Set("measurement", MacroProcessor::ResolveMacros(tmpl->Get("measurement"), resolvers, cr)); @@ -158,16 +157,10 @@ retry: } } - // If the service was appiled via a 'apply Service for' command then resolve the - // instance and add it as a tag (e.g. check_command = mtu, name = mtueth0, instance = eth0) - if (service && (service->GetName() != service->GetCheckCommand()->GetName())) { - tags->Set("instance", service->GetName().SubStr(service->GetCheckCommand()->GetName().GetLength())); - } - SendPerfdata(tmpl, cr, ts); } -void InfluxdbWriter::SendPerfdata(const Dictionary::Ptr tmpl, const CheckResult::Ptr& cr, double ts) +void InfluxdbWriter::SendPerfdata(const Dictionary::Ptr& tmpl, const CheckResult::Ptr& cr, double ts) { Array::Ptr perfdata = cr->GetPerformanceData(); @@ -190,37 +183,86 @@ void InfluxdbWriter::SendPerfdata(const Dictionary::Ptr tmpl, const CheckResult: } } - SendMetric(tmpl, pdv->GetLabel(), "value", pdv->GetValue(), ts); - + Dictionary::Ptr fields = new Dictionary(); + fields->Set(String("value"), pdv->GetValue()); if (GetEnableSendThresholds()) { if (pdv->GetCrit()) - SendMetric(tmpl, pdv->GetLabel(), "crit", pdv->GetCrit(), ts); + fields->Set(String("crit"), pdv->GetCrit()); if (pdv->GetWarn()) - SendMetric(tmpl, pdv->GetLabel(), "warn", pdv->GetWarn(), ts); + fields->Set(String("warn"), pdv->GetWarn()); if (pdv->GetMin()) - SendMetric(tmpl, pdv->GetLabel(), "min", pdv->GetMin(), ts); + fields->Set(String("min"), pdv->GetMin()); if (pdv->GetMax()) - SendMetric(tmpl, pdv->GetLabel(), "max", pdv->GetMax(), ts); + fields->Set(String("max"), pdv->GetMax()); } + + SendMetric(tmpl, pdv->GetLabel(), fields, ts); } } -void InfluxdbWriter::SendMetric(const Dictionary::Ptr tmpl, const String& label, const String& type, double value, double ts) +String InfluxdbWriter::EscapeKey(const String& str) +{ + // Iterate over the key name and escape commas and spaces with a backslash + String result = str; + boost::algorithm::replace_all(result, ",", "\\,"); + boost::algorithm::replace_all(result, " ", "\\ "); + return str; +} + +String InfluxdbWriter::EscapeField(const String& str) +{ + // Technically everything entering here from PerfdataValue is a + // double, but best have the safety net in place. + + // Handle numerics + boost::regex numeric("-?\\d+(\\.\\d+)?((e|E)[+-]?\\d+)?"); + if (boost::regex_match(str.GetData(), numeric)) { + return str; + } + + // Handle booleans + boost::regex boolean_true("t|true", boost::regex::icase); + if (boost::regex_match(str.GetData(), boolean_true)) + return "true"; + boost::regex boolean_false("f|false", boost::regex::icase); + if (boost::regex_match(str.GetData(), boolean_false)) + return "false"; + + // Otherwise it's a string and needs escaping and quoting + String result = str; + boost::algorithm::replace_all(result, "\"", "\\\""); + return "\"" + result + "\""; +} + +void InfluxdbWriter::SendMetric(const Dictionary::Ptr& tmpl, const String& label, const Dictionary::Ptr& fields, double ts) { std::ostringstream msgbuf; - msgbuf << tmpl->Get("measurement"); + msgbuf << EscapeKey(tmpl->Get("measurement")); Dictionary::Ptr tags = tmpl->Get("tags"); if (tags) { ObjectLock olock(tags); BOOST_FOREACH(const Dictionary::Pair& pair, tags) { // Empty macro expansion, no tag - if (!pair.second.IsEmpty()) - msgbuf << "," << pair.first << "=" << pair.second; + if (!pair.second.IsEmpty()) { + msgbuf << "," << EscapeKey(pair.first) << "=" << EscapeKey(pair.second); + } } } - msgbuf << ",metric=" << label << ",type=" << type << " value=" << value << " " << static_cast(ts); + msgbuf << ",metric=" << label << " "; + + bool first = true; + ObjectLock fieldLock(fields); + BOOST_FOREACH(const Dictionary::Pair& pair, fields) { + if (first) + first = false; + else + msgbuf << ","; + msgbuf << EscapeKey(pair.first) << "=" << EscapeField(pair.second); + } + + msgbuf << " " << static_cast(ts); Log(LogDebug, "InfluxdbWriter") << "Add to metric list:'" << msgbuf.str() << "'."; diff --git a/lib/perfdata/influxdbwriter.hpp b/lib/perfdata/influxdbwriter.hpp index a241f08c0..22b806012 100644 --- a/lib/perfdata/influxdbwriter.hpp +++ b/lib/perfdata/influxdbwriter.hpp @@ -54,11 +54,14 @@ private: Array::Ptr m_DataBuffer; void CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr); - void SendPerfdata(const Dictionary::Ptr tmpl, const CheckResult::Ptr& cr, double ts); - void SendMetric(const Dictionary::Ptr tmpl, const String& label, const String& type, double value, double ts); + void SendPerfdata(const Dictionary::Ptr& tmpl, const CheckResult::Ptr& cr, double ts); + void SendMetric(const Dictionary::Ptr& tmpl, const String& label, const Dictionary::Ptr& fields, double ts); void FlushTimeout(void); void Flush(void); + static String EscapeKey(const String& str); + static String EscapeField(const String& str); + Stream::Ptr Connect(void); };