diff --git a/puppetboard/app.py b/puppetboard/app.py index 8fddc6c..a2d38d2 100644 --- a/puppetboard/app.py +++ b/puppetboard/app.py @@ -13,7 +13,7 @@ from itertools import tee from flask import ( Flask, render_template, abort, url_for, Response, stream_with_context, redirect, - request, session + request, session, jsonify ) from pypuppetdb import connect @@ -24,6 +24,7 @@ from puppetboard.utils import ( get_or_abort, yield_or_stop, jsonprint, prettyprint, Pagination ) +from puppetboard.dailychart import get_daily_reports_chart import werkzeug.exceptions as ex @@ -1109,3 +1110,23 @@ def radiator(env): stats=stats, total=num_nodes ) + + +@app.route('/daily_reports_chart.json', + defaults={'env': app.config['DEFAULT_ENVIRONMENT']}) +@app.route('//daily_reports_chart.json') +def daily_reports_chart(env): + """Return JSON data to generate a bar chart of daily runs. + + If certname is passed as GET argument, the data will target that + node only. + """ + certname = request.args.get('certname') + result = get_or_abort( + get_daily_reports_chart, + db=puppetdb, + env=env, + days_number=app.config['DAILY_REPORTS_CHART_DAYS'], + certname=certname, + ) + return jsonify(result=result) diff --git a/puppetboard/dailychart.py b/puppetboard/dailychart.py new file mode 100644 index 0000000..d5b893a --- /dev/null +++ b/puppetboard/dailychart.py @@ -0,0 +1,78 @@ +from datetime import datetime, timedelta +from pypuppetdb.utils import UTC +from pypuppetdb.QueryBuilder import ( + ExtractOperator, FunctionOperator, AndOperator, + GreaterEqualOperator, LessOperator, EqualsOperator, +) + +DATE_FORMAT = "%Y-%m-%d" +DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" + + +def _iter_dates(days_number): + """Return a list of datetime pairs AB, BC, CD, ... that represent the + 24hs time ranges of today (until this midnight) and the + previous days. + """ + one_day = timedelta(days=1) + today = datetime.utcnow().replace(hour=0, minute=0, second=0, + microsecond=0, tzinfo=UTC()) + days_list = list(today + one_day * (1 - i) for i in range(days_number + 1)) + return zip(days_list[1:], days_list) + + +def _build_query(env, start, end, certname=None): + """Build a extract query with optional certname and environment.""" + query = ExtractOperator() + query.add_field(FunctionOperator('count')) + query.add_field('status') + subquery = AndOperator() + subquery.add(GreaterEqualOperator('start_time', start)) + subquery.add(LessOperator('start_time', end)) + if certname is not None: + subquery.add(EqualsOperator('certname', certname)) + if env != '*': + subquery.add(EqualsOperator('environment', env)) + query.add_query(subquery) + query.add_group_by("status") + return query + + +def _format_report_data(day, query_output): + """Format the output of the query to a simpler dict.""" + result = {'day': day, 'changed': 0, 'unchanged': 0, 'failed': 0} + for out in query_output: + if out['status'] == 'changed': + result['changed'] = out['count'] + elif out['status'] == 'unchanged': + result['unchanged'] = out['count'] + elif out['status'] == 'failed': + result['failed'] = out['count'] + return result + + +def get_daily_reports_chart(db, env, days_number, certname=None): + """Return the sum of each report status (changed, unchanged, failed) + per day, for today and the previous N days. + + This information is used to present a chart. + + :param db: The puppetdb. + :param env: Sum up the reports in this environment. + :param days_number: How many days to sum, including today. + :param certname: If certname is passed, only the reports of that + certname will be added. If certname is not passed, all reports in + the database will be considered. + """ + result = [] + for start, end in reversed(_iter_dates(days_number)): + query = _build_query( + env=env, + start=start.strftime(DATETIME_FORMAT), + end=end.strftime(DATETIME_FORMAT), + certname=certname, + ) + day = start.strftime(DATE_FORMAT) + output = db._query('reports', query=query) + result.append(_format_report_data(day, output)) + return result diff --git a/puppetboard/default_settings.py b/puppetboard/default_settings.py index 60947a2..794ef94 100644 --- a/puppetboard/default_settings.py +++ b/puppetboard/default_settings.py @@ -38,3 +38,5 @@ INVENTORY_FACTS = [('Hostname', 'fqdn'), ('Kernel Version', 'kernelrelease'), ('Puppet Version', 'puppetversion'), ] REFRESH_RATE = 30 +DAILY_REPORTS_CHART_ENABLED = True +DAILY_REPORTS_CHART_DAYS = 8 diff --git a/puppetboard/static/css/puppetboard.css b/puppetboard/static/css/puppetboard.css index 9cd3116..bf2a823 100644 --- a/puppetboard/static/css/puppetboard.css +++ b/puppetboard/static/css/puppetboard.css @@ -194,3 +194,7 @@ th.tablesorter-headerDesc::after { margin-right: -50%; transform: translate(-50%, -50%) } + +#dailyReportsChartContainer { + height: 160px; +} diff --git a/puppetboard/static/js/dailychart.js b/puppetboard/static/js/dailychart.js new file mode 100644 index 0000000..a14dca2 --- /dev/null +++ b/puppetboard/static/js/dailychart.js @@ -0,0 +1,39 @@ +jQuery(function ($) { + function generateChart(el) { + var url = "/daily_reports_chart.json"; + var certname = $(el).attr('data-certname'); + if (typeof certname !== typeof undefined && certname !== false) { + url = url + "?certname=" + certname; + } + d3.json(url, function(data) { + var chart = c3.generate({ + bindto: '#dailyReportsChart', + data: { + type: 'bar', + json: data['result'], + keys: { + x: 'day', + value: ['failed', 'changed', 'unchanged'], + }, + groups: [ + ['failed', 'changed', 'unchanged'] + ], + colors: { // Must match CSS colors + 'failed':'#AA4643', + 'changed':'#4572A7', + 'unchanged':'#89A54E', + } + }, + size: { + height: 160 + }, + axis: { + x: { + type: 'category' + } + } + }); + }); + } + generateChart($("#dailyReportsChart")); +}); diff --git a/puppetboard/templates/index.html b/puppetboard/templates/index.html index 0619a4b..8b00ea2 100644 --- a/puppetboard/templates/index.html +++ b/puppetboard/templates/index.html @@ -1,5 +1,10 @@ {% extends 'layout.html' %} {% import '_macros.html' as macros %} +{% block head %} +{% if config.DAILY_REPORTS_CHART_ENABLED %} + +{% endif %} +{% endblock head %} {% block content %} {% if config.REFRESH_RATE > 0 %} @@ -59,6 +64,11 @@ Avg. resources/node + {% if config.DAILY_REPORTS_CHART_ENABLED %} +
+
+
+ {% endif %}
@@ -114,3 +124,10 @@
{% endblock content %} +{% if config.DAILY_REPORTS_CHART_ENABLED %} +{% block script %} + + + +{% endblock script %} +{% endif %} diff --git a/puppetboard/templates/layout.html b/puppetboard/templates/layout.html index 18a359b..efe7c6f 100644 --- a/puppetboard/templates/layout.html +++ b/puppetboard/templates/layout.html @@ -17,6 +17,7 @@ {% endif %} + {% block head %} {% endblock head %} diff --git a/puppetboard/templates/node.html b/puppetboard/templates/node.html index 8001c73..5d92b8f 100644 --- a/puppetboard/templates/node.html +++ b/puppetboard/templates/node.html @@ -1,5 +1,10 @@ {% extends 'layout.html' %} {% import '_macros.html' as macros %} +{% block head %} +{% if config.DAILY_REPORTS_CHART_ENABLED %} + +{% endif %} +{% endblock head %} {% block content %}
@@ -28,6 +33,11 @@

Reports

+ {% if config.DAILY_REPORTS_CHART_ENABLED %} +
+
+
+ {% endif %} {{ macros.reports_table(reports, reports_count, report_event_counts, condensed=True, hash_truncate=True, show_conf_col=False, show_agent_col=False, show_host_col=False, current_env=current_env)}} Show All
@@ -38,3 +48,10 @@
{% endblock content %} +{% if config.DAILY_REPORTS_CHART_ENABLED %} +{% block script %} + + + +{% endblock script %} +{% endif %}