Merge ~afreiberger/charm-grafana:lp#1858490 into charm-grafana:master

Proposed by Drew Freiberger
Status: Merged
Approved by: Alvaro Uria
Approved revision: 30ef537101fb86158750682f2e8697afeb58691b
Merged at revision: 220f3b5b72fffbf3dae8f151f32836b5eaa0d18d
Proposed branch: ~afreiberger/charm-grafana:lp#1858490
Merge into: charm-grafana:master
Diff against target: 208 lines (+130/-27)
3 files modified
.gitignore (+13/-0)
reactive/grafana.py (+116/-27)
wheelhouse.txt (+1/-0)
Reviewer Review Type Date Requested Status
Alvaro Uria (community) Approve
Drew Freiberger (community) Needs Resubmitting
Joe Guo (community) Approve
Canonical IS Reviewers Pending
Review via email: mp+377467@code.launchpad.net

Commit message

Check dashboards before uploading new revisions

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
Joe Guo (guoqiao) wrote :

Except pep8 style, looks good to me.

review: Approve
Revision history for this message
Alvaro Uria (aluria) wrote :

Please find comments inline. I think the json_diff_result condition is the opposite of what it should be.

review: Needs Fixing
Revision history for this message
Drew Freiberger (afreiberger) wrote :

merged fixes, added .gitignore to cover tox coverage/report files. Thanks, Alvaro!

review: Needs Resubmitting
Revision history for this message
Alvaro Uria (aluria) wrote :

lgtm

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 220f3b5b72fffbf3dae8f151f32836b5eaa0d18d

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2new file mode 100644
3index 0000000..88f01bf
4--- /dev/null
5+++ b/.gitignore
6@@ -0,0 +1,13 @@
7+*.swp
8+*~
9+.idea
10+.tox
11+.coverage
12+.vscode
13+/builds/
14+/deb_dist
15+/dist
16+/repo-info
17+__pycache__
18+/report/
19+
20diff --git a/reactive/grafana.py b/reactive/grafana.py
21index e1dc444..713aa8f 100644
22--- a/reactive/grafana.py
23+++ b/reactive/grafana.py
24@@ -9,6 +9,7 @@ import shutil
25 import six
26 import subprocess
27 import time
28+from jsondiff import diff
29
30 from charmhelpers.contrib.charmsupport import nrpe
31 from charmhelpers.core import (
32@@ -523,20 +524,125 @@ def render_custom(source, context, **parameters):
33 return template.render(context)
34
35
36+def get_current_dashboards(port, passwd):
37+ """Returns list of available dashboards.
38+
39+ https://grafana.com/docs/grafana/latest/http_api/folder_dashboard_search/
40+ """
41+ dash_req = requests.get(
42+ "http://127.0.0.1:{}/api/search?type=dash-db".format(port),
43+ auth=('admin', passwd),
44+ )
45+ return dash_req.json() if dash_req.status_code == 200 else []
46+
47+
48+def get_current_dashboard_json(uid, port, passwd):
49+ """Returns the dashboard revision information (fake rev if not found).
50+
51+ https://grafana.com/docs/grafana/latest/http_api/dashboard/#get-dashboard-by-uid
52+ """
53+ default_dashboard = json.loads('{"version": 0}')
54+ if not uid:
55+ return default_dashboard
56+
57+ dash_req = requests.get(
58+ "http://127.0.0.1:{}/api/dashboards/uid/{}".format(port, uid),
59+ auth=('admin', passwd),
60+ )
61+ if dash_req.status_code != 200:
62+ return default_dashboard
63+
64+ try:
65+ return json.loads(dash_req.text)["dashboard"]
66+ except json.decoder.JSONDecodeError:
67+ return default_dashboard
68+
69+
70+def check_and_add_dashboard(
71+ filename, context, prom_metrics, dash_to_uid, port, gf_adminpasswd
72+):
73+ """Verifies if the rendered dashboard from template is new or do nothing.
74+
75+ * Renders a template with context gathered from
76+ generate_prometheus_dashboards
77+ * Checks that metrics in the rendered dashboard have already been retrieved
78+ by Prometheus
79+
80+ https://grafana.com/docs/grafana/latest/features/datasources/prometheus/#query-editor
81+ """
82+ dashboard_str = render_custom(
83+ source=filename,
84+ context=context,
85+ variable_start_string="<<",
86+ variable_end_string=">>",
87+ )
88+ hookenv.log("Checking Dashboard Template: {}".format(filename))
89+ expr = str(re.findall('"expr":(.*),', dashboard_str))
90+ metrics = set(re.findall("[a-zA-Z0-9]*_[a-zA-Z0-9_]*", expr))
91+ if not metrics:
92+ hookenv.log(
93+ "Skipping Dashboard Template: {} no metrics in template"
94+ " {}".format(filename, metrics)
95+ )
96+ return
97+
98+ missing_metrics = set([x for x in metrics if x not in prom_metrics])
99+ if missing_metrics:
100+ hookenv.log(
101+ "Skipping Dashboard Template: {} missing {} metrics."
102+ "Missing: {}".format(
103+ filename, len(missing_metrics), ', '.join(missing_metrics)
104+ ),
105+ hookenv.DEBUG,
106+ )
107+ return
108+
109+ dashboard_json = json.loads(dashboard_str)
110+ # before uploading the dashboard, we should check that it doesn't already exist with same data lp#1858490
111+ new = dashboard_json["dashboard"]
112+ curr = get_current_dashboard_json(
113+ dash_to_uid.get(dashboard_json["dashboard"]["title"], None),
114+ port,
115+ gf_adminpasswd,
116+ )
117+ # must remove the versions as they will likely be different
118+ del new["version"]
119+ del curr["version"]
120+ dashboard_changed = diff(new, curr)
121+ if not dashboard_changed:
122+ hookenv.log(
123+ "Skipping Dashboard Template: already up to date: {}".format(filename)
124+ )
125+ return
126+
127+ hookenv.log("Using Dashboard Template: {}".format(filename))
128+ post_req = "http://127.0.0.1:{}/api/dashboards/db".format(port)
129+ r = requests.post(post_req, json=dashboard_json, auth=("admin", gf_adminpasswd))
130+
131+ if r.status_code != 200:
132+ hookenv.log(
133+ "Posting template {} failed with error: {}".format(filename, r.text),
134+ hookenv.ERROR,
135+ )
136+
137+
138 def generate_prometheus_dashboards(gf_adminpasswd, ds):
139 # prometheus_host = ds
140 ds_name = '{} - {}'.format(ds['service_name'], ds['description'])
141 config = hookenv.config()
142
143+ # https://prometheus.io/docs/prometheus/latest/querying/api/#querying-label-values
144 response = requests.get('{}/api/v1/label/__name__/values'.format(ds['url']))
145 if response.status_code != 200:
146 hookenv.log('Could not reach prometheus API status code: '
147 ' {}'.format(response.status_code), 'ERROR')
148 return
149
150+ current_dashboards = get_current_dashboards(config["port"], gf_adminpasswd)
151+ dash_to_uid = {dash['title']: dash['uid'] for dash in current_dashboards if 'title' in dash and 'uid' in dash}
152+
153 prom_metrics = response.json()['data']
154 templates_dir = 'templates/dashboards/prometheus'
155- post_req = 'http://127.0.0.1:{}/api/dashboards/db'.format(config['port'])
156 context = {'datasource': ds_name,
157 'external_network': config['external_network'],
158 'bcache_enabled': "bcache_cache_hit_ratio" in prom_metrics,
159@@ -551,33 +657,16 @@ def generate_prometheus_dashboards(gf_adminpasswd, ds):
160 'ip_status', 'neutron_net', config['external_network'],
161 'neutron_public_ip_usage']
162 prom_metrics.extend(ignore_metrics)
163- for filename in os.listdir(templates_dir):
164- dashboard_str = render_custom(source=filename,
165- context=context,
166- variable_start_string="<<",
167- variable_end_string=">>",
168- )
169- hookenv.log("Checking Dashboard Template: {}".format(filename))
170- expr = str(re.findall('"expr":(.*),', dashboard_str))
171- metrics = set(re.findall('[a-zA-Z0-9]*_[a-zA-Z0-9_]*', expr))
172- if not metrics:
173- hookenv.log("Skipping Dashboard Template: {} no metrics in template"
174- " {}".format(filename, metrics))
175- continue
176- missing_metrics = set([x for x in metrics if x not in prom_metrics])
177- if missing_metrics:
178- hookenv.log("Skipping Dashboard Template: {} missing {} metrics."
179- "Missing: {}".format(filename, len(missing_metrics),
180- ', '.join(missing_metrics)
181- ), hookenv.DEBUG)
182- else:
183- hookenv.log("Using Dashboard Template: {}".format(filename))
184- dashboard_json = json.loads(dashboard_str)
185- r = requests.post(post_req, json=dashboard_json, auth=('admin', gf_adminpasswd))
186
187- if r.status_code != 200:
188- hookenv.log("Posting template {} failed with error:"
189- " {}".format(filename, r.text), 'ERROR')
190+ for filename in os.listdir(templates_dir):
191+ check_and_add_dashboard(
192+ filename,
193+ context,
194+ prom_metrics,
195+ dash_to_uid,
196+ config["port"],
197+ gf_adminpasswd,
198+ )
199
200
201 def generate_query(ds, is_default, id=None):
202diff --git a/wheelhouse.txt b/wheelhouse.txt
203index f229360..ffdadef 100644
204--- a/wheelhouse.txt
205+++ b/wheelhouse.txt
206@@ -1 +1,2 @@
207 requests
208+jsondiff

Subscribers

People subscribed via source and target branches

to all changes: