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

Support for multi es instances #548

Merged
merged 14 commits into from
Nov 14, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- Added support for shortening Kibana Discover URLs using Kibana Shorten URL API - [#512](https://github.com/jertel/elastalert2/pull/512) - @JeffAshton
- Added new alerter `HTTP Post 2` which allow more flexibility to build the body/headers of the request. - [#530](https://github.com/jertel/elastalert2/pull/530) - @lepouletsuisse
- [Slack] Added new option to include url to jira ticket if it is created in the same pipeline. - [#547](https://github.com/jertel/elastalert2/pull/547) - @hugefarsen
- Added support for multi ElasticSearch instances. - [#548](https://github.com/jertel/elastalert2/pull/548) - @buratinopy

## Other changes
- [Docs] Add exposed metrics documentation - [#498](https://github.com/jertel/elastalert2/pull/498) - @thisisxgp
Expand Down
17 changes: 16 additions & 1 deletion docs/source/ruletypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,24 @@ Required Settings
es_host
^^^^^^^

``es_host``: The hostname of the Elasticsearch cluster the rule will use to query. (Required, string, no default)
``es_host``: The hostname (or comma separated list of hostnames) of the Elasticsearch cluster the rule will use to query. (Required, string, no default)
The environment variable ``ES_HOST`` will override this field.

Example of working with one host::

es_host: hostname

Example of working with multiple hosts::

hostname0, hostname1, hostname2

Example of working with multiple hosts and specific ports::

hostname0:9200, hostname1, hostname2:9201

Nodes with a specified port will be used as is, nodes without a port will be used with the port from es_port


es_port
^^^^^^^

Expand Down
2 changes: 1 addition & 1 deletion elastalert/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def __init__(self, conf):
"""
:arg conf: es_conn_config dictionary. Ref. :func:`~util.build_es_conn_config`
"""
super(ElasticSearchClient, self).__init__(host=conf['es_host'],
super(ElasticSearchClient, self).__init__(hosts=conf['es_host'],
port=conf['es_port'],
url_prefix=conf['es_url_prefix'],
use_ssl=conf['use_ssl'],
Expand Down
45 changes: 32 additions & 13 deletions elastalert/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,11 @@ def build_es_conn_config(conf):
parsed_conf['aws_region'] = None
parsed_conf['profile'] = None
parsed_conf['headers'] = None
parsed_conf['es_host'] = os.environ.get('ES_HOST', conf['es_host'])
parsed_conf['es_port'] = int(os.environ.get('ES_PORT', conf['es_port']))
es_host = os.environ.get('ES_HOST', conf['es_host'])
es_port = int(os.environ.get('ES_PORT', conf['es_port']))
parsed_conf['es_host'] = parse_host(es_host, es_port)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is es_host getting the port appended to it and being converted to a list? Pretty sure this will break AWS auth:

es_conn_conf['http_auth'] = auth(host=es_conn_conf['es_host'],

Copy link
Owner

Choose a reason for hiding this comment

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

When you say "will break AWS auth" are you referring to OpenSearch?

Copy link
Contributor

@JeffAshton JeffAshton Nov 8, 2021

Choose a reason for hiding this comment

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

If I'm reading the change correctly, es_host will become

["{host}:{port}".format(host=host, port=port)]

From https://github.com/jertel/elastalert2/pull/548/files#diff-c94be3c6634086358da8944c7febe9c9e98fc1b4425fcc4e116e0740f5cf4eb6R556

This would break AWS auth because the host is used in the signing process.

Maybe I'm wrong and build_es_conn_config is used separately.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure looks like it would break it:

es_conn_conf = build_es_conn_config(conf)

es_conn_conf['http_auth'] = auth(host=es_conn_conf['es_host'],

Copy link
Owner

Choose a reason for hiding this comment

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

This is the code that I think helps explain your argument better: https://github.com/DavidMuller/aws-requests-auth/blob/2e1dd0f37e3815c417c3b0630215a77aab5af617/aws_requests_auth/aws_auth.py#L118

Based on that, I agree. However, we get this request for supporting multiple ES hosts every month or so, so perhaps there's something we can do for the community to make this easier without the need for a load balancer. Would a safer way to do this be to introduce a new config parameter called es_hosts for those who want to use multiple ES hosts, outside of AWS? util.py::build_es_conn_config() could prefer that parameter if specified, and it would override the es_host parameter.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think an array over a csv string makes more sense anyway.

Copy link
Author

Choose a reason for hiding this comment

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

Ok i can redo

parsed_conf['es_port'] = es_port

parsed_conf['es_url_prefix'] = ''
parsed_conf['es_conn_timeout'] = conf.get('es_conn_timeout', 20)
parsed_conf['send_get_body_as'] = conf.get('es_send_get_body_as', 'GET')
Expand Down Expand Up @@ -486,34 +489,34 @@ def should_scrolling_continue(rule_conf):
return not stop_the_scroll


def _expand_string_into_dict(string, value, sep='.'):
def _expand_string_into_dict(string, value, sep='.'):
"""
Converts a encapsulated string-dict to a sequence of dict. Use separator (default '.') to split the string.
Example:
Example:
string1.string2.stringN : value -> {string1: {string2: {string3: value}}

:param string: The encapsulated "string-dict"
:param value: Value associated to the last field of the "string-dict"
:param sep: Separator character. Default: '.'
:rtype: dict
"""
if sep not in string:
return {string : value}
return {string: value}
key, val = string.split(sep, 1)
return {key: _expand_string_into_dict(val, value)}
def expand_string_into_dict(dictionary, string , value, sep='.'):


def expand_string_into_dict(dictionary, string, value, sep='.'):
"""
Useful function to "compile" a string-dict string used in metric and percentage rules into a dictionary sequence.

:param dictionary: The dictionary dict
:param string: String Key
:param string: String Key
:param value: String Value
:param sep: Separator character. Default: '.'
:rtype: dict
"""

if sep not in string:
dictionary[string] = value
return dictionary
Expand All @@ -526,7 +529,7 @@ def expand_string_into_dict(dictionary, string , value, sep='.'):
def format_string(format_config, target_value):
"""
Formats number, supporting %-format and str.format() syntax.

:param format_config: string format syntax, for example '{:.2%}' or '%.2f'
:param target_value: number to format
:rtype: string
Expand All @@ -536,3 +539,19 @@ def format_string(format_config, target_value):
else:
return format_config % (target_value)


def parse_host(host, port=9200):
"""
Convet host str like "host1:port1, host2:port2" to list
:param host str: hostnames (separated with comma ) or single host name
:param port: default to 9200
:return: list of hosts

"""
if "," in host:
host_list = host.split(",")
host_list = [("{host}:{port}".format(host=x.strip(), port=port)
if ":" not in x else x.strip()) for x in host_list]
return host_list
else:
return ["{host}:{port}".format(host=host, port=port)]
jertel marked this conversation as resolved.
Show resolved Hide resolved
36 changes: 33 additions & 3 deletions tests/util_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from elastalert.util import unixms_to_dt
from elastalert.util import format_string
from elastalert.util import pretty_ts
from elastalert.util import parse_host


@pytest.mark.parametrize('spec, expected_delta', [
Expand Down Expand Up @@ -337,7 +338,7 @@ def test_ts_utc_to_tz():
'aws_region': None,
'profile': None,
'headers': None,
'es_host': 'localhost',
'es_host': ['localhost:9200'],
'es_port': 9200,
'es_url_prefix': '',
'es_conn_timeout': 20,
Expand All @@ -360,7 +361,7 @@ def test_ts_utc_to_tz():
'aws_region': 'us-east-1',
'profile': 'default',
'headers': None,
'es_host': 'localhost',
'es_host': ['localhost:9200'],
'es_port': 9200,
'es_url_prefix': 'elasticsearch',
'es_conn_timeout': 30,
Expand Down Expand Up @@ -435,7 +436,7 @@ def test_build_es_conn_config2():
'aws_region': None,
'profile': None,
'headers': None,
'es_host': 'localhost',
'es_host': ['localhost:9200'],
'es_port': 9200,
'es_url_prefix': '',
'es_conn_timeout': 20,
Expand Down Expand Up @@ -519,3 +520,32 @@ def test_pretty_ts():
assert '2021-08-16 16:35 UTC' == pretty_ts(ts)
assert '2021-08-16 16:35 ' == pretty_ts(ts, False)
assert '2021-08-16 16:35 +0000' == pretty_ts(ts, ts_format='%Y-%m-%d %H:%M %z')


def test_parse_host():
assert parse_host("localhost", port=9200) == ["localhost:9200"]
assert parse_host("host1:9200, host2:9200, host3:9300") == ["host1:9200",
"host2:9200",
"host3:9300"]


def test_build_cofig_for_multi():
assert build_es_conn_config({
"es_host": "localhost",
"es_port": 9200
})['es_host'] == ['localhost:9200']

assert build_es_conn_config({
"es_host": "host1:9200, host2:9200, host3:9300",
"es_port": 9200
})['es_host'] == ["host1:9200", "host2:9200", "host3:9300"]

assert build_es_conn_config({
"es_host": "host1, host2, host3",
"es_port": 9200
})['es_host'] == ["host1:9200", "host2:9200", "host3:9200"]

assert build_es_conn_config({
"es_host": "host1, host2:9200, host3:9300",
"es_port": 9200
})['es_host'] == ["host1:9200", "host2:9200", "host3:9300"]