diff --git a/README.rst b/README.rst index 85e294a..735510c 100644 --- a/README.rst +++ b/README.rst @@ -52,6 +52,11 @@ For example, here is a minimal dashboard specification environments. Users can specify their dashboards via a normal review process and tests can validate their correctness. +The tool can also take JSON manually exported from the Grafana +interface and load it as a dashboard. This allows keeping dashboards +that have been edited with the inbuilt editor externally version +controlled. + A large number of examples are available in the OpenStack `project-config `__ diff --git a/doc/source/grafana-dashboard.rst b/doc/source/grafana-dashboard.rst index 4ded481..1f00275 100644 --- a/doc/source/grafana-dashboard.rst +++ b/doc/source/grafana-dashboard.rst @@ -38,7 +38,8 @@ Update Command ``grafana-dashboard`` [options] update -Updates each specified dashboard to the lastest layout from parsed yaml files. +Updates each specified dashboard to the lastest layout from parsed +yaml or json files. FILES ===== diff --git a/grafana_dashboards/builder.py b/grafana_dashboards/builder.py index 425ee21..7332128 100644 --- a/grafana_dashboards/builder.py +++ b/grafana_dashboards/builder.py @@ -51,7 +51,8 @@ class Builder(object): files_to_process.extend([os.path.join(path, f) for f in os.listdir(path) if (f.endswith('.yaml') - or f.endswith('.yml'))]) + or f.endswith('.yml') + or f.endswith('.json'))]) else: files_to_process.append(path) diff --git a/grafana_dashboards/parser.py b/grafana_dashboards/parser.py index 20fc8d7..4e6e3c2 100644 --- a/grafana_dashboards/parser.py +++ b/grafana_dashboards/parser.py @@ -50,21 +50,29 @@ class YamlParser(object): def parse_fp(self, fp): data = yaml.safe_load(fp) - result = self.validate(data) - for item in result.items(): - group = self.data.get(item[0], {}) - # Create slug to make it easier to find dashboards. - if item[0] == 'dashboard': - name = item[1]['title'] - else: - name = item[1]['name'] - slug = slugify(name) - if slug in group: - raise Exception( - "Duplicate {0} found in '{1}: '{2}' " - "already defined".format(item[0], fp.name, name)) - group[slug] = item[1] - self.data[item[0]] = group + # Since a json file is valid YAML, we just pass through + # any JSON files + if fp.name.endswith('.json'): + slug = slugify(data['title']) + if not self.data.get('dashboard'): + self.data['dashboard'] = {} + self.data['dashboard'][slug] = data + else: + result = self.validate(data) + for item in result.items(): + group = self.data.get(item[0], {}) + # Create slug to make it easier to find dashboards. + if item[0] == 'dashboard': + name = item[1]['title'] + else: + name = item[1]['name'] + slug = slugify(name) + if slug in group: + raise Exception( + "Duplicate {0} found in '{1}: '{2}' " + "already defined".format(item[0], fp.name, name)) + group[slug] = item[1] + self.data[item[0]] = group def validate(self, data): schema = Schema() diff --git a/tests/fixtures/parser/json-dashboard-0001.json b/tests/fixtures/parser/json-dashboard-0001.json new file mode 100644 index 0000000..2279700 --- /dev/null +++ b/tests/fixtures/parser/json-dashboard-0001.json @@ -0,0 +1,141 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 38, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "stats.haproxy.balance_git_http.gitea01.opendev.org.bout" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Fresh update", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "schemaVersion": 25, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "test json", + "uid": "M-GEcyWMk", + "version": 1 +} diff --git a/tests/test_parser.py b/tests/test_parser.py index e5f4d8b..221cc54 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -100,3 +100,12 @@ class TestCaseParser(TestCase): def _get_empty_dashboard(self, name): res, md5 = self.parser.get_dashboard(name) self.assertEqual(res, None) + + def test_parse_json(self): + path = os.path.join( + os.path.dirname(__file__), + 'fixtures/parser/json-dashboard-0001.json') + self.parser.parse(path) + # Get parsed dashboard + res, md5 = self.parser.get_dashboard('test-json') + self.assertEqual(res['title'], 'test json')