Skip to content

Commit 74ef682

Browse files
committed
Adding ability to map match fields into opsgenie details
1 parent 872826c commit 74ef682

File tree

4 files changed

+300
-1
lines changed

4 files changed

+300
-1
lines changed

docs/source/ruletypes.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1648,6 +1648,15 @@ Optional:
16481648

16491649
``opsgenie_priority``: Set the OpsGenie priority level. Possible values are P1, P2, P3, P4, P5.
16501650

1651+
``opsgenie_details``: Map of custom key/value pairs to include in the alert's details. The value can sourced from either fields in the first match, environment variables, or a constant value.
1652+
1653+
Example usage::
1654+
1655+
opsgenie_details:
1656+
Author: 'Bob Smith' # constant value
1657+
Environment: '$VAR' # environment variable
1658+
Message: { field: message } # field in the first match
1659+
16511660
SNS
16521661
~~~
16531662

elastalert/opsgenie.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
import json
33
import logging
4-
4+
import os.path
55
import requests
66

77
from .alerts import Alerter
@@ -33,6 +33,7 @@ def __init__(self, *args):
3333
self.alias = self.rule.get('opsgenie_alias')
3434
self.opsgenie_proxy = self.rule.get('opsgenie_proxy', None)
3535
self.priority = self.rule.get('opsgenie_priority')
36+
self.opsgenie_details = self.rule.get('opsgenie_details', {})
3637

3738
def _parse_responders(self, responders, responder_args, matches, default_responders):
3839
if responder_args:
@@ -97,6 +98,10 @@ def alert(self, matches):
9798
if self.alias is not None:
9899
post['alias'] = self.alias.format(**matches[0])
99100

101+
details = self.get_details(matches)
102+
if details:
103+
post['details'] = details
104+
100105
logging.debug(json.dumps(post))
101106

