From 6f4fc8ca12d500a3e668a5b9f89e78beb27af7db Mon Sep 17 00:00:00 2001 From: Joe Ray Date: Wed, 13 Apr 2016 11:53:01 +0100 Subject: [PATCH 1/6] Add events view which displays checks from the /events API --- griddata.py | 46 ++++++++++++++++++++++++++++++++++++++ sensugrid.py | 39 ++++++++++++++++++++++---------- templates/events.html | 21 +++++++++++++++++ templates/menu_detail.html | 2 +- templates/menu_main.html | 13 ++++++++++- 5 files changed, 107 insertions(+), 14 deletions(-) create mode 100644 templates/events.html diff --git a/griddata.py b/griddata.py index 0c3e71c..9a7005c 100644 --- a/griddata.py +++ b/griddata.py @@ -95,6 +95,52 @@ def get_stashes(dc): return data +def filter_object(obj, search): + for value in obj.values(): + if type(value) == object: + return filter_object(value, search) + + if unicode(search) in unicode(value): + return True + + return False + + +def filter_events(filters): + def filter_event(event): + for f in filters: + if filter_object(event['client'], f): + return True + if filter_object(event['check'], f): + return True + return False + + return filter_event + + +def get_events(dc, filters=[]): + url = 'http://{0}:{1}/events'.format(dc['url'], dc['port']) + + data = [] + r = None + + try: + if 'user' and 'password' in dc: + r = requests.get(url, auth=(dc['user'], dc['password'])) + data = r.json() + else: + r = requests.get(url) + data = r.json() + finally: + if r: + r.close() + + if len(filters) > 0: + return filter(filter_events(filters), data) + else: + return data + + def agg_data(dc, data, stashes, client_data=None, filters=None): """ Aggregates json data and returns count of ok, warn, crit diff --git a/sensugrid.py b/sensugrid.py index 0bfaf27..fef2fce 100644 --- a/sensugrid.py +++ b/sensugrid.py @@ -39,24 +39,30 @@ def root(): return render_template('data.html', dcs=dcs, data=aggregated, filter_data=get_filter_data(dcs), appcfg=appcfg) -@app.route('/filtered/', methods=['GET']) -def filtered(subscriptions): +@app.route('/filtered/', methods=['GET']) +def filtered(filters): aggregated = [] for dc in dcs: if check_connection(dc): - aggregated.append(agg_data(dc, get_data(dc), get_stashes(dc), get_clients(dc), subscriptions)) + aggregated.append(agg_data(dc, get_data(dc), get_stashes(dc), get_clients(dc), filters)) return render_template('data.html', dcs=dcs, data=aggregated, filter_data=get_filter_data(dcs), appcfg=appcfg) @app.route('/show/', methods=['GET']) -def showgrid(d): +@app.route('/show//filtered/', methods=['GET']) +def showgrid(d, filters=None): data_detail = {} if dcs: for dc in dcs: if dc['name'] == d: if check_connection(dc): - data_detail = agg_host_data(get_data(dc), get_stashes(dc)) + if filters: + clients = get_clients(dc) + else: + clients = None + + data_detail = agg_host_data(get_data(dc), get_stashes(dc), clients, filters) if data_detail: break else: @@ -64,18 +70,27 @@ def showgrid(d): return render_template('detail.html', dc=dc, data=data_detail, filter_data=get_filter_data(dcs), appcfg=appcfg) -@app.route('/show//filtered/', methods=['GET']) -def showgrid_filtered(d, subscriptions): - aggregated = {} +@app.route('/events/') +@app.route('/events//filtered/') +def events(d, filters=''): + results = [] + + dc_found = False + if dcs: for dc in dcs: if dc['name'] == d: + dc_found = True if check_connection(dc): - aggregated = (agg_host_data(get_data(dc), get_stashes(dc), get_clients(dc), subscriptions)) - if len(aggregated) > 0: - break + results += get_events(dc, filters.split(',')) + break + + if dc_found is False: + abort(404) + + results = sorted(results, lambda x, y: cmp(x['check']['status'], y['check']['status']), reverse=True) - return render_template('detail.html', dc=dc, data=aggregated, filter_data=get_filter_data(dcs), appcfg=appcfg) + return render_template('events.html', dc=dc, data=results, filter_data=get_filter_data(dcs), appcfg=appcfg) @app.route('/healthcheck', methods=['GET']) diff --git a/templates/events.html b/templates/events.html new file mode 100644 index 0000000..c1f223f --- /dev/null +++ b/templates/events.html @@ -0,0 +1,21 @@ +{% extends "menu_detail.html" %} +{% block data %} + + {% for event in data %} + {% if loop.first or loop.index0 % 3 == 0 %}
{% endif %} +
+
+
+

{{ event.check.name }}

+

{{ event.client.display_name or event.client.displayName or event.client.name }}

+
+ + Detail + + +
+
+ {% if loop.last or loop.index0 % 3 == 2 %}
{% endif %} + {% endfor %} + +{% endblock %} \ No newline at end of file diff --git a/templates/menu_detail.html b/templates/menu_detail.html index 9594ac9..7dcf0b2 100644 --- a/templates/menu_detail.html +++ b/templates/menu_detail.html @@ -18,7 +18,7 @@ Filter by diff --git a/templates/menu_main.html b/templates/menu_main.html index 8362c99..c75315f 100644 --- a/templates/menu_main.html +++ b/templates/menu_main.html @@ -29,6 +29,17 @@ {% endif %} + + {% if filter_data and filter_data | length > 0 %}
  • {{ d }}
  • +
  • {{ d }}
  • {% endfor %} From 42e22bb777f907d6bf22f8ed2450d619ddee8ad9 Mon Sep 17 00:00:00 2001 From: Joe Ray Date: Wed, 13 Apr 2016 11:58:15 +0100 Subject: [PATCH 2/6] Make the provided config an example This ensures that the actual conf/config.yaml file won't get checked in. --- .gitignore | 3 +++ README.md | 2 ++ conf/{config.yaml => config.yaml.sample} | 0 3 files changed, 5 insertions(+) rename conf/{config.yaml => config.yaml.sample} (100%) diff --git a/.gitignore b/.gitignore index 09cf52c..42f6f3b 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ crashlytics-build.properties # testing .coverage + +# Config +conf/config.yaml diff --git a/README.md b/README.md index de9e6ae..2b493cb 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,8 @@ Add via pip install or via your package management ## configuration +You should copy the ```conf/config.yaml.sample``` file to ```conf/config.yaml``` and edit as appropriate. + If you use username/password, ensure you set the appropriate permissions - e.g. ```chmod 640 conf/config.yaml``` and set the owner to ```sensu-grid``` which you'll be using to run this app. ### example config diff --git a/conf/config.yaml b/conf/config.yaml.sample similarity index 100% rename from conf/config.yaml rename to conf/config.yaml.sample From 4c85af0d64fcd8326b22c6b6ed4443bb0d0f5595 Mon Sep 17 00:00:00 2001 From: Joe Ray Date: Wed, 13 Apr 2016 11:59:00 +0100 Subject: [PATCH 3/6] Add a success screen to the events page If you specify no_fail_image in the app: section of the config, this image will be displayed. --- templates/events.html | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/templates/events.html b/templates/events.html index c1f223f..8da58a9 100644 --- a/templates/events.html +++ b/templates/events.html @@ -16,6 +16,23 @@

    {{ event.check.name }}

    {% if loop.last or loop.index0 % 3 == 2 %}{% endif %} + {% else %} +
    +
    +
    +
    + {% if appcfg.no_fail_image %} + All checks passing + {% else %} +

    All checks passing

    + {% endif %} +
    + +
    +
    +
    +
    +
    {% endfor %} {% endblock %} \ No newline at end of file From 510964f1a0352faf4afb6e3413eebcaefef51393 Mon Sep 17 00:00:00 2001 From: Joe Ray Date: Wed, 13 Apr 2016 14:09:37 +0100 Subject: [PATCH 4/6] Better format events view with icons and clear names --- sensugrid.py | 27 +++++++++++++++++++++++++++ templates/events.html | 7 +++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/sensugrid.py b/sensugrid.py index fef2fce..0cf148c 100644 --- a/sensugrid.py +++ b/sensugrid.py @@ -120,6 +120,33 @@ def healthcheck(): return json.dumps(ret) +@app.template_filter('color_for_event') +def color_for_event(event): + if event['check']['name'] == 'keepalive': + return 'purple' + if event['check']['status'] == 1: + return 'yellow' + if event['check']['status'] == 2: + return 'red' + if event['check']['status'] == 0: + return 'green' + + return 'gray' + + +@app.template_filter('icon_for_event') +def icon_for_event(event): + if event['check']['name'] == 'keepalive': + return 'arrow-circle-down' + if event['check']['status'] == 1: + return 'exclamation-circle' + if event['check']['status'] == 2: + return 'times-circle-o' + if event['check']['status'] == 0: + return 'check-circle' + + return 'question-circle' + if __name__ == '__main__': app.run(host='0.0.0.0', diff --git a/templates/events.html b/templates/events.html index 8da58a9..8e0d2a0 100644 --- a/templates/events.html +++ b/templates/events.html @@ -4,9 +4,12 @@ {% for event in data %} {% if loop.first or loop.index0 % 3 == 0 %}
    {% endif %}
    -
    +
    -

    {{ event.check.name }}

    +

    {{ event.check.name|replace('_', ' ')|replace('-', ' ')|replace('.', ' ') }}

    +
    + +

    {{ event.client.display_name or event.client.displayName or event.client.name }}

    From 33531b8aa65654cf5bb6224e590d30a1a28fd295 Mon Sep 17 00:00:00 2001 From: Joe Ray Date: Thu, 14 Apr 2016 12:28:40 +0100 Subject: [PATCH 5/6] Update README to reflect Events view --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2b493cb..fb8f372 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Flask application to connect to a list of sensu-api servers and displays a grid - overview by data centre ( Name, OK, WARN, CRIT, DOWN, ACK ) - detail view by data centre ( Grid of hosts, color changes based on amount of alerting checks, 1 = yellow, > 1 = red, down = purple ) - +- Events view by data centre (Grid of events which are currently alerting/warning/unknown) - filter by hosts' subscription/s ( Only shows matchin hosts' check results in overview and detail view) ## screenshots @@ -19,14 +19,14 @@ Overview (DCs) # faq -#### how can I filter by more than 1 subscription ? +#### how can I filter by more than 1 value? -Amend the URL and add all the subscriptions together as a comma-separated list, e.g.: +Amend the URL and add all the filters together as a comma-separated list, e.g.: http://localhost:5000/filtered/aaa,bbb,ccc,ddd #### what do the filters filter by ? -They filter based on the hosts' subscriptions. +They filter based on the hosts' subscriptions, except in the Events view where they filter on all properties of the check and the host. # docker From 67c1332c802bca83833be296512154cf7346f02b Mon Sep 17 00:00:00 2001 From: Joe Ray Date: Thu, 14 Apr 2016 15:06:31 +0100 Subject: [PATCH 6/6] Improve filtering logic to cope with varied nested datatypes --- griddata.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/griddata.py b/griddata.py index 9a7005c..dce12d2 100644 --- a/griddata.py +++ b/griddata.py @@ -96,12 +96,16 @@ def get_stashes(dc): def filter_object(obj, search): - for value in obj.values(): - if type(value) == object: - return filter_object(value, search) - - if unicode(search) in unicode(value): - return True + if type(obj) == dict: + for k, value in obj.iteritems(): + if filter_object(value, search): + return True + elif type(obj) == list: + for value in obj: + if filter_object(value, search): + return True + else: + return unicode(search) in unicode(obj) return False @@ -109,9 +113,7 @@ def filter_object(obj, search): def filter_events(filters): def filter_event(event): for f in filters: - if filter_object(event['client'], f): - return True - if filter_object(event['check'], f): + if filter_object(event, f): return True return False