elastalert2 2.29.0__tar.gz → 2.31.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. {elastalert2-2.29.0/elastalert2.egg-info → elastalert2-2.31.0}/PKG-INFO +13 -13
  2. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/__init__.py +10 -1
  3. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/datadog.py +2 -4
  4. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerts.py +24 -9
  5. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/elastalert.py +5 -4
  6. elastalert2-2.31.0/elastalert/esql.py +76 -0
  7. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/kibana_discover.py +31 -1
  8. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/loaders.py +0 -2
  9. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/schema.yaml +12 -28
  10. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/test_rule.py +1 -1
  11. {elastalert2-2.29.0 → elastalert2-2.31.0/elastalert2.egg-info}/PKG-INFO +13 -13
  12. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert2.egg-info/SOURCES.txt +1 -2
  13. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert2.egg-info/requires.txt +12 -12
  14. {elastalert2-2.29.0 → elastalert2-2.31.0}/setup.py +13 -13
  15. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/datadog_test.py +67 -0
  16. elastalert2-2.29.0/elastalert/alerters/teams.py +0 -120
  17. elastalert2-2.29.0/tests/alerters/teams_test.py +0 -650
  18. {elastalert2-2.29.0 → elastalert2-2.31.0}/LICENSE +0 -0
  19. {elastalert2-2.29.0 → elastalert2-2.31.0}/README.md +0 -0
  20. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/__init__.py +0 -0
  21. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/alerta.py +0 -0
  22. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/alertmanager.py +0 -0
  23. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/chatwork.py +0 -0
  24. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/command.py +0 -0
  25. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/debug.py +0 -0
  26. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/dingtalk.py +0 -0
  27. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/discord.py +0 -0
  28. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/email.py +0 -0
  29. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/exotel.py +0 -0
  30. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/flashduty.py +0 -0
  31. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/gelf.py +0 -0
  32. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/gitter.py +0 -0
  33. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/googlechat.py +0 -0
  34. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/httppost.py +0 -0
  35. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/httppost2.py +0 -0
  36. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/indexer.py +0 -0
  37. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/iris.py +0 -0
  38. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/jira.py +0 -0
  39. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/lark.py +0 -0
  40. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/line.py +0 -0
  41. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/matrixhookshot.py +0 -0
  42. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/mattermost.py +0 -0
  43. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/opsgenie.py +0 -0
  44. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/pagerduty.py +0 -0
  45. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/pagertree.py +0 -0
  46. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/powerautomate.py +0 -0
  47. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/rocketchat.py +0 -0
  48. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/servicenow.py +0 -0
  49. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/ses.py +0 -0
  50. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/slack.py +0 -0
  51. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/smseagle.py +0 -0
  52. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/sns.py +0 -0
  53. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/sqs.py +0 -0
  54. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/stomp.py +0 -0
  55. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/telegram.py +0 -0
  56. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/tencentsms.py +0 -0
  57. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/thehive.py +0 -0
  58. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/twilio.py +0 -0
  59. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/victorops.py +0 -0
  60. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/webex_webhook.py +0 -0
  61. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/workwechat.py +0 -0
  62. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/yzj.py +0 -0
  63. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/alerters/zabbix.py +0 -0
  64. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/auth.py +0 -0
  65. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/config.py +0 -0
  66. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/create_index.py +0 -0
  67. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/enhancements.py +0 -0
  68. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/eql.py +0 -0
  69. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/es_mappings/7/elastalert.json +0 -0
  70. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/es_mappings/7/elastalert_error.json +0 -0
  71. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/es_mappings/7/elastalert_status.json +0 -0
  72. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/es_mappings/7/past_elastalert.json +0 -0
  73. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/es_mappings/7/silence.json +0 -0
  74. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/es_mappings/8/elastalert.json +0 -0
  75. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/es_mappings/8/elastalert_error.json +0 -0
  76. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/es_mappings/8/elastalert_status.json +0 -0
  77. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/es_mappings/8/past_elastalert.json +0 -0
  78. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/es_mappings/8/silence.json +0 -0
  79. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/kibana_external_url_formatter.py +0 -0
  80. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/opensearch_discover.py +0 -0
  81. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/opensearch_external_url_formatter.py +0 -0
  82. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/prometheus_wrapper.py +0 -0
  83. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/ruletypes.py +0 -0
  84. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/util.py +0 -0
  85. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert/yaml.py +0 -0
  86. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert2.egg-info/dependency_links.txt +0 -0
  87. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert2.egg-info/entry_points.txt +0 -0
  88. {elastalert2-2.29.0 → elastalert2-2.31.0}/elastalert2.egg-info/top_level.txt +0 -0
  89. {elastalert2-2.29.0 → elastalert2-2.31.0}/setup.cfg +0 -0
  90. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/__init__.py +0 -0
  91. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/alerta_test.py +0 -0
  92. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/alertmanager_test.py +0 -0
  93. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/chatwork_test.py +0 -0
  94. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/command_test.py +0 -0
  95. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/debug_test.py +0 -0
  96. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/dingtalk_test.py +0 -0
  97. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/discord_test.py +0 -0
  98. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/email_test.py +0 -0
  99. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/exotel_test.py +0 -0
  100. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/flashduty_test.py +0 -0
  101. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/gelf_test.py +0 -0
  102. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/gitter_test.py +0 -0
  103. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/googlechat_test.py +0 -0
  104. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/httppost2_test.py +0 -0
  105. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/httppost_test.py +0 -0
  106. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/indexer_test.py +0 -0
  107. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/iris_test.py +0 -0
  108. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/jira_test.py +0 -0
  109. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/lark_test.py +0 -0
  110. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/line_test.py +0 -0
  111. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/matrixhookshot_test.py +0 -0
  112. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/mattermost_test.py +0 -0
  113. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/opsgenie_test.py +0 -0
  114. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/pagerduty_test.py +0 -0
  115. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/pagertree_test.py +0 -0
  116. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/powerautomate_test.py +0 -0
  117. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/rocketchat_test.py +0 -0
  118. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/servicenow_test.py +0 -0
  119. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/ses_test.py +0 -0
  120. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/slack_test.py +0 -0
  121. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/smseagle_test.py +0 -0
  122. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/sns_test.py +0 -0
  123. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/sqs_test.py +0 -0
  124. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/stomp_test.py +0 -0
  125. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/telegram_test.py +0 -0
  126. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/tencentsms_test.py +0 -0
  127. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/thehive_test.py +0 -0
  128. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/twilio_test.py +0 -0
  129. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/victorops_test.py +0 -0
  130. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/webex_webhook_test.py +0 -0
  131. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/workwechat_test.py +0 -0
  132. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/yzj_test.py +0 -0
  133. {elastalert2-2.29.0 → elastalert2-2.31.0}/tests/alerters/zabbix_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elastalert2