102107
headers = {
@@ -162,3 +167,19 @@ def get_info(self):
162167
if self.teams:
163168
ret['teams'] = self.teams
164169
return ret
170+
171+
def get_details(self, matches):
172+
details = {}
173+
174+
for key, value in self.opsgenie_details.items():
175+
176+
if type(value) is dict:
177+
if 'field' in value:
178+
field_value = lookup_es_key(matches[0], value['field'])
179+
if field_value is not None:
180+
details[key] = str(field_value)
181+
182+
elif type(value) is str:
183+
details[key] = os.path.expandvars(value)
184+
185+
return details

elastalert/schema.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,20 @@ properties:
298298
mattermost_msg_pretext: {type: string}
299299
mattermost_msg_fields: *mattermostField
300300

301+
## Opsgenie
302+
opsgenie_details:
303+
type: object
304+
minProperties: 1
305+
patternProperties:
306+
"^.+$":
307+
oneOf:
308+
- type: string
309+
- type: object
310+
additionalProperties: false
311+
required: [field]
312+
properties:
313+
field: {type: string, minLength: 1}
314+
301315
### PagerDuty
302316
pagerduty_service_key: {type: string}
303317
pagerduty_client_name: {type: string}

tests/alerts_test.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,261 @@ def test_opsgenie_default_alert_routing():
456456
assert alert.get_info()['recipients'] == ['[email protected]']
457457

458458

459+
def test_opsgenie_details_with_constant_value():
460+
rule = {
461+
'name': 'Opsgenie Details',
462+
'type': mock_rule(),
463+
'opsgenie_account': 'genies',
464+
'opsgenie_key': 'ogkey',
465+
'opsgenie_details': {'Foo': 'Bar'}
466+
}
467+
match = {
468+
'@timestamp': '2014-10-31T00:00:00'
469+
}
470+
alert = OpsGenieAlerter(rule)
471+
472+
with mock.patch('requests.post') as mock_post_request:
473+
alert.alert([match])
474+
475+
mock_post_request.assert_called_once_with(
476+
'https://api.opsgenie.com/v2/alerts',
477+
headers={
478+
'Content-Type': 'application/json',
479+
'Authorization': 'GenieKey ogkey'
480+
},
481+
json=mock.ANY,
482+
proxies=None
483+
)
484+
485+
expected_json = {
486+
'description': BasicMatchString(rule, match).__str__(),
487+
'details': {'Foo': 'Bar'},
488+
'message': 'ElastAlert: Opsgenie Details',
489+
'priority': None,
490+
'source': 'ElastAlert',
491+
'tags': ['ElastAlert', 'Opsgenie Details'],
492+
'user': 'genies'
493+
}
494+
actual_json = mock_post_request.call_args_list[0][1]['json']
495+
assert expected_json == actual_json
496+
497+
498+
def test_opsgenie_details_with_field():
499+
rule = {
500+
'name': 'Opsgenie Details',
501+
'type': mock_rule(),
502+
'opsgenie_account': 'genies',
503+
'opsgenie_key': 'ogkey',
504+
'opsgenie_details': {'Foo': {'field': 'message'}}
505+
}
506+
match = {
507+
'message': 'Bar',
508+
'@timestamp': '2014-10-31T00:00:00'
509+
}
510+
alert = OpsGenieAlerter(rule)
511+
512+
with mock.patch('requests.post') as mock_post_request:
513+
alert.alert([match])
514+
515+
mock_post_request.assert_called_once_with(
516+
'https://api.opsgenie.com/v2/alerts',
517+
headers={
518+
'Content-Type': 'application/json',
519+
'Authorization': 'GenieKey ogkey'
520+
},
521+
json=mock.ANY,
522+
proxies=None
523+
)
524+
525+
expected_json = {
526+
'description': BasicMatchString(rule, match).__str__(),
527+
'details': {'Foo': 'Bar'},
528+
'message': 'ElastAlert: Opsgenie Details',
529+
'priority': None,
530+
'source': 'ElastAlert',
531+
'tags': ['ElastAlert', 'Opsgenie Details'],
532+
'user': 'genies'
533+
}
534+
actual_json = mock_post_request.call_args_list[0][1]['json']
535+
assert expected_json == actual_json
536+
537+
538+
def test_opsgenie_details_with_nested_field():
539+
rule = {
540+
'name': 'Opsgenie Details',
541+
'type': mock_rule(),
542+
'opsgenie_account': 'genies',
543+
'opsgenie_key': 'ogkey',
544+
'opsgenie_details': {'Foo': {'field': 'nested.field'}}
545+
}
546+
match = {
547+
'nested': {
548+
'field': 'Bar'
549+
},
550+
'@timestamp': '2014-10-31T00:00:00'
551+
}
552+
alert = OpsGenieAlerter(rule)
553+
554+
with mock.patch('requests.post') as mock_post_request:
555+
alert.alert([match])
556+
557+
mock_post_request.assert_called_once_with(
558+
'https://api.opsgenie.com/v2/alerts',
559+
headers={
560+
'Content-Type': 'application/json',
561+
'Authorization': 'GenieKey ogkey'
562+
},
563+
json=mock.ANY,
564+
proxies=None
565+
)
566+
567+
expected_json = {
568+
'description': BasicMatchString(rule, match).__str__(),
569+
'details': {'Foo': 'Bar'},
570+
'message': 'ElastAlert: Opsgenie Details',
571+
'priority': None,
572+
'source': 'ElastAlert',
573+
'tags': ['ElastAlert', 'Opsgenie Details'],
574+
'user': 'genies'
575+
}
576+
actual_json = mock_post_request.call_args_list[0][1]['json']
577+
assert expected_json == actual_json
578+
579+
580+
def test_opsgenie_details_with_non_string_field():
581+
rule = {
582+
'name': 'Opsgenie Details',
583+
'type': mock_rule(),
584+
'opsgenie_account': 'genies',
585+
'opsgenie_key': 'ogkey',
586+
'opsgenie_details': {
587+
'Age': {'field': 'age'},
588+
'Message': {'field': 'message'}
589+
}
590+
}
591+
match = {
592+
'age': 10,
593+
'message': {
594+
'format': 'The cow goes %s!',
595+
'arg0': 'moo'
596+
}
597+
}
598+
alert = OpsGenieAlerter(rule)
599+
600+
with mock.patch('requests.post') as mock_post_request:
601+
alert.alert([match])
602+
603+
mock_post_request.assert_called_once_with(
604+
'https://api.opsgenie.com/v2/alerts',
605+
headers={
606+
'Content-Type': 'application/json',
607+
'Authorization': 'GenieKey ogkey'
608+
},
609+
json=mock.ANY,
610+
proxies=None
611+
)
612+
613+
expected_json = {
614+
'description': BasicMatchString(rule, match).__str__(),
615+
'details': {
616+
'Age': '10',
617+
'Message': "{'format': 'The cow goes %s!', 'arg0': 'moo'}"
618+
},
619+
'message': 'ElastAlert: Opsgenie Details',
620+
'priority': None,
621+
'source': 'ElastAlert',
622+
'tags': ['ElastAlert', 'Opsgenie Details'],
623+
'user': 'genies'
624+
}
625+
actual_json = mock_post_request.call_args_list[0][1]['json']
626+
assert expected_json == actual_json
627+
628+
629+
def test_opsgenie_details_with_missing_field():
630+
rule = {
631+
'name': 'Opsgenie Details',
632+
'type': mock_rule(),
633+
'opsgenie_account': 'genies',
634+
'opsgenie_key': 'ogkey',
635+
'opsgenie_details': {
636+
'Message': {'field': 'message'},
637+
'Missing': {'field': 'missing'}
638+
}
639+
}
640+
match = {
641+
'message': 'Testing',
642+
'@timestamp': '2014-10-31T00:00:00'
643+
}
644+
alert = OpsGenieAlerter(rule)
645+
646+
with mock.patch('requests.post') as mock_post_request:
647+
alert.alert([match])
648+
649+
mock_post_request.assert_called_once_with(
650+
'https://api.opsgenie.com/v2/alerts',
651+
headers={
652+
'Content-Type': 'application/json',
653+
'Authorization': 'GenieKey ogkey'
654+
},
655+
json=mock.ANY,
656+
proxies=None
657+
)
658+
659+
expected_json = {
660+
'description': BasicMatchString(rule, match).__str__(),
661+
'details': {'Message': 'Testing'},
662+
'message': 'ElastAlert: Opsgenie Details',
663+
'priority': None,
664+
'source': 'ElastAlert',
665+
'tags': ['ElastAlert', 'Opsgenie Details'],
666+
'user': 'genies'
667+
}
668+
actual_json = mock_post_request.call_args_list[0][1]['json']
669+
assert expected_json == actual_json
670+
671+
672+
def test_opsgenie_details_with_environment_variable_replacement(environ):
673+
environ.update({
674+
'TEST_VAR': 'Bar'
675+
})
676+
rule = {
677+
'name': 'Opsgenie Details',
678+
'type': mock_rule(),
679+
'opsgenie_account': 'genies',
680+
'opsgenie_key': 'ogkey',
681+
'opsgenie_details': {'Foo': '$TEST_VAR'}
682+
}
683+
match = {
684+
'@timestamp': '2014-10-31T00:00:00'
685+
}
686+
alert = OpsGenieAlerter(rule)
687+
688+
with mock.patch('requests.post') as mock_post_request:
689+
alert.alert([match])
690+
691+
mock_post_request.assert_called_once_with(
692+
'https://api.opsgenie.com/v2/alerts',
693+
headers={
694+
'Content-Type': 'application/json',
695+
'Authorization': 'GenieKey ogkey'
696+
},
697+
json=mock.ANY,
698+
proxies=None
699+
)
700+
701+
expected_json = {
702+
'description': BasicMatchString(rule, match).__str__(),
703+
'details': {'Foo': 'Bar'},
704+
'message': 'ElastAlert: Opsgenie Details',
705+
'priority': None,
706+
'source': 'ElastAlert',
707+
'tags': ['ElastAlert', 'Opsgenie Details'],
708+
'user': 'genies'
709+
}
710+
actual_json = mock_post_request.call_args_list[0][1]['json']
711+
assert expected_json == actual_json
712+
713+
459714
def test_jira():
460715
description_txt = "Description stuff goes here like a runbook link."
461716
rule = {

0 commit comments

Comments
 (0)