Merge ~rharding/charm-grafana:add-mysql-datasource into ~prometheus-charmers/charm-grafana:master

Proposed by Richard Harding
Status: Superseded
Proposed branch: ~rharding/charm-grafana:add-mysql-datasource
Merge into: ~prometheus-charmers/charm-grafana:master
Diff against target: 204 lines (+108/-35)
1 file modified
reactive/grafana.py (+108/-35)
Reviewer Review Type Date Requested Status
Jamon Camisso (community) Needs Fixing
Paul Collins Needs Fixing
Tom Haddon Needs Fixing
Review via email: mp+329855@code.launchpad.net

This proposal has been superseded by a proposal from 2017-10-18.

Description of the change

This change adds support for a mysql based data source.

This is needed to allow building grafana dashboards on top of MySQL data that is generated as part of Juju KPIs.

In order to do this the current grafana-source endpoints needs to be made more generic. There's work around for issues in the current interface where it auto prefixes things with http:// and pulls the IP address of the relation unit vs getting it fed to the system.

The idea is that I've got a charm that pulls log data and stores it parsed into a mysql db.

mysql -> db relation -> my application charmed -> feeds the data source to grafana for dashboards.

To post a comment you must log in.
Revision history for this message
Richard Harding (rharding) wrote :

Current test to make this work:

juju deploy mysql
juju deploy cs:~rharding/gypsy-danger-1
juju relate mysql:db gypsy-danger
juju deploy grafana (this updated charm)
juju relate grafana:grafana-source gypsy-danger

Log into grafana and see the data source. Hitting the verify it works button should show it works.

https://usercontent.irccloud-cdn.com/file/3KOzJdGO/Selection_010.png

Revision history for this message
Richard Harding (rharding) wrote :

Also wanted to make sure the prometheus grafana-source relation was still in working order and validated that in a gce model:

https://usercontent.irccloud-cdn.com/file/fS9oUqzx/Selection_011.png

Revision history for this message
Richard Harding (rharding) wrote :
Revision history for this message
Tom Haddon (mthaddon) wrote :

Could we have a documentation update for this too explaining the feature?

review: Needs Fixing
Revision history for this message
Jacek Nykis (jacekn) wrote :

Thanks! It's a nice addition and I only had quick look but some early feedback and questions inline. We filed RT#105512 to review and test properly.

Could you clarify what the gypsy-danger charm is? Is it mysql charm fork? Or will it proxy to the DB?

My first instinct, without knowing what the gypsy-danger is, is that if you want to talk to the DB you should relate grafana to mysql.

Revision history for this message
Richard Harding (rharding) wrote :

Thanks for the initial review.

gypsy-danger is a python project to parse the apache log files from the charmstore that are tossed into swift looking for important juju kpi data. It then loads that data into several tables in a mysql database that then allows for building queries against the parsed data.

The python code project is: https://github.com/canonicalltd/gypsy-danger
and I started a charm that will eventually deploy and cron job the fetching of new apache logs from swift each day (updating the mysql database with new log data each day).

I've gotten this working with hand coding/deploying here at: kpi.jujugui.org where it talks to a mysql server and I've put the gypsy-danger python parser code on it's own server and so I'm working to charm this up.

If I build a new relation for mysql->grafana directly then it adds a second path to add a new grafana datasource. It also breaks the usability. My code needs a database to store parsed data from apache logs. It needs to be the same exact database with the same user/credential as grafana needs to build a dashboard upon. In this way the theory is that:

gypsy-danger needs a database to store data in. It requests from mysql a fresh db, username, password. MySQL provides that and gypsy-danger then starts to populate the database with tables and records of data. Then, gypsy-danger wants to have a visual front end of that data and sends the data source information to grafana for visualization.

In fact, one long term thing in my wishlist, is that gypsy-danger could actually send over a saved dashboard to grafana so that both the datasource and the dashboard are loaded and ready to go out of the box.

I hope that clears up what I'm trying to do with this. Let me know if I'm unclear or I've taken a dark path of wrongness that I should stay away from.

b558079... by Richard Harding

Rebase away the initial lint work

Revision history for this message
Paul Collins (pjdc) wrote :

A couple remarks inline. As well as this, I think it'd be valuable to have a Mojo spec to deploy grafana, etc., along with gypsy-danger and mysql and exercise the new grafana functionality so that we can CI it, although I don't know if that should be a blocker. There are a couple of likely-looking jobs on Mojo CI Jenkins, although neither passes currently:

https://jenkins.canonical.com/is-mojo-ci/job/live-is-charm-test-grafana-xenial/
https://jenkins.canonical.com/is-mojo-ci/job/live-is-prometheus/

review: Needs Fixing
Revision history for this message
Jacek Nykis (jacekn) wrote :

> Thanks for the initial review.

Thank you for your contribution!

> If I build a new relation for mysql->grafana directly then it adds a second
> path to add a new grafana datasource. It also breaks the usability. My code
> needs a database to store parsed data from apache logs. It needs to be the
> same exact database with the same user/credential as grafana needs to build a
> dashboard upon.

I see, if it needs to be the same DB then the way you want to do it is probably the only sensible one.

> I hope that clears up what I'm trying to do with this. Let me know if I'm
> unclear or I've taken a dark path of wrongness that I should stay away from.

Thanks, I think it's clear why direct grafana<->mysql relation will not work.

What I'd say though is that it's still worth moving some of the mysql source handling logic into the grafana-source interface. mysql is a valid grafana source and IMO it makes logical sense for the interface to handle it without need for grafana to mangle relation data.

Revision history for this message
Richard Harding (rharding) wrote :

New interfaces MP located here:
https://code.launchpad.net/~rharding/interface-grafana-source/+git/interface-grafana-source/+merge/330041

Will push an updated PR here that supports using that shortly.

18aa0c5... by Richard Harding

Use the updated interface layer

ec70312... by Richard Harding

Revert the status_set lint cleanup

5bc388d... by Richard Harding

More cleanup from the rebase

Revision history for this message
Richard Harding (rharding) wrote :

I've updated the interface and updated this MP removing much of the lint cleanup previously done.

I can look into mojo stuff there but that'll be a longer road to hoe as I've not gotten that setup and since they're not currently working...I'd prefer to add a test/support for gypsy-danger as part of something that was working to test current Grafana/Prometheus if required.

7fa684a... by Richard Harding

Add hook to trigger when relation broken to remove record of datasource

Revision history for this message
Richard Harding (rharding) wrote :

Updated the testing bundle to:

 juju deploy cs:~rharding/gypsy-danger-testing-2

This includes the changes that add the hook to properly remove the datasource when the relation is broken so that it can be readded a second time.

Tested with mysql and prometheus and verified the datasources are removed and re-add successfully.

This goes with the interfaces changes in:

https://code.launchpad.net/~rharding/interface-grafana-source/+git/interface-grafana-source/+merge/330041

Revision history for this message
Jamon Camisso (jamon) wrote :

Looks good to me, it is working well any way that I try it along with the grafana-source charm.

Just two tiny flake8 issues and it's ready to merge and promulgate to the charmstore.

review: Approve
e2c9dc0... by Richard Harding

Flake8 cleanup per reviewj

Revision history for this message
Richard Harding (rharding) wrote :

Updated the flake8 issues per review.

Revision history for this message
Jamon Camisso (jamon) wrote :

Ah so close, but when I go to merge this I see that upstream has progressed ahead a bit in terms of admin-password, google analytics etc.

Could you please rebase your branch against master and push again? Sorry about that, I didn't realize the upstream charm had those changes.

review: Needs Fixing
d78ca59... by Richard Harding

Merge branch 'master' of git+ssh://git.launchpad.net/grafana-charm into add-mysql-datasource

Unmerged commits

