Skip to content

Commit c33b87c

Browse files
committed
Improve compatibility with Grafana 8.3 / dashboard schema version 33
1 parent f20bc47 commit c33b87c

File tree

6 files changed

+130
-50
lines changed

6 files changed

+130
-50
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ in progress
99
- Add two more examples about using `explore dashboards` with `jq`
1010
- CI: Prepare test suite for testing two different dashboard schema versions, v27 and v33
1111
- Improve determinism by returning stable sort order of dashboard results
12+
- Improve compatibility with Grafana 8.3 by handling dashboard schema version 33 properly
1213

1314
2021-12-11 0.12.0
1415
=================

doc/backlog.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ grafana-wtf backlog
66
******
77
Prio 1
88
******
9+
- [o] With Grafana >8.3, resolve datasource name and add to ``{'type': 'influxdb', 'uid': 'PDF2762CDFF14A314'}``
10+
11+
12+
*********
13+
Prio 1.25
14+
*********
915
- [o] Statistics reports for data sources and panels: https://github.com/panodata/grafana-wtf/issues/18
1016
- [o] Finding invalid data sources: https://github.com/panodata/grafana-wtf/issues/19
1117
- [o] Add test fixture for adding dashboards at runtime from branch ``amo/test-dashboard-runtime``

grafana_wtf/core.py

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# (c) 2019-2021 Andreas Motl <[email protected]>
33
# License: GNU Affero General Public License, Version 3
44
import asyncio
5+
import dataclasses
56
import json
67
import logging
78
from collections import OrderedDict
@@ -18,6 +19,7 @@
1819
DashboardDetails,
1920
DashboardExplorationItem,
2021
DatasourceExplorationItem,
22+
DatasourceItem,
2123
GrafanaDataModel,
2224
)
2325
from grafana_wtf.monkey import monkeypatch_grafana_api
@@ -390,9 +392,10 @@ def explore_datasources(self):
390392
# Compute list of exploration items, associating datasources with the dashboards that use them.
391393
results_used = []
392394
results_unused = []
393-
for name in sorted(ix.datasource_by_name):
394-
datasource = ix.datasource_by_name[name]
395-
dashboard_uids = ix.datasource_dashboard_index.get(name, [])
395+
for ds_identifier in sorted(ix.datasource_by_ident):
396+
397+
datasource = ix.datasource_by_ident[ds_identifier]
398+
dashboard_uids = ix.datasource_dashboard_index.get(ds_identifier, [])
396399
dashboards = list(map(ix.dashboard_by_uid.get, dashboard_uids))
397400
item = DatasourceExplorationItem(datasource=datasource, used_in=dashboards, grafana_url=self.grafana_url)
398401

@@ -404,6 +407,9 @@ def explore_datasources(self):
404407
else:
405408
results_unused.append(result)
406409

410+
results_used = sorted(results_used, key=lambda x: x["datasource"]["uid"] or x["datasource"]["name"])
411+
results_unused = sorted(results_unused, key=lambda x: x["datasource"]["uid"] or x["datasource"]["name"])
412+
407413
response = OrderedDict(
408414
used=results_used,
409415
unused=results_unused,
@@ -422,18 +428,20 @@ def explore_dashboards(self):
422428
for uid in sorted(ix.dashboard_by_uid):
423429

424430
dashboard = ix.dashboard_by_uid[uid]
425-
datasource_names = ix.dashboard_datasource_index[uid]
431+
datasource_items = ix.dashboard_datasource_index[uid]
426432

427433
datasources_existing = []
428-
datasource_names_missing = []
429-
for datasource_name in datasource_names:
430-
if datasource_name == "-- Grafana --":
434+
datasources_missing = []
435+
for datasource_item in datasource_items:
436+
if datasource_item.name == "-- Grafana --":
431437
continue
432-
datasource = ix.datasource_by_name.get(datasource_name)
438+
datasource_by_uid = ix.datasource_by_uid.get(datasource_item.uid)
439+
datasource_by_name = ix.datasource_by_name.get(datasource_item.name)
440+
datasource = datasource_by_uid or datasource_by_name
433441
if datasource:
434442
datasources_existing.append(datasource)
435443
else:
436-
datasource_names_missing.append({"name": datasource_name})
444+
datasources_missing.append(dataclasses.asdict(datasource_item))
437445
item = DashboardExplorationItem(
438446
dashboard=dashboard, datasources=datasources_existing, grafana_url=self.grafana_url
439447
)
@@ -442,8 +450,8 @@ def explore_dashboards(self):
442450
result = item.format_compact()
443451

444452
# Add information about missing data sources.
445-
if datasource_names_missing:
446-
result["datasources_missing"] = datasource_names_missing
453+
if datasources_missing:
454+
result["datasources_missing"] = datasources_missing
447455

448456
results.append(result)
449457

@@ -456,6 +464,8 @@ def __init__(self, engine: GrafanaWtf):
456464

457465
# Prepare index data structures.
458466
self.dashboard_by_uid = {}
467+
self.datasource_by_ident = {}
468+
self.datasource_by_uid = {}
459469
self.datasource_by_name = {}
460470
self.dashboard_datasource_index = {}
461471
self.datasource_dashboard_index = {}
@@ -472,12 +482,16 @@ def index(self):
472482
self.index_datasources()
473483

474484
@staticmethod
475-
def collect_datasource_names(element):
476-
names = []
485+
def collect_datasource_items(element):
486+
items = []
477487
for node in element:
478488
if "datasource" in node and node["datasource"]:
479-
names.append(node.datasource)
480-
return list(sorted(set(names)))
489+
ds = node.datasource
490+
if isinstance(ds, Munch):
491+
ds = dict(ds)
492+
if ds not in items:
493+
items.append(ds)
494+
return sorted(items)
481495

482496
def index_dashboards(self):
483497

@@ -496,21 +510,36 @@ def index_dashboards(self):
496510
self.dashboard_by_uid[uid] = dashboard
497511

498512
# Map to data source names.
499-
ds_panels = self.collect_datasource_names(dbdetails.panels)
500-
ds_annotations = self.collect_datasource_names(dbdetails.annotations)
501-
ds_templating = self.collect_datasource_names(dbdetails.templating)
502-
self.dashboard_datasource_index[uid] = list(sorted(set(ds_panels + ds_annotations + ds_templating)))
513+
ds_panels = self.collect_datasource_items(dbdetails.panels)
514+
ds_annotations = self.collect_datasource_items(dbdetails.annotations)
515+
ds_templating = self.collect_datasource_items(dbdetails.templating)
516+
517+
results = []
518+
for bucket in ds_panels, ds_annotations, ds_templating:
519+
for item in bucket:
520+
item = DatasourceItem.from_payload(item)
521+
if item not in results:
522+
results.append(item)
523+
self.dashboard_datasource_index[uid] = results
503524

504525
def index_datasources(self):
505526

527+
self.datasource_by_ident = {}
528+
self.datasource_by_uid = {}
506529
self.datasource_by_name = {}
507530
self.datasource_dashboard_index = {}
508531

509532
for datasource in self.datasources:
510-
name = datasource.name
511-
self.datasource_by_name[name] = datasource
512-
513-
for dashboard_uid, datasource_names in self.dashboard_datasource_index.items():
514-
for datasource_name in datasource_names:
515-
self.datasource_dashboard_index.setdefault(datasource_name, [])
516-
self.datasource_dashboard_index[datasource_name].append(dashboard_uid)
533+
datasource_name_or_uid = datasource.uid or datasource.name
534+
self.datasource_by_ident[datasource_name_or_uid] = datasource
535+
self.datasource_by_uid[datasource.uid] = datasource
536+
self.datasource_by_name[datasource.name] = datasource
537+
538+
for dashboard_uid, datasource_items in self.dashboard_datasource_index.items():
539+
datasource_item: DatasourceItem
540+
for datasource_item in datasource_items:
541+
datasource_name_or_uid = datasource_item.uid or datasource_item.name
542+
if datasource_name_or_uid in self.datasource_by_name:
543+
datasource_name_or_uid = self.datasource_by_name[datasource_name_or_uid].uid
544+
self.datasource_dashboard_index.setdefault(datasource_name_or_uid, [])
545+
self.datasource_dashboard_index[datasource_name_or_uid].append(dashboard_uid)

grafana_wtf/model.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,24 @@ def templating(self) -> List:
4242
return self.dashboard.dashboard.get("templating", {}).get("list", [])
4343

4444

45+
@dataclasses.dataclass
46+
class DatasourceItem:
47+
uid: Optional[str] = None
48+
name: Optional[str] = None
49+
type: Optional[str] = None
50+
url: Optional[str] = None
51+
52+
@classmethod
53+
def from_payload(cls, payload: any):
54+
if isinstance(payload, Munch):
55+
payload = dict(payload)
56+
if isinstance(payload, dict):
57+
return cls(**payload)
58+
if isinstance(payload, str):
59+
return cls(name=payload)
60+
raise TypeError(f"Unknown payload type for DatasourceItem: {type(payload)}")
61+
62+
4563
@dataclasses.dataclass
4664
class DatasourceExplorationItem:
4765
datasource: Munch
@@ -50,6 +68,7 @@ class DatasourceExplorationItem:
5068

5169
def format_compact(self):
5270
dsshort = OrderedDict(
71+
uid=self.datasource.uid,
5372
name=self.datasource.name,
5473
type=self.datasource.type,
5574
url=self.datasource.url,
@@ -84,6 +103,7 @@ def format_compact(self):
84103
for datasource in self.datasources:
85104
item.setdefault("datasources", [])
86105
dsshort = OrderedDict(
106+
uid=datasource.uid,
87107
name=datasource.name,
88108
type=datasource.type,
89109
url=datasource.url,

grafana_wtf/tabular_report.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from collections import OrderedDict
33

44
from jsonpath_rw import parse
5+
from munch import Munch
56
from tabulate import tabulate
67

78
from grafana_wtf.report import WtfReport
@@ -15,8 +16,8 @@ def __init__(self, grafana_url, tblfmt="psql", verbose=False):
1516
def output_items(self, label, items, url_callback):
1617
items_rows = [
1718
{
18-
"type": label,
19-
"name": self.get_item_name(item),
19+
"Type": label,
20+
"Name": self.get_item_name(item),
2021
**self.get_bibdata_dict(item, URL=url_callback(item)),
2122
}
2223
for item in items
@@ -32,15 +33,35 @@ def get_bibdata_dict(self, item, **kwargs):
3233
bibdata["Title"] = item.data.dashboard.title
3334
bibdata["Folder"] = item.data.meta.folderTitle
3435
bibdata["UID"] = item.data.dashboard.uid
35-
bibdata["Creation date"] = f"{item.data.meta.created}"
36-
bibdata["created by"] = item.data.meta.createdBy
37-
bibdata["last update date"] = f"{item.data.meta.updated}"
36+
bibdata["Created"] = f"{item.data.meta.created}"
37+
bibdata["Updated"] = f"{item.data.meta.updated}"
38+
bibdata["Created by"] = item.data.meta.createdBy
39+
40+
# FIXME: The test fixtures are currently not deterministic,
41+
# because Grafana is not cleared on each test case.
3842
if "PYTEST_CURRENT_TEST" not in os.environ:
39-
bibdata["updated by"] = item.data.meta.updatedBy
40-
_finder = parse("$..datasource")
41-
_datasources = _finder.find(item)
42-
bibdata["datasources"] = ",".join(
43-
sorted(set([str(_ds.value) for _ds in _datasources if _ds.value])) if _datasources else ""
44-
)
43+
bibdata["Updated by"] = item.data.meta.updatedBy
44+
45+
bibdata["Datasources"] = ",".join(map(str, self.get_datasources(item)))
4546
bibdata.update(kwargs)
4647
return bibdata
48+
49+
def get_datasources(self, item):
50+
51+
# Query datasources.
52+
_finder = parse("$..datasource")
53+
_datasources = _finder.find(item)
54+
55+
# Compute unique list of datasources.
56+
datasources = []
57+
for _ds in _datasources:
58+
if not _ds.value:
59+
continue
60+
if isinstance(_ds.value, Munch):
61+
value = dict(_ds.value)
62+
else:
63+
value = str(_ds.value)
64+
if value not in datasources:
65+
datasources.append(value)
66+
67+
return datasources

tests/test_commands.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def test_find_textual_dashboard_success(docker_grafana, capsys):
6767
assert "dashboard.panels.[7].panels.[0].targets.[0].measurement: ldi_readings" in captured.out
6868

6969

70-
def test_find_textual_datasource_dashboard_success(docker_grafana, capsys):
70+
def test_find_textual_datasource_success(docker_grafana, capsys):
7171
set_command("find ldi_v2")
7272
grafana_wtf.commands.run()
7373
captured = capsys.readouterr()
@@ -77,7 +77,7 @@ def test_find_textual_datasource_dashboard_success(docker_grafana, capsys):
7777
assert "name: ldi_v2" in captured.out
7878
assert "database: ldi_v2" in captured.out
7979

80-
assert "Dashboards: 2 hits" in captured.out
80+
assert "Dashboards: 1 hits" in captured.out
8181
assert "luftdaten-info-generic-trend" in captured.out
8282
assert "dashboard.panels.[1].datasource: ldi_v2" in captured.out
8383
assert "dashboard.panels.[7].panels.[0].datasource: ldi_v2" in captured.out
@@ -91,13 +91,13 @@ def test_find_tabular_dashboard_success(docker_grafana, capsys):
9191
assert 'Searching for expression "ldi_readings" at Grafana instance http://localhost:3000' in captured.out
9292

9393
reference_table = """
94-
| type | name | Title | Folder | UID | Creation date | created by | last update date | datasources | URL |
95-
|:-----------|:---------------------------------|:---------------------------------|:----------|:----------|:---------------------|:-------------|:---------------------|:---------------------------------|:-------------------------------------------------------------------|
96-
| Dashboards | luftdaten-info-generic-trend-v27 | luftdaten.info generic trend v27 | Testdrive | ioUrPwQiz | xxxx-xx-xxTxx:xx:xxZ | Anonymous | xxxx-xx-xxTxx:xx:xxZ | -- Grafana --,ldi_v2,weatherbase | http://localhost:3000/d/ioUrPwQiz/luftdaten-info-generic-trend-v27 |
97-
| Dashboards | luftdaten-info-generic-trend-v33 | luftdaten.info generic trend v33 | Testdrive | jpVsQxRja | xxxx-xx-xxTxx:xx:xxZ | Anonymous | xxxx-xx-xxTxx:xx:xxZ | -- Grafana --,ldi_v2,weatherbase | http://localhost:3000/d/jpVsQxRja/luftdaten-info-generic-trend-v33 |
94+
| Type | Name | Title | Folder | UID | Created | Updated | Created by | Datasources | URL |
95+
|:-----------|:---------------------------------|:---------------------------------|:----------|:----------|:---------------------|:---------------------|:-------------|:--------------------------------------------------------------------------------------|:-------------------------------------------------------------------|
96+
| Dashboards | luftdaten-info-generic-trend-v27 | luftdaten.info generic trend v27 | Testdrive | ioUrPwQiz | xxxx-xx-xxTxx:xx:xxZ | xxxx-xx-xxTxx:xx:xxZ | Anonymous | -- Grafana --,ldi_v2,weatherbase | http://localhost:3000/d/ioUrPwQiz/luftdaten-info-generic-trend-v27 |
97+
| Dashboards | luftdaten-info-generic-trend-v33 | luftdaten.info generic trend v33 | Testdrive | jpVsQxRja | xxxx-xx-xxTxx:xx:xxZ | xxxx-xx-xxTxx:xx:xxZ | Anonymous | -- Grafana --,{'type': 'influxdb', 'uid': 'PDF2762CDFF14A314'},{'uid': 'weatherbase'} | http://localhost:3000/d/jpVsQxRja/luftdaten-info-generic-trend-v33 |
9898
""".strip()
9999

100-
output_table = captured.out[captured.out.find("| type") :]
100+
output_table = captured.out[captured.out.find("| Type") :]
101101
output_table_normalized = re.sub(
102102
r"\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ", r"xxxx-xx-xxTxx:xx:xxZ", output_table
103103
).strip()
@@ -123,8 +123,8 @@ def test_replace_dashboard_success(docker_grafana, capsys):
123123
# assert "name: ldi_v2" in captured.out
124124
# assert "database: ldi_v2" in captured.out
125125

126-
assert "Dashboards: 2 hits" in captured.out
127-
assert "luftdaten-info-generic-trend" in captured.out
126+
assert "Dashboards: 1 hits" in captured.out
127+
assert "luftdaten-info-generic-trend-v27" in captured.out
128128
assert "Folder Testdrive" in captured.out
129129
assert "dashboard.panels.[1].datasource: ldi_v3" in captured.out
130130
assert "dashboard.panels.[7].panels.[0].datasource: ldi_v3" in captured.out
@@ -222,15 +222,18 @@ def test_explore_dashboards(docker_grafana, create_datasource, capsys, caplog):
222222
assert len(data) >= 1
223223

224224
missing = find_all_missing_datasources(data)
225-
assert missing == ["weatherbase"]
225+
226+
# Those are bogus!
227+
assert missing[0]["name"] == "weatherbase"
228+
assert missing[1]["uid"] == "weatherbase"
226229

227230

228231
def find_all_missing_datasources(data):
229-
missing_names = []
232+
missing_items = []
230233
for item in data:
231234
if "datasources_missing" in item:
232-
missing_names += map(operator.itemgetter("name"), item["datasources_missing"])
233-
return sorted(set(missing_names))
235+
missing_items += item["datasources_missing"]
236+
return sorted(missing_items, key=lambda x: x["uid"] or x["name"])
234237

235238

236239
def test_info(docker_grafana, capsys, caplog):

0 commit comments

Comments
 (0)