3
- Version: 2.29.0
3
+ Version: 2.31.0
4
4
  Summary: Automated rule-based alerting for Elasticsearch
5
5
  Home-page: https://github.com/jertel/elastalert2
6
6
  License: Apache 2.0
@@ -15,32 +15,32 @@ Classifier: Operating System :: OS Independent
15
15
  Requires-Python: >=3.12
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
- Requires-Dist: apscheduler<4.0,>=3.11.0
18
+ Requires-Dist: apscheduler<4.0,>=3.11.2
19
19
  Requires-Dist: aws-requests-auth>=0.4.3
20
- Requires-Dist: boto3>=1.40.59
20
+ Requires-Dist: boto3>=1.42.89
21
21
  Requires-Dist: cffi>=2.0.0
22
- Requires-Dist: croniter>=6.0.0
22
+ Requires-Dist: croniter>=6.2.2
23
23
  Requires-Dist: elasticsearch==7.10.1
24
24
  Requires-Dist: envparse>=0.2.0
25
25
  Requires-Dist: exotel==0.1.5
26
26
  Requires-Dist: Jinja2>=3.1.6
27
27
  Requires-Dist: jira>=3.10.5
28
- Requires-Dist: jsonpointer>=3.0.0
29
- Requires-Dist: jsonschema>=4.25.1
30
- Requires-Dist: prettytable>=3.16.0
28
+ Requires-Dist: jsonpointer>=3.1.1
29
+ Requires-Dist: jsonschema>=4.26.0
30
+ Requires-Dist: prettytable>=3.17.0
31
31
  Requires-Dist: prison>=0.2.1
32
- Requires-Dist: prometheus_client>=0.23.1
32
+ Requires-Dist: prometheus-client>=0.25.0
33
33
  Requires-Dist: python-dateutil>=2.9.0.post0
34
- Requires-Dist: pytz>=2026.1
34
+ Requires-Dist: pytz>=2026.1.post1
35
35
  Requires-Dist: PyYAML>=6.0.3
