├── default ├── datamodels.conf ├── authorize.conf ├── web.conf ├── commands.conf ├── restmap.conf ├── alert_manager.conf ├── inputs.conf ├── app.conf ├── setup.xml ├── collections.conf ├── transforms.conf ├── templates │ └── notify_user.html ├── data │ └── ui │ │ ├── views │ │ ├── users_settings.xml │ │ ├── incident_settings.xml │ │ ├── email_settings.xml │ │ ├── kpi_report_incident_status.xml │ │ ├── state_transitions.xml │ │ ├── kpi_report_resolved_incidents.xml │ │ └── incident_overview.xml │ │ ├── nav │ │ └── default.xml │ │ └── html │ │ └── incident_posture.html └── macros.conf ├── static ├── appIcon.png ├── appIconAlt.png ├── appIcon_2x.png └── appIconAlt_2x.png ├── appserver ├── static │ ├── wait.gif │ ├── up-black.png │ ├── up-white.png │ ├── down-black.png │ ├── down-white.png │ ├── components │ │ ├── d3 │ │ │ ├── bower.json │ │ │ └── LICENSE │ │ └── sankey │ │ │ ├── bower.json │ │ │ ├── sankey.css │ │ │ ├── contrib │ │ │ └── LICENSE │ │ │ └── sankey.js │ ├── setup.css │ ├── autodiscover.js │ ├── user_settings.css │ ├── incident_settings.css │ ├── alert_manager_guide.js │ ├── email_settings.css │ ├── views │ │ ├── single_trend.js │ │ ├── emailsettingsview.js │ │ ├── usersettingsview.js │ │ ├── emailtemplatesview.js │ │ └── incidentsettingsview.js │ ├── setup.js │ ├── incident_settings.js │ ├── user_settings.js │ ├── email_settings.js │ └── incident_posture.css ├── src │ └── TA-alert_manager.tar.gz └── controllers │ ├── incident_settings.py │ ├── incident_workflow.py │ ├── user_settings.py │ ├── helpers.py │ └── email_settings.py ├── lookups ├── alert_impact.csv ├── kpi_report_dropdowns.csv ├── default_email_templates.csv ├── alert_priority.csv.sample └── alert_status.csv ├── bin ├── alert_manager_scheduler.path ├── alert_manager_scheduler.sh ├── lib │ ├── AlertManagerNotificationsFilter.py │ ├── CsvLookup.py │ ├── CsvResultParser.py │ ├── AlertManagerUsers.py │ └── AlertManagerNotifications.py ├── loadincidentresults.py ├── alert_manager_config.py └── alert_manager_scheduler.py ├── .gitignore ├── metadata └── default.meta ├── README └── alert_manager.conf.spec └── README.md /default/datamodels.conf: -------------------------------------------------------------------------------- 1 | [alert_manager] 2 | acceleration = 0 3 | -------------------------------------------------------------------------------- /static/appIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/alert_manager/master/static/appIcon.png -------------------------------------------------------------------------------- /default/authorize.conf: -------------------------------------------------------------------------------- 1 | [role_alert_manager] 2 | srchIndexesAllowed = alerts 3 | edit_tcp = enabled 4 | -------------------------------------------------------------------------------- /static/appIconAlt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/alert_manager/master/static/appIconAlt.png -------------------------------------------------------------------------------- /static/appIcon_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/alert_manager/master/static/appIcon_2x.png -------------------------------------------------------------------------------- /static/appIconAlt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/alert_manager/master/static/appIconAlt_2x.png -------------------------------------------------------------------------------- /appserver/static/wait.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/alert_manager/master/appserver/static/wait.gif -------------------------------------------------------------------------------- /appserver/static/up-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/alert_manager/master/appserver/static/up-black.png -------------------------------------------------------------------------------- /appserver/static/up-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/alert_manager/master/appserver/static/up-white.png -------------------------------------------------------------------------------- /appserver/static/down-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/alert_manager/master/appserver/static/down-black.png -------------------------------------------------------------------------------- /appserver/static/down-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/alert_manager/master/appserver/static/down-white.png -------------------------------------------------------------------------------- /lookups/alert_impact.csv: -------------------------------------------------------------------------------- 1 | "severity_id","impact" 2 | 1,"low" 3 | 2,"low" 4 | 3,"medium" 5 | 4,"medium" 6 | 5,"high" 7 | 6,"high" -------------------------------------------------------------------------------- /lookups/kpi_report_dropdowns.csv: -------------------------------------------------------------------------------- 1 | label,value 2 | Alert,alert 3 | Category,category 4 | Priority,priority 5 | Owner,owner 6 | -------------------------------------------------------------------------------- /appserver/src/TA-alert_manager.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/alert_manager/master/appserver/src/TA-alert_manager.tar.gz -------------------------------------------------------------------------------- /bin/alert_manager_scheduler.path: -------------------------------------------------------------------------------- 1 | "$SPLUNK_HOME\bin\splunk.exe" cmd python $SPLUNK_HOME\etc\apps\alert_manager\bin\alert_manager_scheduler.py -------------------------------------------------------------------------------- /bin/alert_manager_scheduler.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | $SPLUNK_HOME/bin/splunk cmd python $SPLUNK_HOME/etc/apps/alert_manager/bin/alert_manager_scheduler.py -------------------------------------------------------------------------------- /default/web.conf: -------------------------------------------------------------------------------- 1 | [endpoint:incident_settings] 2 | [endpoint:user_settings] 3 | [endpoint:email_settings] 4 | [endpoint:incident_workflow] 5 | [endpoint:helpers] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | local/alert_manager.conf 4 | local/app.conf 5 | local/collections.conf 6 | local/datamodels.conf 7 | metadata/local.meta 8 | .DS_Store -------------------------------------------------------------------------------- /appserver/static/components/d3/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3", 3 | "version": "3.3.5", 4 | "main": "d3.js", 5 | "ignore": [], 6 | "dependencies": {}, 7 | "devDependencies": {} 8 | } -------------------------------------------------------------------------------- /bin/lib/AlertManagerNotificationsFilter.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | @register.filter(name='get_type') 6 | def get_type(value): 7 | return type(value).__name__ -------------------------------------------------------------------------------- /default/commands.conf: -------------------------------------------------------------------------------- 1 | 2 | [loadincidentresults] 3 | type = python 4 | filename = loadincidentresults.py 5 | local = true 6 | generating = true 7 | passauth = true 8 | stderr_dest = message 9 | supports_getinfo = false -------------------------------------------------------------------------------- /default/restmap.conf: -------------------------------------------------------------------------------- 1 | [admin_external:alert_manager] 2 | handlertype = python 3 | handlerfile = alert_manager_config.py 4 | handleractions = list, edit 5 | 6 | [admin:alert_manager] 7 | match=/configs 8 | members=alert_manager -------------------------------------------------------------------------------- /lookups/default_email_templates.csv: -------------------------------------------------------------------------------- 1 | email_template_name, email_template_file, email_content_type, email_from, email_subject 2 | "notify_user","notify_user.html","html","splunk@localhost", "Splunk Alert: New incident for alert {{ name }}" -------------------------------------------------------------------------------- /appserver/static/components/sankey/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sankey", 3 | "version": "1.0.0", 4 | "main": "sankey.js", 5 | "ignore": [], 6 | "dependencies": { 7 | "d3": "3.3.x" 8 | }, 9 | "devDependencies": {} 10 | } -------------------------------------------------------------------------------- /lookups/alert_priority.csv.sample: -------------------------------------------------------------------------------- 1 | impact,urgency,priority 2 | low,low,informational 3 | low,medium,low 4 | low,high,medium 5 | medium,low,low 6 | medium,medium,medium 7 | medium,high,high 8 | high,low,medium 9 | high,medium,high 10 | high,high,critical -------------------------------------------------------------------------------- /lookups/alert_status.csv: -------------------------------------------------------------------------------- 1 | "status","status_description" 2 | "new","New" 3 | "auto_assigned","Assigned (Auto)" 4 | "assigned","Assigned" 5 | "work_in_progress","Work in progress" 6 | "resolved","Resolved" 7 | "auto_ttl_resolved","Resolved (Auto TTL)" 8 | "auto_previous_resolved","Resolved (Auto Previous)" -------------------------------------------------------------------------------- /default/alert_manager.conf: -------------------------------------------------------------------------------- 1 | ## DO NOT CHANGE SETTINGS HERE - copy file to local/ first ## 2 | [settings] 3 | index = alerts 4 | default_owner = unassigned 5 | default_impact = low 6 | default_urgency = low 7 | default_priority = low 8 | user_directories = both 9 | default_notify_user_template = notify_user -------------------------------------------------------------------------------- /appserver/static/setup.css: -------------------------------------------------------------------------------- 1 | #index_check { 2 | padding-top: 5px; 3 | } 4 | 5 | .index_check-0 { 6 | color: red; 7 | /* font-weight: bold; */ 8 | } 9 | 10 | .index_check-1 { 11 | color: #73A550; 12 | /* font-weight: bold; */ 13 | } 14 | 15 | .index_check-2 { 16 | color: #FF9933; 17 | /* font-weight: bold; */ 18 | } -------------------------------------------------------------------------------- /default/inputs.conf: -------------------------------------------------------------------------------- 1 | 2 | [script://./bin/alert_manager_scheduler.sh] 3 | interval = 30 4 | passAuth = splunk-system-user 5 | index = _internal 6 | sourcetype = alert_auto_ttl_resolve 7 | 8 | 9 | [script://.\bin\alert_manager_scheduler.path] 10 | interval = 30 11 | passAuth = splunk-system-user 12 | index = _internal 13 | sourcetype = alert_auto_ttl_resolve -------------------------------------------------------------------------------- /appserver/static/autodiscover.js: -------------------------------------------------------------------------------- 1 | 2 | require.config({ 3 | paths: { 4 | "app": "../app" 5 | } 6 | }); 7 | require(['splunkjs/mvc/simplexml/ready!'], function(){ 8 | require(['splunkjs/ready!'], function(){ 9 | // The splunkjs/ready loader script will automatically instantiate all elements 10 | // declared in the dashboard's HTML. 11 | }); 12 | }); -------------------------------------------------------------------------------- /appserver/static/user_settings.css: -------------------------------------------------------------------------------- 1 | #save_settings { 2 | margin-left: 0px; 3 | } 4 | 5 | #user_settings .tooltip-link { 6 | top: -0.5em; 7 | position: relative; 8 | font-size: 75%; 9 | line-height: 0; 10 | vertical-align: baseline; 11 | padding: 2px; 12 | cursor: default; 13 | font-weight: normal; 14 | z-index: 1000; 15 | } 16 | 17 | #user_settings { 18 | padding-bottom: 200px; 19 | } -------------------------------------------------------------------------------- /default/app.conf: -------------------------------------------------------------------------------- 1 | [package] 2 | id = alert_manager 3 | check_for_updates = 1 4 | 5 | [install] 6 | is_configured = 0 7 | state = enabled 8 | build = 14 9 | 10 | [ui] 11 | is_visible = 1 12 | label = Alert Manager 13 | 14 | [launcher] 15 | author = Simon Balz , Mika Borner 16 | description = Extended Splunk Alert Manager with advanced reporting on alerts, workflows (modify owner, status, severity) and auto-resolve features 17 | version = 1.1 18 | 19 | -------------------------------------------------------------------------------- /appserver/static/incident_settings.css: -------------------------------------------------------------------------------- 1 | 2 | #save_settings { 3 | margin-top: 10px; 4 | margin-left: 0px; 5 | z-index: 500; 6 | } 7 | 8 | 9 | #incident_settings .tooltip-link { 10 | top: -0.5em; 11 | position: relative; 12 | font-size: 75%; 13 | line-height: 0; 14 | vertical-align: baseline; 15 | padding: 2px; 16 | cursor: default; 17 | font-weight: normal; 18 | z-index: 1000; 19 | } 20 | 21 | #incident_settings { 22 | padding-bottom: 200px; 23 | width: 100%; 24 | overflow: scroll; 25 | } -------------------------------------------------------------------------------- /appserver/static/alert_manager_guide.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | paths: { 3 | "app": "../app", 4 | "showdown": "//softwaremaniacs.org/playground/showdown-highlight/showdown", 5 | }, 6 | shim: { 7 | "showdown": { 8 | deps: [], 9 | exports: "Showdown" 10 | }, 11 | } 12 | }); 13 | 14 | require([ "jquery", "showdown" ], function($, Showdown) { 15 | 16 | var converter = new Showdown.converter(); 17 | var text = $("#markdown_content").text(); 18 | var html = converter.makeHtml(text); 19 | $("#markdown_content").html(html); 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /appserver/static/components/sankey/sankey.css: -------------------------------------------------------------------------------- 1 | .sankey-diagram .node rect { 2 | cursor: move; 3 | fill-opacity: 0.9; 4 | shape-rendering: crispEdges; 5 | } 6 | 7 | .sankey-diagram .node text { 8 | pointer-events: none; 9 | text-shadow: 0 1px 0 white; 10 | } 11 | 12 | .sankey-diagram .link { 13 | fill: none; 14 | stroke: black; 15 | stroke-opacity: 0.2; 16 | } 17 | 18 | .sankey-diagram .link:hover, .sankey-diagram .link.hovering { 19 | stroke-opacity: 0.5; 20 | } 21 | 22 | .sankey-diagram .link.my-selected { 23 | stroke: yellow; 24 | } 25 | 26 | .sankey-diagram scrollable { 27 | overflow-y: auto; 28 | } 29 | -------------------------------------------------------------------------------- /appserver/static/email_settings.css: -------------------------------------------------------------------------------- 1 | #save_templates { 2 | margin-left: 0px; 3 | } 4 | 5 | #save_settings { 6 | margin-left: 0px; 7 | } 8 | 9 | #email_settings .tooltip-link { 10 | top: -0.5em; 11 | position: relative; 12 | font-size: 75%; 13 | line-height: 0; 14 | vertical-align: baseline; 15 | padding: 2px; 16 | cursor: default; 17 | font-weight: normal; 18 | z-index: 1000; 19 | } 20 | 21 | #email_settings { 22 | padding-bottom: 200px; 23 | } 24 | 25 | #email_templates .tooltip-link { 26 | top: -0.5em; 27 | position: relative; 28 | font-size: 75%; 29 | line-height: 0; 30 | vertical-align: baseline; 31 | padding: 2px; 32 | cursor: default; 33 | font-weight: normal; 34 | z-index: 1000; 35 | } 36 | 37 | #email_templates { 38 | padding-bottom: 200px; 39 | } -------------------------------------------------------------------------------- /bin/lib/CsvLookup.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | import json 4 | 5 | class CsvLookup: 6 | 7 | csv_data = [] 8 | 9 | def __init__(self, file_path): 10 | 11 | if not os.path.exists(file_path): 12 | raise Exception("File %s not found." % file_path) 13 | 14 | else: 15 | with open(file_path) as fh: 16 | reader = csv.DictReader(fh) 17 | 18 | for row in reader: 19 | self.csv_data.append(row) 20 | 21 | def lookup(self, input_data, output_fields = None): 22 | match = {} 23 | for row in self.csv_data: 24 | if all(item in row.items() for item in input_data.items()): 25 | match = row 26 | break 27 | 28 | if output_fields != None: 29 | for k in match.keys(): 30 | if k not in output_fields: 31 | del match[k] 32 | 33 | return match 34 | 35 | -------------------------------------------------------------------------------- /metadata/default.meta: -------------------------------------------------------------------------------- 1 | [] 2 | access = read : [ admin, alert_manager ], write : [ admin, alert_manager ] 3 | owner = nobody 4 | export = system 5 | 6 | [views] 7 | access = read : [ admin, alert_manager ], write : [ admin, alert_manager ] 8 | export = none 9 | owner = nobody 10 | version = 6.2.1 11 | 12 | [datamodels] 13 | access = read : [ admin, alert_manager ], write : [ admin, alert_manager ] 14 | export = none 15 | owner = nobody 16 | version = 6.2.1 17 | 18 | [models] 19 | access = read : [ admin, alert_manager ], write : [ admin, alert_manager ] 20 | export = none 21 | owner = nobody 22 | version = 6.2.1 23 | 24 | [transforms] 25 | access = read : [ admin, alert_manager ], write : [ admin, alert_manager ] 26 | export = system 27 | owner = nobody 28 | version = 6.2.1 29 | 30 | [nav] 31 | access = read : [ admin, alert_manager ], write : [ admin, alert_manager ] 32 | export = none 33 | owner = nobody 34 | version = 6.2.1 35 | 36 | [collections] 37 | access = read : [ admin, alert_manager ], write : [ admin, alert_manager ] 38 | export = system 39 | owner = nobody 40 | version = 6.2.1 41 | -------------------------------------------------------------------------------- /README/alert_manager.conf.spec: -------------------------------------------------------------------------------- 1 | [settings] 2 | * If you do not specify an entry for each attribute, the event manager will use the default value. 3 | 4 | index = 5 | * Name of the index where the alert meta events will be written to 6 | * Defaults to "alerts" 7 | 8 | default_owner = 9 | * Default owner for new alerts 10 | * Defaults to "unassigned" 11 | 12 | default_impact = 13 | * Fallback impact for new incidents if the alert handler is unable to parse it 14 | * Defaults to "low" 15 | 16 | default_urgency = 17 | * Fallback urgency for new incidents if the alert handler is unable to parse it from results 18 | * Defaults to "low" 19 | 20 | default_priority = 21 | * Fallback priority for new incidents if the alert handler is unable to parse it 22 | * Defaults to "low" 23 | 24 | user_directories = [both | builtin | alert_manager] 25 | * Configure which user directories are enabled 26 | * Defaults to both 27 | 28 | default_notify_user_template = 29 | * Default template used to notify users on incident assignment 30 | * Defaults to notify_user -------------------------------------------------------------------------------- /default/setup.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | ]]> 7 | 8 | 9 | 10 | text 11 | 12 | 13 | 14 | text 15 | 16 | 17 | 18 | text 19 | 20 | 21 | 22 | text 23 | 24 | 25 | 26 | text 27 | 28 | 29 | 30 | 31 | 32 | text 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /default/collections.conf: -------------------------------------------------------------------------------- 1 | [incident_settings] 2 | enforceTypes = true 3 | field.alert = string 4 | field.category = string 5 | field.subcategory = string 6 | field.tags = string 7 | field.urgency = string 8 | field.display_fields = string 9 | field.run_alert_script = bool 10 | field.alert_script = string 11 | field.auto_assign_owner = string 12 | field.auto_assign = bool 13 | field.auto_ttl_resolve = bool 14 | field.auto_previous_resolve = bool 15 | 16 | [incidents] 17 | enforceTypes = true 18 | field.incident_id = string 19 | field.alert_time = number 20 | field.job_id = string 21 | field.result_id = string 22 | field.alert = string 23 | field.ttl = number 24 | field.owner = string 25 | field.status = string 26 | field.impact = string 27 | field.urgency = string 28 | field.priority = string 29 | 30 | [incident_results] 31 | enforceTypes = true 32 | field.incident_id = string 33 | field.job_id = string 34 | field.result_id = number 35 | 36 | [alert_users] 37 | enforceTypes = true 38 | field.user = string 39 | field.email = string 40 | field.notify_user = bool 41 | 42 | [email_templates] 43 | enforceTypes = true 44 | field.email_template_name = string 45 | field.email_template_file = string 46 | field.email_content_type = string 47 | field.email_from = string 48 | field.email_subject = string 49 | 50 | [email_settings] 51 | enforceTypes = true 52 | field.alert = string 53 | field.notify_user_template = string 54 | -------------------------------------------------------------------------------- /default/transforms.conf: -------------------------------------------------------------------------------- 1 | [alert_impact] 2 | filename = alert_impact.csv 3 | 4 | [alert_priority] 5 | filename = alert_priority.csv.sample 6 | 7 | [alert_status] 8 | filename = alert_status.csv 9 | 10 | [incident_settings] 11 | external_type = kvstore 12 | collection = incident_settings 13 | fields_list = _key, alert, category, subcategory, tags, urgency, display_fields, run_alert_script, alert_script, auto_assign_owner, auto_assign, auto_ttl_resolve, auto_previous_resolve, 14 | 15 | [incidents] 16 | external_type = kvstore 17 | collection = incidents 18 | fields_list = _key, incident_id, alert_time, job_id, result_id, alert, ttl, owner, status, severity_id, impact, urgency, priority, priority, 19 | 20 | [incident_results] 21 | external_type = kvstore 22 | collection = incident_results 23 | fields_list = _key, incident_id, job_id, result_id, fields 24 | 25 | [alert_users] 26 | external_type = kvstore 27 | collection = alert_users 28 | fields_list = _key, user, email, notify_user 29 | 30 | [email_templates] 31 | external_type = kvstore 32 | collection = email_templates 33 | fields_list = _key, email_template_name, email_template_file, email_content_type, email_from, email_subject 34 | 35 | [email_settings] 36 | external_type = kvstore 37 | collection = email_settings 38 | fields_list = _key, alert, notify_user_template 39 | 40 | 41 | [default_email_templates] 42 | filename = default_email_templates.csv 43 | 44 | [kpi_report_dropdowns] 45 | filename = kpi_report_dropdowns.csv 46 | -------------------------------------------------------------------------------- /appserver/static/components/d3/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Michael Bostock 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * The name Michael Bostock may not be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, 21 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 24 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 26 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /appserver/static/components/sankey/contrib/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Michael Bostock 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * The name Michael Bostock may not be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, 21 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 24 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 26 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /bin/loadincidentresults.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import sys 3 | import splunk.Intersplunk as intersplunk 4 | import splunk.rest as rest 5 | import urllib 6 | import json 7 | import re 8 | import collections 9 | 10 | #(isgetinfo, sys.argv) = intersplunk.isGetInfo(sys.argv) 11 | 12 | if len(sys.argv) < 2: 13 | intersplunk.parseError("Please specify a valid incident_id") 14 | 15 | #if isgetinfo: 16 | # intersplunk.outputInfo(False, False, True, False, None, True) 17 | # # outputInfo automatically calls sys.exit() 18 | 19 | stdinArgs = sys.stdin.readline() 20 | stdinArgs = stdinArgs.strip() 21 | stdinArgs = stdinArgs[11:] 22 | stdinArgs = urllib.unquote(stdinArgs).decode('utf8') 23 | match = re.search(r'([^<]+)', stdinArgs) 24 | sessionKey = match.group(1) 25 | 26 | incident_id = sys.argv[1] 27 | 28 | query = {} 29 | query['incident_id'] = incident_id 30 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/incident_results?query=%s' % urllib.quote(json.dumps(query)) 31 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey) 32 | 33 | data = json.loads(serverContent) 34 | #sys.stderr.write("data: %s" % data) 35 | 36 | field_list = None 37 | results = [] 38 | for result in data: 39 | if "field_list" in result: 40 | field_list = result["field_list"] 41 | 42 | for line in result["fields"]: 43 | if type(field_list) is list: 44 | ordered_line = collections.OrderedDict() 45 | for field in field_list: 46 | ordered_line[field] = line[field] 47 | results.append(ordered_line) 48 | else: 49 | results.append(line) 50 | 51 | intersplunk.outputResults(results) 52 | -------------------------------------------------------------------------------- /bin/lib/CsvResultParser.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | import json 4 | import gzip 5 | import re 6 | 7 | class CsvResultParser: 8 | 9 | csv_data = [] 10 | field_names = [] 11 | 12 | def __init__(self, file_path): 13 | 14 | if not os.path.exists(file_path): 15 | raise Exception("File %s not found." % file_path) 16 | 17 | else: 18 | with gzip.open(file_path) as fh: 19 | reader = csv.DictReader(fh) 20 | self.field_names = reader.fieldnames 21 | for row in reader: 22 | self.csv_data.append(row) 23 | 24 | def getResults(self, base_fields = None): 25 | 26 | fields = [] 27 | for line in self.csv_data: 28 | for k in line.keys(): 29 | if k.startswith("__mv_"): 30 | values = [] 31 | if line[k] != "": 32 | for val in line[k].split(";"): 33 | matches = re.match(r'\$(.+)\$', val) 34 | values.append(matches.group(1)) 35 | line[k[5:]] = values 36 | del line[k] 37 | else: 38 | del line[k] 39 | fields.append(line) 40 | 41 | results = {} 42 | results.update({ "field_list": self.getHeader() }) 43 | if base_fields != None: 44 | results.update(base_fields) 45 | results.update({ "fields": fields }) 46 | return results 47 | 48 | def getHeader(self): 49 | columns = [] 50 | for col in self.field_names: 51 | if not col.startswith("__mv_"): 52 | columns.append(col) 53 | return columns -------------------------------------------------------------------------------- /default/templates/notify_user.html: -------------------------------------------------------------------------------- 1 | Dear Human,
2 | There is a new incident for alert {{name}}. Please check the holy moly Alert Manager! 3 |

4 | Sincerely,
5 | Splunk>

6 | Addtl. infos:
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
alert_time{{alert_time}}
name{{name}}
alert.expires{{alert.expires}}
alert.impact{{alert.impact}}
alert.urgency{{alert.urgency}}
alert.priority{{alert.priority}}
alert.expires{{alert.expires}}
alert.digest_mode{{alert.digest_mode}}
owner{{owner}}
app{{app}}
category{{category}}
subcategory{{subcategory}}
tags{{tags}}
results_link{{results_link}}
view_link{{view_link}}
server.version{{server.version}}
server.build{{server.build}}
server.serverName{{server.serverName}}
27 |

28 | Results: 29 | 30 | 31 | {% for i in result.0.keys %} 32 | 33 | {% endfor %} 34 | 35 | {% for row in result %} 36 | 37 | {% for k, v in row.items %} 38 | 47 | {% endfor %} 48 | 49 | {% endfor %} 50 |
{{i}}
39 | {% if v|get_type == 'list' %} 40 | {% for el in v %} 41 | {{el}}
42 | {% endfor %} 43 | {% else %} 44 | {{v}} 45 | {% endif %} 46 |
-------------------------------------------------------------------------------- /default/data/ui/views/users_settings.xml: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | | inputlookup alert_users | eval key=_key | eval type="alert_manager" | append [rest /services/admin/users splunk_server=local | fields title, email | rename title AS user | setfields key="n/a", type="builtin", notify_user=1] | table key, user, email, notify_user, type | sort - type 5 | 0 6 | 7 | 8 | 9 | 10 | 11 | Notes: 12 |
    13 |
  • Splunk built-in users cannot be changed
  • 14 |
  • The 'type' field cannot be changed
  • 15 |
  • Enable/disable user directories below. Possible values: builtin (only use Splunk users), alert_manager (Only use Alert Manager users), both (use both directories)
  • 16 |
17 |
18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 | 27 | Active Alert Manager User Directories 28 | 29 | | rest splunk_server=local /services/admin/alert_manager | fields user_directories 30 | 31 | 32 | 33 | 34 | 35 |
36 | 37 | 38 | 39 | 40 |
46 | 47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /bin/lib/AlertManagerUsers.py: -------------------------------------------------------------------------------- 1 | import json 2 | import splunk.entity as entity 3 | import splunk.rest as rest 4 | 5 | class AlertManagerUsers: 6 | sessionKey = None 7 | 8 | def __init__(self, sessionKey): 9 | self.sessionKey = sessionKey 10 | 11 | def getUserList(self): 12 | 13 | # Get alert manager config 14 | config = {} 15 | config['user_directories'] = 'both' 16 | 17 | restconfig = entity.getEntities('configs/alert_manager', count=-1, sessionKey=self.sessionKey) 18 | if len(restconfig) > 0: 19 | for cfg in config.keys(): 20 | if cfg in restconfig['settings']: 21 | config[cfg] = restconfig['settings'][cfg] 22 | 23 | 24 | user_list = [] 25 | # Get splunk users 26 | if config['user_directories'] == "builtin" or config['user_directories'] == "both": 27 | uri = '/services/admin/users?output_mode=json' 28 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=self.sessionKey, method='GET') 29 | entries = json.loads(serverContent) 30 | 31 | if len(entries['entry']) > 0: 32 | for entry in entries['entry']: 33 | user = { "name": entry['name'], "email": entry['content']['email'], "notify_user": 1, "type": "builtin" } 34 | user_list.append(user) 35 | 36 | if config['user_directories'] == "alert_manager" or config['user_directories'] == "both": 37 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/alert_users?output_mode=json' 38 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=self.sessionKey) 39 | entries = json.loads(serverContent) 40 | 41 | if len(entries) > 0: 42 | for entry in entries: 43 | if "email" not in entry: 44 | entry['email'] = '' 45 | 46 | user = { "name": entry['user'], "email": entry['email'], "notify_user": entry['notify_user'], "type": "alert_manager" } 47 | user_list.append(user) 48 | 49 | return user_list -------------------------------------------------------------------------------- /appserver/static/views/single_trend.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | var _ = require('underscore'); 3 | var mvc = require('splunkjs/mvc'); 4 | var SimpleSplunkView = require('splunkjs/mvc/simplesplunkview'); 5 | 6 | var TrendIndicator = SimpleSplunkView.extend({ 7 | // Override fetch settings 8 | outputMode: 'json', 9 | returnCount: 2, 10 | // Default options 11 | options: { 12 | 13 | }, 14 | // Template for trend indicator 15 | template: _.template( 16 | // '
' + 17 | '
' + 18 | '
<%- diff %>
' 19 | // '
' 20 | ), 21 | displayMessage: function() { 22 | // Don't display messages 23 | }, 24 | createView: function() { 25 | return true; 26 | }, 27 | 28 | updateView: function(viz, data) { 29 | this.$el.empty(); 30 | var model = null; 31 | if (this.settings.has('trendField')) { 32 | var trendClass = 'nochange', diff = '0', 33 | field = this.settings.get('field'); 34 | 35 | var v = parseInt(data[0][this.settings.get('trendField')], 10); 36 | if (v > 0) { 37 | trendClass = 'increase'; 38 | diff = ['+', String(v)].join(''); 39 | } else if (v < 0) { 40 | trendClass = 'decrease'; 41 | diff = [ String(v)].join(''); 42 | } else if (v == 0) { 43 | trendClass = 'nochange'; 44 | diff = ['+/-', String(v)].join(''); 45 | } 46 | 47 | model = { 48 | trendClass: trendClass, 49 | diff: diff 50 | }; 51 | } 52 | if (!model) { 53 | return; 54 | } 55 | // Render the HTML 56 | this.$el.html(this.template(model)); 57 | } 58 | }); 59 | 60 | return TrendIndicator; 61 | 62 | }); -------------------------------------------------------------------------------- /default/data/ui/nav/default.xml: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /appserver/static/setup.js: -------------------------------------------------------------------------------- 1 | 2 | $(function(){ 3 | var check = 0; 4 | var indexel = "#\\/admin\\/alert_manager\\/settings\\/index_id"; 5 | var index_list = []; 6 | 7 | $("").css('display', 'block').attr("id", "index_check").insertAfter($(indexel)); 8 | 9 | 10 | $.getJSON( "/custom/alert_manager/helpers/get_indexes", function( data ) { 11 | 12 | $.each( data, function( key, val ) { 13 | index_list.push( val ); 14 | }); 15 | 16 | var index = $(indexel).val(); 17 | indexCheck(index, index_list); 18 | 19 | console.debug("indexlist", index_list); 20 | }); 21 | 22 | 23 | var indexCheck = function($index, $index_list) { 24 | if ($index == "alerts") { 25 | if($.inArray($index, $index_list) == -1) { 26 | check = 0; 27 | $("#index_check").html('Default index "alerts" doesn\'t exists. Did you install and configure TA-alert_manager correctly?'); 28 | } else { 29 | $("#index_check").text("Default index 'alerts' exists, all set.") 30 | check = 1; 31 | } 32 | } else { 33 | if($.inArray($index, $index_list) == -1) { 34 | check = 0; 35 | $("#index_check").text("Custom index '"+ $index +"' doesn't exists. Please configure it first."); 36 | } else { 37 | check = 2; 38 | $("#index_check").text("Custom index '"+ $index +"' already exists. Are you sure to use this one?") 39 | } 40 | } 41 | $("#index_check").removeClass(); 42 | $("#index_check").addClass("index_check-"+check); 43 | }; 44 | 45 | 46 | $(indexel).keyup(function() { 47 | var index = $( this ).val(); 48 | indexCheck(index, index_list); 49 | }); 50 | 51 | $(".splButton-primary").click(function() { 52 | var index = $(indexel).val(); 53 | if(check != 1) { 54 | if(check == 0) { 55 | if(index == "alerts") { 56 | alert("The default index '"+index+"' doesn't exists. Check if TA-alert_manager is installed and configured correctly.") 57 | return false; 58 | } else { 59 | alert("The index '"+index+"' doesn't exists. Please create it first.") 60 | return false; 61 | } 62 | } 63 | if(check == 2) { 64 | if(confirm("Are you sure to use the pre-existing custom-index '"+index+"'?")) { 65 | return true; 66 | } else { 67 | return false; 68 | } 69 | } 70 | 71 | } else { 72 | return true; 73 | } 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /default/data/ui/views/incident_settings.xml: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | | inputlookup incident_settings| eval key=_key | append [| rest /servicesNS/nobody/$app$/saved/searches/ splunk_server=local | search action.script.filename=alert_handler.py | fields title | rename title as alert] | dedup alert | eval category=if(isnull(category),"unknown",category) | eval subcategory=if(isnull(subcategory),"unknown",subcategory) | eval tags=if(isnull(tags),"[Untagged]",tags) | eval urgency=if(isnull(urgency),"low",urgency) | eval run_alert_script=if(isnull(run_alert_script), 0, run_alert_script) | eval run_alert_script=if(isnull(run_alert_script), 0, run_alert_script) | eval auto_assign_owner=if(isnull(auto_assign_owner),"unassigned",auto_assign_owner) | eval auto_assign=if(isnull(auto_assign),0,auto_assign) | eval auto_ttl_resolve=if(isnull(auto_ttl_resolve),0,auto_ttl_resolve) | eval auto_previous_resolve=if(isnull(auto_previous_resolve),0,auto_previous_resolve) | table key, alert, category, subcategory, tags, urgency, display_fields, run_alert_script, alert_script, auto_assign, auto_assign_owner, auto_ttl_resolve, auto_previous_resolve 5 | 0 6 | 7 | 8 | 9 | 10 | 11 | Note: If your alert doesn't appear, it's probably not visible globally. Try to select the correct app context, save the alert settings once, et voilà, the alert appears from now on. 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | | rest /services/apps/local splunk_server=local | fields title | dedup title | sort title 21 | -1m 22 | now 23 | 24 | title 25 | title 26 | search 27 | 28 | 29 | 30 |
36 | 37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /default/data/ui/views/email_settings.xml: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | | inputlookup email_settings | eval key=_key | append [inputlookup incident_settings ] | dedup alert | table key, alert, notify_user_template 5 | 0 6 | 7 | 8 | 9 | | inputlookup email_templates | eval key=_key | append [inputlookup default_email_templates ] | dedup email_template_name | table key, email_template_name, email_template_file, email_content_type, email_from, email_subject 10 | 0 11 | 12 | 13 | 14 | 15 | Usage 16 | 17 | To enable e-mail notifications, there are different configurations necessary: 18 |
    19 |
  1. Configure users with e-mail addresses and enable notify_user" in the User Settings view
  2. 20 |
  3. **IMPORTANT**: Load default template configuration. Hit "Saved Templates" below at least once!
  4. 21 |
  5. Configure additional templates or modify the default ones. To modify default templates, copy them first from $SPLUNK_HOME/etc/apps/alert_manager/default/templates to $SPLUNK_HOME/etc/apps/alert_manager/local/templates. Refresh this page to map template files (*.html) to the E-Mail Template list.
  6. 22 |
  7. Map templates based on their name (column email_template_name) to Alerts and specific actions (e.g. column notify_user_template)
  8. 23 |
24 | Notes:
25 |
    26 |
  • If your alert doesn't appear, it's probably not configured in incident settings yet. Check the Incident Settingts page. In this case of unconfigured incidents and email settings, the default templates from the Global Settings will be used.
  • 27 |
  • Currently there's only the "notify_user" action available. This action will send an email when an incident will be auto assigned to a user upon creation. More to follow.
  • 28 |
29 | 30 |
31 |
32 | 33 | 34 | Link E-Mail Templates to Alerts 35 | 36 | 37 |
43 | 44 |
45 |
46 | 47 | 48 | E-Mail Templates 49 | 50 | 51 |
57 | 58 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /bin/alert_manager_config.py: -------------------------------------------------------------------------------- 1 | 2 | import splunk.admin as admin 3 | import splunk.entity as entity 4 | 5 | 6 | class AlertHandlerApp(admin.MConfigHandler): 7 | ''' 8 | Set up supported arguments 9 | ''' 10 | 11 | def setup(self): 12 | if self.requestedAction == admin.ACTION_EDIT: 13 | for arg in ['index', 'default_owner', 'default_impact', 'default_urgency', 'default_priority', 'user_directories', 'default_notify_user_template']: 14 | self.supportedArgs.addOptArg(arg) 15 | pass 16 | 17 | def handleList(self, confInfo): 18 | confDict = self.readConf("alert_manager") 19 | if None != confDict: 20 | for stanza, settings in confDict.items(): 21 | for key, val in settings.items(): 22 | #if key in ['save_results']: 23 | # if int(val) == 1: 24 | # val = '1' 25 | # else: 26 | # val = '0' 27 | if key in ['index'] and val in [None, '']: 28 | val = '' 29 | if key in ['default_owner'] and val in [None, '']: 30 | val = '' 31 | if key in ['default_impact'] and val in [None, '']: 32 | val = '' 33 | if key in ['default_urgency'] and val in [None, '']: 34 | val = '' 35 | if key in ['default_priority'] and val in [None, '']: 36 | val = '' 37 | if key in ['user_directories'] and val in [None, '']: 38 | val = '' 39 | if key in ['default_notify_user_template'] and val in [None, '']: 40 | val = '' 41 | 42 | confInfo[stanza].append(key, val) 43 | 44 | def handleEdit(self, confInfo): 45 | name = self.callerArgs.id 46 | args = self.callerArgs 47 | 48 | if self.callerArgs.data['index'][0] in [None, '']: 49 | self.callerArgs.data['index'][0] = '' 50 | 51 | if self.callerArgs.data['default_owner'][0] in [None, '']: 52 | self.callerArgs.data['default_owner'][0] = '' 53 | 54 | if self.callerArgs.data['default_impact'][0] in [None, '']: 55 | self.callerArgs.data['default_impact'][0] = '' 56 | 57 | if self.callerArgs.data['default_urgency'][0] in [None, '']: 58 | self.callerArgs.data['default_urgency'][0] = '' 59 | 60 | if self.callerArgs.data['default_priority'][0] in [None, '']: 61 | self.callerArgs.data['default_priority'][0] = '' 62 | 63 | if self.callerArgs.data['user_directories'][0] in [None, '']: 64 | self.callerArgs.data['user_directories'][0] = '' 65 | 66 | if self.callerArgs.data['default_notify_user_template'][0] in [None, '']: 67 | self.callerArgs.data['default_notify_user_template'][0] = '' 68 | 69 | #if int(self.callerArgs.data['save_results'][0]) == 1: 70 | # self.callerArgs.data['save_results'][0] = '1' 71 | #else: 72 | # self.callerArgs.data['save_results'][0] = '0' 73 | 74 | self.writeConf('alert_manager', 'settings', self.callerArgs.data) 75 | 76 | # initialize the handler 77 | admin.init(AlertHandlerApp, admin.CONTEXT_APP_AND_USER) 78 | -------------------------------------------------------------------------------- /default/macros.conf: -------------------------------------------------------------------------------- 1 | [all_alerts] 2 | #definition = tstats values(all_alerts.alert) as alert values(all_alerts.label) as alert values(all_alerts.app) as app values(all_alerts.event_search) as event_search values(all_alerts.search) as search values(all_alerts.severity) as severity values(all_alerts.earliest) as earliest values(all_alerts.latest) as latest count from datamodel="alert_manager" where nodename="all_alerts" by all_alerts.job_id,all_alerts.incident_id,all_alerts.result_id,_time |rename all_alerts.incident_id as incident_id, all_alerts.job_id as job_id, all_alerts.result_id as result_id| sort - _time | lookup incidents incident_id OUTPUT |lookup alert_status status OUTPUT status_description | makemv delim=" " tags | lookup alert_urgencies severity, priority OUTPUT urgency 3 | definition = tstats values(all_alerts.alert) as alert, values(all_alerts.app) as app, values(all_alerts.event_search) as event_search, values(all_alerts.search) as search, values(all_alerts.impact) as impact, values(all_alerts.earliest) as earliest, values(all_alerts.latest) as latest, count from datamodel="alert_manager" where nodename="all_alerts" by all_alerts.job_id, all_alerts.incident_id, all_alerts.result_id, _time | rename all_alerts.incident_id as incident_id, all_alerts.job_id as job_id, all_alerts.result_id as result_id| sort - _time | lookup incidents incident_id OUTPUTNEW alert, owner, status, impact, urgency | lookup alert_priority impact, urgency OUTPUT priority | lookup incident_settings alert OUTPUT category, subcategory, tags, display_fields | lookup alert_status status OUTPUT status_description | fillnull value="" tags, category, subcategory | eval tags=if(tags=="","[Untagged]",tags) | makemv delim=" " tags 4 | iseval = 0 5 | 6 | [all_alerts_single_trend(2)] 7 | args = earliest, latest 8 | #definition = tstats values(all_alerts.alert) as alert values(all_alerts.label) as alert values(all_alerts.severity) as severity count from datamodel="alert_manager" where nodename="all_alerts" earliest="$earliest$" latest="$latest$" by all_alerts.incident_id |rename all_alerts.incident_id as incident_id | lookup incidents incident_id OUTPUT priority | fillnull value=unknown priority | 9 | definition = tstats count from datamodel="alert_manager" where nodename="all_alerts" earliest="$earliest$" latest="$latest$" by all_alerts.incident_id | rename all_alerts.incident_id AS incident_id | lookup incidents incident_id OUTPUT impact, urgency | lookup alert_priority impact, urgency OUTPUT priority | fields priority 10 | iseval = 0 11 | 12 | [all_alerts_pivot] 13 | definition = pivot alert_manager all_alerts SPLITROW _time PERIOD auto SPLITROW label AS alert SPLITROW app SPLITROW incident_id SPLITROW event_search SPLITROW search SPLITROW severity SPLITROW earliest SPLITROW latest ROWSUMMARY 0 COLSUMMARY 0 NUMCOLS 0 SHOWOTHER 1 | sort - _time | lookup incidents incident_id OUTPUT |lookup incident_settings alert OUTPUT category, subcategory, priority |lookup alert_urgencies severity, priority OUTPUT urgency 14 | iseval = 0 15 | 16 | [incident_history(1)] 17 | args = incident_id 18 | definition = eventtype=incident_change incident_id="$incident_id$" |eval previous_value=coalesce(previous_status, previous_owner, previous_urgency) | eval attribute=case(isnotnull(owner),"owner",isnotnull(urgency),"urgency",isnotnull(status),"status") | eval attribute_val=case(isnotnull(owner),owner,isnotnull(urgency),urgency,isnotnull(status),status) |eval details=case(action="auto_previous_resolve","Incident resolved by system (because of a new incident)",action="auto_ttl_resolve","Incident resolved by system (TTL reached)",action="create","Incident created",action="change",attribute + " has been changed from '" + previous_value + "' to '" + attribute_val+"'") | table _time, user, action, details, comment 19 | iseval = 0 20 | 21 | [incident_details(2)] 22 | args = incident_id, fields 23 | definition = loadincidentresults $incident_id$ | fields $fields$ | table $fields$ | transpose | rename column AS Key, "row 1" AS Value, "row 2" AS "Value 2", "row 3" AS "Value 3", "row 4" AS "Value 4", "row 5" AS "Value 5", 24 | iseval = 0 25 | -------------------------------------------------------------------------------- /appserver/static/incident_settings.js: -------------------------------------------------------------------------------- 1 | require([ 2 | "splunkjs/mvc", 3 | "splunkjs/mvc/utils", 4 | "splunkjs/mvc/tokenutils", 5 | "underscore", 6 | "jquery", 7 | "splunkjs/mvc/simplexml", 8 | 'splunkjs/mvc/tableview', 9 | 'splunkjs/mvc/chartview', 10 | 'splunkjs/mvc/searchmanager', 11 | 'splunk.util', 12 | 'splunk.messenger', 13 | ], function( 14 | mvc, 15 | utils, 16 | TokenUtils, 17 | _, 18 | $, 19 | DashboardController, 20 | TableView, 21 | ChartView, 22 | SearchManager, 23 | splunkUtil, 24 | Messenger 25 | ) { 26 | 27 | // Tokens 28 | var submittedTokens = mvc.Components.getInstance('submitted', {create: true}); 29 | var defaultTokens = mvc.Components.getInstance('default', {create: true}); 30 | 31 | // Save Settings 32 | $(document).on("click", "#save_settings", function(event){ 33 | // save data here 34 | 35 | var data = $("#handson_container").data('handsontable').getData(); 36 | console.debug("save data", data); 37 | 38 | // validate data 39 | var check = _.filter(data, function(entry){ 40 | return entry['alert']== null || (entry['run_alert_script'] == true && entry['alert_script'] == null) || (entry['auto_assign'] == true && entry['auto_assign_owner'] == null); 41 | }); 42 | console.debug("check", check); 43 | if (check.length>0) { 44 | var modal = ''+ 45 | ''; 61 | $('body').prepend(modal); 62 | $('#validation_failed').modal('show'); 63 | } else { 64 | 65 | data = JSON.stringify(data); 66 | var post_data = { 67 | contents : data 68 | }; 69 | 70 | //var url = 'http://splunk.local/en-GB/custom/alert_manager/incident_settings/save'; 71 | var url = splunkUtil.make_url('/custom/alert_manager/incident_settings/save'); 72 | console.debug("post_data", post_data); 73 | 74 | $.ajax( url, 75 | { 76 | uri: url, 77 | type: 'POST', 78 | data: post_data, 79 | 80 | 81 | success: function(jqXHR, textStatus){ 82 | // Reload the table 83 | mvc.Components.get("incident_settings_search").startSearch() 84 | console.debug("success"); 85 | }, 86 | 87 | // Handle cases where the file could not be found or the user did not have permissions 88 | complete: function(jqXHR, textStatus){ 89 | console.debug("complete"); 90 | }, 91 | 92 | error: function(jqXHR,textStatus,errorThrown) { 93 | console.log("Error"); 94 | } 95 | } 96 | ); 97 | } 98 | 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /bin/alert_manager_scheduler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import urllib 4 | import json 5 | import splunk 6 | import splunk.rest as rest 7 | import splunk.input as input 8 | import splunk.entity as entity 9 | import time 10 | import logging 11 | import logging.handlers 12 | import hashlib 13 | import datetime 14 | import socket 15 | 16 | #sys.stdout = open('/tmp/stdout', 'w') 17 | #sys.stderr = open('/tmp/stderr', 'w') 18 | 19 | start = time.time() 20 | 21 | # Setup logger 22 | log = logging.getLogger('alert_manager_scheduler') 23 | fh = logging.handlers.RotatingFileHandler(os.environ.get('SPLUNK_HOME') + "/var/log/splunk/alert_manager_scheduler.log", maxBytes=25000000, backupCount=5) 24 | formatter = logging.Formatter("%(asctime)-15s %(levelname)-5s %(message)s") 25 | fh.setFormatter(formatter) 26 | log.addHandler(fh) 27 | log.setLevel(logging.DEBUG) 28 | 29 | sessionKey = sys.stdin.readline().strip() 30 | splunk.setDefault('sessionKey', sessionKey) 31 | #sessionKey = urllib.unquote(sessionKey[11:]).decode('utf8') 32 | 33 | log.debug("Scheduler started. sessionKey=%s" % sessionKey) 34 | 35 | # 36 | # Get global settings 37 | # 38 | config = {} 39 | config['index'] = 'alerts' 40 | 41 | restconfig = entity.getEntities('configs/alert_manager', count=-1, sessionKey=sessionKey) 42 | if len(restconfig) > 0: 43 | if 'index' in restconfig['settings']: 44 | config['index'] = restconfig['settings']['index'] 45 | 46 | log.debug("Global settings: %s" % config) 47 | 48 | # Look for auto-resolve incidents 49 | query = {} 50 | query['auto_ttl_resolve'] = True 51 | log.debug("Filter: %s" % json.dumps(query)) 52 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/incident_settings?query=%s' % urllib.quote(json.dumps(query)) 53 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey) 54 | alerts = json.loads(serverContent) 55 | if len(alerts) >0: 56 | for alert in alerts: 57 | log.debug("Alert settings: %s" % alert) 58 | #query_incidents = {} 59 | #query_incidents['alert'] = alert['alert'] 60 | #query_incidents['status'] = 'new' 61 | #query_incidents = json.dumps(query_incidents) 62 | query_incidents = '{ "alert": "'+alert['alert']+'", "$or": [ { "status": "auto_assigned" } , { "status": "new" } ] }' 63 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/incidents?query=%s' % urllib.quote(query_incidents) 64 | serverResponseIncidents, serverContentIncidents = rest.simpleRequest(uri, sessionKey=sessionKey) 65 | 66 | incidents = json.loads(serverContentIncidents) 67 | if len(incidents) > 0: 68 | log.info("Found %s incidents of alert %s to check for reached ttl..." % (len(incidents), alert['alert'])) 69 | for incident in incidents: 70 | log.info("Checking incident: %s" % incident['incident_id']) 71 | if (incident['alert_time'] + incident['ttl']) <= time.time(): 72 | log.info("Incident %s (%s) should be resolved. alert_time=%s ttl=%s now=%s" % (incident['incident_id'], incident['_key'], incident['alert_time'], incident['ttl'], time.time())) 73 | old_status = incident['status'] 74 | incident['status'] = 'auto_ttl_resolved' 75 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/incidents/%s' % incident['_key'] 76 | incidentStr = json.dumps(incident) 77 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, jsonargs=incidentStr) 78 | 79 | now = datetime.datetime.now().isoformat() 80 | event_id = hashlib.md5(incident['incident_id'] + now).hexdigest() 81 | log.debug("event_id=%s now=%s" % (event_id, now)) 82 | 83 | event = 'time=%s severity=INFO origin="alert_manager_scheduler" event_id="%s" user="splunk-system-user" action="auto_ttl_resolve" previous_status="%s" status="auto_ttl_resolved" incident_id="%s"' % (now, event_id, old_status, incident['incident_id']) 84 | log.debug("Event will be: %s" % event) 85 | input.submit(event, hostname = socket.gethostname(), sourcetype = 'incident_change', source = 'alert_manager_scheduler.py', index = config['index']) 86 | else: 87 | log.info("Incident %s has not ttl reached yet." % incident['incident_id']) 88 | else: 89 | log.info("No incidents of alert %s to check for reached ttl." % alert['alert']) 90 | 91 | # TODO: Addtl. scheduler scenarios 92 | 93 | 94 | end = time.time() 95 | duration = round((end-start), 3) 96 | log.info("Alert manager scheduler finished. duration=%ss" % duration) 97 | -------------------------------------------------------------------------------- /appserver/controllers/incident_settings.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import json 5 | import shutil 6 | import cherrypy 7 | import re 8 | import time 9 | import datetime 10 | import urllib 11 | 12 | #from splunk import AuthorizationFailed as AuthorizationFailed 13 | import splunk.appserver.mrsparkle.controllers as controllers 14 | import splunk.appserver.mrsparkle.lib.util as util 15 | import splunk.bundle as bundle 16 | import splunk.entity as entity 17 | from splunk.appserver.mrsparkle.lib import jsonresponse 18 | from splunk.appserver.mrsparkle.lib.util import make_splunkhome_path 19 | import splunk.clilib.bundle_paths as bundle_paths 20 | from splunk.util import normalizeBoolean as normBool 21 | from splunk.appserver.mrsparkle.lib.decorators import expose_page 22 | from splunk.appserver.mrsparkle.lib.routes import route 23 | import splunk.rest as rest 24 | 25 | dir = os.path.join(util.get_apps_dir(), __file__.split('.')[-2], 'bin') 26 | 27 | if not dir in sys.path: 28 | sys.path.append(dir) 29 | 30 | 31 | #sys.stdout = open('/tmp/stdout', 'w') 32 | #sys.stderr = open('/tmp/stderr', 'w') 33 | 34 | 35 | def setup_logger(level): 36 | """ 37 | Setup a logger for the REST handler. 38 | """ 39 | 40 | logger = logging.getLogger('splunk.appserver.alert_manager.controllers.IncidentSettings') 41 | logger.propagate = False # Prevent the log messages from being duplicated in the python.log file 42 | logger.setLevel(level) 43 | 44 | file_handler = logging.handlers.RotatingFileHandler(make_splunkhome_path(['var', 'log', 'splunk', 'alert_manager_settings_controller.log']), maxBytes=25000000, backupCount=5) 45 | 46 | formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') 47 | file_handler.setFormatter(formatter) 48 | logger.addHandler(file_handler) 49 | return logger 50 | 51 | logger = setup_logger(logging.DEBUG) 52 | 53 | from splunk.models.base import SplunkAppObjModel 54 | from splunk.models.field import BoolField, Field 55 | 56 | 57 | 58 | class IncidentSettings(controllers.BaseController): 59 | 60 | @expose_page(must_login=True, methods=['GET']) 61 | def easter_egg(self, **kwargs): 62 | return 'Hellow World' 63 | 64 | @expose_page(must_login=True, methods=['POST']) 65 | def delete(self, key, **kwargs): 66 | logger.info("Removing incident settings contents for %s..." % key) 67 | 68 | user = cherrypy.session['user']['name'] 69 | sessionKey = cherrypy.session.get('sessionKey') 70 | 71 | query = {} 72 | query['_key'] = key 73 | logger.debug("Query for incident settings: %s" % urllib.quote(json.dumps(query))) 74 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/incident_settings?query=%s' % urllib.quote(json.dumps(query)) 75 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, method='DELETE') 76 | 77 | logger.debug("Entry removed. serverResponse was %s" % serverResponse) 78 | 79 | return 'Incident settings have been removed for entry with _key=%s' % key 80 | 81 | 82 | @expose_page(must_login=True, methods=['POST']) 83 | def save(self, contents, **kwargs): 84 | 85 | logger.info("Saving incident settings contents...") 86 | 87 | user = cherrypy.session['user']['name'] 88 | sessionKey = cherrypy.session.get('sessionKey') 89 | 90 | 91 | # Parse the JSON 92 | parsed_contents = json.loads(contents) 93 | 94 | logger.debug("Contents: %s" % contents) 95 | 96 | for entry in parsed_contents: 97 | if '_key' in entry and entry['_key'] != None: 98 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/incident_settings/' + entry['_key'] 99 | logger.debug("uri is %s" % uri) 100 | 101 | del entry['_key'] 102 | entry = json.dumps(entry) 103 | 104 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, jsonargs=entry) 105 | logger.debug("Updated entry. serverResponse was %s" % serverResponse) 106 | else: 107 | if '_key' in entry: 108 | del entry['_key'] 109 | ['' if val is None else val for val in entry] 110 | 111 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/incident_settings/' 112 | logger.debug("uri is %s" % uri) 113 | 114 | entry = json.dumps(entry) 115 | logger.debug("entry is %s" % entry) 116 | 117 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, jsonargs=entry) 118 | logger.debug("Added entry. serverResponse was %s" % serverResponse) 119 | 120 | return 'Data has been saved' 121 | 122 | -------------------------------------------------------------------------------- /appserver/controllers/incident_workflow.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import json 5 | import shutil 6 | import cherrypy 7 | import re 8 | import time 9 | import datetime 10 | import urllib 11 | import socket 12 | import hashlib 13 | 14 | #from splunk import AuthorizationFailed as AuthorizationFailed 15 | import splunk 16 | import splunk.appserver.mrsparkle.controllers as controllers 17 | import splunk.appserver.mrsparkle.lib.util as util 18 | import splunk.input as input 19 | import splunk.bundle as bundle 20 | import splunk.entity as entity 21 | from splunk.appserver.mrsparkle.lib import jsonresponse 22 | from splunk.appserver.mrsparkle.lib.util import make_splunkhome_path 23 | import splunk.clilib.bundle_paths as bundle_paths 24 | from splunk.util import normalizeBoolean as normBool 25 | from splunk.appserver.mrsparkle.lib.decorators import expose_page 26 | from splunk.appserver.mrsparkle.lib.routes import route 27 | import splunk.rest as rest 28 | 29 | dir = os.path.join(util.get_apps_dir(), __file__.split('.')[-2], 'bin') 30 | 31 | if not dir in sys.path: 32 | sys.path.append(dir) 33 | 34 | 35 | #sys.stdout = open('/tmp/stdout', 'w') 36 | #sys.stderr = open('/tmp/stderr', 'w') 37 | 38 | 39 | def setup_logger(level): 40 | """ 41 | Setup a logger for the REST handler. 42 | """ 43 | 44 | logger = logging.getLogger('splunk.appserver.alert_manager.controllers.IncidentSettings') 45 | logger.propagate = False # Prevent the log messages from being duplicated in the python.log file 46 | logger.setLevel(level) 47 | 48 | file_handler = logging.handlers.RotatingFileHandler(make_splunkhome_path(['var', 'log', 'splunk', 'alert_manager_settings_controller.log']), maxBytes=25000000, backupCount=5) 49 | 50 | formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') 51 | file_handler.setFormatter(formatter) 52 | logger.addHandler(file_handler) 53 | return logger 54 | 55 | logger = setup_logger(logging.DEBUG) 56 | 57 | from splunk.models.base import SplunkAppObjModel 58 | from splunk.models.field import BoolField, Field 59 | 60 | 61 | 62 | class IncidentSettings(controllers.BaseController): 63 | 64 | @expose_page(must_login=True, methods=['POST']) 65 | def save(self, contents, **kwargs): 66 | """ 67 | Save the contents of a lookup file 68 | """ 69 | 70 | logger.info("Saving incident settings contents...") 71 | 72 | user = cherrypy.session['user']['name'] 73 | sessionKey = cherrypy.session.get('sessionKey') 74 | splunk.setDefault('sessionKey', sessionKey) 75 | 76 | # 77 | # Get global settings 78 | # 79 | config = {} 80 | config['index'] = 'alerts' 81 | 82 | restconfig = entity.getEntities('configs/alert_manager', count=-1, sessionKey=sessionKey) 83 | if len(restconfig) > 0: 84 | if 'index' in restconfig['settings']: 85 | config['index'] = restconfig['settings']['index'] 86 | 87 | logger.debug("Global settings: %s" % config) 88 | 89 | # Parse the JSON 90 | contents = json.loads(contents) 91 | 92 | logger.debug("Contents: %s" % json.dumps(contents)) 93 | 94 | # Get key 95 | query = {} 96 | query['incident_id'] = contents['incident_id'] 97 | logger.debug("Filter: %s" % json.dumps(query)) 98 | 99 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/incidents?query=%s' % urllib.quote(json.dumps(query)) 100 | serverResponse, incident = rest.simpleRequest(uri, sessionKey=sessionKey) 101 | logger.debug("Settings for incident: %s" % incident) 102 | incident = json.loads(incident) 103 | 104 | # Update incident 105 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/incidents/' + incident[0]['_key'] 106 | logger.debug("URI for incident update: %s" % uri ) 107 | 108 | # Prepared new entry 109 | now = datetime.datetime.now().isoformat() 110 | for key in incident[0].keys(): 111 | if (key in contents) and (incident[0][key] != contents[key]): 112 | logger.info("%s for incident %s changed. Writing change event to index %s." % (key, incident[0]['incident_id'], config['index'])) 113 | event_id = hashlib.md5(incident[0]['incident_id'] + now).hexdigest() 114 | event = 'time=%s severity=INFO origin="incident_posture" event_id="%s" user="%s" action="change" incident_id="%s" %s="%s" previous_%s="%s" comment="%s"' % (now, event_id, user, incident[0]['incident_id'], key, contents[key], key, incident[0][key], contents['comment']) 115 | logger.debug("Event will be: %s" % event) 116 | input.submit(event, hostname = socket.gethostname(), sourcetype = 'incident_change', source = 'incident_settings.py', index = config['index']) 117 | incident[0][key] = contents[key] 118 | else: 119 | logger.info("%s for incident %s didn't change." % (key, incident[0]['incident_id'])) 120 | 121 | del incident[0]['_key'] 122 | contentsStr = json.dumps(incident[0]) 123 | logger.debug("content for update: %s" % contentsStr) 124 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, jsonargs=contentsStr) 125 | logger.debug("Response from update incident entry was %s " % serverResponse) 126 | 127 | 128 | return 'Data has been saved' 129 | 130 | -------------------------------------------------------------------------------- /appserver/controllers/user_settings.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import json 5 | import shutil 6 | import cherrypy 7 | import re 8 | import time 9 | import datetime 10 | import urllib 11 | 12 | #from splunk import AuthorizationFailed as AuthorizationFailed 13 | import splunk.appserver.mrsparkle.controllers as controllers 14 | import splunk.appserver.mrsparkle.lib.util as util 15 | import splunk.bundle as bundle 16 | import splunk.entity as entity 17 | from splunk.entity import Entity 18 | from splunk.appserver.mrsparkle.lib import jsonresponse 19 | from splunk.appserver.mrsparkle.lib.util import make_splunkhome_path 20 | import splunk.clilib.bundle_paths as bundle_paths 21 | from splunk.util import normalizeBoolean as normBool 22 | from splunk.appserver.mrsparkle.lib.decorators import expose_page 23 | from splunk.appserver.mrsparkle.lib.routes import route 24 | import splunk.rest as rest 25 | 26 | dir = os.path.join(util.get_apps_dir(), __file__.split('.')[-2], 'bin') 27 | 28 | if not dir in sys.path: 29 | sys.path.append(dir) 30 | 31 | 32 | #sys.stdout = open('/tmp/stdout', 'w') 33 | #sys.stderr = open('/tmp/stderr', 'w') 34 | 35 | 36 | def setup_logger(level): 37 | """ 38 | Setup a logger for the REST handler. 39 | """ 40 | 41 | logger = logging.getLogger('splunk.appserver.alert_manager.controllers.UserSettings') 42 | logger.propagate = False # Prevent the log messages from being duplicated in the python.log file 43 | logger.setLevel(level) 44 | 45 | file_handler = logging.handlers.RotatingFileHandler(make_splunkhome_path(['var', 'log', 'splunk', 'alert_manager_settings_controller.log']), maxBytes=25000000, backupCount=5) 46 | 47 | formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') 48 | file_handler.setFormatter(formatter) 49 | logger.addHandler(file_handler) 50 | return logger 51 | 52 | logger = setup_logger(logging.DEBUG) 53 | 54 | from splunk.models.base import SplunkAppObjModel 55 | from splunk.models.field import BoolField, Field 56 | 57 | 58 | 59 | class UserSettings(controllers.BaseController): 60 | 61 | @expose_page(must_login=True, methods=['POST']) 62 | def set_user_directory(self, user_directory, **kwargs): 63 | logger.info("Set active user directory to %s" % user_directory) 64 | user = cherrypy.session['user']['name'] 65 | sessionKey = cherrypy.session.get('sessionKey') 66 | 67 | config = entity.getEntities('configs/alert_manager', count=-1, sessionKey=sessionKey) 68 | 69 | settings = dict(config['settings']) 70 | if 'eai:acl' in settings: 71 | del settings['eai:acl'] 72 | 73 | settings['user_directories'] = user_directory 74 | 75 | logger.debug("settings: %s" % settings) 76 | 77 | uri = '/servicesNS/nobody/alert_manager/admin/alert_manager/settings?%s' % urllib.urlencode(settings) 78 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, method='POST') 79 | 80 | logger.debug("Active directory changed. Response: %s" % serverResponse) 81 | 82 | return 'Ok' 83 | 84 | @expose_page(must_login=True, methods=['POST']) 85 | def delete(self, key, **kwargs): 86 | logger.info("Removing user settings for %s..." % key) 87 | 88 | user = cherrypy.session['user']['name'] 89 | sessionKey = cherrypy.session.get('sessionKey') 90 | 91 | query = {} 92 | query['_key'] = key 93 | logger.debug("Query for user settings: %s" % urllib.quote(json.dumps(query))) 94 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/alert_users?query=%s' % urllib.quote(json.dumps(query)) 95 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, method='DELETE') 96 | 97 | logger.debug("User removed. serverResponse was %s" % serverResponse) 98 | 99 | return 'User settings have been removed for entry with _key=%s' % key 100 | 101 | 102 | @expose_page(must_login=True, methods=['POST']) 103 | def save(self, contents, **kwargs): 104 | 105 | logger.info("Saving user settings contents...") 106 | 107 | user = cherrypy.session['user']['name'] 108 | sessionKey = cherrypy.session.get('sessionKey') 109 | 110 | 111 | # Parse the JSON 112 | parsed_contents = json.loads(contents) 113 | 114 | logger.debug("Contents: %s" % contents) 115 | 116 | for entry in parsed_contents: 117 | if '_key' in entry and entry['_key'] != None and entry['_key'] != 'n/a': 118 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/alert_users/' + entry['_key'] 119 | logger.debug("uri is %s" % uri) 120 | 121 | del entry['_key'] 122 | if 'type' in entry: 123 | del entry['type'] 124 | 125 | entry = json.dumps(entry) 126 | 127 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, jsonargs=entry) 128 | logger.debug("Updated entry. serverResponse was %s" % serverResponse) 129 | else: 130 | if '_key' in entry: 131 | del entry['_key'] 132 | if 'type' in entry: 133 | del entry['type'] 134 | 135 | ['' if val is None else val for val in entry] 136 | 137 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/alert_users/' 138 | logger.debug("uri is %s" % uri) 139 | 140 | entry = json.dumps(entry) 141 | logger.debug("entry is %s" % entry) 142 | 143 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, jsonargs=entry) 144 | logger.debug("Added entry. serverResponse was %s" % serverResponse) 145 | 146 | return 'Data has been saved' 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /appserver/controllers/helpers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import json 5 | import shutil 6 | import cherrypy 7 | import re 8 | import time 9 | import datetime 10 | import urllib 11 | 12 | #from splunk import AuthorizationFailed as AuthorizationFailed 13 | import splunk.appserver.mrsparkle.controllers as controllers 14 | import splunk.appserver.mrsparkle.lib.util as util 15 | import splunk.bundle as bundle 16 | import splunk.entity as entity 17 | from splunk.appserver.mrsparkle.lib import jsonresponse 18 | from splunk.appserver.mrsparkle.lib.util import make_splunkhome_path 19 | import splunk.clilib.bundle_paths as bundle_paths 20 | from splunk.util import normalizeBoolean as normBool 21 | from splunk.appserver.mrsparkle.lib.decorators import expose_page 22 | from splunk.appserver.mrsparkle.lib.routes import route 23 | import splunk.rest as rest 24 | 25 | dir = os.path.join(util.get_apps_dir(), 'alert_manager', 'bin', 'lib') 26 | if not dir in sys.path: 27 | sys.path.append(dir) 28 | 29 | from AlertManagerUsers import * 30 | 31 | #sys.stdout = open('/tmp/stdout', 'w') 32 | #sys.stderr = open('/tmp/stderr', 'w') 33 | 34 | 35 | def setup_logger(level): 36 | """ 37 | Setup a logger for the REST handler. 38 | """ 39 | 40 | logger = logging.getLogger('splunk.appserver.alert_manager.controllers.Helpers') 41 | logger.propagate = False # Prevent the log messages from being duplicated in the python.log file 42 | logger.setLevel(level) 43 | 44 | file_handler = logging.handlers.RotatingFileHandler(make_splunkhome_path(['var', 'log', 'splunk', 'alert_manager_helpers_controller.log']), maxBytes=25000000, backupCount=5) 45 | 46 | formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') 47 | file_handler.setFormatter(formatter) 48 | logger.addHandler(file_handler) 49 | return logger 50 | 51 | logger = setup_logger(logging.DEBUG) 52 | 53 | from splunk.models.base import SplunkAppObjModel 54 | from splunk.models.field import BoolField, Field 55 | 56 | 57 | 58 | class Helpers(controllers.BaseController): 59 | 60 | @expose_page(must_login=True, methods=['GET']) 61 | def get_users(self, **kwargs): 62 | logger.info("Get users") 63 | 64 | user = cherrypy.session['user']['name'] 65 | sessionKey = cherrypy.session.get('sessionKey') 66 | 67 | users = AlertManagerUsers(sessionKey=sessionKey) 68 | user_list = users.getUserList() 69 | 70 | logger.debug("user_list: %s " % json.dumps(user_list)) 71 | 72 | return json.dumps(user_list) 73 | 74 | @expose_page(must_login=True, methods=['GET']) 75 | def get_indexes(self, **kwargs): 76 | logger.info("Get indexes") 77 | 78 | user = cherrypy.session['user']['name'] 79 | sessionKey = cherrypy.session.get('sessionKey') 80 | 81 | 82 | uri = '/services/admin/indexes?output_mode=json' 83 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, method='GET') 84 | #logger.debug("response: %s" % serverContent) 85 | entries = json.loads(serverContent) 86 | 87 | index_list = [] 88 | if len(entries['entry']) > 0: 89 | for entry in entries['entry']: 90 | index_list.append(entry['name']) 91 | 92 | 93 | return json.dumps(index_list) 94 | 95 | @expose_page(must_login=True, methods=['GET']) 96 | def get_email_templates(self, **kwargs): 97 | logger.info("Get templates") 98 | 99 | user = cherrypy.session['user']['name'] 100 | sessionKey = cherrypy.session.get('sessionKey') 101 | 102 | 103 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/email_templates?q=&output_mode=json' 104 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, method='GET') 105 | logger.debug("response: %s" % serverContent) 106 | entries = json.loads(serverContent) 107 | 108 | template_list = [ "notify_user" ] 109 | if len(entries) > 0: 110 | for entry in entries: 111 | template_list.append(entry['email_template_name']) 112 | 113 | 114 | return json.dumps(template_list) 115 | 116 | @expose_page(must_login=True, methods=['GET']) 117 | def get_email_template_files(self, **kwargs): 118 | logger.info("Get templates files") 119 | 120 | user = cherrypy.session['user']['name'] 121 | sessionKey = cherrypy.session.get('sessionKey') 122 | 123 | file_list = [] 124 | 125 | file_default_dir = os.path.join(os.environ.get('SPLUNK_HOME'), "etc", "apps", "alert_manager", "default", "templates") 126 | if os.path.exists(file_default_dir): 127 | for f in os.listdir(file_default_dir): 128 | if re.match(r'.*\.html', f): 129 | if f not in file_list: 130 | file_list.append(f) 131 | 132 | file_local_dir = os.path.join(os.environ.get('SPLUNK_HOME'), "etc", "apps", "alert_manager", "local", "templates") 133 | if os.path.exists(file_local_dir): 134 | for f in os.listdir(file_local_dir): 135 | if re.match(r'.*\.html', f): 136 | if f not in file_list: 137 | file_list.append(f) 138 | 139 | return json.dumps(file_list) 140 | 141 | 142 | @expose_page(must_login=True, methods=['GET']) 143 | def get_savedsearch_description(self, savedsearch, app, **kwargs): 144 | user = cherrypy.session['user']['name'] 145 | sessionKey = cherrypy.session.get('sessionKey') 146 | 147 | uri = '/servicesNS/nobody/%s/admin/savedsearch/%s?output_mode=json' % (app, savedsearch) 148 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, method='GET') 149 | 150 | savedSearchContent = json.loads(serverContent) 151 | 152 | if savedSearchContent["entry"][0]["content"]["description"]: 153 | return savedSearchContent["entry"][0]["content"]["description"] 154 | else: 155 | return "" 156 | -------------------------------------------------------------------------------- /default/data/ui/views/kpi_report_incident_status.xml: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 | -24h@h 8 | now 9 | 10 | 11 |
12 | 13 | 14 | Average Time Spent at Status 15 | 16 | 17 | 18 | |inputlookup alert_status |table status, status_description 19 | 20 | status_description 21 | status 22 | new,assigned,auto_assigned,work_in_progress,resolved,auto_ttl_resolved,auto_previous_resolved 23 | " 24 | OR 25 | status=" 26 | 27 | 28 | 29 | priority 30 | 31 | |inputlookup kpi_report_dropdowns | table label, value 32 | 33 | label 34 | value 35 | 36 | 37 | 38 | eventtype=incident_change status=* | eval etime=_time | transaction mvlist=incident_id,status,etime incident_id |eval zip=mvzip(incident_id,mvzip(alert,mvzip( etime,status))) |rex field=zip "(?<incident_id>[^,]+),(?<alert>[^,]+),(?<etime>[^,]+),(?<status>\w+)" | streamstats current=f global=f window=1 last(etime) as last_time by incident_id | eval dtime=coalesce(last_time-etime,0) |addinfo |eval dtime=if(status=="new" AND dtime=0, info_max_time-etime, dtime) |eval dtime=if((status=="resolved" OR status=="auto_ttl_resolved" OR status=="auto_previous_resolved") AND dtime=0, info_max_time-etime, dtime) |table _time, incident_id, alert, etime, last_time, dtime, status |lookup incidents incident_id OUTPUT alert, owner, impact, urgency | lookup alert_priority impact,urgency OUTPUT priority | lookup incident_settings alert OUTPUT category, subcategory | lookup alert_status status OUTPUT status_description | search $status$ | chart avg(dtime) as "average duration" over $series$ by status_description 39 | $global_timerange.earliest$ 40 | $global_timerange.latest$ 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | Top 10 Longrunner Incidents in Status New 75 | 76 | 77 | eventtype=incident_change status=* |stats count(status) as status_count values(status) as status first(alert) as alert first(owner) as owner min(_time) as _time by incident_id |search status=new status_count=1 |addinfo |eval duration=info_max_time-_time | lookup incidents incident_id OUTPUT impact, urgency | lookup alert_priority impact, urgency OUTPUT priority | lookup incident_settings alert OUTPUT category, subcategory | sort 10 -duration | fieldformat duration=tostring(duration,"duration") |table _time, incident_id, alert, category, owner, priority, status, duration 78 | $global_timerange.earliest$ 79 | $global_timerange.latest$ 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 |
95 |
96 |
97 |
98 | -------------------------------------------------------------------------------- /appserver/static/user_settings.js: -------------------------------------------------------------------------------- 1 | require([ 2 | "splunkjs/mvc", 3 | "splunkjs/mvc/utils", 4 | "splunkjs/mvc/tokenutils", 5 | "underscore", 6 | "jquery", 7 | "splunkjs/mvc/simplexml", 8 | 'splunkjs/mvc/tableview', 9 | 'splunkjs/mvc/chartview', 10 | 'splunkjs/mvc/searchmanager', 11 | 'splunk.util', 12 | ], function( 13 | mvc, 14 | utils, 15 | TokenUtils, 16 | _, 17 | $, 18 | DashboardController, 19 | TableView, 20 | ChartView, 21 | SearchManager, 22 | splunkUtil 23 | ) { 24 | 25 | // Tokens 26 | var submittedTokens = mvc.Components.getInstance('submitted', {create: true}); 27 | var defaultTokens = mvc.Components.getInstance('default', {create: true}); 28 | 29 | var user_directories = ["both", "builtin", "alert_manager"]; 30 | var active_user_directory_search = mvc.Components.getInstance('active_user_directory'); 31 | var active_user_directory_data = active_user_directory_search.data("preview", { count: 1 }); 32 | var active_user_directory = ''; 33 | 34 | active_user_directory_data.on("data", function() { 35 | active_user_directory = active_user_directory_data.data()["rows"][0][0]; 36 | 37 | if($("#user_directories").length == 0) { 38 | $(" 5 | 6 | -24h@h 7 | now 8 | 9 | 10 | 11 | 12 | 13 | |inputlookup incidents |dedup owner |table owner |sort owner 14 | -1m 15 | now 16 | 17 | owner 18 | owner 19 | All 20 | * 21 | 22 | 23 | 24 | All 25 | * 26 | * 27 | 28 | |inputlookup incident_settings |dedup alert |table alert |sort alert 29 | -1m 30 | now 31 | 32 | alert 33 | alert 34 | 35 | 36 | 37 | All 38 | * 39 | * 40 | 41 | |inputlookup incident_settings |dedup category |table category |sort category 42 | -1m 43 | now 44 | 45 | category 46 | category 47 | 48 | 49 | 50 | All 51 | * 52 | * 53 | 54 | |inputlookup incident_settings |dedup subcategory |table subcategory |sort subcategory 55 | -1m 56 | now 57 | 58 | subcategory 59 | subcategory 60 | 61 | 62 | 63 | All 64 | * 65 | *,[Untagged] 66 | 67 | |inputlookup incident_settings | makemv delim=" " tags | mvexpand tags | dedup tags | table tags | sort tags 68 | -1m 69 | now 70 | 71 | tags 72 | tags 73 | " 74 | OR 75 | tags=" 76 | 77 | 78 | 79 | * 80 | 81 | 82 | 83 | All 84 | * 85 | * 86 | 87 | |inputlookup alert_priority | dedup impact | table impact 88 | -1m 89 | now 90 | 91 | impact 92 | impact 93 | " 94 | OR 95 | impact=" 96 | 97 | 98 | 99 | All 100 | * 101 | * 102 | 103 | |inputlookup alert_priority | dedup urgency | table urgency 104 | -1m 105 | now 106 | 107 | urgency 108 | urgency 109 | " 110 | OR 111 | urgency=" 112 | 113 | 114 | 115 | All 116 | * 117 | * 118 | 119 | |inputlookup alert_priority | dedup priority | table priority 120 | -1m 121 | now 122 | 123 | priority 124 | priority 125 | " 126 | OR 127 | priority=" 128 | 129 | 130 | 131 | 132 | 133 | 134 | 137 | $earliest$ 138 | $latest$ 139 | 140 | 141 |
148 |
149 | 150 |
151 |
152 | 153 | -------------------------------------------------------------------------------- /appserver/static/email_settings.js: -------------------------------------------------------------------------------- 1 | require([ 2 | "splunkjs/mvc", 3 | "splunkjs/mvc/utils", 4 | "splunkjs/mvc/tokenutils", 5 | "underscore", 6 | "jquery", 7 | "splunkjs/mvc/simplexml", 8 | 'splunkjs/mvc/tableview', 9 | 'splunkjs/mvc/chartview', 10 | 'splunkjs/mvc/searchmanager', 11 | 'splunk.util', 12 | ], function( 13 | mvc, 14 | utils, 15 | TokenUtils, 16 | _, 17 | $, 18 | DashboardController, 19 | TableView, 20 | ChartView, 21 | SearchManager, 22 | splunkUtil 23 | ) { 24 | 25 | // Tokens 26 | var submittedTokens = mvc.Components.getInstance('submitted', {create: true}); 27 | var defaultTokens = mvc.Components.getInstance('default', {create: true}); 28 | 29 | // Save templates 30 | $(document).on("click", "#save_templates", function(event){ 31 | // save data here 32 | 33 | var data = $("#handson_container_templates").data('handsontable').getData(); 34 | console.debug("save template data", data); 35 | 36 | // remove builtin-users 37 | var data = _.filter(data, function(entry){ 38 | return entry['_key'] != "n/a" 39 | }); 40 | 41 | // validate data 42 | var check = _.filter(data, function(entry){ 43 | return entry['email_template_name']== null || entry['email_template_file'] == true || entry['email_content_type'] == null || entry['email_from'] == "" || entry['email_subject'] == null; 44 | }); 45 | console.debug("check", check); 46 | if (check.length>0) { 47 | var modal = ''+ 48 | ''; 64 | $('body').prepend(modal); 65 | $('#validation_failed').modal('show'); 66 | } else { 67 | 68 | data = JSON.stringify(data); 69 | var post_data = { 70 | contents : data 71 | }; 72 | 73 | var url = splunkUtil.make_url('/custom/alert_manager/email_settings/save_templates'); 74 | console.debug("url", url); 75 | 76 | $.ajax( url, 77 | { 78 | uri: url, 79 | type: 'POST', 80 | data: post_data, 81 | 82 | 83 | success: function(jqXHR, textStatus){ 84 | // Reload the table 85 | mvc.Components.get("email_templates_search").startSearch() 86 | console.debug("success"); 87 | }, 88 | 89 | // Handle cases where the file could not be found or the user did not have permissions 90 | complete: function(jqXHR, textStatus){ 91 | console.debug("complete"); 92 | }, 93 | 94 | error: function(jqXHR,textStatus,errorThrown) { 95 | console.log("Error"); 96 | } 97 | } 98 | ); 99 | } 100 | 101 | }); 102 | 103 | 104 | // Save Settings 105 | $(document).on("click", "#save_settings", function(event){ 106 | // save data here 107 | 108 | var data = $("#handson_container_settings").data('handsontable').getData(); 109 | console.debug("save settings data", data); 110 | 111 | // remove builtin-users 112 | var data = _.filter(data, function(entry){ 113 | return entry['_key'] != "n/a" 114 | }); 115 | 116 | // validate data 117 | var check = _.filter(data, function(entry){ 118 | return entry['alert']== null || entry['notify_user_template'] == null; 119 | }); 120 | console.debug("check", check); 121 | if (check.length>0) { 122 | var modal = ''+ 123 | ''; 139 | $('body').prepend(modal); 140 | $('#validation_failed').modal('show'); 141 | } else { 142 | 143 | data = JSON.stringify(data); 144 | var post_data = { 145 | contents : data 146 | }; 147 | 148 | var url = splunkUtil.make_url('/custom/alert_manager/email_settings/save_settings'); 149 | console.debug("url", url); 150 | 151 | $.ajax( url, 152 | { 153 | uri: url, 154 | type: 'POST', 155 | data: post_data, 156 | 157 | 158 | success: function(jqXHR, textStatus){ 159 | // Reload the table 160 | mvc.Components.get("email_settings_search").startSearch() 161 | console.debug("success"); 162 | }, 163 | 164 | // Handle cases where the file could not be found or the user did not have permissions 165 | complete: function(jqXHR, textStatus){ 166 | console.debug("complete"); 167 | }, 168 | 169 | error: function(jqXHR,textStatus,errorThrown) { 170 | console.log("Error"); 171 | } 172 | } 173 | ); 174 | } 175 | 176 | }); 177 | }); -------------------------------------------------------------------------------- /appserver/controllers/email_settings.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import json 5 | import shutil 6 | import cherrypy 7 | import re 8 | import time 9 | import datetime 10 | import urllib 11 | 12 | #from splunk import AuthorizationFailed as AuthorizationFailed 13 | import splunk.appserver.mrsparkle.controllers as controllers 14 | import splunk.appserver.mrsparkle.lib.util as util 15 | import splunk.bundle as bundle 16 | import splunk.entity as entity 17 | from splunk.entity import Entity 18 | from splunk.appserver.mrsparkle.lib import jsonresponse 19 | from splunk.appserver.mrsparkle.lib.util import make_splunkhome_path 20 | import splunk.clilib.bundle_paths as bundle_paths 21 | from splunk.util import normalizeBoolean as normBool 22 | from splunk.appserver.mrsparkle.lib.decorators import expose_page 23 | from splunk.appserver.mrsparkle.lib.routes import route 24 | import splunk.rest as rest 25 | 26 | dir = os.path.join(util.get_apps_dir(), __file__.split('.')[-2], 'bin') 27 | 28 | if not dir in sys.path: 29 | sys.path.append(dir) 30 | 31 | 32 | #sys.stdout = open('/tmp/stdout', 'w') 33 | #sys.stderr = open('/tmp/stderr', 'w') 34 | 35 | 36 | def setup_logger(level): 37 | """ 38 | Setup a logger for the REST handler. 39 | """ 40 | 41 | logger = logging.getLogger('splunk.appserver.alert_manager.controllers.EmailSettings') 42 | logger.propagate = False # Prevent the log messages from being duplicated in the python.log file 43 | logger.setLevel(level) 44 | 45 | file_handler = logging.handlers.RotatingFileHandler(make_splunkhome_path(['var', 'log', 'splunk', 'alert_manager_settings_controller.log']), maxBytes=25000000, backupCount=5) 46 | 47 | formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') 48 | file_handler.setFormatter(formatter) 49 | logger.addHandler(file_handler) 50 | return logger 51 | 52 | logger = setup_logger(logging.DEBUG) 53 | 54 | from splunk.models.base import SplunkAppObjModel 55 | from splunk.models.field import BoolField, Field 56 | 57 | 58 | 59 | class EmailSettings(controllers.BaseController): 60 | 61 | @expose_page(must_login=True, methods=['POST']) 62 | def delete_template(self, key, **kwargs): 63 | logger.info("Removing template for %s..." % key) 64 | 65 | user = cherrypy.session['user']['name'] 66 | sessionKey = cherrypy.session.get('sessionKey') 67 | 68 | query = {} 69 | query['_key'] = key 70 | logger.debug("Query for email templates: %s" % urllib.quote(json.dumps(query))) 71 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/email_templates?query=%s' % urllib.quote(json.dumps(query)) 72 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, method='DELETE') 73 | 74 | logger.debug("Template removed. serverResponse was %s" % serverResponse) 75 | 76 | return 'Template has been removed for entry with _key=%s' % key 77 | 78 | @expose_page(must_login=True, methods=['POST']) 79 | def delete_settings(self, key, **kwargs): 80 | logger.info("Removing settings for %s..." % key) 81 | 82 | user = cherrypy.session['user']['name'] 83 | sessionKey = cherrypy.session.get('sessionKey') 84 | 85 | query = {} 86 | query['_key'] = key 87 | logger.debug("Query for email settings: %s" % urllib.quote(json.dumps(query))) 88 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/email_settings?query=%s' % urllib.quote(json.dumps(query)) 89 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, method='DELETE') 90 | 91 | logger.debug("Email settings removed. serverResponse was %s" % serverResponse) 92 | 93 | return 'Email settings have been removed for entry with _key=%s' % key 94 | 95 | @expose_page(must_login=True, methods=['POST']) 96 | def save_templates(self, contents, **kwargs): 97 | 98 | logger.info("Saving email_templates contents...") 99 | 100 | user = cherrypy.session['user']['name'] 101 | sessionKey = cherrypy.session.get('sessionKey') 102 | 103 | 104 | # Parse the JSON 105 | parsed_contents = json.loads(contents) 106 | 107 | logger.debug("Contents: %s" % contents) 108 | 109 | for entry in parsed_contents: 110 | if '_key' in entry and entry['_key'] != None and entry['_key'] != 'n/a': 111 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/email_templates/' + entry['_key'] 112 | logger.debug("uri is %s" % uri) 113 | 114 | entry = json.dumps(entry) 115 | 116 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, jsonargs=entry) 117 | logger.debug("Updated entry. serverResponse was %s" % serverResponse) 118 | else: 119 | if '_key' in entry: 120 | del entry['_key'] 121 | 122 | ['' if val is None else val for val in entry] 123 | 124 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/email_templates/' 125 | logger.debug("uri is %s" % uri) 126 | 127 | entry = json.dumps(entry) 128 | logger.debug("entry is %s" % entry) 129 | 130 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, jsonargs=entry) 131 | logger.debug("Added entry. serverResponse was %s" % serverResponse) 132 | 133 | return 'Data has been saved' 134 | 135 | @expose_page(must_login=True, methods=['POST']) 136 | def save_settings(self, contents, **kwargs): 137 | 138 | logger.info("Saving email_settings contents...") 139 | 140 | user = cherrypy.session['user']['name'] 141 | sessionKey = cherrypy.session.get('sessionKey') 142 | 143 | 144 | # Parse the JSON 145 | parsed_contents = json.loads(contents) 146 | 147 | logger.debug("Contents: %s" % contents) 148 | 149 | for entry in parsed_contents: 150 | if '_key' in entry and entry['_key'] != None and entry['_key'] != 'n/a': 151 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/email_settings/' + entry['_key'] 152 | logger.debug("uri is %s" % uri) 153 | 154 | entry = json.dumps(entry) 155 | 156 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, jsonargs=entry) 157 | logger.debug("Updated entry. serverResponse was %s" % serverResponse) 158 | else: 159 | if '_key' in entry: 160 | del entry['_key'] 161 | 162 | ['' if val is None else val for val in entry] 163 | 164 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/email_settings/' 165 | logger.debug("uri is %s" % uri) 166 | 167 | entry = json.dumps(entry) 168 | logger.debug("entry is %s" % entry) 169 | 170 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=sessionKey, jsonargs=entry) 171 | logger.debug("Added entry. serverResponse was %s" % serverResponse) 172 | 173 | return 'Data has been saved' 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /default/data/ui/views/kpi_report_resolved_incidents.xml: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 | -24h@h 8 | now 9 | 10 | 11 | 12 | 13 | 14 | |inputlookup alert_status |search status="*resolved*" 15 | 16 | status_description 17 | status 18 | resolved 19 | 20 | 21 | 22 | 23 | |inputlookup kpi_report_dropdowns | table label, value 24 | 25 | label 26 | value 27 | category 28 | 29 | 30 | 31 | priority 32 | 33 | |inputlookup kpi_report_dropdowns | table label, value |search value!="$series$" 34 | 35 | label 36 | value 37 | 38 |
39 | 40 | eventtype=incident_change status=* |transaction incident_id |search status="new" status="$status$" |lookup incident_settings alert OUTPUT category, subcategory |lookup incidents incident_id OUTPUT impact, urgency | lookup alert_priority impact, urgency OUTPUT priority | stats avg(duration) as duration list(alert) as alert last(status) as status last(owner) as owner last(impact) as impact last(urgency) as urgency last(priority) as priority values(category) as category by job_id | eval duration=round(duration, 0) 41 | $global_time.earliest$ 42 | $global_time.latest$ 43 | 44 | 45 | 46 | Average Resolve Duration 47 | 48 | 49 | | chart avg(duration) as duration over $series$ by $splitby$ 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | chart avg(duration) as duration over $series$ by $splitby$ 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
90 |
91 | 92 | Number of Resolved Incidents 93 | 94 | 95 | chart count over $series$ by $splitby$ 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | chart count over $series$ by $splitby$ 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 |
136 |
137 |
138 |
139 | -------------------------------------------------------------------------------- /default/data/ui/html/incident_posture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Incident Posture | Splunk 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Screen reader users, click here to skip the navigation bar 16 |
17 |
18 | splunk> 19 |
20 |
21 |
22 | 23 |
24 |
25 |

Incident Posture

26 |
27 |
28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 |

Informational

45 |
46 |
47 |
48 |
49 |
50 |

Low

51 |
52 |
53 |
54 |
55 |
56 |

Medium

57 |
58 |
59 |
60 |
61 |
62 |

High

63 |
64 |
65 |
66 |
67 |
68 |

Critical

69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |

Recent Incidents

80 |
81 |
82 | 83 |
84 |
85 | 86 |
87 |
88 | 89 |
90 |
91 | 92 |
93 |
94 | 95 |
96 |
97 | 98 |
99 |
100 | 101 |
102 |
103 | 104 |
105 |
106 | 107 |
108 |
109 | 110 |
111 |
112 | 113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 | 125 |
126 |
127 |
128 |

Alert Results

129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | 138 | 139 | 140 | 141 | 142 | 143 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /appserver/static/views/emailsettingsview.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | paths: { 3 | "app": "../app" 4 | }, 5 | shim: { 6 | "app/alert_manager/contrib/handsontable-0.12.2/handsontable.full.min": { 7 | deps: ['css!../handsontable-0.12.2/handsontable.full.min.css'], 8 | exports: "Handsontable" 9 | }, 10 | } 11 | }); 12 | 13 | 14 | 15 | 16 | define(function(require, exports, module) { 17 | 18 | var _ = require('underscore'); 19 | var $ = require('jquery'); 20 | var mvc = require('splunkjs/mvc'); 21 | var SimpleSplunkView = require('splunkjs/mvc/simplesplunkview'); 22 | var Handsontable = require('app/alert_manager/contrib/handsontable-0.12.2/handsontable.full.min'); 23 | var splunkUtil = require('splunk.util'); 24 | 25 | var EmailSettingsView = SimpleSplunkView.extend({ 26 | className: "emailsettingsview", 27 | 28 | del_key_container: '', 29 | 30 | // Set options for the visualization 31 | options: { 32 | data: "preview", // The data results model from a search 33 | }, 34 | output_mode: 'json', 35 | 36 | 37 | createView: function() { 38 | console.log("createView"); 39 | return { container: this.$el, } ; 40 | }, 41 | 42 | updateView: function(viz, data) { 43 | 44 | console.log("updateView", data); 45 | 46 | this.$el.empty(); 47 | 48 | $('
').attr('id', 'handson_container_settings').appendTo(this.$el); 49 | 50 | //debugger; 51 | var templates = new Array(); 52 | 53 | var url = splunkUtil.make_url('/custom/alert_manager/helpers/get_email_templates'); 54 | $.get( url,function(data) { 55 | _.each(data, function(el) { 56 | templates.push(el); 57 | }); 58 | }, "json"); 59 | console.debug("templates", templates); 60 | 61 | headers = [ { col: "_key", tooltip: false }, 62 | { col: "alert", tooltip: false }, 63 | { col: "notify_user_template", tooltip: "Send a notification to users when an incident has been assigned or auto-assgined." } ]; 64 | $("#handson_container_settings").handsontable({ 65 | data: data, 66 | minSpareRows: 1, 67 | columns: [ 68 | { 69 | data: "_key", 70 | readOnly: true 71 | }, 72 | { 73 | data: "alert", 74 | }, 75 | { 76 | data: "notify_user_template", 77 | type: "dropdown", 78 | source: templates 79 | } 80 | ], 81 | colHeaders: true, 82 | colHeaders: function (col) { 83 | if (headers[col]["tooltip"] != false) { 84 | colval = headers[col]["col"] + '?'; 85 | } 86 | else { 87 | colval = headers[col]["col"]; 88 | } 89 | return colval; 90 | }, 91 | cells: function (row, col, prop) { 92 | var cellProperties = {}; 93 | if (this.instance.getData()[row]["_key"] === 'n/a') { 94 | cellProperties.readOnly = true; 95 | } 96 | return cellProperties; 97 | }, 98 | stretchH: 'all', 99 | contextMenu: ['row_above', 'row_below', 'remove_row', 'undo', 'redo'], 100 | startRows: 1, 101 | startCols: 1, 102 | minSpareRows: 0, 103 | minSpareCols: 0, 104 | afterRender: function() { 105 | $(function () { 106 | $('[data-toggle="tooltip"]').tooltip() 107 | }) 108 | }, 109 | beforeRemoveRow: function(row) { 110 | console.debug("row", row); 111 | var data = $("#handson_container_settings").data('handsontable').getData(); 112 | console.log("_key", data[row]['_key']); 113 | 114 | if(!data[row]['_key'] && !data[row]['alert'] && !data[row]['notify_user_template']) { 115 | this.del_key_container = false; 116 | return true; 117 | } else { 118 | if(confirm('Are you sure to remove email settings for alert "' + data[row]['alert'] + '"?')) { 119 | if(!data[row]['_key']) { 120 | this.del_key_container = false; 121 | } else { 122 | this.del_key_container = data[row]['_key']; 123 | } 124 | return true; 125 | } else { 126 | return false; 127 | } 128 | } 129 | 130 | }, 131 | afterRemoveRow: function(row) { 132 | console.debug("afterRemoveRow"); 133 | //var data = $("#handson_container").data('handsontable').getData(); 134 | console.debug("row", row); 135 | //console.debug("data", data); 136 | console.debug("key", this.del_key_container); 137 | if(this.del_key_container == false) { 138 | // Removal of empty row - nothing to do 139 | return true; 140 | } 141 | 142 | var post_data = { 143 | key : this.del_key_container 144 | }; 145 | 146 | var url = splunkUtil.make_url('/custom/alert_manager/email_settings/delete_settings'); 147 | console.debug("url", url); 148 | 149 | $.ajax( url, 150 | { 151 | uri: url, 152 | type: 'POST', 153 | data: post_data, 154 | 155 | 156 | success: function(jqXHR, textStatus){ 157 | this.del_key_container = ''; 158 | // Reload the table 159 | mvc.Components.get("email_settings_search").startSearch() 160 | console.debug("success"); 161 | }, 162 | 163 | // Handle cases where the file could not be found or the user did not have permissions 164 | complete: function(jqXHR, textStatus){ 165 | console.debug("complete"); 166 | }, 167 | 168 | error: function(jqXHR,textStatus,errorThrown) { 169 | console.log("Error"); 170 | } 171 | } 172 | ); 173 | } 174 | }); 175 | //console.debug("id", id); 176 | 177 | 178 | //debugger; 179 | //id 180 | 181 | }, 182 | 183 | // Override this method to format the data for the view 184 | formatData: function(data) { 185 | console.log("formatData", data); 186 | 187 | myData = [] 188 | _(data).chain().map(function(val) { 189 | return { 190 | _key: val.key, 191 | alert: val.alert, 192 | notify_user_template: val.notify_user_template 193 | }; 194 | }).each(function(line) { 195 | myData.push(line); 196 | }); 197 | 198 | return myData; 199 | }, 200 | 201 | }); 202 | return EmailSettingsView; 203 | }); 204 | -------------------------------------------------------------------------------- /bin/lib/AlertManagerNotifications.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import json 4 | import splunk.entity as entity 5 | import splunk.rest as rest 6 | import logging 7 | import urllib 8 | import smtplib 9 | import re 10 | import socket 11 | import django 12 | from django import template 13 | from django.core.mail import send_mail 14 | from django.core.mail import EmailMultiAlternatives 15 | from django.template.loader import get_template 16 | from django.template import Template, Context 17 | from django.conf import settings 18 | from django.utils.html import strip_tags 19 | from django.template.base import TemplateSyntaxError 20 | 21 | 22 | django.template.base.add_to_builtins("AlertManagerNotificationsFilter") 23 | 24 | class AlertManagerNotifications: 25 | 26 | # Setup logger 27 | log = logging.getLogger('alert_manager_notifications') 28 | lf = os.path.join(os.environ.get('SPLUNK_HOME'), "var", "log", "splunk", "alert_manager_notifications.log") 29 | fh = logging.handlers.RotatingFileHandler(lf, maxBytes=25000000, backupCount=5) 30 | formatter = logging.Formatter("%(asctime)-15s %(levelname)-5s %(message)s") 31 | fh.setFormatter(formatter) 32 | log.addHandler(fh) 33 | log.setLevel(logging.DEBUG) 34 | 35 | sessionKey = None 36 | 37 | def __init__(self, sessionKey): 38 | self.sessionKey = sessionKey 39 | 40 | # Setup template paths 41 | local_dir = os.path.join(os.environ.get('SPLUNK_HOME'), "etc", "apps", "alert_manager", "default", "templates") 42 | default_dir = os.path.join(os.environ.get('SPLUNK_HOME'), "etc", "apps", "alert_manager", "local", "templates") 43 | 44 | # Get mailserver settings from splunk 45 | uri = '/servicesNS/nobody/system/configs/conf-alert_actions/email?output_mode=json' 46 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=self.sessionKey) 47 | server_settings = json.loads(serverContent) 48 | server_settings = server_settings["entry"][0]["content"] 49 | #self.log.debug("server settings from splunk: %s" % json.dumps(server_settings)) 50 | 51 | # Parse mailserver 52 | if ":" in server_settings['mailserver']: 53 | match = re.search(r'([^\:]+)\:(\d+)', server_settings['mailserver']) 54 | mailserver = match.group(1) 55 | mailport = int(match.group(2)) 56 | else: 57 | mailserver = server_settings['mailserver'] 58 | mailport = 25 59 | self.log.debug("Parsed mailserver settings. Host: %s, Port: %s" % (mailserver, mailport)) 60 | 61 | use_ssl = False 62 | if server_settings['use_ssl'] == "1": 63 | use_ssl = True 64 | 65 | use_tls = False 66 | if server_settings['use_tls'] == "1": 67 | use_tls = True 68 | 69 | # Configure django settings 70 | settings.configure( TEMPLATE_DIRS=(default_dir, local_dir), 71 | EMAIL_HOST=mailserver, 72 | EMAIL_PORT=mailport, 73 | EMAIL_HOST_USER=server_settings['auth_username'], 74 | EMAIL_HOST_PASSWORD=server_settings['auth_password'], 75 | EMAIL_USE_TLS=use_tls, 76 | EMAIL_USE_SSL=use_ssl 77 | ) 78 | 79 | def send_notification(self, alert, recipient, action, context = {}): 80 | self.log.info("Start trying to send notification to %s with action=%s of alert %s" % (recipient, action, alert)) 81 | 82 | # Get the settings related to the alert 83 | settings = self.get_email_settings(alert) 84 | 85 | # Now get the email template related to the alert and action 86 | if settings == False: 87 | self.log.debug("No email template found for %s, falling back to defaults." % alert) 88 | # TODO: get alert manager settings for defaults. At the moment, there's only notify_user so static entry 89 | alert_email_template_name = 'notify_user' 90 | else: 91 | alert_email_template_name = settings[action + '_template'] 92 | 93 | mail_template = self.get_email_template(alert_email_template_name) 94 | 95 | self.log.debug("Found settings and template file. Ready to send notification.") 96 | 97 | 98 | # Parse html template with django 99 | try: 100 | # Parse body as django template 101 | context = Context(context) 102 | content = get_template(mail_template['email_template_file']).render(context) 103 | self.log.debug("Parsed message body: \"%s\" (Context was %s)" % (content, context)) 104 | 105 | text_content = strip_tags(content) 106 | 107 | # Parse subject as django template 108 | subject_template = Template(mail_template['email_subject']) 109 | subject = subject_template.render(context) 110 | self.log.debug("Parsed message subject: %s" % subject) 111 | 112 | # Prepare message 113 | msg = EmailMultiAlternatives(subject, text_content, mail_template['email_from'], [ recipient ]) 114 | 115 | # Add content as HTML if necessary 116 | if mail_template['email_content_type'] == "html": 117 | msg.attach_alternative(content, "text/html") 118 | 119 | msg.send() 120 | self.log.info("Notification sent successfully to %s" % recipient) 121 | 122 | except TemplateSyntaxError, e: 123 | self.log.error("Unable to parse template %s. Error: %s. Continuing without sending notification..." % (mail_template['email_template_file'], e)) 124 | except smtplib.SMTPServerDisconnected, e: 125 | self.log.error("SMTP server disconnected the connection. Error: %s" % e) 126 | except socket.error, e: 127 | self.log.error("Wasn't able to connect to mailserver. Reason: %s" % e) 128 | except: 129 | exc_type, exc_obj, exc_tb = sys.exc_info() 130 | self.log.error("Unable to send notification. Unexpected error: %s. Line: %s. Continuing without sending notification..." % (exc_type, exc_tb.tb_lineno)) 131 | 132 | 133 | def get_email_settings(self, alert): 134 | query = {} 135 | query["alert"] = alert 136 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/email_settings?output_mode=json&query=%s' % urllib.quote(json.dumps(query)) 137 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=self.sessionKey) 138 | self.log.debug("Response for email settings: %s" % serverContent) 139 | entries = json.loads(serverContent) 140 | 141 | if len(entries) > 0: 142 | return entries[0] 143 | else: 144 | return False 145 | 146 | def get_email_template(self, template_name): 147 | query = {} 148 | query["email_template_name"] = template_name 149 | uri = '/servicesNS/nobody/alert_manager/storage/collections/data/email_templates?output_mode=json&query=%s' % urllib.quote(json.dumps(query)) 150 | serverResponse, serverContent = rest.simpleRequest(uri, sessionKey=self.sessionKey) 151 | self.log.debug("Response for template listing: %s" % serverContent) 152 | entries = json.loads(serverContent) 153 | 154 | if len(entries) > 0: 155 | return entries[0] 156 | else: 157 | return False 158 | 159 | def get_template_file(self, template_file_name): 160 | 161 | self.log.debug("Parsed template file from settings: %s" % template_file_name) 162 | 163 | local_file = os.path.join(os.environ.get('SPLUNK_HOME'), "etc", "apps", "alert_manager", "default", "templates", template_file_name) 164 | default_file = os.path.join(os.environ.get('SPLUNK_HOME'), "etc", "apps", "alert_manager", "local", "templates", template_file_name) 165 | 166 | if os.path.isfile(local_file): 167 | self.log.debug("%s exists in local, using this one..." % template_file_name) 168 | return local_file 169 | else: 170 | self.log.debug("%s not found in local folder, checking if there's one in default..." % template_file_name) 171 | if os.path.isfile(default_file): 172 | self.log.debug("%s exists in default, using this one..." % template_file_name) 173 | return default_file 174 | else: 175 | self.log.debug("%s doesn't exist at all, stopping here.") 176 | return False 177 | 178 | -------------------------------------------------------------------------------- /appserver/static/views/usersettingsview.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | paths: { 3 | "app": "../app" 4 | }, 5 | shim: { 6 | "app/alert_manager/contrib/handsontable-0.12.2/handsontable.full.min": { 7 | deps: ['css!../handsontable-0.12.2/handsontable.full.min.css'], 8 | exports: "Handsontable" 9 | }, 10 | } 11 | }); 12 | 13 | 14 | 15 | 16 | define(function(require, exports, module) { 17 | 18 | var _ = require('underscore'); 19 | var $ = require('jquery'); 20 | var mvc = require('splunkjs/mvc'); 21 | var SimpleSplunkView = require('splunkjs/mvc/simplesplunkview'); 22 | var Handsontable = require('app/alert_manager/contrib/handsontable-0.12.2/handsontable.full.min'); 23 | var splunkUtil = require('splunk.util'); 24 | 25 | var UserSettingsView = SimpleSplunkView.extend({ 26 | className: "usersettingsview", 27 | 28 | del_key_container: '', 29 | 30 | // Set options for the visualization 31 | options: { 32 | data: "preview", // The data results model from a search 33 | }, 34 | output_mode: 'json', 35 | 36 | 37 | createView: function() { 38 | console.log("createView"); 39 | return { container: this.$el, } ; 40 | }, 41 | 42 | updateView: function(viz, data) { 43 | 44 | console.log("updateView", data); 45 | 46 | this.$el.empty(); 47 | 48 | $('
').attr('id', 'handson_container').appendTo(this.$el); 49 | 50 | //debugger; 51 | headers = [ { col: "_key", tooltip: false }, 52 | { col: "user", tooltip: false }, 53 | { col: "email", tooltip: false }, 54 | { col: "notify_user", tooltip: "Check whether the user shall receive a notification on incident assignment" }, 55 | { col: "type", tooltip: false} ]; 56 | $("#handson_container").handsontable({ 57 | data: data, 58 | minSpareRows: 1, 59 | columns: [ 60 | { 61 | data: "_key", 62 | readOnly: true 63 | }, 64 | { 65 | data: "user", 66 | }, 67 | { 68 | data: "email", 69 | }, 70 | { 71 | data: "notify_user", 72 | type: "checkbox", 73 | }, 74 | { 75 | data: "type", 76 | readOnly: true 77 | } 78 | ], 79 | colHeaders: true, 80 | colHeaders: function (col) { 81 | if (headers[col]["tooltip"] != false) { 82 | colval = headers[col]["col"] + '?'; 83 | } 84 | else { 85 | colval = headers[col]["col"]; 86 | } 87 | return colval; 88 | }, 89 | cells: function (row, col, prop) { 90 | var cellProperties = {}; 91 | if (this.instance.getData()[row]["type"] === 'builtin') { 92 | cellProperties.readOnly = true; 93 | } 94 | return cellProperties; 95 | }, 96 | stretchH: 'all', 97 | contextMenu: ['row_above', 'row_below', 'remove_row', 'undo', 'redo'], 98 | startRows: 1, 99 | startCols: 1, 100 | minSpareRows: 0, 101 | minSpareCols: 0, 102 | afterRender: function() { 103 | $(function () { 104 | $('[data-toggle="tooltip"]').tooltip() 105 | }) 106 | }, 107 | beforeRemoveRow: function(row) { 108 | console.debug("row", row); 109 | var data = $("#handson_container").data('handsontable').getData(); 110 | console.log("_key", data[row]['_key']); 111 | console.debug("data[row]['type']", data[row]['type']); 112 | if(data[row]['type'] && data[row]['type'] == "builtin") { 113 | this.del_key_container = false; 114 | return false; 115 | } else { 116 | if(!data[row]['_key'] && !data[row]['user'] && !data[row]['email']) { 117 | this.del_key_container = false; 118 | return true; 119 | } else { 120 | if(confirm('Are you sure to remove user "' + data[row]['user'] + '"?')) { 121 | if(!data[row]['_key']) { 122 | this.del_key_container = false; 123 | } else { 124 | this.del_key_container = data[row]['_key']; 125 | } 126 | return true; 127 | } else { 128 | return false; 129 | } 130 | } 131 | } 132 | }, 133 | afterRemoveRow: function(row) { 134 | console.debug("afterRemoveRow"); 135 | //var data = $("#handson_container").data('handsontable').getData(); 136 | console.debug("row", row); 137 | //console.debug("data", data); 138 | console.debug("key", this.del_key_container); 139 | if(this.del_key_container == false) { 140 | // Removal of empty row - nothing to do 141 | return true; 142 | } 143 | 144 | var post_data = { 145 | key : this.del_key_container 146 | }; 147 | 148 | var url = splunkUtil.make_url('/custom/alert_manager/user_settings/delete'); 149 | console.debug("url", url); 150 | 151 | $.ajax( url, 152 | { 153 | uri: url, 154 | type: 'POST', 155 | data: post_data, 156 | 157 | 158 | success: function(jqXHR, textStatus){ 159 | this.del_key_container = ''; 160 | // Reload the table 161 | mvc.Components.get("user_settings_search").startSearch() 162 | console.debug("success"); 163 | }, 164 | 165 | // Handle cases where the file could not be found or the user did not have permissions 166 | complete: function(jqXHR, textStatus){ 167 | console.debug("complete"); 168 | }, 169 | 170 | error: function(jqXHR,textStatus,errorThrown) { 171 | console.log("Error"); 172 | } 173 | } 174 | ); 175 | } 176 | }); 177 | //console.debug("id", id); 178 | 179 | 180 | //debugger; 181 | //id 182 | 183 | }, 184 | 185 | // Override this method to format the data for the view 186 | formatData: function(data) { 187 | console.log("formatData", data); 188 | 189 | myData = [] 190 | _(data).chain().map(function(val) { 191 | return { 192 | _key: val.key, 193 | user: val.user, 194 | email: val.email, 195 | notify_user: parseInt(val.notify_user) ? true : false, 196 | type: val.type 197 | }; 198 | }).each(function(line) { 199 | myData.push(line); 200 | }); 201 | 202 | return myData; 203 | }, 204 | 205 | }); 206 | return UserSettingsView; 207 | }); 208 | -------------------------------------------------------------------------------- /appserver/static/views/emailtemplatesview.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | paths: { 3 | "app": "../app" 4 | }, 5 | shim: { 6 | "app/alert_manager/contrib/handsontable-0.12.2/handsontable.full.min": { 7 | deps: ['css!../handsontable-0.12.2/handsontable.full.min.css'], 8 | exports: "Handsontable" 9 | }, 10 | } 11 | }); 12 | 13 | 14 | 15 | 16 | define(function(require, exports, module) { 17 | 18 | var _ = require('underscore'); 19 | var $ = require('jquery'); 20 | var mvc = require('splunkjs/mvc'); 21 | var SimpleSplunkView = require('splunkjs/mvc/simplesplunkview'); 22 | var Handsontable = require('app/alert_manager/contrib/handsontable-0.12.2/handsontable.full.min'); 23 | var splunkUtil = require('splunk.util'); 24 | 25 | var EmailTemplatesView = SimpleSplunkView.extend({ 26 | className: "emailtemplatesview", 27 | 28 | del_key_container: '', 29 | 30 | // Set options for the visualization 31 | options: { 32 | data: "preview", // The data results model from a search 33 | }, 34 | output_mode: 'json', 35 | 36 | 37 | createView: function() { 38 | console.log("createView"); 39 | return { container: this.$el, } ; 40 | }, 41 | 42 | updateView: function(viz, data) { 43 | 44 | console.log("updateView", data); 45 | 46 | this.$el.empty(); 47 | 48 | $('
').attr('id', 'handson_container_templates').appendTo(this.$el); 49 | 50 | //debugger; 51 | var template_files = new Array(); 52 | 53 | var url = splunkUtil.make_url('/custom/alert_manager/helpers/get_email_template_files'); 54 | $.get( url,function(data) { 55 | _.each(data, function(el) { 56 | template_files.push(el); 57 | }); 58 | }, "json"); 59 | console.debug("template_files", template_files); 60 | 61 | tl_headers = [ { col: "_key", tooltip: false }, 62 | { col: "email_template_name", tooltip: "Set a name for the e-mail template configuration. This reference will be used to refer in the Apps' Global Settings and E-Mail settings (see above)." }, 63 | { col: "email_template_file", tooltip: "Select the template's file name located in the App's default/templates or local/templates folder. Refresh this page when the templates doesn't appear." }, 64 | { col: "email_content_type", tooltip: false, }, 65 | { col: "email_from", tooltip: "Set the sender of the notification.\nExample: Foo Bar \nOr: foo@bar.com", }, 66 | { col: "email_subject", tooltip: false } ]; 67 | 68 | $("#handson_container_templates").handsontable({ 69 | data: data, 70 | minSpareRows: 1, 71 | columns: [ 72 | { 73 | data: "_key", 74 | readOnly: true 75 | }, 76 | { 77 | data: "email_template_name", 78 | }, 79 | { 80 | data: "email_template_file", 81 | type: "dropdown", 82 | source: template_files, 83 | }, 84 | { 85 | data: "email_content_type", 86 | type: "dropdown", 87 | source: ["plain_text", "html"], 88 | }, 89 | { 90 | data: "email_from" 91 | }, 92 | { 93 | data: "email_subject" 94 | } 95 | ], 96 | colHeaders: true, 97 | colHeaders: function (col) { 98 | colval = tl_headers[col]["col"]; 99 | 100 | if (tl_headers[col]["tooltip"] != undefined) { 101 | if (tl_headers[col]["tooltip"] != false) { 102 | colval = tl_headers[col]["col"] + '?'; 103 | } 104 | } 105 | 106 | return colval; 107 | }, 108 | cells: function (row, col, prop) { 109 | var cellProperties = {}; 110 | if (this.instance.getData()[row]["_key"] === 'n/a') { 111 | //cellProperties.readOnly = true; 112 | } 113 | return cellProperties; 114 | }, 115 | stretchH: 'all', 116 | contextMenu: ['row_above', 'row_below', 'remove_row', 'undo', 'redo'], 117 | startRows: 1, 118 | startCols: 1, 119 | minSpareRows: 0, 120 | minSpareCols: 0, 121 | afterRender: function() { 122 | $(function () { 123 | $('[data-toggle="tooltip"]').tooltip() 124 | }) 125 | }, 126 | beforeRemoveRow: function(row) { 127 | console.debug("row", row); 128 | var data = $("#handson_container_templates").data('handsontable').getData(); 129 | console.log("_key", data[row]['_key']); 130 | 131 | if(!data[row]['_key'] && !data[row]['email_template_name'] && !data[row]['email_template_file']) { 132 | this.del_key_container = false; 133 | return true; 134 | } else { 135 | if(confirm('Are you sure to remove email template "' + data[row]['email_template_name'] + '"?')) { 136 | if(!data[row]['_key']) { 137 | this.del_key_container = false; 138 | } else { 139 | this.del_key_container = data[row]['_key']; 140 | } 141 | return true; 142 | } else { 143 | return false; 144 | } 145 | } 146 | 147 | }, 148 | afterRemoveRow: function(row) { 149 | console.debug("afterRemoveRow"); 150 | //var data = $("#handson_container").data('handsontable').getData(); 151 | console.debug("row", row); 152 | //console.debug("data", data); 153 | console.debug("key", this.del_key_container); 154 | if(this.del_key_container == false) { 155 | // Removal of empty row - nothing to do 156 | return true; 157 | } 158 | 159 | var post_data = { 160 | key : this.del_key_container 161 | }; 162 | 163 | var url = splunkUtil.make_url('/custom/alert_manager/email_settings/delete_template'); 164 | console.debug("url", url); 165 | 166 | $.ajax( url, 167 | { 168 | uri: url, 169 | type: 'POST', 170 | data: post_data, 171 | 172 | 173 | success: function(jqXHR, textStatus){ 174 | this.del_key_container = ''; 175 | // Reload the table 176 | mvc.Components.get("email_templates_search").startSearch() 177 | console.debug("success"); 178 | }, 179 | 180 | // Handle cases where the file could not be found or the user did not have permissions 181 | complete: function(jqXHR, textStatus){ 182 | console.debug("complete"); 183 | }, 184 | 185 | error: function(jqXHR,textStatus,errorThrown) { 186 | console.log("Error"); 187 | } 188 | } 189 | ); 190 | } 191 | }); 192 | //console.debug("id", id); 193 | 194 | 195 | //debugger; 196 | //id 197 | 198 | }, 199 | 200 | // Override this method to format the data for the view 201 | formatData: function(data) { 202 | console.log("formatData", data); 203 | 204 | myData = [] 205 | _(data).chain().map(function(val) { 206 | return { 207 | _key: val.key, 208 | email_template_name: val.email_template_name, 209 | email_template_file: val.email_template_file, 210 | email_content_type: val.email_content_type, 211 | email_from: val.email_from, 212 | email_subject: val.email_subject 213 | }; 214 | }).each(function(line) { 215 | myData.push(line); 216 | }); 217 | 218 | return myData; 219 | }, 220 | 221 | }); 222 | return EmailTemplatesView; 223 | }); 224 | -------------------------------------------------------------------------------- /appserver/static/incident_posture.css: -------------------------------------------------------------------------------- 1 | 2 | /* Single Values and Cell Highlighting */ 3 | 4 | #sv_info { 5 | background: #5378AD; 6 | border-top: 3px solid #214e8d; 7 | } 8 | #sv_info .single-value, #creates .single-drilldown, #creates .single-value .under-label { 9 | color: #214e8d; 10 | } 11 | #sv_info .single-value .single-result{ 12 | font-size: 50px; 13 | font-weight: 200; 14 | color: #ffffff; 15 | } 16 | 17 | 18 | #incident_overview td.range-info { 19 | background-color: #5378AD !important; 20 | color: #ffffff; 21 | } 22 | 23 | #sv_low { 24 | background: #98BF3B; 25 | border-top: 3px solid #5b7c0b; 26 | } 27 | #sv_low .single-value, #creates .single-drilldown, #creates .single-value .under-label { 28 | color: #5b7c0b; 29 | } 30 | #sv_low .single-value .single-result{ 31 | font-size: 50px; 32 | font-weight: 200; 33 | color: #ffffff 34 | } 35 | 36 | #incident_overview td.range-low { 37 | background-color: #98BF3B !important; 38 | color: #ffffff; 39 | } 40 | 41 | 42 | #sv_medium { 43 | background: #F0BE1B; 44 | border-top: 3px solid #f0841b; 45 | } 46 | #sv_medium .single-value, #creates .single-drilldown, #creates .single-value .under-label { 47 | color: #f0841b; 48 | } 49 | #sv_medium .single-value .single-result{ 50 | font-size: 50px; 51 | font-weight: 200; 52 | color: #ffffff; 53 | } 54 | 55 | #incident_overview td.range-medium { 56 | background-color: #F0BE1B !important; 57 | color: #ffffff; 58 | } 59 | 60 | #sv_high { 61 | background: #FF8800; 62 | border-top: 3px solid #ac5f06; 63 | } 64 | #sv_high .single-value, #creates .single-drilldown, #creates .single-value .under-label { 65 | color: #ac5f06; 66 | } 67 | #sv_high .single-value .single-result{ 68 | font-size: 50px; 69 | font-weight: 200; 70 | color: #ffffff 71 | } 72 | 73 | #incident_overview td.range-high { 74 | background-color: #FF8800 !important; 75 | color: #ffffff; 76 | } 77 | 78 | #sv_critical { 79 | background: #D25B3B; 80 | border-top: 3px solid #982d10; 81 | } 82 | #sv_critical .single-value, #creates .single-drilldown, #creates .single-value .under-label { 83 | color: #982d10; 84 | } 85 | #sv_critical .single-value .single-result{ 86 | font-size: 50px; 87 | font-weight: 200; 88 | color: #ffffff 89 | } 90 | 91 | #incident_overview td.range-critical { 92 | background-color: #D25B3B !important; 93 | color: #ffffff; 94 | } 95 | 96 | 97 | /* Custom Cell Icons */ 98 | td.icon { 99 | text-align: center; 100 | } 101 | td.icon i { 102 | font-size: 25px; 103 | text-shadow: 1px 1px #aaa; 104 | } 105 | .icon-inline i { 106 | font-size: 18px; 107 | } 108 | 109 | td.table_inline_icon { 110 | width: 13px; 111 | border-right: 1px solid #d5d5d5; 112 | } 113 | 114 | 115 | 116 | 117 | /* #incident_overview panel */ 118 | 119 | /* Hidden Cells */ 120 | #incident_overview [data\-sort\-key='incident_id'], 121 | #incident_overview [data\-sort\-key='job_id'], 122 | #incident_overview [data\-sort\-key='result_id'], 123 | #incident_overview [data\-sort\-key='status'], 124 | #incident_overview [data\-sort\-key='search'], 125 | #incident_overview [data\-sort\-key='event_search'], 126 | #incident_overview [data\-sort\-key='earliest'], 127 | #incident_overview [data\-sort\-key='impact'], 128 | #incident_overview [data\-sort\-key='urgency'], 129 | #incident_overview [data\-sort\-key='latest'], 130 | #incident_overview [data\-sort\-key='display_fields'], 131 | #incident_overview [data\-sort\-key='alert_time'] { 132 | display:none; 133 | } 134 | 135 | #incident_overview .incident_id, #incident_overview .job_id, #incident_overview .result_id, #incident_overview .status, #incident_overview .search, #incident_overview .event_search, #incident_overview .earliest, #incident_overview .latest, #incident_overview .impact, #incident_overview .urgency, #incident_overview .display_fields, #incident_overview .alert_time { 136 | display:none; 137 | } 138 | 139 | /* Custom Cells */ 140 | 141 | #incident_overview .owner { 142 | } 143 | 144 | #incident_overview [data\-sort\-key='dosearch'] { 145 | min-width: 20px !important; 146 | max-width: 20px !important; 147 | width: 20px !important; 148 | padding:0px; 149 | margin:0px; 150 | } 151 | #incident_overview [data\-sort\-key='dosearch'] a{ 152 | /* color:transparent !important; */ 153 | display:none; 154 | } 155 | 156 | #incident_overview [data\-sort\-key='doedit'] { 157 | min-width: 20px !important; 158 | max-width: 20px !important; 159 | width: 20px !important; 160 | padding:0px; 161 | margin:0px; 162 | } 163 | #incident_overview [data\-sort\-key='doedit'] a{ 164 | /* color:transparent !important; */ 165 | display:none; 166 | } 167 | 168 | #incident_overview [data\-sort\-key='_time'] { 169 | min-width: 140px !important; 170 | max-width: 140px !important; 171 | width: 140px !important; 172 | } 173 | 174 | /* Visible Cells */ 175 | #incident_overview td { 176 | cursor: pointer; 177 | } 178 | 179 | #incident_overview .description { 180 | height: 22px !important; 181 | max-height: 22px !important; 182 | line-height: 22px !important; 183 | font-size:12px; 184 | font-weight:bold !important; 185 | 186 | overflow: overlay; 187 | cursor:default; 188 | } 189 | 190 | #incident_overview .owner { 191 | overflow: overlay; 192 | } 193 | 194 | #incident_overview .search_icon { 195 | border-right: 1px solid #dcdcdc; 196 | border-left: 1px solid #dcdcdc; 197 | text-align:center; 198 | padding: 4px 0 0px 4px; 199 | } 200 | 201 | 202 | /* Closer */ 203 | .closer { 204 | float: right; 205 | padding:5px; 206 | line-height:10px; 207 | font-size:12px; 208 | border: 1px solid #efefef; 209 | cursor:pointer; 210 | margin-bottom: 2px; 211 | } 212 | 213 | 214 | /* Incident Edit Dialog */ 215 | .input-label-incident_id { 216 | padding: 6px 0 4px 0; 217 | line-height: 15px; 218 | font-size: 12px; 219 | display: inline-block; 220 | } 221 | 222 | 223 | /* Incident details row expansion */ 224 | #incident_details_exp, #incident_history_exp { 225 | width: 70%; 226 | } 227 | 228 | 229 | #incident_overview div tr .expanded-content-row .odd 230 | { 231 | background-color: #ffffff !important; 232 | } 233 | 234 | 235 | #incident_overview .results-table tr.expanded-row .odd td, .results-table tr.expanded-content-row .odd td 236 | { 237 | background-color: #ffffff !important; 238 | } 239 | 240 | #incident_overview .results-table tr.expanded-row .even td, .results-table tr.expanded-content-row .even td 241 | { 242 | background-color: #f5f5f5 !important; 243 | } 244 | 245 | #incident_details_exp_container div { 246 | padding-right: 6px; 247 | } 248 | 249 | #incident_details_exp_container span { 250 | -moz-border-radius: 4px; 251 | -webkit-border-radius: 4px; 252 | border-radius: 4px; 253 | } 254 | 255 | #incident_details_exp_container span.unknown { 256 | background: #6d6d6d; 257 | color: #ffffff; 258 | font-weight: bold; 259 | border: 3px solid #6d6d6d; 260 | } 261 | 262 | #incident_details_exp_container span.informational { 263 | background: #5378AD; 264 | color: #ffffff; 265 | /* font-weight: bold; */ 266 | border: 3px solid #5378AD; 267 | } 268 | 269 | #incident_details_exp_container span.low { 270 | background: #98BF3B; 271 | color: #ffffff; 272 | /* font-weight: bold; */ 273 | border: 3px solid #98BF3B; 274 | } 275 | 276 | #incident_details_exp_container span.medium { 277 | background: #F0BE1B; 278 | color: #ffffff; 279 | /* font-weight: bold; */ 280 | border: 3px solid #F0BE1B; 281 | } 282 | 283 | #incident_details_exp_container span.high { 284 | background: #FF8800; 285 | color: #ffffff; 286 | /* font-weight: bold; */ 287 | border: 3px solid #FF8800; 288 | } 289 | #incident_details_exp_container span.critical { 290 | background: #D25B3B; 291 | color: #ffffff; 292 | /* font-weight: bold; */ 293 | border: 3px solid #D25B3B; 294 | } 295 | 296 | #incident_details_exp_container span.incidentid { 297 | background: #666666; 298 | color: #ffffff; 299 | border: 3px solid #666666; 300 | } 301 | 302 | .incident_details_description { 303 | width: 69%; 304 | background-color: #ffffff; 305 | border-top: 1px solid #bfbfbf; 306 | border-bottom: 1px solid #bfbfbf; 307 | padding-left: 20px; 308 | padding: 4px 8px; 309 | } 310 | 311 | 312 | .single-result { 313 | width: 24%; 314 | vertical-align: top; 315 | text-align: right; 316 | top: 10px; 317 | } 318 | .single-value { 319 | margin-right: 0px !important; 320 | } 321 | 322 | .single-value > br { 323 | display: none; 324 | } 325 | 326 | .before-label, .after-label, .under-label { 327 | display: none; 328 | } 329 | 330 | .trend-ctr { 331 | position: relative; 332 | height: 40px; 333 | width: 14%; 334 | margin-left: 0; 335 | } 336 | 337 | .single .panel-body { 338 | text-align: center; 339 | width: 100%; 340 | display: block; 341 | white-space: nowrap; 342 | overflow: hidden; 343 | height: 64px; 344 | vertical-align: middle; 345 | } 346 | 347 | .single .panel-body > div { 348 | display: inline-block; 349 | position: relative; 350 | } 351 | 352 | .delta { 353 | text-align: center; 354 | padding-top: 3px; 355 | font-size: 20px; 356 | height: 16px; 357 | width: 2.4em; 358 | color: #ffffff; 359 | position: relative; 360 | top: 13px; 361 | } 362 | .trend-arrow { 363 | text-align: center; 364 | height: 10px; 365 | margin-right: 20px; 366 | } 367 | 368 | .trend-arrow.arrow-decrease::after { 369 | content: url(/static/app/alert_manager/down-white.png); 370 | } 371 | 372 | .trend-arrow.arrow-increase::after { 373 | content: url(/static/app/alert_manager/up-white.png); 374 | } 375 | 376 | 377 | /* SimpleXML -> HTML fixes */ 378 | /* Time Refresh */ 379 | .refresh-time-indicator .time-freshness { 380 | font-size: 11px; 381 | color: #555; 382 | cursor: default; 383 | padding: 0; 384 | margin: 0; 385 | } 386 | 387 | .refresh-time-indicator { 388 | padding: 4px 10px 6px 10px; 389 | right: 0; 390 | z-index: 10; 391 | height: 14px; 392 | } 393 | -------------------------------------------------------------------------------- /appserver/static/views/incidentsettingsview.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | paths: { 3 | "app": "../app" 4 | }, 5 | shim: { 6 | "app/alert_manager/contrib/handsontable-0.12.2/handsontable.full.min": { 7 | deps: ['css!../handsontable-0.12.2/handsontable.full.min.css'], 8 | exports: "Handsontable" 9 | }, 10 | } 11 | }); 12 | 13 | 14 | define(function(require, exports, module) { 15 | 16 | var _ = require('underscore'); 17 | var $ = require('jquery'); 18 | var mvc = require('splunkjs/mvc'); 19 | var SimpleSplunkView = require('splunkjs/mvc/simplesplunkview'); 20 | var Handsontable = require('app/alert_manager/contrib/handsontable-0.12.2/handsontable.full.min'); 21 | var splunkUtil = require('splunk.util'); 22 | 23 | //require("css!../lib/handsontable.full.css"); 24 | 25 | var IncidentSettingsView = SimpleSplunkView.extend({ 26 | className: "incidentsettingsview", 27 | 28 | del_key_container: '', 29 | 30 | // Set options for the visualization 31 | options: { 32 | data: "preview", // The data results model from a search 33 | }, 34 | output_mode: 'json', 35 | 36 | 37 | createView: function() { 38 | console.log("createView"); 39 | return { container: this.$el, } ; 40 | }, 41 | 42 | updateView: function(viz, data) { 43 | 44 | console.log("updateView", data); 45 | 46 | this.$el.empty(); 47 | 48 | $('
').attr('id', 'handson_container').appendTo(this.$el); 49 | 50 | var users = new Array(); 51 | users.push("unassigned"); 52 | 53 | var url = splunkUtil.make_url('/custom/alert_manager/helpers/get_users'); 54 | $.get( url,function(data) { 55 | _.each(data, function(el) { 56 | users.push(el.name); 57 | }); 58 | }, "json"); 59 | 60 | 61 | headers = [ { col: "_key", tooltip: false }, 62 | { col: "alert", tooltip: false }, 63 | { col: "category", tooltip: false }, 64 | { col: "subcategory", tooltip: false }, 65 | { col: "tags", tooltip: "Space separated list of tags" }, 66 | { col: "urgency", tooltip: "The default urgency for this alert if urgency field is not provided in the results. Used together with impact to calculate the alert's priority" }, 67 | { col: "display_fields", tooltip: "Space separated list of fields to display in incident details."}, 68 | { col: "run_alert_script", tooltip: "Run classic Splunk scripted alert script. The Alert Manager will pass all arguments" }, 69 | { col: "alert_script", tooltip: "Name of the Splunk alert script" }, 70 | { col: "auto_assign", tooltip: "Auto-assign new incidents and change status to 'assigned'." }, 71 | { col: "auto_assign_owner", tooltip: "Username of the user the incident will be assigned to" }, 72 | { col: "auto_ttl_resolve", tooltip: "Auto-resolve incidents in status 'new' who reached their expiry" }, 73 | { col: "auto_previous_resolve", tooltip: "Auto-resolve previously created incidents in status 'new'" } ]; 74 | $("#handson_container").handsontable({ 75 | data: data, 76 | //colHeaders: ["_key", "alert", "category", "subcategory", "tags", "urgency", "run_alert_script", "alert_script", "auto_assign", "auto_assign_owner", "auto_ttl_resolve", "auto_previous_resolve"], 77 | columns: [ 78 | { 79 | data: "_key", 80 | readOnly: true 81 | }, 82 | { 83 | data: "alert", 84 | }, 85 | { 86 | data: "category", 87 | }, 88 | { 89 | data: "subcategory", 90 | }, 91 | { 92 | data: "tags", 93 | }, 94 | { 95 | data: "urgency", 96 | type: "dropdown", 97 | source: ["low", "medium", "high"], 98 | }, 99 | { 100 | data: "display_fields", 101 | }, 102 | { 103 | data: "run_alert_script", 104 | type: "checkbox" 105 | }, 106 | { 107 | data: "alert_script", 108 | }, 109 | { 110 | data: "auto_assign", 111 | type: "checkbox" 112 | }, 113 | { 114 | data: "auto_assign_owner", 115 | type: "dropdown", 116 | source: users, 117 | }, 118 | { 119 | data: "auto_ttl_resolve", 120 | type: "checkbox" 121 | }, 122 | { 123 | data: "auto_previous_resolve", 124 | type: "checkbox" 125 | } 126 | ], 127 | colHeaders: true, 128 | colHeaders: function (col) { 129 | if (headers[col]["tooltip"] != false) { 130 | colval = headers[col]["col"] + '?'; 131 | } 132 | else { 133 | colval = headers[col]["col"]; 134 | } 135 | return colval; 136 | }, 137 | stretchH: 'all', 138 | contextMenu: ['row_above', 'row_below', 'remove_row', 'undo', 'redo'], 139 | startRows: 1, 140 | startCols: 1, 141 | minSpareRows: 0, 142 | minSpareCols: 0, 143 | afterRender: function() { 144 | $(function () { 145 | $('[data-toggle="tooltip"]').tooltip() 146 | }) 147 | }, 148 | beforeRemoveRow: function(row) { 149 | var data = $("#handson_container").data('handsontable').getData(); 150 | if(confirm('Are you sure to remove settings for alert "' + data[row]['alert'] + '"?')) { 151 | this.del_key_container = data[row]['_key']; 152 | return true; 153 | } else { 154 | return false; 155 | } 156 | }, 157 | afterRemoveRow: function(row) { 158 | console.debug("afterRemoveRow"); 159 | //var data = $("#handson_container").data('handsontable').getData(); 160 | console.debug("row", row); 161 | //console.debug("data", data); 162 | console.debug("key", this.del_key_container); 163 | 164 | var post_data = { 165 | key : this.del_key_container 166 | }; 167 | 168 | var url = splunkUtil.make_url('/custom/alert_manager/incident_settings/delete'); 169 | console.debug("url", url); 170 | 171 | $.ajax( url, 172 | { 173 | uri: url, 174 | type: 'POST', 175 | data: post_data, 176 | 177 | 178 | success: function(jqXHR, textStatus){ 179 | this.del_key_container = ''; 180 | // Reload the table 181 | mvc.Components.get("incident_settings_search").startSearch() 182 | console.debug("success"); 183 | }, 184 | 185 | // Handle cases where the file could not be found or the user did not have permissions 186 | complete: function(jqXHR, textStatus){ 187 | console.debug("complete"); 188 | }, 189 | 190 | error: function(jqXHR,textStatus,errorThrown) { 191 | console.log("Error"); 192 | } 193 | } 194 | ); 195 | } 196 | }); 197 | //console.debug("id", id); 198 | 199 | 200 | //debugger; 201 | //id 202 | 203 | }, 204 | 205 | // Override this method to format the data for the view 206 | formatData: function(data) { 207 | console.log("formatData", data); 208 | 209 | myData = [] 210 | _(data).chain().map(function(val) { 211 | return { 212 | _key: val.key, 213 | alert: val.alert, 214 | category: val.category, 215 | subcategory: val.subcategory, 216 | tags: val.tags, 217 | urgency: val.urgency, 218 | display_fields: val.display_fields, 219 | run_alert_script: parseInt(val.run_alert_script) ? true : false, 220 | alert_script: val.alert_script, 221 | auto_assign: parseInt(val.auto_assign) ? true : false, 222 | auto_assign_owner: val.auto_assign_owner, 223 | auto_ttl_resolve: parseInt(val.auto_ttl_resolve) ? true : false, 224 | auto_previous_resolve: parseInt(val.auto_previous_resolve) ? true : false 225 | }; 226 | }).each(function(line) { 227 | myData.push(line); 228 | }); 229 | 230 | return myData; 231 | }, 232 | 233 | }); 234 | return IncidentSettingsView; 235 | }); 236 | -------------------------------------------------------------------------------- /default/data/ui/views/incident_overview.xml: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 | -24h 8 | now 9 | 10 | 11 | 12 | 13 | All 14 | * 15 | * 16 | 17 | |inputlookup incident_settings |dedup alert |table alert |sort alert 18 | -1m 19 | now 20 | 21 | alert 22 | alert 23 | 24 |
25 | 26 | 27 | Incidents over Time 28 | 29 | 30 | Priority 31 | Urgency 32 | Impact 33 | Status 34 | Owner 35 | Category 36 | priority 37 | 38 | 39 | 40 | | `all_alerts` | search alert=$alert$ | timechart count by $split$ 41 | $global_time.earliest$ 42 | $global_time.latest$ 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Incidents by Priority 76 | 77 | |`all_alerts` | search alert="$alert$" | stats count by priority 78 | $global_time.earliest$ 79 | $global_time.latest$ 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | Incidents by Status 117 | 118 | | `all_alerts` | search alert="$alert$" | chart count by status_description, status 119 | $global_time.earliest$ 120 | $global_time.latest$ 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | Incidents by Owner 156 | 157 | | `all_alerts` | search alert="$alert$" | stats count by owner 158 | $global_time.earliest$ 159 | $global_time.latest$ 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 |
194 | -------------------------------------------------------------------------------- /appserver/static/components/sankey/sankey.js: -------------------------------------------------------------------------------- 1 | // AppFramework Sankey Plug-In 2 | // --------------------------- 3 | // 4 | // Provide an easy-to-use plug-in that takes data that relates a 5 | // many-to-many relationship with scores into a Sankey display, a form 6 | // of flow display. Any relationship between two fields can be 7 | // illustrated, although the most common is 8 | // "stats count by field1, field2" 9 | 10 | define(function(require, exports, module) { 11 | var _ = require("underscore"); 12 | var $ = require("jquery"); 13 | var SimpleSplunkView = require("splunkjs/mvc/simplesplunkview"); 14 | var d3 = require("../d3/d3"); 15 | // Load D3 Sankey plugin 16 | require("./contrib/d3-sankey"); 17 | 18 | // Import CSS for the sankey chart. 19 | require("css!./sankey.css"); 20 | 21 | var SankeyChart = SimpleSplunkView.extend({ 22 | moduleId: module.id, 23 | 24 | className: "sankey-diagram", 25 | 26 | options: { 27 | managerid: null, 28 | data: "preview", 29 | formatLabel: _.identity, 30 | height: 300, 31 | formatTooltip: function(d) { 32 | return (d.source.name + ' -> ' + d.target.name + ': ' + d.value); 33 | } 34 | }, 35 | 36 | // This is how we extend the SimpleSplunkView's options value for 37 | // this object, so that these values are available when 38 | // SimpleSplunkView initializes. 39 | initialize: function() { 40 | SimpleSplunkView.prototype.initialize.apply(this, arguments); 41 | 42 | this.settings.on("change:formatLabel change:formatTooltip", this.render, this); 43 | 44 | 45 | // Set up resize callback. 46 | $(window).resize(_.debounce(_.bind(this._handleResize, this), 20)); 47 | }, 48 | 49 | _handleResize: function() { 50 | this.render(); 51 | }, 52 | 53 | // The object this method returns will be passed to the 54 | // updateView() method as the first argument, to be 55 | // manipulated according to the data and the visualization's 56 | // needs. 57 | createView: function() { 58 | var margin = {top: 10, right: 10, bottom: 10, left: 10}; 59 | var availableWidth = parseInt(this.settings.get("width") || this.$el.width()); 60 | var availableHeight = parseInt(this.settings.get("height") || this.$el.height()); 61 | 62 | this.$el.html(""); 63 | 64 | var svg = d3.select(this.el) 65 | .append("svg") 66 | .attr("width", availableWidth) 67 | .attr("height", availableHeight) 68 | .attr("pointer-events", "all"); 69 | 70 | return { svg: svg, margin: margin}; 71 | }, 72 | 73 | // Where the data and the visualization meet. Both 'viz' and 74 | // 'data' are the data structures returned from their 75 | // respective construction methods, createView() above and 76 | // onData(), below. 77 | updateView: function(viz, data) { 78 | var that = this; 79 | var containerHeight = this.$el.height(); 80 | var containerWidth = this.$el.width(); 81 | 82 | // Clear svg 83 | var svg = $(viz.svg[0]); 84 | svg.empty(); 85 | svg.height(containerHeight); 86 | svg.width(containerWidth); 87 | 88 | // Add the graph group as a child of the main svg 89 | var graphWidth = containerWidth - viz.margin.left - viz.margin.right; 90 | var graphHeight = containerHeight - viz.margin.top - viz.margin.bottom; 91 | var graph = viz.svg 92 | .append("g") 93 | .attr("width", graphWidth) 94 | .attr("height", graphHeight) 95 | .attr("transform", "translate(" + viz.margin.left + "," + viz.margin.top + ")"); 96 | 97 | var formatLabel = this.settings.get('formatLabel') || _.identity; 98 | var formatTooltip = this.settings.get('formatTooltip'); 99 | 100 | var sankey = d3.sankey() 101 | .nodeWidth(15) 102 | .nodePadding(10) 103 | .size([graphWidth, graphHeight]); 104 | 105 | var path = sankey.link(); 106 | 107 | sankey.nodes(data.nodes) 108 | .links(data.links) 109 | .layout(1); 110 | 111 | var link = graph.append("g").selectAll(".link") 112 | .data(data.links) 113 | .enter().append("path") 114 | .attr("class", "link") 115 | .attr("d", path) 116 | .style("stroke-width", function(d) { 117 | return Math.max(1, d.dy); 118 | }) 119 | .sort(function(a, b) { 120 | return b.dy - a.dy; 121 | }); 122 | 123 | link.append("title") 124 | .text(function(d) { 125 | return formatTooltip(d); 126 | }); 127 | 128 | var node = graph.append("g").selectAll(".node") 129 | .data(data.nodes) 130 | .enter() 131 | .append("g") 132 | .attr("class", "node") 133 | .attr("transform", function(d) { 134 | return "translate(" + d.x + "," + d.y + ")"; 135 | }); 136 | 137 | var color = d3.scale.category20(); 138 | 139 | // Draw the rectangles at each end of the link that 140 | // correspond to a given node, and then decorate the chart 141 | // with the names for each node. 142 | node.append("rect") 143 | .attr("height", function(d) { 144 | return d.dy; 145 | }) 146 | .attr("width", sankey.nodeWidth()) 147 | .style("fill", function(d) { 148 | d.color = color(d.name.replace(/ .*/, "")); 149 | return d.color; 150 | }) 151 | .style("stroke", function(d) { 152 | return d3.rgb(d.color).darker(2); 153 | }) 154 | .on("mouseover", function(node) { 155 | var linksToHighlight = link.filter(function(d) { 156 | return d.source.name === node.name || d.target.name === node.name; 157 | }); 158 | linksToHighlight.classed('hovering', true); 159 | }) 160 | .on("mouseout", function(node) { 161 | var linksToHighlight = link.filter(function(d) { 162 | return d.source.name === node.name || d.target.name === node.name; 163 | }); 164 | linksToHighlight.classed('hovering', false); 165 | }) 166 | .append("title") 167 | .text(function(d) { 168 | return formatLabel(d.name) + "\n" + d.value; 169 | }); 170 | 171 | node.attr("transform", function(d) { 172 | return "translate(" + d.x + "," + d.y + ")"; 173 | }) 174 | .call(d3.behavior.drag() 175 | .origin(function(d) { 176 | return d; 177 | }) 178 | .on("dragstart", function() { 179 | this.parentNode.appendChild(this); 180 | }) 181 | .on("drag", dragmove)); 182 | 183 | node.append("text") 184 | .attr("x", -6) 185 | .attr("y", function(d) { 186 | return d.dy / 2; 187 | }) 188 | .attr("dy", ".35em") 189 | .attr("text-anchor", "end") 190 | .attr("transform", null) 191 | .text(function(d) { 192 | return formatLabel(d.name); 193 | }) 194 | .filter(function(d) { 195 | return d.x < graphWidth / 2; 196 | }) 197 | .attr("x", 6 + sankey.nodeWidth()) 198 | .attr("text-anchor", "start"); 199 | 200 | // This view publishes the 'click:link' event that 201 | // other Splunk views can then use to drill down 202 | // further into the data. We return the source and target 203 | // names as values to be used in further Splunk searches. 204 | // This allows us to accept events from the visualization 205 | // library and provide them consistently to other Splunk 206 | // views. 207 | var format_event_data = function(e) { 208 | return { 209 | source: e.source.name, 210 | target: e.target.name, 211 | value: e.value 212 | }; 213 | }; 214 | 215 | function dragmove(d) { 216 | d3.select(this).attr("transform", "translate(" + d.x + "," + (d.y = Math.max(0, Math.min(graphHeight - d.dy, d3.event.y))) + ")"); 217 | sankey.relayout(); 218 | link.attr("d", path); 219 | } 220 | 221 | link.on('click', function(e) { 222 | that.trigger('click:link', format_event_data(e)); 223 | }); 224 | 225 | node.on('mousedown', function(e) { 226 | var linksToNodes = function(links, type) { 227 | return _.map(links, function(link) { 228 | return { 229 | name: link[type].name, 230 | value: link[type].value 231 | }; 232 | }); 233 | }; 234 | 235 | var clickEvent = { 236 | name: e.name, 237 | value: e.value, 238 | incomingLinks: linksToNodes(e.targetLinks, "source"), 239 | outgoingLinks: linksToNodes(e.sourceLinks, "target") 240 | }; 241 | that.trigger('click:node', clickEvent); 242 | }); 243 | }, 244 | 245 | // This function turns the three expected data items into data 246 | // structures Sankey understands, and then calls 247 | // updateView(). This is the function that is called when 248 | // new data is available, and triggers the actual rendering of 249 | // the visualization above. The data passed here corresponds 250 | // to the basic format requested by the view. 251 | formatData: function(data) { 252 | var nodeList = _.uniq(_.pluck(data, 0).concat(_.pluck(data, 1))); 253 | 254 | var links = _.map(data, function(item) { 255 | return { 256 | source: nodeList.indexOf(item[0]), 257 | target: nodeList.indexOf(item[1]), 258 | value: parseInt(item[2], 10) 259 | }; 260 | }); 261 | 262 | var nodes = _.map(nodeList, function(node) { 263 | return { 264 | name: node 265 | }; 266 | }); 267 | 268 | return { nodes: nodes, links: links }; 269 | }, 270 | render: function() { 271 | this.$el.height(this.settings.get('height')); 272 | return SimpleSplunkView.prototype.render.apply(this, arguments); 273 | } 274 | }); 275 | 276 | return SankeyChart; 277 | }); 278 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alert Manager 2 | - **Authors**: Simon Balz , Mika Borner 3 | - **Description**: Extended Splunk Alert Manager with advanced reporting on alerts, workflows (modify assignee, status, severity) and auto-resolve features 4 | - **Version**: 1.1 5 | 6 | ## Introduction 7 | The Alert Manager adds simple incident workflows to Splunk. The general purpose is to provide a common app with dashboards in order to investigate fired alerts or notable events. It can be used with every Splunk alert and works as an extension on top of Splunk's built-in alerting mechanism. 8 | 9 | - Awareness of your current operational situation with the incident posture dashboard 10 | - Analyze root cause of incidents with only a few clicks 11 | - Review and adjust the urgency of incidents to improve operations scheduling 12 | - Dispatch incidents to the person in charge 13 | - Track and report incident workflow KPIs 14 | - Tag and categorize incidents 15 | 16 | ## Features 17 | - Works as scripted alert action to catch enriched metadata of fired alerts and stores them in a configurable separate index 18 | - Each fired alert creates an incident 19 | - Configured incidents to run well-known scripted alert scripts 20 | - Reassign incidents manually or auto-assign them to specific users 21 | - Change incidents to another priority and status 22 | - Incidents can be configured to get auto-resolved when a new incident is created from the same alert 23 | - Incidents can be configured to get auto-resolved when the alert's ttl is reached 24 | 25 | ## Additional Notes for Apptitude App Contest 26 | - The app utilizes the Common Information Model 27 | - Demo data is provided with a separate app. Due to the nature of the app, we couldn't use Eventgen. 28 | - The app uses only portable code and is tested thoroughly on *nix and Windows systems. 29 | - The app will be used within customer projects, and improved according to customer and community needs. Development of the app will happen in public. Bugs/Issues and improvement requests can be opened on the project's Github page (). 30 | 31 | ## Release Notes 32 | - **v1.1** / 2015-03-12 33 | - Fixed support for per-result alert actions 34 | - Added support for search results in e-mail templates 35 | - Enhanced incident details with description form saved search and selectable list of fields 36 | - Bugfixes 37 | - **v1.0** / 2015-01-19 38 | - Major release with e-mail notifications and templates 39 | - Lots of bugfixes and enhancements 40 | - Final release for Splunk Apptitude submission 41 | - **v0.10** / 2015-01-04 42 | - Bugfix & optimization release 43 | - **v0.9** / 2014-12-28 44 | - Lots of bugfixes 45 | - New KPI dashboard with sankey visualization 46 | - Full support to add/remove alert manager users 47 | - Improved app setup (check for index existence) and configuration (configure which user directories should be used) 48 | - Removed hardcoded index from searches 49 | - **v0.8** / 2014-12-26 50 | - Minor bugfixes & enhancements 51 | - Documentation improvements 52 | - App for demo data 53 | - **v0.7** / 2014-12-21 54 | - Trend indicators for single values in incident posture dashboard 55 | - Full Windows support 56 | - Bugfixes 57 | - **v0.6** / 2014-12-18 58 | - New TA for distributed Splunk environment support 59 | - Improved incident settings (former alert settings) to work with non-global visible alerts 60 | - Added incident change events and KPI reporting based on them; 61 | - **v0.5** / 2014-12-16 62 | - Added change incidents (workflow, priority) feature 63 | - Indexed events on incident creation or update 64 | - Bugfixes 65 | - **v0.4** / 2014-12-14 66 | - Again a lot of updates and improvements 67 | - CIM compliancy 68 | - Ability to run classical alert scripts; incident categorization and tagging 69 | - ES-like urgency calculation; many UI improvements 70 | - **v0.3** / 2014-12-10 71 | - Release with major improvements (better see changelog :-) ) 72 | - **v0.2** / 2014-12-07 73 | - Added config parsing (alert_manager.conf) 74 | - **v0.1** / 2014-12-07 75 | - First working version 76 | 77 | ## Changelog 78 | - **2015-02-10** simon@balz.me 79 | - Fixed trend timerange to depend on timepicker in incident posture 80 | - **2015-02-04** simon@balz.me 81 | - Improved inicdent list when tags are empty 82 | - **2015-02-04** mika.borner@gmail.com 83 | - Fixed issue #60 84 | - **2015-02-03** simon@balz.me 85 | - Added support to display selected fields in incident row expansion on incident_posture 86 | - **2015-02-01** simon@balz.me 87 | - Prepared CsvResultParser for per-result fixing 88 | - Code optimizations 89 | - Improved email notifications to support multi value fields 90 | - Added alert description to incident details 91 | - Added support for sorted field list in incident results 92 | - **2015-02-01** mika.borner@gmail.com 93 | - Fixed per-result incident creation for all alerting types 94 | 95 | Please find the full changelog here: . 96 | 97 | ## Credits 98 | - Visualization snippets from Splunk 6.x Dashboard Examples app (https://apps.splunk.com/app/1603/) 99 | - Single value design from Splunk App from AWS (https://apps.splunk.com/app/1274/) 100 | - Trend indicator design from Splunk App for Microsoft Exchange (https://apps.splunk.com/app/1660/) 101 | - Handsontable (http://handsontable.com/) 102 | - ziegfried (https://github.com/ziegfried/) for support 103 | - atremar (https://github.com/atremar) for documentation reviews 104 | 105 | ## Prerequisites 106 | - Splunk v6.2+ (we use the App Key Value Store) 107 | - Alerts (Saved searches with alert actions) 108 | - Technology Add-on for Alert Manager 109 | 110 | ## Installation and Usage 111 | ### Deployment Matrix 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 |
Alert ManagerTechnology Add-on for Alert ManagerSupporting Add-on for Alert Manager Demo Data
Search Headxxx
Indexerx
133 | 134 | **Note:** If you forward events from the search head through heavy forwarders to the indexer, install the Add-on on the heavy forwarder and disable the index there. 135 | 136 | ### Installation 137 | 1. Unpack and install the App and Add-on according to the deployment matrix 138 | - The Add-on is located at alert_manager/appserver/src/TA-alert_manager.tar.gz 139 | 2. Link $SPLUNK_HOME/etc/apps/alert_manager/bin/alert_handler.py to $SPLUNK_HOME/bin/scripts/: 140 | - Linux: 141 | 142 | `cd $SPLUNK_HOME/bin/scripts && ln -s ../../etc/apps/alert_manager/bin/alert_handler.py alert_handler.py` 143 | 144 | - Windows (run with administrative privileges): 145 | 146 | `cd %SPLUNK_HOME && mklink alert_handler.py ..\..\etc\apps\alert_manager\bin\alert_handler.py` 147 | 148 | 3. Restart Splunk 149 | 4. Configure the Alert Manager global settings in the app setup 150 | 151 | #### Demo Data 152 | For testing purposes, we ship a separate App containing static demo data and demo alerts. 153 | - Static demo data adds pre-generated incidents with some workflow examples in order to see the KPI dashboards working 154 | - Demo alerts are configured to see some different live alert examples, like auto assign/resolve scenarios and support for realtime alerts 155 | 156 | To add demo data, follow these instructions: 157 | 158 | 1. Unpack and install the "Supporting Add-on for Alert Manager Demo Data" (app folder name SA-alert_manager_demo) to $SPLUNK_HOME/etc/apps 159 | 2. Restart Splunk 160 | 3. Open Splunk and switch to the "Alert Manager Demo Data" App 161 | 4. Follow the instructions in the "Demo Data Setup" view 162 | 163 | #### Note for distributed environments 164 | - The alert manager runs mostly on the search head (since we use the App Key Value Store) 165 | - Due to the usage of the App Key Value Store, there's no compatibility with Search Head Clustering (SHC) introduced in Splunk v6.2 166 | - The Alert Manager runs a script every 30 seconds (as a scripted input) to search for incidents that should be resolved after their ttl is reached 167 | 168 | ### Alert Manager Settings 169 | 1. Configure global settings in the App's setup page (Manage Apps -> Alert Manager -> Set up) 170 | - **Index:** Where the Alert Manager will store the alert's metadata, alert results and change events 171 | - **Default Assignee:** Username of the assignee a newly created incident will be assigned to 172 | - **Default Priority:** Priority to be used for new incidents created by the alert 173 | - **Disable saving alert results to index:** Whether to index alert results again in the index specified above. Then they are still visible after they expired. Currently, there's no related feature in the Alert Manager. 174 | 2. Configure per-alert settings in the "Alert Settings" page 175 | 176 | ### Configure Alerts 177 | 1. Set "alert_handler.py" (without quotes) as the script's file name of an alert action 178 | 2. Configure the alert to be visible in Splunk's Triggered Alert view (necessary to view the alert results without indexing them) 179 | 3. Configure incident settings (Alert Manager App -> Settings -> Incident Settings) 180 | - Note: By default, only alerts configured as globally visible are shown in the list. In case you're missing an alert, try to select the correct App scope within the pulldown. 181 | 182 | ### Per Alert Settings 183 | - **Run Alert Script:** You can run a classical alert script (). Place your script in $SPLUNKH_HOME/bin/scripts, enable run_alert_script and add the file name (without path!) to the alert_script field. All arguments will be passed to the script as you would configure it directly as an alert action. 184 | - **Auto Assign:** Assign newly created incidents related to this alert to a special user. Enter the username to the auto_assign_user field and enable auto_assign 185 | - **Auto Resolve Previous:** Automatically resolve already existing incidents with status=new related to this alert when creating a new one 186 | - **Auto Resolve after TTL:** Automatically resolve existing incidents with status=new when alert.expires time is reached 187 | 188 | ## Roadmap 189 | - Custom incident handlers to extend the alert manager’s functionality 190 | 191 | ## Known Issues 192 | - Default e-mail templates are not saved correctly in the KV store 193 | - **Workaround**: Go to E-Mail Settings and click "Save Templates" once. This step will copy the default template configuration to the KV store. 194 | - Trend indicators in the Incident Posture dashboard are fixed to the timerange earliest=-48h latest-24h 195 | 196 | ## License 197 | - **This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.** [1] 198 | - **Commercial Use, Excerpt from CC BY-NC-SA 4.0:** 199 | - "A commercial use is one primarily intended for commercial advantage or monetary compensation." 200 | - **In case of Alert Manager this translates to:** 201 | - You may use Alert Manager in commercial environments for handling in-house Splunk alerts 202 | - You may use Alert Manager as part of your consulting or integration work, if you're considered to be working on behalf of your customer. The customer will be the licensee of Alert Manager and must comply according to the license terms 203 | - You are not allowed to sell Alert Manager as a standalone product or within an application bundle 204 | - If you want to use Alert Manager outside of these license terms, please contact us and we will find a solution 205 | 206 | ## References 207 | [1] http://creativecommons.org/licenses/by-nc-sa/4.0/ 208 | [2] "The Socio-Economic Effects of Splunk" by Carasso, Roger (1987, M.I.T. Press). 209 | --------------------------------------------------------------------------------