Skip to content

Commit 2ce4a94

Browse files
committed
Add explore dashboards --data-details option
This extends the output by many more details about data inquiry/queries.
1 parent 7ece73d commit 2ce4a94

File tree

6 files changed

+135
-12
lines changed

6 files changed

+135
-12
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ in progress
1010
- CI: Update to Grafana 8.5.27, 9.5.8, and 10.1.1
1111
- Grafana 9.3: Work around delete folder operation returning empty body
1212
- Grafana 9.5: Use standard UUIDs instead of short UIDs
13+
- Add ``explore dashboards --data-details`` option, to extend the output
14+
by many more details about data inquiry / queries. Thanks, @meyerder.
1315

1416
2023-07-30 0.15.2
1517
=================

README.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ How to find dashboards which use non-existing data sources?
154154
# Display only dashboards which have missing data sources, along with their names.
155155
grafana-wtf explore dashboards --format=json | jq '.[] | select( .datasources_missing ) | .dashboard + {ds_missing: .datasources_missing[] | [.name]}'
156156

157+
How to list all queries used in all dashboards?
158+
::
159+
160+
grafana-wtf explore dashboards --data-details --format=json | \
161+
jq -r '.[].details | values[] | .[].query // "null"'
162+
157163

158164
Searching for strings
159165
=====================

grafana_wtf/commands.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def run():
2929
Usage:
3030
grafana-wtf [options] info
3131
grafana-wtf [options] explore datasources
32-
grafana-wtf [options] explore dashboards
32+
grafana-wtf [options] explore dashboards [--data-details]
3333
grafana-wtf [options] find [<search-expression>]
3434
grafana-wtf [options] replace <search-expression> <replacement> [--dry-run]
3535
grafana-wtf [options] log [<dashboard_uid>] [--number=<count>] [--head=<count>] [--tail=<count>] [--reverse] [--sql=<sql>]
@@ -91,6 +91,12 @@ def run():
9191
# Display all dashboards using data sources with a specific type. Here: InfluxDB.
9292
grafana-wtf explore dashboards --format=json | jq 'select( .[] | .datasources | .[].type=="influxdb" )'
9393
94+
# Display dashboards and many more details about where data source queries are happening.
95+
# Specifically, within "panels/targets", "annotations", and "templating" slots.
96+
grafana-wtf explore dashboards --data-details --format=json
97+
98+
# Display all database queries within dashboards.
99+
grafana-wtf explore dashboards --data-details --format=json | jq -r '.[].details | values[] | .[].query // "null"'
94100
95101
Find dashboards and data sources:
96102
@@ -298,7 +304,7 @@ def run():
298304
output_results(output_format, results)
299305

300306
if options.explore and options.dashboards:
301-
results = engine.explore_dashboards()
307+
results = engine.explore_dashboards(with_data_details=options.data_details)
302308
output_results(output_format, results)
303309

304310
if options.info:

grafana_wtf/core.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ def explore_datasources(self):
447447

448448
return response
449449

450-
def explore_dashboards(self):
450+
def explore_dashboards(self, with_data_details: bool = False):
451451
# Prepare indexes, mapping dashboards by uid, datasources by name
452452
# as well as dashboards to datasources and vice versa.
453453
ix = Indexer(engine=self)
@@ -481,8 +481,8 @@ def explore_dashboards(self):
481481
dashboard=dashboard, datasources=datasources_existing, grafana_url=self.grafana_url
482482
)
483483

484-
# Format results in a more compact form, using only a subset of all the attributes.
485-
result = item.format_compact()
484+
# Format results, using only a subset of all the attributes.
485+
result = item.format(with_data_details=with_data_details)
486486

487487
# Add information about missing data sources.
488488
if datasources_missing:

grafana_wtf/model.py

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,21 +91,99 @@ class DashboardExplorationItem:
9191
datasources: List[Munch]
9292
grafana_url: str
9393

94-
def format_compact(self):
94+
def format(self, with_data_details: bool = False):
95+
"""
96+
Generate a representation from selected information.
97+
98+
- dashboard
99+
- datasources
100+
- details
101+
- panels/targets
102+
- annotations
103+
- templating
104+
"""
95105
dbshort = OrderedDict(
96106
title=self.dashboard.dashboard.title,
97107
uid=self.dashboard.dashboard.uid,
98108
path=self.dashboard.meta.url,
99109
url=urljoin(self.grafana_url, self.dashboard.meta.url),
100110
)
101-
item = OrderedDict(dashboard=dbshort)
111+
112+
dsshort = []
102113
for datasource in self.datasources:
103-
item.setdefault("datasources", [])
104-
dsshort = OrderedDict(
114+
item = OrderedDict(
105115
uid=datasource.get("uid"),
106116
name=datasource.name,
107117
type=datasource.type,
108118
url=datasource.url,
109119
)
110-
item["datasources"].append(dsshort)
111-
return item
120+
dsshort.append(item)
121+
122+
data = Munch(dashboard=dbshort, datasources=dsshort)
123+
if with_data_details:
124+
data.details = self.collect_data_details()
125+
return data
126+
127+
def collect_data_details(self):
128+
"""
129+
Collect details concerned about data from dashboard information.
130+
"""
131+
132+
dbdetails = DashboardDetails(dashboard=self.dashboard)
133+
134+
ds_panels = self.collect_data_nodes(dbdetails.panels)
135+
ds_annotations = self.collect_data_nodes(dbdetails.annotations)
136+
ds_templating = self.collect_data_nodes(dbdetails.templating)
137+
138+
targets = []
139+
for panel in ds_panels:
140+
panel_item = self._format_panel_compact(panel)
141+
for target in panel.targets:
142+
target["_panel"] = panel_item
143+
targets.append(target)
144+
145+
response = OrderedDict(targets=targets, annotations=ds_annotations, templating=ds_templating)
146+
147+
return response
148+
149+
@staticmethod
150+
def collect_data_nodes(element):
151+
"""
152+
Select all element nodes which have a "datasource" attribute.
153+
"""
154+
element = element or []
155+
items = []
156+
157+
def add(item):
158+
if item is not None and item not in items:
159+
items.append(item)
160+
161+
for node in element:
162+
if "datasource" in node and node["datasource"]:
163+
add(node)
164+
165+
return items
166+
167+
@staticmethod
168+
def _format_panel_compact(panel):
169+
"""
170+
Return a compact representation of panel information.
171+
"""
172+
attributes = ["id", "title", "type", "datasource"]
173+
data = OrderedDict()
174+
for attribute in attributes:
175+
data[attribute] = panel.get(attribute)
176+
return data
177+
178+
@staticmethod
179+
def _format_data_node_compact(item: Dict) -> Dict:
180+
"""
181+
Return a compact representation of an element concerned about data.
182+
"""
183+
data = OrderedDict()
184+
data["datasource"] = item.get("datasource")
185+
data["type"] = item.get("type")
186+
for key, value in item.items():
187+
if "query" in key.lower():
188+
data[key] = value
189+
return data

tests/test_commands.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import warnings
22

3+
from munch import munchify
34
from packaging import version
45

56
warnings.filterwarnings("ignore", category=DeprecationWarning, module=".*docopt.*")
@@ -463,6 +464,36 @@ def test_explore_dashboards_grafana7up(grafana_version, ldi_resources, capsys, c
463464
assert dashboard["datasources_missing"][0]["type"] is None
464465

465466

467+
def test_explore_dashboards_data_details(ldi_resources, capsys, caplog):
468+
"""
469+
Explore more details of dashboards, wrt. to data and queries.
470+
"""
471+
472+
# Only provision specific dashboard.
473+
ldi_resources(dashboards=["tests/grafana/dashboards/ldi-v33.json"])
474+
475+
# Compute exploration.
476+
set_command("explore dashboards --data-details", "--format=yaml")
477+
478+
# Run command and capture YAML output.
479+
with caplog.at_level(logging.DEBUG):
480+
grafana_wtf.commands.run()
481+
captured = capsys.readouterr()
482+
data = yaml.safe_load(captured.out)
483+
484+
# Proof the output is correct.
485+
assert len(data) == 1
486+
dashboard = munchify(data[0])
487+
assert dashboard.details.targets[0]._panel.id == 18
488+
assert dashboard.details.targets[0]._panel.type == "graph"
489+
assert dashboard.details.targets[0]._panel.datasource.type == "influxdb"
490+
assert dashboard.details.targets[0]._panel.datasource.uid == "PDF2762CDFF14A314"
491+
assert dashboard.details.targets[0].fields == [{'func': 'mean', 'name': 'P1'}]
492+
assert dashboard.details.templating[0].query == \
493+
"SELECT osm_country_code AS __value, country_and_countrycode AS __text " \
494+
"FROM ldi_network ORDER BY osm_country_code"
495+
496+
466497
def test_explore_dashboards_empty_annotations(grafana_version, create_datasource, create_dashboard, capsys, caplog):
467498
# Create a dashboard with an anomalous value in the "annotations" slot.
468499
dashboard = mkdashboard(title="foo")
@@ -488,7 +519,7 @@ def test_explore_dashboards_empty_annotations(grafana_version, create_datasource
488519
assert len(dashboard["dashboard"]["uid"]) == 36
489520
else:
490521
assert len(dashboard["dashboard"]["uid"]) == 9
491-
assert "datasources" not in dashboard
522+
assert dashboard["datasources"] == []
492523
assert "datasources_missing" not in dashboard
493524

494525

0 commit comments

Comments
 (0)