36
- Requires-Dist: zabbix-utils>=2.0.3
37
- Requires-Dist: requests>=2.31.0
36
+ Requires-Dist: zabbix-utils>=2.0.4
37
+ Requires-Dist: requests>=2.33.1
38
38
  Requires-Dist: sortedcontainers>=2.4.0
39
39
  Requires-Dist: statsd-tags==3.2.1.post1
40
40
  Requires-Dist: stomp.py>=8.2.0
41
- Requires-Dist: tencentcloud-sdk-python>=3.0.1479
41
+ Requires-Dist: tencentcloud-sdk-python>=3.1.79
42
42
  Requires-Dist: texttable>=1.7.0
43
- Requires-Dist: twilio>=9.8.4
43
+ Requires-Dist: twilio>=9.10.5
44
44
  Dynamic: classifier
45
45
  Dynamic: description
46
46
  Dynamic: description-content-type
@@ -1,6 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import copy
3
3
  import elastalert.eql as eql
4
+ import elastalert.esql as esql
4
5
 
5
6
  from elasticsearch import Elasticsearch
6
7
  from elasticsearch import RequestsHttpConnection
@@ -134,6 +135,7 @@ class ElasticSearchClient(Elasticsearch):
134
135
 
135
136
  path = _make_path(index, doc_type, "_search")
136
137
  eql_body = eql.format_request(body)
138
+ esql_body = esql.format_request(body)
137
139
  if eql_body is not None:
138
140
  path = path.replace('/_search', '/_eql/search')
139
141
  body = eql_body
@@ -143,6 +145,10 @@ class ElasticSearchClient(Elasticsearch):
143
145
  params.pop('scroll')
144
146
  if '_source_includes' in params:
145
147
  params.pop('_source_includes')
148
+ elif esql_body is not None:
149
+ path = "/_query"
150
+ body = esql_body
151
+ params = {'format': 'json'}
146
152
 
147
153
  results = self.transport.perform_request(
148
154
  "POST",
@@ -152,6 +158,9 @@ class ElasticSearchClient(Elasticsearch):
152
158
  body=body,
153
159
  )
154
160
 
155
- eql.format_results(results);
161
+ if esql_body is not None:
162
+ esql.format_results(results, default_index=index)
163
+ else:
164
+ eql.format_results(results)
156
165
 
157
166
  return results
@@ -1,8 +1,6 @@
1
1
  import json
2
-
3
2
  import requests
4
3
  from requests import RequestException
5
-
6
4
  from elastalert.alerts import Alerter, DateTimeEncoder
7
5
  from elastalert.util import EAException, elastalert_logger
8
6
 
@@ -15,9 +13,9 @@ class DatadogAlerter(Alerter):
15
13
  super(DatadogAlerter, self).__init__(rule)
16
14
  self.dd_api_key = self.rule.get('datadog_api_key', None)
17
15
  self.dd_app_key = self.rule.get('datadog_app_key', None)
16
+ self.dd_api_url = self.rule.get('datadog_api_url', 'https://api.datadoghq.com/api/v1/events')
18
17
 
19
18
  def alert(self, matches):
20
- url = 'https://api.datadoghq.com/api/v1/events'
21
19
  headers = {
22
20
  'Content-Type': 'application/json',
23
21
  'DD-API-KEY': self.dd_api_key,
@@ -28,7 +26,7 @@ class DatadogAlerter(Alerter):
28
26
  'text': self.create_alert_body(matches)
29
27
  }
30
28
  try:
31
- response = requests.post(url, data=json.dumps(payload, cls=DateTimeEncoder), headers=headers)
29
+ response = requests.post(self.dd_api_url, data=json.dumps(payload, cls=DateTimeEncoder), headers=headers)
32
30
  response.raise_for_status()
33
31
  except RequestException as e:
34
32
  raise EAException('Error posting event to Datadog: %s' % e)
@@ -256,10 +256,25 @@ class Alerter(object):
256
256
  if not isinstance(summary_table_fields, list):
257
257
  summary_table_fields = [summary_table_fields]
258
258
 
259
+ # Each entry is either a string field path, or a dict with required 'path' and optional 'header'.
260
+ # Split into parallel lists so paths drive lookups/logging while headers drive rendered column names.
261
+ summary_table_paths = []
262
+ summary_table_headers = []
263
+ for entry in summary_table_fields:
264
+ if isinstance(entry, dict):
265
+ path = entry['path']
266
+ header = entry.get('header', path)
267
+ else:
268
+ path = entry
269
+ header = entry
270
+ summary_table_paths.append(path)
271
+ summary_table_headers.append(header)
272
+
259
273
  # Include a count aggregation so that we can see at a glance how many of each aggregation_key were encountered
