@@ -13,7 +13,7 @@ from itertools import tee
|
|||||||
from flask import (
|
from flask import (
|
||||||
Flask, render_template, abort, url_for,
|
Flask, render_template, abort, url_for,
|
||||||
Response, stream_with_context, redirect,
|
Response, stream_with_context, redirect,
|
||||||
request, session
|
request, session, jsonify
|
||||||
)
|
)
|
||||||
|
|
||||||
from pypuppetdb import connect
|
from pypuppetdb import connect
|
||||||
@@ -24,6 +24,7 @@ from puppetboard.utils import (
|
|||||||
get_or_abort, yield_or_stop,
|
get_or_abort, yield_or_stop,
|
||||||
jsonprint, prettyprint, Pagination
|
jsonprint, prettyprint, Pagination
|
||||||
)
|
)
|
||||||
|
from puppetboard.dailychart import get_daily_reports_chart
|
||||||
|
|
||||||
import werkzeug.exceptions as ex
|
import werkzeug.exceptions as ex
|
||||||
|
|
||||||
@@ -1109,3 +1110,23 @@ def radiator(env):
|
|||||||
stats=stats,
|
stats=stats,
|
||||||
total=num_nodes
|
total=num_nodes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/daily_reports_chart.json',
|
||||||
|
defaults={'env': app.config['DEFAULT_ENVIRONMENT']})
|
||||||
|
@app.route('/<env>/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)
|
||||||
|
|||||||
78
puppetboard/dailychart.py
Normal file
78
puppetboard/dailychart.py
Normal file
@@ -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
|
||||||
@@ -38,3 +38,5 @@ INVENTORY_FACTS = [('Hostname', 'fqdn'),
|
|||||||
('Kernel Version', 'kernelrelease'),
|
('Kernel Version', 'kernelrelease'),
|
||||||
('Puppet Version', 'puppetversion'), ]
|
('Puppet Version', 'puppetversion'), ]
|
||||||
REFRESH_RATE = 30
|
REFRESH_RATE = 30
|
||||||
|
DAILY_REPORTS_CHART_ENABLED = True
|
||||||
|
DAILY_REPORTS_CHART_DAYS = 8
|
||||||
|
|||||||
1
puppetboard/static/css/c3.min.css
vendored
Normal file
1
puppetboard/static/css/c3.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.c3 svg{font:10px sans-serif;-webkit-tap-highlight-color:transparent}.c3 line,.c3 path{fill:none;stroke:#000}.c3 text{-webkit-user-select:none;-moz-user-select:none;user-select:none}.c3-bars path,.c3-event-rect,.c3-legend-item-tile,.c3-xgrid-focus,.c3-ygrid{shape-rendering:crispEdges}.c3-chart-arc path{stroke:#fff}.c3-chart-arc text{fill:#fff;font-size:13px}.c3-grid line{stroke:#aaa}.c3-grid text{fill:#aaa}.c3-xgrid,.c3-ygrid{stroke-dasharray:3 3}.c3-text.c3-empty{fill:gray;font-size:2em}.c3-line{stroke-width:1px}.c3-circle._expanded_{stroke-width:1px;stroke:#fff}.c3-selected-circle{fill:#fff;stroke-width:2px}.c3-bar{stroke-width:0}.c3-bar._expanded_{fill-opacity:.75}.c3-target.c3-focused{opacity:1}.c3-target.c3-focused path.c3-line,.c3-target.c3-focused path.c3-step{stroke-width:2px}.c3-target.c3-defocused{opacity:.3!important}.c3-region{fill:#4682b4;fill-opacity:.1}.c3-brush .extent{fill-opacity:.1}.c3-legend-item{font-size:12px}.c3-legend-item-hidden{opacity:.15}.c3-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.c3-title{font:14px sans-serif}.c3-tooltip-container{z-index:10}.c3-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;-webkit-box-shadow:7px 7px 12px -9px #777;-moz-box-shadow:7px 7px 12px -9px #777;box-shadow:7px 7px 12px -9px #777;opacity:.9}.c3-tooltip tr{border:1px solid #CCC}.c3-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#FFF}.c3-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.c3-tooltip td>span{display:inline-block;width:10px;height:10px;margin-right:6px}.c3-tooltip td.value{text-align:right}.c3-area{stroke-width:0;opacity:.2}.c3-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}.c3-chart-arcs .c3-chart-arcs-background{fill:#e0e0e0;stroke:none}.c3-chart-arcs .c3-chart-arcs-gauge-unit{fill:#000;font-size:16px}.c3-chart-arcs .c3-chart-arcs-gauge-max,.c3-chart-arcs .c3-chart-arcs-gauge-min{fill:#777}.c3-chart-arc .c3-gauge-value{fill:#000}
|
||||||
@@ -194,3 +194,7 @@ th.tablesorter-headerDesc::after {
|
|||||||
margin-right: -50%;
|
margin-right: -50%;
|
||||||
transform: translate(-50%, -50%)
|
transform: translate(-50%, -50%)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#dailyReportsChartContainer {
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|||||||
9
puppetboard/static/js/c3.min.js
vendored
9
puppetboard/static/js/c3.min.js
vendored
File diff suppressed because one or more lines are too long
39
puppetboard/static/js/dailychart.js
Normal file
39
puppetboard/static/js/dailychart.js
Normal file
@@ -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"));
|
||||||
|
});
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% import '_macros.html' as macros %}
|
{% import '_macros.html' as macros %}
|
||||||
|
{% block head %}
|
||||||
|
{% if config.DAILY_REPORTS_CHART_ENABLED %}
|
||||||
|
<link href="{{ url_for('static', filename='css/c3.min.css') }}" rel="stylesheet">
|
||||||
|
{% endif %}
|
||||||
|
{% endblock head %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if config.REFRESH_RATE > 0 %}
|
{% if config.REFRESH_RATE > 0 %}
|
||||||
<meta http-equiv="refresh" content="{{config.REFRESH_RATE}}">
|
<meta http-equiv="refresh" content="{{config.REFRESH_RATE}}">
|
||||||
@@ -59,6 +64,11 @@
|
|||||||
<span>Avg. resources/node</span>
|
<span>Avg. resources/node</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if config.DAILY_REPORTS_CHART_ENABLED %}
|
||||||
|
<div id="dailyReportsChartContainer" class="one column row">
|
||||||
|
<div id="dailyReportsChart"></div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<div class="one column row">
|
<div class="one column row">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
@@ -114,3 +124,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
{% if config.DAILY_REPORTS_CHART_ENABLED %}
|
||||||
|
{% block script %}
|
||||||
|
<script src="{{url_for('static', filename='js/d3.min.js')}}"></script>
|
||||||
|
<script src="{{url_for('static', filename='js/c3.min.js')}}"></script>
|
||||||
|
<script src="{{url_for('static', filename='js/dailychart.js')}}"></script>
|
||||||
|
{% endblock script %}
|
||||||
|
{% endif %}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<link href="{{ url_for('static', filename='Semantic-UI-2.1.8/semantic.min.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='Semantic-UI-2.1.8/semantic.min.css') }}" rel="stylesheet">
|
||||||
<link href="{{ url_for('static', filename='css/puppetboard.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='css/puppetboard.css') }}" rel="stylesheet">
|
||||||
|
{% block head %} {% endblock head %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% import '_macros.html' as macros %}
|
{% import '_macros.html' as macros %}
|
||||||
|
{% block head %}
|
||||||
|
{% if config.DAILY_REPORTS_CHART_ENABLED %}
|
||||||
|
<link href="{{ url_for('static', filename='css/c3.min.css') }}" rel="stylesheet">
|
||||||
|
{% endif %}
|
||||||
|
{% endblock head %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class='ui two column grid'>
|
<div class='ui two column grid'>
|
||||||
<div class='column'>
|
<div class='column'>
|
||||||
@@ -28,6 +33,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
<h1>Reports</h1>
|
<h1>Reports</h1>
|
||||||
|
{% if config.DAILY_REPORTS_CHART_ENABLED %}
|
||||||
|
<div id="dailyReportsChartContainer" class="one column row">
|
||||||
|
<div id="dailyReportsChart" data-certname="{{node.name}}"></div>
|
||||||
|
</div>
|
||||||
|
{% 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)}}
|
{{ 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)}}
|
||||||
<a href="{{url_for('reports_node', node_name=node.name)}}">Show All</a>
|
<a href="{{url_for('reports_node', node_name=node.name)}}">Show All</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,3 +48,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
{% if config.DAILY_REPORTS_CHART_ENABLED %}
|
||||||
|
{% block script %}
|
||||||
|
<script src="{{url_for('static', filename='js/d3.min.js')}}"></script>
|
||||||
|
<script src="{{url_for('static', filename='js/c3.min.js')}}"></script>
|
||||||
|
<script src="{{url_for('static', filename='js/dailychart.js')}}"></script>
|
||||||
|
{% endblock script %}
|
||||||
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user