d78ca59... by Richard Harding

Merge branch 'master' of git+ssh://git.launchpad.net/grafana-charm into add-mysql-datasource

e2c9dc0... by Richard Harding

Flake8 cleanup per reviewj

7fa684a... by Richard Harding

Add hook to trigger when relation broken to remove record of datasource

5bc388d... by Richard Harding

More cleanup from the rebase

ec70312... by Richard Harding

Revert the status_set lint cleanup

18aa0c5... by Richard Harding

Use the updated interface layer

b558079... by Richard Harding

Rebase away the initial lint work

37eba3c... by Richard Harding

Fix the various cleanup required from the layer

031a16d... by Richard Harding

First stab at getting the mysql source to load into the db correctly

b44f4d5... by Richard Harding

Add first bit of mysql relation definition

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/reactive/grafana.py b/reactive/grafana.py
2index 8b3b126..f63ee5a 100644
3--- a/reactive/grafana.py
4+++ b/reactive/grafana.py
5@@ -1,19 +1,33 @@
6-import six
7-import os
8+import base64
9+import datetime
10 import glob
11 import hashlib
12-import datetime
13-import requests
14 import json
15-import subprocess
16-import base64
17+import os
18+import requests
19 import shutil
20-from charmhelpers import fetch
21-from charmhelpers.core import host, hookenv, unitdata
22-from charmhelpers.core.templating import render
23+import six
24+import subprocess
25+
26 from charmhelpers.contrib.charmsupport import nrpe
27-from charms.reactive import hook, remove_state, set_state, when, when_not
28-from charms.reactive.helpers import any_file_changed, data_changed, is_state
29+from charmhelpers.core import (
30+ hookenv,
31+ host,
32+ unitdata,
33+)
34+from charmhelpers.core.templating import render
35+from charmhelpers import fetch
36+from charms.reactive.helpers import (
37+ any_file_changed,
38+ is_state,
39+)
40+from charms.reactive import (
41+ hook,
42+ remove_state,
43+ set_state,
44+ when,
45+ when_not,
46+)
47
48 import time
49
50@@ -284,20 +298,38 @@ def wipe_nrpe_checks():
51 @when('grafana-source.available')
52 def configure_sources(relation):
53 sources = relation.datasources()
54- if not data_changed('grafana.sources', sources):
55- return
56 for ds in sources:
57 hookenv.log('Found datasource: {}'.format(str(ds)))
58 # Ensure datasource is configured
59 check_datasource(ds)
60
61
62-@when('grafana.started')
63-@when_not('grafana-source.available')
64-def sources_gone():
65- # Last datasource gone, remove as needed
66- # TODO implementation
67- pass
68+@hook('grafana-source-relation-departed')
69+def sources_gone(relation):
70+ hookenv.log('sources gone')
71+ hookenv.log(relation)
72+ # Update the datasources now with the new information that something is
73+ # not available.
74+ conv = relation.conversation()
75+ ds = {
76+ 'service_name': conv.scope.split('/')[0],
77+ 'type': conv.get_remote('type'),
78+ 'url': conv.get_remote('url'),
79+ 'description': conv.get_remote('description')
80+ }
81+ hookenv.log('Removing datasource:')
82+ hookenv.log(ds)
83+
84+ if ds:
85+ conn = sqlite3.connect('/var/lib/grafana/grafana.db', timeout=30)
86+ cur = conn.cursor()
87+ cur.execute(
88+ 'DELETE FROM DATA_SOURCE WHERE type=? AND url=?',
89+ (ds['type'], ds['url'])
90+ )
91+ conn.commit()
92+ cur.close()
93+ relation.set_local = None
94
95
96 @when('website.available')
97@@ -352,14 +384,11 @@ def check_datasource(ds):
98 # 'username': 'username,
99 # 'password': 'password
100 # }
101-
102 conn = sqlite3.connect('/var/lib/grafana/grafana.db', timeout=30)
103 cur = conn.cursor()
104 query = cur.execute('SELECT id, type, name, url, is_default FROM DATA_SOURCE')
105 rows = query.fetchall()
106 ds_name = '{} - {}'.format(ds['service_name'], ds['description'])
107- print(ds_name)
108- print(rows)
109 for row in rows:
110 if (row[1] == ds['type'] and row[2] == ds_name and row[3] == ds['url']):
111 hookenv.log('Datasource already exist, updating: {}'.format(ds_name))
112@@ -371,7 +400,6 @@ def check_datasource(ds):
113 return
114 hookenv.log('Adding new datasource: {}'.format(ds_name))
115 stmt, values = generate_query(ds, 0)
116- print(stmt, values)
117 cur.execute(stmt, values)
118 conn.commit()
119 conn.close()
120@@ -379,15 +407,20 @@ def check_datasource(ds):
121
122 def generate_query(ds, is_default, id=None):
123 if not id:
124- stmt = 'INSERT INTO DATA_SOURCE (org_id, version, type, name' + \
125- ', access, url, is_default, created, updated, basic_auth'
126- if 'username' in ds and 'password' in ds:
127- stmt += ', basic_auth_user, basic_auth_password)' + \
128- ' VALUES (?,?,?,?,?,?,?,?,?,?,?,?)'
129- else:
130- stmt += ') VALUES (?,?,?,?,?,?,?,?,?,?)'
131+ fields = [
132+ 'org_id',
133+ 'version',
134+ 'type',
135+ 'name',
136+ 'access',
137+ 'url',
138+ 'is_default',
139+ 'created',
140+ 'updated',
141+ ]
142+
143 dtime = datetime.datetime.today().strftime("%F %T")
144- values = (1,
145+ values = [1,
146 0,
147 ds['type'],
148 '{} - {}'.format(ds['service_name'], ds['description']),
149@@ -395,11 +428,51 @@ def generate_query(ds, is_default, id=None):
150 ds['url'],
151 is_default,
152 dtime,
153- dtime)
154- if 'username' in ds and 'password' in ds:
155- values = values + (1, ds['username'], ds['password'])
156+ dtime]
157+
158+ # Used in connecting a mysql source
159+ if 'database' in ds:
160+ hookenv.log('Adding fields for the database')
161+ fields.append('database')
162+ values.append(ds['database'])
163+
164+ if ds['type'] == 'mysql':
165+ # There's only one username/password on the interface to we coopt
166+ # it for the mysql login in the mysql case
167+ fields.append('user')
168+ values.append(ds['username'])
169+ fields.append('password')
170+ values.append(ds['password'])
171+
172+ if 'username' in ds and 'password' in ds and ds['type'] != 'msyql':
173+ # We use the username and password in a backward compatible way
174+ # for basic_auth when it's not a mysql data source.
175+ hookenv.log('Adding basic_auth info')
176+ fields.append('basic_auth')
177+ fields.append('basic_auth_user')
178+ fields.append('basic_auth_password')
179+ values.append(1)
180+ values.append(ds['username'])
181+ values.append(ds['password'])
182 else:
183- values = values + (0,)
184+ fields.append('basic_auth')
185+ values.append(0)
186+
187+ values_placeholders = '?, ' * len(values)
188+ # Remove the space [, ] at the end of the generated string.
189+ values_placeholders = values_placeholders[:-2]
190+
191+ stmt = """
192+ INSERT INTO DATA_SOURCE ({})
193+ VALUES ({})
194+ """.format(
195+ ", ".join(fields),
196+ values_placeholders
197+ )
198+
199+ hookenv.log('Statement is: \n{}'.format(stmt))
200+ hookenv.log(values)
201+
202 else:
203 if 'username' in ds and 'password' in ds:
204 stmt = 'UPDATE DATA_SOURCE SET basic_auth_user = ?, basic_auth_password = ?, basic_auth = 1'

Subscribers

People subscribed via source and target branches