260
- summary_table_fields_with_count = summary_table_fields + ['count']
274
+ summary_table_paths_with_count = summary_table_paths + ['count']
275
+ summary_table_headers_with_count = summary_table_headers + ['count']
261
276
  text += "Aggregation resulted in the following data for summary_table_fields ==> {0}:\n\n".format(
262
- summary_table_fields_with_count
277
+ summary_table_paths_with_count
263
278
  )
264
279
 
265
280
  # Prepare match_aggregation used in both table types
@@ -267,7 +282,7 @@ class Alerter(object):
267
282
 
268
283
  # Maintain an aggregate count for each unique key encountered in the aggregation period
269
284
  for match in matches:
270
- key_tuple = tuple([str(lookup_es_key(match, key)) for key in summary_table_fields])
285
+ key_tuple = tuple([str(lookup_es_key(match, key)) for key in summary_table_paths])
271
286
  if key_tuple not in match_aggregation:
272
287
  match_aggregation[key_tuple] = 1
273
288
  else:
@@ -281,9 +296,9 @@ class Alerter(object):
281
296
  # Type dependent table style
282
297
  if summary_table_type == 'ascii':
283
298
  text_table = Texttable(max_width=self.get_aggregation_summary_text__maximum_width())
284
- text_table.header(summary_table_fields_with_count)
299
+ text_table.header(summary_table_headers_with_count)
285
300
  # Format all fields as 'text' to avoid long numbers being shown as scientific notation
286
- text_table.set_cols_dtype(['t' for i in summary_table_fields_with_count])
301
+ text_table.set_cols_dtype(['t' for i in summary_table_headers_with_count])
287
302
 
288
303
  for keys, count in match_aggregation.items():
289
304
  text_table.add_row([key for key in keys] + [count])
@@ -292,9 +307,9 @@ class Alerter(object):
292
307
  elif summary_table_type == 'markdown':
293
308
  # Adapted from https://github.com/codazoda/tomark/blob/master/tomark/tomark.py
294
309
  # Create table header
295
- text += '| ' + ' | '.join(map(str, summary_table_fields_with_count)) + ' |\n'
310
+ text += '| ' + ' | '.join(map(str, summary_table_headers_with_count)) + ' |\n'
296
311
  # Create header separator
297
- text += '|-----' * len(summary_table_fields_with_count) + '|\n'
312
+ text += '|-----' * len(summary_table_headers_with_count) + '|\n'
298
313
  # Create table row
299
314
  for keys, count in match_aggregation.items():
300
315
  markdown_row = ""
@@ -302,12 +317,12 @@ class Alerter(object):
302
317
  markdown_row += '| ' + str(key) + ' '
303
318
  text += markdown_row + '| ' + str(count) + ' |\n'
304
319
  text += '\n'
305
-
320
+
306
321
  elif summary_table_type == 'html':
307
322
  # Portions of the following block of HTML formatting code was taken from
308
323
  # an abandoned PR (https://github.com/jertel/elastalert2/pull/1227).
309
324
  text_table = PrettyTable()
310
- text_table.field_names = summary_table_fields_with_count
325
+ text_table.field_names = summary_table_headers_with_count
311
326
  text_table.set_style(TableStyle.MSWORD_FRIENDLY)
312
327
  text_table.border = True
313
328
  text_table.header = True
@@ -337,9 +337,10 @@ class ElastAlerter(object):
337
337
 
338
338
  # Convert the timestamp to a datetime
339
339
  ts = lookup_es_key(hit['_source'], rule['timestamp_field'])
340
- if not ts and not rule["_source_enabled"]:
340
+ if ts is None:
341
341
  raise EAException(
342
- "Error: No timestamp was found for hit. '_source_enabled' is set to false, check your mappings for stored fields"
342
+ f"Error: The configured timestamp_field '{rule['timestamp_field']}' was not found in the query results. "
343
+ f"Available fields: {list(hit['_source'].keys())}"
343
344
  )
344
345
 
345
346
  set_es_key(hit['_source'], rule['timestamp_field'], rule['ts_to_dt'](ts))
@@ -1606,7 +1607,7 @@ class ElastAlerter(object):
1606
1607
 
1607
1608
  # Delete it from the index
1608
1609
  try:
1609
- self.writeback_es.delete(index=self.writeback_index, id=_id)
1610
+ self.writeback_es.delete_by_query(index=self.writeback_index, body={'query': {'term': {'_id': _id}}})
1610
1611
  except ElasticsearchException: # TODO: Give this a more relevant exception, try:except: is evil.
