elastalert2 2.26.0__tar.gz → 2.28.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 (131) hide show
  1. {elastalert2-2.26.0/elastalert2.egg-info → elastalert2-2.28.0}/PKG-INFO +18 -16
  2. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/chatwork.py +11 -4
  3. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/jira.py +15 -3
  4. elastalert2-2.28.0/elastalert/alerters/line.py +61 -0
  5. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/zabbix.py +6 -6
  6. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/elastalert.py +10 -3
  7. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/loaders.py +29 -2
  8. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/opensearch_external_url_formatter.py +1 -10
  9. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/prometheus_wrapper.py +5 -2
  10. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/ruletypes.py +24 -19
  11. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/schema.yaml +7 -0
  12. {elastalert2-2.26.0 → elastalert2-2.28.0/elastalert2.egg-info}/PKG-INFO +18 -16
  13. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert2.egg-info/SOURCES.txt +2 -0
  14. elastalert2-2.28.0/elastalert2.egg-info/requires.txt +25 -0
  15. {elastalert2-2.26.0 → elastalert2-2.28.0}/setup.py +18 -16
  16. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/chatwork_test.py +18 -16
  17. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/jira_test.py +172 -6
  18. elastalert2-2.28.0/tests/alerters/line_test.py +185 -0
  19. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/zabbix_test.py +6 -4
  20. elastalert2-2.26.0/elastalert2.egg-info/requires.txt +0 -25
  21. {elastalert2-2.26.0 → elastalert2-2.28.0}/LICENSE +0 -0
  22. {elastalert2-2.26.0 → elastalert2-2.28.0}/README.md +0 -0
  23. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/__init__.py +0 -0
  24. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/__init__.py +0 -0
  25. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/alerta.py +0 -0
  26. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/alertmanager.py +0 -0
  27. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/command.py +0 -0
  28. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/datadog.py +0 -0
  29. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/debug.py +0 -0
  30. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/dingtalk.py +0 -0
  31. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/discord.py +0 -0
  32. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/email.py +0 -0
  33. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/exotel.py +0 -0
  34. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/flashduty.py +0 -0
  35. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/gelf.py +0 -0
  36. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/gitter.py +0 -0
  37. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/googlechat.py +0 -0
  38. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/httppost.py +0 -0
  39. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/httppost2.py +0 -0
  40. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/indexer.py +0 -0
  41. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/iris.py +0 -0
  42. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/lark.py +0 -0
  43. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/matrixhookshot.py +0 -0
  44. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/mattermost.py +0 -0
  45. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/opsgenie.py +0 -0
  46. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/pagerduty.py +0 -0
  47. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/pagertree.py +0 -0
  48. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/powerautomate.py +0 -0
  49. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/rocketchat.py +0 -0
  50. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/servicenow.py +0 -0
  51. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/ses.py +0 -0
  52. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/slack.py +0 -0
  53. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/smseagle.py +0 -0
  54. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/sns.py +0 -0
  55. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/stomp.py +0 -0
  56. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/teams.py +0 -0
  57. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/telegram.py +0 -0
  58. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/tencentsms.py +0 -0
  59. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/thehive.py +0 -0
  60. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/twilio.py +0 -0
  61. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/victorops.py +0 -0
  62. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/webex_webhook.py +0 -0
  63. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/workwechat.py +0 -0
  64. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerters/yzj.py +0 -0
  65. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/alerts.py +0 -0
  66. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/auth.py +0 -0
  67. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/config.py +0 -0
  68. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/create_index.py +0 -0
  69. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/enhancements.py +0 -0
  70. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/eql.py +0 -0
  71. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/es_mappings/7/elastalert.json +0 -0
  72. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/es_mappings/7/elastalert_error.json +0 -0
  73. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/es_mappings/7/elastalert_status.json +0 -0
  74. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/es_mappings/7/past_elastalert.json +0 -0
  75. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/es_mappings/7/silence.json +0 -0
  76. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/es_mappings/8/elastalert.json +0 -0
  77. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/es_mappings/8/elastalert_error.json +0 -0
  78. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/es_mappings/8/elastalert_status.json +0 -0
  79. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/es_mappings/8/past_elastalert.json +0 -0
  80. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/es_mappings/8/silence.json +0 -0
  81. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/kibana_discover.py +0 -0
  82. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/kibana_external_url_formatter.py +0 -0
  83. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/opensearch_discover.py +0 -0
  84. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/test_rule.py +0 -0
  85. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/util.py +0 -0
  86. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert/yaml.py +0 -0
  87. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert2.egg-info/dependency_links.txt +0 -0
  88. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert2.egg-info/entry_points.txt +0 -0
  89. {elastalert2-2.26.0 → elastalert2-2.28.0}/elastalert2.egg-info/top_level.txt +0 -0
  90. {elastalert2-2.26.0 → elastalert2-2.28.0}/setup.cfg +0 -0
  91. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/__init__.py +0 -0
  92. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/alerta_test.py +0 -0
  93. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/alertmanager_test.py +0 -0
  94. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/command_test.py +0 -0
  95. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/datadog_test.py +0 -0
  96. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/debug_test.py +0 -0
  97. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/dingtalk_test.py +0 -0
  98. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/discord_test.py +0 -0
  99. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/email_test.py +0 -0
  100. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/exotel_test.py +0 -0
  101. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/flashduty_test.py +0 -0
  102. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/gelf_test.py +0 -0
  103. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/gitter_test.py +0 -0
  104. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/googlechat_test.py +0 -0
  105. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/httppost2_test.py +0 -0
  106. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/httppost_test.py +0 -0
  107. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/indexer_test.py +0 -0
  108. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/iris_test.py +0 -0
  109. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/lark_test.py +0 -0
  110. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/matrixhookshot_test.py +0 -0
  111. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/mattermost_test.py +0 -0
  112. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/opsgenie_test.py +0 -0
  113. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/pagerduty_test.py +0 -0
  114. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/pagertree_test.py +0 -0
  115. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/powerautomate_test.py +0 -0
  116. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/rocketchat_test.py +0 -0
  117. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/servicenow_test.py +0 -0
  118. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/ses_test.py +0 -0
  119. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/slack_test.py +0 -0
  120. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/smseagle_test.py +0 -0
  121. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/sns_test.py +0 -0
  122. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/stomp_test.py +0 -0
  123. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/teams_test.py +0 -0
  124. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/telegram_test.py +0 -0
  125. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/tencentsms_test.py +0 -0
  126. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/thehive_test.py +0 -0
  127. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/twilio_test.py +0 -0
  128. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/victorops_test.py +0 -0
  129. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/webex_webhook_test.py +0 -0
  130. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/workwechat_test.py +0 -0
  131. {elastalert2-2.26.0 → elastalert2-2.28.0}/tests/alerters/yzj_test.py +0 -0
@@ -1,43 +1,45 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elastalert2
3
- Version: 2.26.0
3
+ Version: 2.28.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
7
7
  Project-URL: Documentation, https://elastalert2.readthedocs.io
8
8
  Project-URL: Source Code, https://github.com/jertel/elastalert2
9
9
  Project-URL: Discussion Forum, https://github.com/jertel/elastalert2/discussions
10
+ Classifier: Programming Language :: Python :: 3.12
10
11
  Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
11
13
  Classifier: License :: OSI Approved :: Apache Software License
12
14
  Classifier: Operating System :: OS Independent
13
15
  Requires-Python: >=3.12
14
16
  Description-Content-Type: text/markdown
15
17
  License-File: LICENSE
16
- Requires-Dist: apscheduler<4.0,>=3.10.4
18
+ Requires-Dist: apscheduler<4.0,>=3.11.0
17
19
  Requires-Dist: aws-requests-auth>=0.4.3
18
- Requires-Dist: boto3>=1.34.54
19
- Requires-Dist: cffi>=1.16.0
20
- Requires-Dist: croniter>=2.0.2
20
+ Requires-Dist: boto3>=1.40.59
21
+ Requires-Dist: cffi>=2.0.0
22
+ Requires-Dist: croniter>=6.0.0
21
23
  Requires-Dist: elasticsearch==7.10.1
22
24
  Requires-Dist: envparse>=0.2.0
23
25
  Requires-Dist: exotel==0.1.5
24
- Requires-Dist: Jinja2>=3.1.3
25
- Requires-Dist: jira>=3.6.0
26
- Requires-Dist: jsonpointer>=2.4
27
- Requires-Dist: jsonschema>=4.21.1
28
- Requires-Dist: prettytable>=3.10.0
26
+ Requires-Dist: Jinja2>=3.1.6
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
29
31
  Requires-Dist: prison>=0.2.1
30
- Requires-Dist: prometheus_client>=0.20.0
32
+ Requires-Dist: prometheus_client>=0.23.1
31
33
  Requires-Dist: python-dateutil>=2.9.0.post0
32
- Requires-Dist: PyYAML>=6.0.1
33
- Requires-Dist: py-zabbix>=1.1.7
34
+ Requires-Dist: PyYAML>=6.0.3
35
+ Requires-Dist: zabbix-utils>=2.0.3
34
36
  Requires-Dist: requests>=2.31.0
35
37
  Requires-Dist: sortedcontainers>=2.4.0
36
38
  Requires-Dist: statsd-tags==3.2.1.post1
37
- Requires-Dist: stomp.py>=8.1.0
38
- Requires-Dist: tencentcloud-sdk-python>=3.0.1098
39
+ Requires-Dist: stomp.py>=8.2.0
40
+ Requires-Dist: tencentcloud-sdk-python>=3.0.1479
39
41
  Requires-Dist: texttable>=1.7.0
40
- Requires-Dist: twilio>=8.13.0
42
+ Requires-Dist: twilio>=9.8.4
41
43
  Dynamic: classifier
42
44
  Dynamic: description
43
45
  Dynamic: description-content-type
@@ -29,17 +29,24 @@ class ChatworkAlerter(Alerter):
29
29
  body += '\n----------------------------------------\n'
30
30
  if len(body) > 2047:
31
31
  body = body[0:1950] + '\n *message was cropped according to chatwork embed description limits!*'
32
- headers = {'X-ChatWorkToken': self.chatwork_apikey}
32
+ headers = {
33
+ 'accept': 'application/json',
34
+ 'content-type': 'application/x-www-form-urlencoded',
35
+ 'x-chatworktoken': self.chatwork_apikey
36
+ }
33
37
  # set https proxy, if it was provided
34
38
  proxies = {'https': self.chatwork_proxy} if self.chatwork_proxy else None
35
39
  auth = HTTPProxyAuth(self.chatwork_proxy_login, self.chatwork_proxy_pass) if self.chatwork_proxy_login else None
36
- params = {'body': body}
40
+ payload = {
41
+ 'self_unread': 0,
42
+ 'body': body
43
+ }
37
44
 
38
45
  try:
39
- response = requests.post(self.url, params=params, headers=headers, proxies=proxies, auth=auth)
46
+ response = requests.post(self.url, data=payload, headers=headers, proxies=proxies, auth=auth)
40
47
  response.raise_for_status()
41
48
  except RequestException as e:
42
- raise EAException("Error posting to Chattwork: %s. Details: %s" % (e, "" if e.response is None else e.response.text))
49
+ raise EAException("Error posting to Chatwork: %s. Details: %s" % (e, "" if e.response is None else e.response.text))
43
50
 
44
51
  elastalert_logger.info(
45
52
  "Alert sent to Chatwork room %s" % self.chatwork_room_id)
@@ -262,14 +262,26 @@ class JiraAlerter(Alerter):
262
262
  if self.bump_not_in_statuses:
263
263
  jql = '%s and status not in (%s)' % (jql, ','.join(["\"%s\"" % status if ' ' in status else status
264
264
  for status in self.bump_not_in_statuses]))
265
+
266
+ # Force use of enhanced_search_issues (new API)
267
+ # This method should be available in updated jira library versions
268
+ if not hasattr(self.client, 'enhanced_search_issues'):
269
+ raise EAException("enhanced_search_issues method not available. Please update your jira library to a version that supports the new Jira API endpoints.")
270
+
265
271
  try:
266
- issues = self.client.search_issues(jql)
272
+ issues = self.client.enhanced_search_issues(jql)
273
+
274
+ # Ensure issues is a list-like object before checking length
275
+ if issues and len(issues):
276
+ return issues[0]
267
277
  except JIRAError as e:
268
278
  elastalert_logger.exception("Error while searching for Jira ticket using jql '%s': %s" % (jql, e))
269
279
  return None
280
+ except Exception as e:
281
+ elastalert_logger.exception("Unexpected error while searching for Jira ticket: %s" % e)
282
+ return None
270
283
 
271
- if len(issues):
272
- return issues[0]
284
+ return None
273
285
 
274
286
  def comment_on_ticket(self, ticket, match):
275
287
  text = str(JiraFormattedMatchString(self.rule, match))
@@ -0,0 +1,61 @@
1
+ import requests
2
+ from requests import RequestException
3
+ import json
4
+
5
+ from elastalert.alerts import Alerter, BasicMatchString
6
+ from elastalert.util import EAException, elastalert_logger
7
+
8
+
9
+ class LineMessageAPIAlerter(Alerter):
10
+ """Creates a LINE Messaging API for each alert"""
11
+ required_option = frozenset(['line_channel_access_token', 'line_to'])
12
+
13
+ def __init__(self, rule):
14
+ super(LineMessageAPIAlerter, self).__init__(rule)
15
+
16
+ self.line_channel_access_token = self.rule.get('line_channel_access_token')
17
+ self.line_to = self.rule.get('line_to')
18
+
19
+ def alert(self, matches):
20
+ body = ''
21
+ for match in matches:
22
+ body += str(BasicMatchString(self.rule, match))
23
+ if len(matches) > 1:
24
+ body += '\n----------------------------------------\n'
25
+
26
+ if len(body) > 4800:
27
+ body = body[:4700] + '\n\n(message cropped due to LINE limit)'
28
+
29
+ headers = {
30
+ 'Content-Type': 'application/json',
31
+ 'Authorization': 'Bearer {}'.format(self.line_channel_access_token)
32
+ }
33
+
34
+ payload = {
35
+ 'to': self.line_to,
36
+ 'messages': [
37
+ {
38
+ 'type': 'text',
39
+ 'text': body
40
+ }
41
+ ]
42
+ }
43
+
44
+ try:
45
+ response = requests.post(
46
+ 'https://api.line.me/v2/bot/message/push',
47
+ headers=headers,
48
+ data=json.dumps(payload)
49
+ )
50
+ response.raise_for_status()
51
+ except RequestException as e:
52
+ raise EAException('Error posting to LINE Messaging API: %s' % e)
53
+
54
+ elastalert_logger.info('Alert sent to LINE Messaging API')
55
+
56
+ def get_info(self):
57
+ return {
58
+ 'type': 'line',
59
+ 'line_to': self.line_to,
60
+ 'line_channel_access_token': self.line_channel_access_token
61
+ }
@@ -1,6 +1,6 @@
1
1
  from datetime import datetime
2
2
 
3
- from pyzabbix import ZabbixSender, ZabbixMetric, ZabbixAPI
3
+ from zabbix_utils import Sender, ItemValue, ZabbixAPI
4
4
 
5
5
  from elastalert.alerts import Alerter
6
6
  from elastalert.util import elastalert_logger, lookup_es_key, EAException
@@ -23,20 +23,20 @@ class ZabbixClient(ZabbixAPI):
23
23
  password=password)
24
24
 
25
25
  def send_metric(self, hostname, key, data):
26
- zm = ZabbixMetric(hostname, key, data)
26
+ zm = ItemValue(hostname, key, data)
27
27
  if self.send_aggregated_metrics:
28
28
  self.aggregated_metrics.append(zm)
29
29
  if len(self.aggregated_metrics) > self.metrics_chunk_size:
30
30
  elastalert_logger.info("Sending: %s metrics" % (len(self.aggregated_metrics)))
31
31
  try:
32
- ZabbixSender(zabbix_server=self.sender_host, zabbix_port=self.sender_port) \
32
+ Sender(server=self.sender_host, port=self.sender_port) \
33
33
  .send(self.aggregated_metrics)
34
34
  self.aggregated_metrics = []
35
35
  except Exception as e:
36
36
  elastalert_logger.exception(e)
37
37
  else:
38
38
  try:
39
- ZabbixSender(zabbix_server=self.sender_host, zabbix_port=self.sender_port).send([zm])
39
+ Sender(server=self.sender_host, port=self.sender_port).send([zm])
40
40
  except Exception as e:
41
41
  elastalert_logger.exception(e)
42
42
 
@@ -81,10 +81,10 @@ class ZabbixAlerter(Alerter):
81
81
  zbx_host = lookup_es_key(match, self.rule["zbx_host"])
82
82
  else:
83
83
  zbx_host = self.zbx_host
84
- zm.append(ZabbixMetric(host=zbx_host, key=self.zbx_key, value='1', clock=ts_epoch))
84
+ zm.append(ItemValue(host=zbx_host, key=self.zbx_key, value='1', clock=ts_epoch))
85
85
 
86
86
  try:
87
- response = ZabbixSender(zabbix_server=self.zbx_sender_host, zabbix_port=self.zbx_sender_port).send(zm)
87
+ response = Sender(server=self.zbx_sender_host, port=self.zbx_sender_port).send(zm)
88
88
  if response.failed:
89
89
  if self.zbx_host_from_field and not zbx_host:
90
90
  elastalert_logger.warning("Missing term '%s' or host's item '%s', alert will be discarded"
@@ -926,7 +926,14 @@ class ElastAlerter(object):
926
926
  if query_key_value is not None:
927
927
  silence_cache_key += '.' + query_key_value
928
928
 
929
- if self.is_silenced(rule['name'] + "._silence") or self.is_silenced(silence_cache_key):
929
+ # Check cache first to reduce unnecessary ES queries
930
+ # Effectively, this checks query key cache first. Then checks rule cache. Then checks the rule via ES.
931
+ # Followed by a second (duplicated) query key cache check, and then finally a query key check via ES.
932
+ # It could be further improved to eliminate the second query key cache check but this implementation
933
+ # favors both cache checks up front, before any ES hit, which is an overall performance improvement
934
+ # compared to the original logic, which required a rule check via ES before checking the query key cache.
935
+ if self.is_silenced(silence_cache_key, check_cache_only=True) or \
936
+ self.is_silenced(rule['name'] + "._silence") or self.is_silenced(silence_cache_key):
930
937
  elastalert_logger.info('Ignoring match for silenced rule %s' % (silence_cache_key,))
931
938
  continue
932
939
 
@@ -1785,13 +1792,13 @@ class ElastAlerter(object):
1785
1792
  self.silence_cache[silence_cache_key] = (timestamp, exponent)
1786
1793
  return self.writeback('silence', body)
1787
1794
 
1788
- def is_silenced(self, rule_name):
1795
+ def is_silenced(self, rule_name, check_cache_only=False):
1789
1796
  """ Checks if rule_name is currently silenced. Returns false on exception. """
1790
1797
  if rule_name in self.silence_cache:
1791
1798
  if ts_now() < self.silence_cache[rule_name][0]:
1792
1799
  return True
1793
1800
 
1794
- if self.debug:
1801
+ if self.debug or check_cache_only:
1795
1802
  return False
1796
1803
  query = {'term': {'rule_name': rule_name}}
1797
1804
  sort = {'sort': {'until': {'order': 'desc'}}}
@@ -11,7 +11,6 @@ import yaml.scanner
11
11
  from elastalert.alerters.flashduty import FlashdutyAlerter
12
12
  from jinja2 import Environment
13
13
  from jinja2 import FileSystemLoader
14
- from jinja2 import Template
15
14
 
16
15
  import elastalert.alerters.alerta
17
16
  import elastalert.alerters.chatwork
@@ -28,6 +27,7 @@ import elastalert.alerters.httppost
28
27
  import elastalert.alerters.httppost2
29
28
  import elastalert.alerters.iris
30
29
  import elastalert.alerters.lark
30
+ import elastalert.alerters.line
31
31
  import elastalert.alerters.pagertree
32
32
  import elastalert.alerters.rocketchat
33
33
  import elastalert.alerters.servicenow
@@ -136,6 +136,7 @@ class RulesLoader(object):
136
136
  'discord': elastalert.alerters.discord.DiscordAlerter,
137
137
  'dingtalk': elastalert.alerters.dingtalk.DingTalkAlerter,
138
138
  'lark': elastalert.alerters.lark.LarkAlerter,
139
+ 'line': elastalert.alerters.line.LineMessageAPIAlerter,
139
140
  'webex_webhook': elastalert.alerters.webex_webhook.WebexWebhookAlerter,
140
141
  'workwechat': elastalert.alerters.workwechat.WorkWechatAlerter,
141
142
  'chatwork': elastalert.alerters.chatwork.ChatworkAlerter,
@@ -161,6 +162,7 @@ class RulesLoader(object):
161
162
  base_config = {}
162
163
 
163
164
  jinja_environment = Environment(loader=FileSystemLoader(""))
165
+ jinja_filters = []
164
166
 
165
167
  def __init__(self, conf):
166
168
  self.rule_schema = load_rule_schema()
@@ -479,13 +481,38 @@ class RulesLoader(object):
479
481
 
480
482
  self.load_jinja_template(rule)
481
483
 
484
+ def load_jinja_filters(self, filters = []):
485
+ """ Load custom Jinja2 filters into the Environment
486
+ :param list filters: List of filter paths to load, format should be path to a module, ie 'jinja2.filters.MyFilterClass'
487
+ """
488
+ for module_path in filters:
489
+
490
+ filter_class = get_module(module_path)
491
+ for method in dir(filter_class):
492
+ if not method.startswith('filter_'):
493
+ continue
494
+ filter_func = getattr(filter_class, method)
495
+ filter_name = method.removeprefix('filter_')
496
+
497
+ # Skip if filter already loaded
498
+ if filter_name in self.jinja_filters:
499
+ continue
500
+
501
+ self.jinja_environment.filters[filter_name] = filter_func
502
+ self.jinja_filters.append(filter_name)
503
+ elastalert_logger.debug('Loaded custom Jinja2 filter: %s.%s', module_path, filter_name)
504
+
482
505
  def load_jinja_template(self, rule):
483
506
  if rule.get('alert_text_type') == 'alert_text_jinja':
484
507
  jinja_template_path = rule.get('jinja_template_path')
508
+
509
+ if rule.get('jinja_filters'):
510
+ self.load_jinja_filters(rule.get('jinja_filters'))
511
+
485
512
  if jinja_template_path:
486
513
  rule["jinja_template"] = self.jinja_environment.get_or_select_template(jinja_template_path)
487
514
  else:
488
- rule["jinja_template"] = Template(str(rule.get('alert_text', '')))
515
+ rule["jinja_template"] = self.jinja_environment.from_string(str(rule.get('alert_text', '')))
489
516
 
490
517
  def load_modules(self, rule, args=None):
491
518
  """ Loads things that could be modules. Enhancements, alerts and rule type. """
@@ -1,13 +1,4 @@
1
- import boto3
2
- import os
3
- from urllib.parse import parse_qsl, urlencode, urljoin, urlparse, urlsplit, urlunsplit
4
-
5
- import requests
6
- from requests import RequestException
7
- from requests.auth import AuthBase, HTTPBasicAuth
8
-
9
- from elastalert.auth import RefeshableAWSRequestsAuth
10
- from elastalert.util import EAException
1
+ from urllib.parse import urljoin
11
2
 
12
3
  class OpensearchExternalUrlFormatter:
13
4
  '''Interface for formatting external Opensearch urls'''
@@ -29,10 +29,12 @@ class PrometheusWrapper:
29
29
 
30
30
  def metrics_run_rule(self, rule, endtime, starttime=None):
31
31
  """ Increment counter every time rule is run """
32
+ result = None
32
33
  try:
33
34
  self.prom_scrapes.labels(rule['name']).inc()
34
35
  finally:
35
- return self.run_rule(rule, endtime, starttime)
36
+ result = self.run_rule(rule, endtime, starttime)
37
+ return result
36
38
 
37
39
  def metrics_writeback(self, doc_type, body, rule=None, match_body=None):
38
40
  """ Update various prometheus metrics accoording to the doc_type """
@@ -53,4 +55,5 @@ class PrometheusWrapper:
53
55
  elif doc_type == 'silence':
54
56
  self.prom_alerts_silenced.labels(body['rule_name']).inc()
55
57
  finally:
56
- return res
58
+ pass
59
+ return res
@@ -1030,8 +1030,22 @@ class BaseAggregationRule(RuleType):
1030
1030
  if total_seconds(self.rules['buffer_time']) % total_seconds(self.rules['bucket_interval_timedelta']) != 0:
1031
1031
  raise EAException("Buffer_time must be evenly divisible by bucket_interval if specified")
1032
1032
 
1033
+ allowed_percent_aggregations = frozenset(['percentiles'])
1034
+
1033
1035
  def generate_aggregation_query(self):
1034
- raise NotImplementedError()
1036
+ if self.rules.get('metric_agg_script'):
1037
+ agg_data = self.rules['metric_agg_script']
1038
+ if isinstance(agg_data, dict):
1039
+ agg_data = agg_data.copy()
1040
+ else:
1041
+ agg_data = {'script': agg_data}
1042
+ query = {self.metric_key: {self.rules['metric_agg_type']: agg_data}}
1043
+ else:
1044
+ query = {self.metric_key: {self.rules['metric_agg_type']: {'field': self.rules['metric_agg_key']}}}
1045
+ if self.rules['metric_agg_type'] in self.allowed_percent_aggregations:
1046
+ if 'percents' not in query[self.metric_key][self.rules['metric_agg_type']]:
1047
+ query[self.metric_key][self.rules['metric_agg_type']]['percents'] = [self.rules['percentile_range']]
1048
+ return query
1035
1049
 
1036
1050
  def add_aggregation_data(self, payload):
1037
1051
  for timestamp, payload_data in payload.items():
@@ -1072,8 +1086,9 @@ class MetricAggregationRule(BaseAggregationRule):
1072
1086
 
1073
1087
  self.metric_key = 'metric_' + self.rules['metric_agg_key'] + '_' + self.rules['metric_agg_type']
1074
1088
 
1075
- if not self.rules['metric_agg_type'] in self.allowed_aggregations.union(self.allowed_percent_aggregations):
1076
- raise EAException("metric_agg_type must be one of %s" % (str(self.allowed_aggregations)))
1089
+ all_allowed_aggregations = self.allowed_aggregations.union(self.allowed_percent_aggregations)
1090
+ if not self.rules['metric_agg_type'] in all_allowed_aggregations:
1091
+ raise EAException("metric_agg_type must be one of %s" % (str(all_allowed_aggregations)))
1077
1092
  if self.rules['metric_agg_type'] in self.allowed_percent_aggregations and self.rules['percentile_range'] is None:
1078
1093
  raise EAException("percentile_range must be specified for percentiles aggregation")
1079
1094
 
@@ -1090,13 +1105,7 @@ class MetricAggregationRule(BaseAggregationRule):
1090
1105
  )
1091
1106
  return message
1092
1107
 
1093
- def generate_aggregation_query(self):
1094
- if self.rules.get('metric_agg_script'):
1095
- return {self.metric_key: {self.rules['metric_agg_type']: self.rules['metric_agg_script']}}
1096
- query = {self.metric_key: {self.rules['metric_agg_type']: {'field': self.rules['metric_agg_key']}}}
1097
- if self.rules['metric_agg_type'] in self.allowed_percent_aggregations:
1098
- query[self.metric_key][self.rules['metric_agg_type']]['percents'] = [self.rules['percentile_range']]
1099
- return query
1108
+
1100
1109
 
1101
1110
  def check_matches(self, timestamp, query_key, aggregation_data):
1102
1111
  if "compound_query_key" in self.rules:
@@ -1137,7 +1146,10 @@ class MetricAggregationRule(BaseAggregationRule):
1137
1146
  if 'interval_aggs' in aggregation_data:
1138
1147
  metric_val_arr = [term[self.metric_key]['value'] for term in aggregation_data['interval_aggs']['buckets']]
1139
1148
  else:
1140
- metric_val_arr = [aggregation_data[self.metric_key]['value']]
1149
+ if self.rules['metric_agg_type'] in self.allowed_percent_aggregations:
1150
+ metric_val_arr = list(aggregation_data[self.metric_key]['values'].values())
1151
+ else:
1152
+ metric_val_arr = [aggregation_data[self.metric_key]['value']]
1141
1153
  for metric_val in metric_val_arr:
1142
1154
  if self.crossed_thresholds(metric_val):
1143
1155
  match_data[self.rules['timestamp_field']] = timestamp
@@ -1186,14 +1198,7 @@ class SpikeMetricAggregationRule(BaseAggregationRule, SpikeRule):
1186
1198
 
1187
1199
  self.rules['aggregation_query_element'] = self.generate_aggregation_query()
1188
1200
 
1189
- def generate_aggregation_query(self):
1190
- """Lifted from MetricAggregationRule"""
1191
- if self.rules.get('metric_agg_script'):
1192
- return {self.metric_key: {self.rules['metric_agg_type']: self.rules['metric_agg_script']}}
1193
- query = {self.metric_key: {self.rules['metric_agg_type']: {'field': self.rules['metric_agg_key']}}}
1194
- if self.rules['metric_agg_type'] in self.allowed_percent_aggregations:
1195
- query[self.metric_key][self.rules['metric_agg_type']]['percents'] = [self.rules['percentile_range']]
1196
- return query
1201
+
1197
1202
 
1198
1203
  def add_aggregation_data(self, payload):
1199
1204
  """
@@ -184,6 +184,7 @@ oneOf:
184
184
  threshold_cur: {type: number}
185
185
  min_doc_count: {type: integer}
186
186
  percentile_range: {type: integer}
187
+ metric_agg_script: {type: [string, object]}
187
188
 
188
189
  - title: Flatline
189
190
  required: [threshold, timeframe]
@@ -231,6 +232,7 @@ oneOf:
231
232
  bucket_interval: *timeframe
232
233
  sync_bucket_interval: {type: boolean}
233
234
  metric_format_string: {type: string}
235
+ metric_agg_script: {type: [string, object]}
234
236
 
235
237
  - title: Percentage Match
236
238
  required: [match_bucket_filter]
@@ -307,6 +309,7 @@ properties:
307
309
  use_local_time: {type: boolean}
308
310
  custom_pretty_ts_format: {type: string}
309
311
  match_enhancements: {type: array, items: {type: string}}
312
+ jinja_filters: {type: array, items: {type: string}}
310
313
  query_key: *arrayOfString
311
314
  replace_dots_in_field_names: {type: boolean}
312
315
  scan_entire_timeframe: {type: boolean}
@@ -659,6 +662,10 @@ properties:
659
662
  lark_bot_id: { type: string }
660
663
  lark_msgtype: { type: string, enum: [ 'text' ] }
661
664
 
665
+ ### LINE Messaging API
666
+ line_channel_access_token: {type: string}
667
+ line_to: {type: string}
668
+
662
669
  ### Matrix Hookshot
663
670
  matrixhookshot_ca_certs: {type: [boolean, string]}
664
671
  matrixhookshot_webhook_url: *arrayOfString
@@ -1,43 +1,45 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elastalert2
3
- Version: 2.26.0
3
+ Version: 2.28.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
7
7
  Project-URL: Documentation, https://elastalert2.readthedocs.io
8
8
  Project-URL: Source Code, https://github.com/jertel/elastalert2
9
9
  Project-URL: Discussion Forum, https://github.com/jertel/elastalert2/discussions
10
+ Classifier: Programming Language :: Python :: 3.12
10
11
  Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
11
13
  Classifier: License :: OSI Approved :: Apache Software License
12
14
  Classifier: Operating System :: OS Independent
13
15
  Requires-Python: >=3.12
14
16
  Description-Content-Type: text/markdown
15
17
  License-File: LICENSE
16
- Requires-Dist: apscheduler<4.0,>=3.10.4
18
+ Requires-Dist: apscheduler<4.0,>=3.11.0
17
19
  Requires-Dist: aws-requests-auth>=0.4.3
18
- Requires-Dist: boto3>=1.34.54
19
- Requires-Dist: cffi>=1.16.0
20
- Requires-Dist: croniter>=2.0.2
20
+ Requires-Dist: boto3>=1.40.59
21
+ Requires-Dist: cffi>=2.0.0
22
+ Requires-Dist: croniter>=6.0.0
21
23
  Requires-Dist: elasticsearch==7.10.1
22
24
  Requires-Dist: envparse>=0.2.0
23
25
  Requires-Dist: exotel==0.1.5
24
- Requires-Dist: Jinja2>=3.1.3
25
- Requires-Dist: jira>=3.6.0
26
- Requires-Dist: jsonpointer>=2.4
27
- Requires-Dist: jsonschema>=4.21.1
28
- Requires-Dist: prettytable>=3.10.0
26
+ Requires-Dist: Jinja2>=3.1.6
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
29
31
  Requires-Dist: prison>=0.2.1
30
- Requires-Dist: prometheus_client>=0.20.0
32
+ Requires-Dist: prometheus_client>=0.23.1
31
33
  Requires-Dist: python-dateutil>=2.9.0.post0
32
- Requires-Dist: PyYAML>=6.0.1
33
- Requires-Dist: py-zabbix>=1.1.7
34
+ Requires-Dist: PyYAML>=6.0.3
35
+ Requires-Dist: zabbix-utils>=2.0.3
34
36
  Requires-Dist: requests>=2.31.0
35
37
  Requires-Dist: sortedcontainers>=2.4.0
36
38
  Requires-Dist: statsd-tags==3.2.1.post1
37
- Requires-Dist: stomp.py>=8.1.0
38
- Requires-Dist: tencentcloud-sdk-python>=3.0.1098
39
+ Requires-Dist: stomp.py>=8.2.0
40
+ Requires-Dist: tencentcloud-sdk-python>=3.0.1479
39
41
  Requires-Dist: texttable>=1.7.0
40
- Requires-Dist: twilio>=8.13.0
42
+ Requires-Dist: twilio>=9.8.4
41
43
  Dynamic: classifier
42
44
  Dynamic: description
43
45
  Dynamic: description-content-type
@@ -42,6 +42,7 @@ elastalert/alerters/indexer.py
42
42
  elastalert/alerters/iris.py
43
43
  elastalert/alerters/jira.py
44
44
  elastalert/alerters/lark.py
45
+ elastalert/alerters/line.py
45
46
  elastalert/alerters/matrixhookshot.py
46
47
  elastalert/alerters/mattermost.py
47
48
  elastalert/alerters/opsgenie.py
@@ -102,6 +103,7 @@ tests/alerters/indexer_test.py
102
103
  tests/alerters/iris_test.py
103
104
  tests/alerters/jira_test.py
104
105
  tests/alerters/lark_test.py
106
+ tests/alerters/line_test.py
105
107
  tests/alerters/matrixhookshot_test.py
106
108
  tests/alerters/mattermost_test.py
107
109
  tests/alerters/opsgenie_test.py
@@ -0,0 +1,25 @@
1
+ apscheduler<4.0,>=3.11.0
2
+ aws-requests-auth>=0.4.3
3
+ boto3>=1.40.59
4
+ cffi>=2.0.0
5
+ croniter>=6.0.0
6
+ elasticsearch==7.10.1
7
+ envparse>=0.2.0
8
+ exotel==0.1.5
9
+ Jinja2>=3.1.6
10
+ jira>=3.10.5
11
+ jsonpointer>=3.0.0
12
+ jsonschema>=4.25.1
13
+ prettytable>=3.16.0
14
+ prison>=0.2.1
15
+ prometheus_client>=0.23.1
16
+ python-dateutil>=2.9.0.post0
17
+ PyYAML>=6.0.3
18
+ zabbix-utils>=2.0.3
19
+ requests>=2.31.0
20
+ sortedcontainers>=2.4.0
21
+ statsd-tags==3.2.1.post1
22
+ stomp.py>=8.2.0
23
+ tencentcloud-sdk-python>=3.0.1479
24
+ texttable>=1.7.0
25
+ twilio>=9.8.4
@@ -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.26.0',
11
+ version='2.28.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",
@@ -21,7 +21,9 @@ setup(
21
21
  "Discussion Forum": "https://github.com/jertel/elastalert2/discussions",
22
22
  },
23
23
  classifiers=[
24
+ 'Programming Language :: Python :: 3.12',
24
25
  'Programming Language :: Python :: 3.13',
26
+ 'Programming Language :: Python :: 3.14',
25
27
  'License :: OSI Approved :: Apache Software License',
26
28
  'Operating System :: OS Independent',
27
29
  ],
@@ -33,30 +35,30 @@ setup(
33
35
  package_data={'elastalert': ['schema.yaml', 'es_mappings/**/*.json']},
34
36
  python_requires='>=3.12',
35
37
  install_requires=[
36
- 'apscheduler>=3.10.4,<4.0',
38
+ 'apscheduler>=3.11.0,<4.0',
37
39
  'aws-requests-auth>=0.4.3',
38
- 'boto3>=1.34.54',
39
- 'cffi>=1.16.0',
40
- 'croniter>=2.0.2',
40
+ 'boto3>=1.40.59',
41
+ 'cffi>=2.0.0',
42
+ 'croniter>=6.0.0',
41
43
  'elasticsearch==7.10.1',
42
44
  'envparse>=0.2.0',
43
45
  'exotel==0.1.5',
44
- 'Jinja2>=3.1.3',
45
- 'jira>=3.6.0',
46
- 'jsonpointer>=2.4',
47
- 'jsonschema>=4.21.1',
48
- 'prettytable>=3.10.0',
46
+ 'Jinja2>=3.1.6',
47
+ 'jira>=3.10.5',
48
+ 'jsonpointer>=3.0.0',
49
+ 'jsonschema>=4.25.1',
50
+ 'prettytable>=3.16.0',
49
51
  'prison>=0.2.1',
50
- 'prometheus_client>=0.20.0',
52
+ 'prometheus_client>=0.23.1',
51
53
  'python-dateutil>=2.9.0.post0',
52
- 'PyYAML>=6.0.1',
53
- 'py-zabbix>=1.1.7',
54
+ 'PyYAML>=6.0.3',
55
+ 'zabbix-utils>=2.0.3',
54
56
  'requests>=2.31.0',
55
57
  'sortedcontainers>=2.4.0',
56
58
  'statsd-tags==3.2.1.post1',
57
- 'stomp.py>=8.1.0',
58
- 'tencentcloud-sdk-python>=3.0.1098',
59
+ 'stomp.py>=8.2.0',
60
+ 'tencentcloud-sdk-python>=3.0.1479',
59
61
  'texttable>=1.7.0',
60
- 'twilio>=8.13.0',
62
+ 'twilio>=9.8.4',
61
63
  ]
62
64
  )