Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/tags for elasticsearchwriter #10074

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ Sascha Westermann <[email protected]>
Sebastian Brückner <[email protected]>
Sebastian Chrostek <[email protected]>
Sebastian Eikenberg <[email protected]>
Sebastian Grund <[email protected]>
Sebastian Marsching <[email protected]>
Silas <[email protected]>
Simon Murray <[email protected]>
Expand Down
8 changes: 7 additions & 1 deletion doc/09-object-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -1181,7 +1181,7 @@ Configuration Attributes:

### ElasticsearchWriter <a id="objecttype-elasticsearchwriter"></a>

Writes check result metrics and performance data to an Elasticsearch instance.
Writes check result metrics and performance data to an Elasticsearch or OpenSearch instance.
This configuration object is available as [elasticsearch feature](14-features.md#elasticsearch-writer).

Example:
Expand All @@ -1194,6 +1194,10 @@ object ElasticsearchWriter "elasticsearch" {

enable_send_perfdata = true

host_tags_template = {
os_name = "$host.vars.os$"
}

flush_threshold = 1024
flush_interval = 10
}
Expand All @@ -1215,6 +1219,8 @@ Configuration Attributes:
password | String | **Optional.** Basic auth password if Elasticsearch is hidden behind an HTTP proxy.
enable\_tls | Boolean | **Optional.** Whether to use a TLS stream. Defaults to `false`. Requires an HTTP proxy.
insecure\_noverify | Boolean | **Optional.** Disable TLS peer verification.
host\_tags\_template | Dictionary | **Optional.** Allows to apply additional tags to the Elasticsearch host entries. They will be added under `tags`.
service\_tags\_template | Dictionary | **Optional.** Allows to apply additional tags to the Elasticsearch service entries. They will be added under `tags`.
ca\_path | String | **Optional.** Path to CA certificate to validate the remote host. Requires `enable_tls` set to `true`.
cert\_path | String | **Optional.** Path to host certificate to present to the remote host for mutual verification. Requires `enable_tls` set to `true`.
key\_path | String | **Optional.** Path to host key to accompany the cert\_path. Requires `enable_tls` set to `true`.
Expand Down
26 changes: 21 additions & 5 deletions doc/14-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,16 +335,14 @@ More integrations:
#### Elasticsearch Writer <a id="elasticsearch-writer"></a>

This feature forwards check results, state changes and notification events
to an [Elasticsearch](https://www.elastic.co/products/elasticsearch) installation over its HTTP API.
to an [Elasticsearch](https://www.elastic.co/products/elasticsearch) or an [OpenSearch](https://opensearch.org/) installation over its HTTP API.

The check results include parsed performance data metrics if enabled.

> **Note**
>
> Elasticsearch 5.x or 6.x are required. This feature has been successfully tested with
> Elasticsearch 5.6.7 and 6.3.1.


> Elasticsearch 7.x, 8.x or Opensearch 2.12.x are required. This feature has been successfully tested with
> Elasticsearch 7.17.10, 8.8.1 and OpenSearch 2.13.0.

Enable the feature and restart Icinga 2.

Expand Down Expand Up @@ -398,6 +396,24 @@ check_result.perfdata.<perfdata-label>.warn
check_result.perfdata.<perfdata-label>.crit
```

Additionaly it is possible to configure custom tags that are applied to the metrics via `host_tags_template` or `service_tags_template`.
Depending on whether the write event was triggered on a service or host object, additional tags are added to the ElasticSearch entries.

A host metrics entry configured with the following `host_tags_template`:
```
host_tags_template = {
os_name = "$host.vars.os$"
custom_label = "A Custom Label"
a_list_of_items = [ "foo", "bar" ]
}
```
Will in addition to the above mentioned lines also contain:
```
tags.os_name = "Linux"
tags.custom_label = "A Custom Label"
tags.a_list_of_items = [ "foo", "bar" ]
```

#### Elasticsearch in Cluster HA Zones <a id="elasticsearch-writer-cluster-ha"></a>

The Elasticsearch feature supports [high availability](06-distributed-monitoring.md#distributed-monitoring-high-availability-features)
Expand Down
87 changes: 87 additions & 0 deletions lib/perfdata/elasticsearchwriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "remote/url.hpp"
#include "icinga/compatutility.hpp"
#include "icinga/service.hpp"
#include "icinga/macroprocessor.hpp"
#include "icinga/checkcommand.hpp"
#include "base/application.hpp"
#include "base/defer.hpp"
Expand Down Expand Up @@ -131,6 +132,40 @@ void ElasticsearchWriter::Pause()
ObjectImpl<ElasticsearchWriter>::Pause();
}

void ElasticsearchWriter::AddTemplateTags(const Dictionary::Ptr& fields, const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
{
Host::Ptr host;
Service::Ptr service;
tie(host, service) = GetHostService(checkable);

Dictionary::Ptr tmpl;
if (service && GetServiceTagsTemplate()) {
tmpl = static_pointer_cast<Dictionary>(GetServiceTagsTemplate()->ShallowClone());
} else if (GetHostTagsTemplate()) {
tmpl = static_pointer_cast<Dictionary>(GetHostTagsTemplate()->ShallowClone());
}

if (tmpl) {
MacroProcessor::ResolverList resolvers;
resolvers.emplace_back("host", host);
if (service) {
resolvers.emplace_back("service", service);
}

Dictionary::Ptr tags = new Dictionary();
ObjectLock olock(tmpl);
for (const Dictionary::Pair& pair : tmpl) {
String missingMacro;
Value value = MacroProcessor::ResolveMacros(pair.second, resolvers, cr, &missingMacro);

if (missingMacro.IsEmpty()) {
tags->Set(pair.first, value);
}
}
fields->Set("tags", tags);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite sure if tags is the right field for this. Isn't it common that tags is just a simple list instead of key-value pairs? I would set a different field or make it configureable.

Copy link
Contributor Author

@SebastianOpeni SebastianOpeni Sep 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field tags as a dictionary was used because the influxdb-writer is also using it that way.
The functionality would stay the same with a different field name so if you have a preference I could change it accordingly.
(labels would come to mind as a alternative.)

Copy link
Contributor Author

@SebastianOpeni SebastianOpeni Sep 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave it some thought and I could also simply allow tags to be of type array. (in addition to dict)
That would change some of the logic we had reviewed until now but if that would be preferred by you, I could also implement it that way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it common that tags is just a simple list instead of key-value pairs?

I'm afraid, in perfdata DB context, they indeed are key-value pairs:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid, in perfdata DB context, they indeed are key-value pairs:

Please show me where we set a field called tags in one of them. Supporting tags in both just means adding key-value pairs, with no reference to a tags field, right?. But here we set the field where I wonder if users of Elasticsearch or similar would expect a list rather than key-value pairs. tag_set sounds more reasonable to me. Anyway, before we just merge this, I'd like to get an informed opinion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Icingas influxdbcommonwriter this is done here:
https://github.com/Icinga/icinga2/blob/master/lib/perfdata/influxdbcommonwriter.cpp#L253

But tags never reaches Influx DB.

The graphite writer does only allow to configure templates for the host and service name.

Otherwise there would not be a tags field either.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found it as well just after Commenting. 🙈
That was my bad.
I'm very sorry for the confusion.

Copy link
Contributor Author

@SebastianOpeni SebastianOpeni Sep 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To prevent confusion about the Type of tags, would you prefer to:

  • Rename the field to tag_set or
  • Add the configured key-values directly into the elasticSearch-Entries as done in the influxdb case?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To prevent confusion about the Type of tags, would you prefer to:

  • Rename the field to tag_set or

I think a tag_set property would be rather confusing in ES/Kibana.

  • Add the configured key-values directly into the elasticSearch-Entries as done in the influxdb case?

This, in contrast, has the advantages:

  • No tags. boilerplate in our ES data
  • Consistency with InfluxDB

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After my changes the configured tags are now added to the root element.
That also allows for tags = ["foo", "bar"] if desired.

Notice:
Since the templated_tags are added after the default fields and the performance Values this change will also allow to overwrite them.

}
}

void ElasticsearchWriter::AddCheckResult(const Dictionary::Ptr& fields, const Checkable::Ptr& checkable, const CheckResult::Ptr& cr)
{
String prefix = "check_result.";
Expand Down Expand Up @@ -257,6 +292,8 @@ void ElasticsearchWriter::InternalCheckResultHandler(const Checkable::Ptr& check
ts = cr->GetExecutionEnd();
}

AddTemplateTags(fields, checkable, cr);

Enqueue(checkable, "checkresult", fields, ts);
}

Expand Down Expand Up @@ -307,6 +344,8 @@ void ElasticsearchWriter::StateChangeHandlerInternal(const Checkable::Ptr& check
ts = cr->GetExecutionEnd();
}

AddTemplateTags(fields, checkable, cr);

Enqueue(checkable, "statechange", fields, ts);
}

Expand Down Expand Up @@ -377,6 +416,8 @@ void ElasticsearchWriter::NotificationSentToAllUsersHandlerInternal(const Notifi
ts = cr->GetExecutionEnd();
}

AddTemplateTags(fields, checkable, cr);

Enqueue(checkable, "notification", fields, ts);
}

Expand Down Expand Up @@ -683,3 +724,49 @@ String ElasticsearchWriter::FormatTimestamp(double ts)

return Utility::FormatDateTime("%Y-%m-%dT%H:%M:%S", ts) + "." + Convert::ToString(milliSeconds) + Utility::FormatDateTime("%z", ts);
}

void ElasticsearchWriter::ValidateHostTagsTemplate(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
{
ObjectImpl<ElasticsearchWriter>::ValidateHostTagsTemplate(lvalue, utils);

Dictionary::Ptr tags = lvalue();
if (tags) {
ObjectLock olock(tags);
for (const Dictionary::Pair& pair : tags) {
if (pair.second.IsObjectType<Array>()) {
Array::Ptr arrObject = pair.second;
ObjectLock arrLock(arrObject);
for (const Value& arrValue : arrObject) {
if (!MacroProcessor::ValidateMacroString(arrValue)) {
BOOST_THROW_EXCEPTION(ValidationError(this, { "host_tags_template", pair.first }, "Closing $ not found in macro format string '" + arrValue + "'."));
}
}
} else if (!MacroProcessor::ValidateMacroString(pair.second)) {
BOOST_THROW_EXCEPTION(ValidationError(this, { "host_tags_template", pair.first }, "Closing $ not found in macro format string '" + pair.second + "'."));
}
}
}
}

void ElasticsearchWriter::ValidateServiceTagsTemplate(const Lazy<Dictionary::Ptr>& lvalue, const ValidationUtils& utils)
{
ObjectImpl<ElasticsearchWriter>::ValidateServiceTagsTemplate(lvalue, utils);

Dictionary::Ptr tags = lvalue();
if (tags) {
ObjectLock olock(tags);
for (const Dictionary::Pair& pair : tags) {
if (pair.second.IsObjectType<Array>()) {
Array::Ptr arrObject = pair.second;
ObjectLock arrLock(arrObject);
for (const Value& arrValue : arrObject) {
if (!MacroProcessor::ValidateMacroString(arrValue)) {
BOOST_THROW_EXCEPTION(ValidationError(this, { "service_tags_template", pair.first }, "Closing $ not found in macro format string '" + arrValue + "'."));
}
}
} else if (!MacroProcessor::ValidateMacroString(pair.second)) {
BOOST_THROW_EXCEPTION(ValidationError(this, { "service_tags_template", pair.first }, "Closing $ not found in macro format string '" + pair.second + "'."));
}
}
}
}
4 changes: 4 additions & 0 deletions lib/perfdata/elasticsearchwriter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ class ElasticsearchWriter final : public ObjectImpl<ElasticsearchWriter>

static String FormatTimestamp(double ts);

void ValidateHostTagsTemplate(const Lazy<Dictionary::Ptr> &lvalue, const ValidationUtils &utils) override;
void ValidateServiceTagsTemplate(const Lazy<Dictionary::Ptr> &lvalue, const ValidationUtils &utils) override;

protected:
void OnConfigLoaded() override;
void Resume() override;
Expand All @@ -37,6 +40,7 @@ class ElasticsearchWriter final : public ObjectImpl<ElasticsearchWriter>
std::mutex m_DataBufferMutex;

void AddCheckResult(const Dictionary::Ptr& fields, const Checkable::Ptr& checkable, const CheckResult::Ptr& cr);
void AddTemplateTags(const Dictionary::Ptr& fields, const Checkable::Ptr& checkable, const CheckResult::Ptr& cr);

void StateChangeHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type);
void StateChangeHandlerInternal(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, StateType type);
Expand Down
18 changes: 18 additions & 0 deletions lib/perfdata/elasticsearchwriter.ti
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class ElasticsearchWriter : ConfigObject
[config] bool enable_tls {
default {{{ return false; }}}
};

[config] Dictionary::Ptr host_tags_template;
[config] Dictionary::Ptr service_tags_template;
[config] bool insecure_noverify {
default {{{ return false; }}}
};
Expand All @@ -47,4 +50,19 @@ class ElasticsearchWriter : ConfigObject
};
};

validator ElasticsearchWriter {
Dictionary host_tags_template {
String "*";
Array "*" {
String "*";
};
};
Dictionary service_tags_template {
String "*";
Array "*" {
String "*";
};
};
};

}
Loading