1611
1612
  self.handle_error("Failed to delete alert %s at %s" % (_id, alert_time))
1612
1613
 
@@ -1640,7 +1641,7 @@ class ElastAlerter(object):
1640
1641
  size=self.max_aggregation)
1641
1642
  for match in res['hits']['hits']:
1642
1643
  matches.append(match['_source'])
1643
- self.writeback_es.delete(index=self.writeback_index, id=match['_id'])
1644
+ self.writeback_es.delete_by_query(index=self.writeback_index, body={'query': {'term': {'_id': match['_id']}}})
1644
1645
  except (KeyError, ElasticsearchException) as e:
1645
1646
  self.handle_error("Error fetching aggregated matches: %s" % (e), {'id': _id})
1646
1647
  return matches
@@ -0,0 +1,76 @@
1
+ import hashlib
2
+ import json
3
+
4
+
5
+ def format_request(body):
6
+ query = body.get('query')
7
+ if not query:
8
+ return None
9
+
10
+ query_bool = query.get('bool')
11
+ if not query_bool:
12
+ return None
13
+
14
+ filt = query_bool.get('filter')
15
+ if not filt:
16
+ return None
17
+
18
+ filter_bool = filt.get('bool')
19
+ if not filter_bool:
20
+ return None
21
+
22
+ filter_bool_must = filter_bool.get('must')
23
+ if not filter_bool_must:
24
+ return None
25
+
26
+ other_filters = []
27
+ esql = None
28
+ for f in filter_bool_must:
29
+ if f.get('esql'):
30
+ esql = f['esql']
31
+ else:
32
+ other_filters.append(f)
33
+
34
+ if esql:
35
+ new_body = {'filter': {'bool': {'must': other_filters}}, 'query': esql}
36
+ return new_body
37
+
38
+ return None
39
+
40
+
41
+ def format_results(results, default_index=None):
42
+ columns = results.pop('columns', None)
43
+ values = results.pop('values', None)
44
+ if columns is None or values is None:
45
+ return results
46
+
47
+ col_names = [col['name'] for col in columns]
48
+ hits = []
49
+ for val_row in values:
50
+ doc = dict(zip(col_names, val_row))
51
+
52
+ # Pull metadata fields if present
53
+ doc_id = doc.get('_id', None)
54
+ doc_index = doc.get('_index', default_index)
55
+
56
+ if not doc_id:
57
+ # Generate deterministic hash of the document to use as unique ID
58
+ doc_hash = hashlib.sha256(json.dumps(doc, sort_keys=True, default=str).encode('utf-8')).hexdigest()
59
+ doc_id = doc_hash
60
+
61
+ hit = {
62
+ '_index': doc_index,
63
+ '_id': doc_id,
64
+ '_source': doc
65
+ }
66
+ hits.append(hit)
67
+
68
+ results['hits'] = {
69
+ 'total': {
70
+ 'value': len(values),
71
+ 'relation': 'eq'
72
+ },
73
+ 'hits': hits
74
+ }
75
+ results['esql'] = True
76
+ return results
@@ -74,6 +74,36 @@ def kibana7_disover_global_state(from_time, to_time):
74
74
  } )
75
75
 
76
76
 
77
+ def _normalize_query_value(value):
78
+ """Collapse empty/single-element query_key values before filter emission.
79
+
80
+ Elasticsearch returns multi-valued fields as arrays even when they hold a
81
+ single item, and rules using such fields as ``query_key`` otherwise render
82
+ Kibana filters with ``params.query: ['']`` — a phrase filter that matches
83
+ no documents, producing an empty Discover view for the reviewer.
84
+
85
+ Returns:
86
+ - ``None`` when the value is absent, an empty string, or a list that
87
+ collapses to nothing after dropping ``None``/empty-string entries;
88
+ callers emit the existing ``negate exists`` filter for this case.
89
+ - The sole surviving element when the input is a single-item list.
90
+ - The original value otherwise (scalars pass through; multi-value lists
91
+ are returned unchanged so callers may extend to a ``phrases`` filter).
92
+ """
93
+ if value is None:
94
+ return None
95
+ if isinstance(value, str):
96
+ return None if value == '' else value
97
+ if isinstance(value, list):
98
+ cleaned = [v for v in value if v not in (None, '')]
99
+ if not cleaned:
100
+ return None
101
+ if len(cleaned) == 1:
102
+ return cleaned[0]
103
+ return cleaned
104
+ return value
105
+
106
+
77
107
  def kibana_discover_app_state(index, columns, filters, query_keys, match):
78
108
  app_filters = []
79
109
 
@@ -105,7 +135,7 @@ def kibana_discover_app_state(index, columns, filters, query_keys, match):
105
135
  } )
106
136
 
107
137
  for query_key in query_keys:
108
- query_value = lookup_es_key(match, query_key)
138
+ query_value = _normalize_query_value(lookup_es_key(match, query_key))
109
139
 
110
140
  if query_value is None:
111
141
  app_filters.append( {
@@ -53,7 +53,6 @@ from elastalert.alerters.slack import SlackAlerter
53
53
  from elastalert.alerters.smseagle import SMSEagleAlerter
54
54
  from elastalert.alerters.sns import SnsAlerter
55
55
  from elastalert.alerters.sqs import SqsAlerter
56
- from elastalert.alerters.teams import MsTeamsAlerter
57
56
  from elastalert.alerters.powerautomate import MsPowerAutomateAlerter
58
57
  from elastalert.alerters.yzj import YzjAlerter
59
58
  from elastalert.alerters.zabbix import ZabbixAlerter
@@ -117,7 +116,6 @@ class RulesLoader(object):
117
116
  'command': elastalert.alerters.command.CommandAlerter,
118
117
  'sns': SnsAlerter,
119
118
  'sqs': SqsAlerter,
120
- 'ms_teams': MsTeamsAlerter,
121
119
  'ms_power_automate': MsPowerAutomateAlerter,
122
120
  'slack': SlackAlerter,
123
121
  'mattermost': MattermostAlerter,
@@ -51,13 +51,6 @@ definitions:
51
51
  type: array
52
52
  items: *slackField
53
53
 
54
- msTeamsFact: &msTeamsFact
55
- type: object
56
- additionalProperties: false
57
- properties:
58
- name: {type: string}
59
- value: {type: string}
60
-
61
54
  msPowerAutomateFact: &msPowerAutomateFact
62
55
  type: object
63
56
  additionalProperties: false
@@ -65,10 +58,6 @@ definitions:
65
58
  name: {type: string}
66
59
  value: {type: string}
67
60
 
68
- arrayOfMsTeamsFacts: &arrayOfMsTeamsFacts
69
- type: array
70
- items: *msTeamsFact
71
-
72
61
  arrayOfMsPowerAutomateFacts: &arrayOfMsPowerAutomateFacts
73
62
  type: array
74
63
  items: *msPowerAutomateFact
@@ -319,7 +308,17 @@ properties:
319
308
  notify_all_errors: {type: boolean}
320
309
 
321
310
  ### summary table
322
- summary_table_fields: {type: array, items: {type: string}}
311
+ summary_table_fields:
312
+ type: array
313
+ items:
314
+ oneOf:
315
+ - type: string
316
+ - type: object
317
+ additionalProperties: false
318
+ required: [path]
319
+ properties:
320
+ path: {type: string, minLength: 1}
321
+ header: {type: string}
323
322
  summary_table_type: {type: string, enum: ['ascii', 'html', 'markdown']}
324
323
  summary_table_max_rows: {type: integer, minimum: 0}
325
324
  summary_prefix: {type: string}
@@ -468,6 +467,7 @@ properties:
468
467
  ### Datadog
469
468
  datadog_api_key: {type: string}
470
469
  datadog_app_key: {type: string}
470
+ datadog_api_url: {type: string}
471
471
 
472
472
  ### Dingtalk
473
473
  dingtalk_access_token: {type: string}
@@ -710,22 +710,6 @@ properties:
710
710
  mattermost_opensearch_discover_color: {type: string}
711
711
  mattermost_opensearch_discover_title: {type: string}
712
712
 
713
-
714
- ### Microsoft Teams
715
- ms_teams_webhook_url: *arrayOfString
716
- ms_teams_alert_summary: {type: string}
717
- ms_teams_theme_color: {type: string}
718
- ms_teams_proxy: {type: string}
719
- ms_teams_alert_fixed_width: {type: boolean}
720
- ms_teams_alert_facts: *arrayOfMsTeamsFacts
721
- ms_teams_attach_kibana_discover_url: {type: boolean}
722
- ms_teams_kibana_discover_title: {type: string}
723
- ms_teams_ca_certs: {type: [boolean, string]}
724
- ms_teams_ignore_ssl_errors: {type: boolean}
725
- ms_teams_attach_opensearch_discover_url: {type: boolean}
726
- ms_teams_opensearch_discover_title: {type: string}
727
-
728
-
729
713
  ### Microsoft Power Automate
730
714
  ms_power_automate_webhook_url: *arrayOfString
731
715
  ms_power_automate_webhook_url_from_field: { type: string }
@@ -211,7 +211,7 @@ class MockElastAlerter(object):
211
211
 
212
212
  terms = res['hits']['hits'][0]['_source']
213
213
 
214
- is_eql = res.get('eql') == True
214
+ is_eql = res.get('eql') == True or res.get('esql') == True
215
215
 
216
216
  if not is_eql:
217
217
  # Get a count of all docs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elastalert2
3
- Version: 2.29.0
3
+ Version: 2.31.0
4
4
  Summary: Automated rule-based alerting for Elasticsearch
5
5
  Home-page: https://github.com/jertel/elastalert2
6
6
  License: Apache 2.0
@@ -15,32 +15,32 @@ Classifier: Operating System :: OS Independent
15
15
  Requires-Python: >=3.12
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
- Requires-Dist: apscheduler<4.0,>=3.11.0
18
+ Requires-Dist: apscheduler<4.0,>=3.11.2
19
19
  Requires-Dist: aws-requests-auth>=0.4.3
20
- Requires-Dist: boto3>=1.40.59
20
+ Requires-Dist: boto3>=1.42.89
21
21
  Requires-Dist: cffi>=2.0.0
22
- Requires-Dist: croniter>=6.0.0
22
+ Requires-Dist: croniter>=6.2.2
23
23
  Requires-Dist: elasticsearch==7.10.1
24
24
  Requires-Dist: envparse>=0.2.0
25
25
  Requires-Dist: exotel==0.1.5
26
26
  Requires-Dist: Jinja2>=3.1.6
27
27
  Requires-Dist: jira>=3.10.5
28
- Requires-Dist: jsonpointer>=3.0.0
29
- Requires-Dist: jsonschema>=4.25.1
30
- Requires-Dist: prettytable>=3.16.0
28
+ Requires-Dist: jsonpointer>=3.1.1
29
+ Requires-Dist: jsonschema>=4.26.0
30
+ Requires-Dist: prettytable>=3.17.0
31
31
  Requires-Dist: prison>=0.2.1
32
- Requires-Dist: prometheus_client>=0.23.1
32
+ Requires-Dist: prometheus-client>=0.25.0
33
33
  Requires-Dist: python-dateutil>=2.9.0.post0
34
- Requires-Dist: pytz>=2026.1
34
+ Requires-Dist: pytz>=2026.1.post1
35
35
  Requires-Dist: PyYAML>=6.0.3
36
- Requires-Dist: zabbix-utils>=2.0.3
37
- Requires-Dist: requests>=2.31.0
36
+ Requires-Dist: zabbix-utils>=2.0.4
37
+ Requires-Dist: requests>=2.33.1
38
38
  Requires-Dist: sortedcontainers>=2.4.0
39
39
  Requires-Dist: statsd-tags==3.2.1.post1
40
40
  Requires-Dist: stomp.py>=8.2.0
41
- Requires-Dist: tencentcloud-sdk-python>=3.0.1479
41
+ Requires-Dist: tencentcloud-sdk-python>=3.1.79
42
42
  Requires-Dist: texttable>=1.7.0
43
- Requires-Dist: twilio>=9.8.4
43
+ Requires-Dist: twilio>=9.10.5
44
44
  Dynamic: classifier
45
45
  Dynamic: description
46
46
  Dynamic: description-content-type
@@ -10,6 +10,7 @@ elastalert/create_index.py
10
10
  elastalert/elastalert.py
11
11
  elastalert/enhancements.py
12
12
  elastalert/eql.py
13
+ elastalert/esql.py
13
14
  elastalert/kibana_discover.py
14
15
  elastalert/kibana_external_url_formatter.py
15
16
  elastalert/loaders.py
@@ -57,7 +58,6 @@ elastalert/alerters/smseagle.py
57
58
  elastalert/alerters/sns.py
58
59
  elastalert/alerters/sqs.py
59
60
  elastalert/alerters/stomp.py
60
- elastalert/alerters/teams.py
61
61
  elastalert/alerters/telegram.py
62
62
  elastalert/alerters/tencentsms.py
63
63
  elastalert/alerters/thehive.py
@@ -119,7 +119,6 @@ tests/alerters/smseagle_test.py
119
119
  tests/alerters/sns_test.py
120
120
  tests/alerters/sqs_test.py
121
121
  tests/alerters/stomp_test.py
122
- tests/alerters/teams_test.py
123
122
  tests/alerters/telegram_test.py
124
123
  tests/alerters/tencentsms_test.py
125
124
  tests/alerters/thehive_test.py
@@ -1,26 +1,26 @@
1
- apscheduler<4.0,>=3.11.0
1
+ apscheduler<4.0,>=3.11.2
2
2
  aws-requests-auth>=0.4.3
3
- boto3>=1.40.59
3
+ boto3>=1.42.89
4
4
  cffi>=2.0.0
5
- croniter>=6.0.0
5
+ croniter>=6.2.2
6
6
  elasticsearch==7.10.1
7
7
  envparse>=0.2.0
8
8
  exotel==0.1.5
9
9
  Jinja2>=3.1.6
10
10
  jira>=3.10.5
11
- jsonpointer>=3.0.0
12
- jsonschema>=4.25.1
13
- prettytable>=3.16.0
11
+ jsonpointer>=3.1.1
12
+ jsonschema>=4.26.0
13
+ prettytable>=3.17.0
14
14
  prison>=0.2.1
15
- prometheus_client>=0.23.1
15
+ prometheus-client>=0.25.0
16
16
  python-dateutil>=2.9.0.post0
17
- pytz>=2026.1
17
+ pytz>=2026.1.post1
18
18
  PyYAML>=6.0.3
19
- zabbix-utils>=2.0.3
20
- requests>=2.31.0
19
+ zabbix-utils>=2.0.4
20
+ requests>=2.33.1
21
21
  sortedcontainers>=2.4.0
22
22
  statsd-tags==3.2.1.post1
23
23
  stomp.py>=8.2.0
24
- tencentcloud-sdk-python>=3.0.1479
24
+ tencentcloud-sdk-python>=3.1.79
25
25
  texttable>=1.7.0
26
- twilio>=9.8.4
26
+ twilio>=9.10.5
@@ -8,7 +8,7 @@ from setuptools import setup
8
8
  base_dir = os.path.dirname(__file__)
9
9
  setup(
10
10
  name='elastalert2',
11
- version='2.29.0',
11
+ version='2.31.0',
12
12
  description='Automated rule-based alerting for Elasticsearch',
13
13
  long_description=open('README.md').read(),
14
14
  long_description_content_type="text/markdown",
@@ -35,31 +35,31 @@ setup(
35
35
  package_data={'elastalert': ['schema.yaml', 'es_mappings/**/*.json']},
36
36
  python_requires='>=3.12',
37
37
  install_requires=[
38
- 'apscheduler>=3.11.0,<4.0',
38
+ 'apscheduler>=3.11.2,<4.0',
39
39
  'aws-requests-auth>=0.4.3',
40
- 'boto3>=1.40.59',
40
+ 'boto3>=1.42.89',
41
41
  'cffi>=2.0.0',
42
- 'croniter>=6.0.0',
42
+ 'croniter>=6.2.2',
43
43
  'elasticsearch==7.10.1',
44
44
  'envparse>=0.2.0',
45
45
  'exotel==0.1.5',
46
46
  'Jinja2>=3.1.6',
47
47
  'jira>=3.10.5',
48
- 'jsonpointer>=3.0.0',
49
- 'jsonschema>=4.25.1',
50
- 'prettytable>=3.16.0',
48
+ 'jsonpointer>=3.1.1',
49
+ 'jsonschema>=4.26.0',
50
+ 'prettytable>=3.17.0',
51
51
  'prison>=0.2.1',
52
- 'prometheus_client>=0.23.1',
52
+ 'prometheus-client>=0.25.0',
53
53
  'python-dateutil>=2.9.0.post0',
54
- 'pytz>=2026.1',
54
+ 'pytz>=2026.1.post1',
55
55
  'PyYAML>=6.0.3',
56
- 'zabbix-utils>=2.0.3',
57
- 'requests>=2.31.0',
56
+ 'zabbix-utils>=2.0.4',
57
+ 'requests>=2.33.1',
58
58
  'sortedcontainers>=2.4.0',
59
59
  'statsd-tags==3.2.1.post1',
60
60
  'stomp.py>=8.2.0',
61
- 'tencentcloud-sdk-python>=3.0.1479',
61
+ 'tencentcloud-sdk-python>=3.1.79',
62
62
  'texttable>=1.7.0',
63
- 'twilio>=9.8.4',
63
+ 'twilio>=9.10.5',
64
64
  ]
65
65
  )