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
diff --git a/.gitignore b/.gitignore
0new file mode 1006440new file mode 100644
index 0000000..88f01bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
1*.swp
2*~
3.idea
4.tox
5.coverage
6.vscode
7/builds/
8/deb_dist
9/dist
10/repo-info
11__pycache__
12/report/
13
diff --git a/reactive/grafana.py b/reactive/grafana.py
index e1dc444..713aa8f 100644
--- a/reactive/grafana.py
+++ b/reactive/grafana.py
@@ -9,6 +9,7 @@ import shutil
9import six9import six
10import subprocess10import subprocess
11import time11import time
12from jsondiff import diff
1213
13from charmhelpers.contrib.charmsupport import nrpe14from charmhelpers.contrib.charmsupport import nrpe
14from charmhelpers.core import (15from charmhelpers.core import (
@@ -523,20 +524,125 @@ def render_custom(source, context, **parameters):
523 return template.render(context)524 return template.render(context)
524525
525526
527def get_current_dashboards(port, passwd):
528 """Returns list of available dashboards.
529
530 https://grafana.com/docs/grafana/latest/http_api/folder_dashboard_search/
531 """
532 dash_req = requests.get(
533 "http://127.0.0.1:{}/api/search?type=dash-db".format(port),
534 auth=('admin', passwd),
535 )
536 return dash_req.json() if dash_req.status_code == 200 else []
537
538
539def get_current_dashboard_json(uid, port, passwd):
540 """Returns the dashboard revision information (fake rev if not found).
541
542 https://grafana.com/docs/grafana/latest/http_api/dashboard/#get-dashboard-by-uid
543 """
544 default_dashboard = json.loads('{"version": 0}')
545 if not uid:
546 return default_dashboard
547
548 dash_req = requests.get(
549 "http://127.0.0.1:{}/api/dashboards/uid/{}".format(port, uid),
550 auth=('admin', passwd),
551 )
552 if dash_req.status_code != 200:
553 return default_dashboard
554
555 try:
556 return json.loads(dash_req.text)["dashboard"]
557 except json.decoder.JSONDecodeError:
558 return default_dashboard
559
560
561def check_and_add_dashboard(
562 filename, context, prom_metrics, dash_to_uid, port, gf_adminpasswd
563):
564 """Verifies if the rendered dashboard from template is new or do nothing.
565
566 * Renders a template with context gathered from
567 generate_prometheus_dashboards
568 * Checks that metrics in the rendered dashboard have already been retrieved
569 by Prometheus
570
571 https://grafana.com/docs/grafana/latest/features/datasources/prometheus/#query-editor
572 """
573 dashboard_str = render_custom(
574 source=filename,
575 context=context,
576 variable_start_string="<<",
577 variable_end_string=">>",
578 )
579 hookenv.log("Checking Dashboard Template: {}".format(filename))
580 expr = str(re.findall('"expr":(.*),', dashboard_str))
581 metrics = set(re.findall("[a-zA-Z0-9]*_[a-zA-Z0-9_]*", expr))
582 if not metrics:
583 hookenv.log(
584 "Skipping Dashboard Template: {} no metrics in template"
585 " {}".format(filename, metrics)
586 )
587 return
588
589 missing_metrics = set([x for x in metrics if x not in prom_metrics])
590 if missing_metrics:
591 hookenv.log(
592 "Skipping Dashboard Template: {} missing {} metrics."
593 "Missing: {}".format(
594 filename, len(missing_metrics), ', '.join(missing_metrics)
595 ),
596 hookenv.DEBUG,
597 )
598 return
599
600 dashboard_json = json.loads(dashboard_str)
601 # before uploading the dashboard, we should check that it doesn't already exist with same data lp#1858490
602 new = dashboard_json["dashboard"]
603 curr = get_current_dashboard_json(
604 dash_to_uid.get(dashboard_json["dashboard"]["title"], None),
605 port,
606 gf_adminpasswd,
607 )
608 # must remove the versions as they will likely be different
609 del new["version"]
610 del curr["version"]
611 dashboard_changed = diff(new, curr)
612 if not dashboard_changed:
613 hookenv.log(
614 "Skipping Dashboard Template: already up to date: {}".format(filename)
615 )
616 return
617
618 hookenv.log("Using Dashboard Template: {}".format(filename))
619 post_req = "http://127.0.0.1:{}/api/dashboards/db".format(port)
620 r = requests.post(post_req, json=dashboard_json, auth=("admin", gf_adminpasswd))
621
622 if r.status_code != 200:
623 hookenv.log(
624 "Posting template {} failed with error: {}".format(filename, r.text),
625 hookenv.ERROR,
626 )
627
628
526def generate_prometheus_dashboards(gf_adminpasswd, ds):629def generate_prometheus_dashboards(gf_adminpasswd, ds):
527 # prometheus_host = ds630 # prometheus_host = ds
528 ds_name = '{} - {}'.format(ds['service_name'], ds['description'])631 ds_name = '{} - {}'.format(ds['service_name'], ds['description'])
529 config = hookenv.config()632 config = hookenv.config()
530633
634 # https://prometheus.io/docs/prometheus/latest/querying/api/#querying-label-values
531 response = requests.get('{}/api/v1/label/__name__/values'.format(ds['url']))635 response = requests.get('{}/api/v1/label/__name__/values'.format(ds['url']))
532 if response.status_code != 200:636 if response.status_code != 200:
533 hookenv.log('Could not reach prometheus API status code: '637 hookenv.log('Could not reach prometheus API status code: '
534 ' {}'.format(response.status_code), 'ERROR')638 ' {}'.format(response.status_code), 'ERROR')
535 return639 return
536640
641 current_dashboards = get_current_dashboards(config["port"], gf_adminpasswd)
642 dash_to_uid = {dash['title']: dash['uid'] for dash in current_dashboards if 'title' in dash and 'uid' in dash}
643
537 prom_metrics = response.json()['data']644 prom_metrics = response.json()['data']
538 templates_dir = 'templates/dashboards/prometheus'645 templates_dir = 'templates/dashboards/prometheus'
539 post_req = 'http://127.0.0.1:{}/api/dashboards/db'.format(config['port'])
540 context = {'datasource': ds_name,646 context = {'datasource': ds_name,
541 'external_network': config['external_network'],647 'external_network': config['external_network'],
542 'bcache_enabled': "bcache_cache_hit_ratio" in prom_metrics,648 'bcache_enabled': "bcache_cache_hit_ratio" in prom_metrics,
@@ -551,33 +657,16 @@ def generate_prometheus_dashboards(gf_adminpasswd, ds):
551 'ip_status', 'neutron_net', config['external_network'],657 'ip_status', 'neutron_net', config['external_network'],
552 'neutron_public_ip_usage']658 'neutron_public_ip_usage']
553 prom_metrics.extend(ignore_metrics)659 prom_metrics.extend(ignore_metrics)
554 for filename in os.listdir(templates_dir):
555 dashboard_str = render_custom(source=filename,
556 context=context,
557 variable_start_string="<<",
558 variable_end_string=">>",
559 )
560 hookenv.log("Checking Dashboard Template: {}".format(filename))
561 expr = str(re.findall('"expr":(.*),', dashboard_str))
562 metrics = set(re.findall('[a-zA-Z0-9]*_[a-zA-Z0-9_]*', expr))
563 if not metrics:
564 hookenv.log("Skipping Dashboard Template: {} no metrics in template"
565 " {}".format(filename, metrics))
566 continue
567 missing_metrics = set([x for x in metrics if x not in prom_metrics])
568 if missing_metrics:
569 hookenv.log("Skipping Dashboard Template: {} missing {} metrics."
570 "Missing: {}".format(filename, len(missing_metrics),
571 ', '.join(missing_metrics)
572 ), hookenv.DEBUG)
573 else:
574 hookenv.log("Using Dashboard Template: {}".format(filename))
575 dashboard_json = json.loads(dashboard_str)
576 r = requests.post(post_req, json=dashboard_json, auth=('admin', gf_adminpasswd))
577660
578 if r.status_code != 200:661 for filename in os.listdir(templates_dir):
579 hookenv.log("Posting template {} failed with error:"662 check_and_add_dashboard(
580 " {}".format(filename, r.text), 'ERROR')663 filename,
664 context,
665 prom_metrics,
666 dash_to_uid,
667 config["port"],
668 gf_adminpasswd,
669 )
581670
582671
583def generate_query(ds, is_default, id=None):672def generate_query(ds, is_default, id=None):
diff --git a/wheelhouse.txt b/wheelhouse.txt
index f229360..ffdadef 100644
--- a/wheelhouse.txt
+++ b/wheelhouse.txt
@@ -1 +1,2 @@
1requests1requests
2jsondiff

Subscribers

People subscribed via source and target branches

to all changes: