Skip to content

Commit 1bd0881

Browse files
committed
Add --queries-only option to explore dashboards subcommand
1 parent 9d277e4 commit 1bd0881

File tree

6 files changed

+138
-74
lines changed

6 files changed

+138
-74
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ in progress
1010
- Fix wrong ``jq`` commands in documentation. Thanks, @rahulnandan.
1111
- Fix collecting data information from dashboards w/o ``targets`` slots
1212
in panels
13+
- Add ``--queries-only`` option to ``explore dashboards`` subcommand
1314

1415
2023-10-03 0.16.0
1516
=================

README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ How to find dashboards using specific data sources?
206206
How to list all queries used in all dashboards?
207207
::
208208

209-
grafana-wtf explore dashboards --data-details --format=json | \
210-
jq -r '.[].details | values[] | .[].query // "null"'
209+
grafana-wtf explore dashboards --data-details --queries-only --format=json | \
210+
jq '.[].details | values[] | .[] | .expr,.jql,.query,.rawSql | select( . != null and . != "" )'
211211

212212

213213
Searching for strings

grafana_wtf/commands.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def run():
3333
Usage:
3434
grafana-wtf [options] info
3535
grafana-wtf [options] explore datasources
36-
grafana-wtf [options] explore dashboards [--data-details]
36+
grafana-wtf [options] explore dashboards [--data-details] [--queries-only]
3737
grafana-wtf [options] find [<search-expression>]
3838
grafana-wtf [options] replace <search-expression> <replacement> [--dry-run]
3939
grafana-wtf [options] log [<dashboard_uid>] [--number=<count>] [--head=<count>] [--tail=<count>] [--reverse] [--sql=<sql>]
@@ -98,11 +98,11 @@ def run():
9898
grafana-wtf explore dashboards --format=json | jq '.[] | select(.datasources | .[].type=="influxdb")'
9999
100100
# Display dashboards and many more details about where data source queries are happening.
101-
# Specifically, within "panels/targets", "annotations", and "templating" slots.
101+
# Specifically, within "panels", "annotations", and "templating" slots.
102102
grafana-wtf explore dashboards --data-details --format=json
103103
104104
# Display all database queries within dashboards.
105-
grafana-wtf explore dashboards --data-details --format=json | jq -r '.[].details | values[] | .[].query // "null"'
105+
grafana-wtf explore dashboards --data-details --queries-only --format=json | jq '.[].details | values[] | .[] | .expr,.jql,.query,.rawSql | select( . != null and . != "" )'
106106
107107
Find dashboards and data sources:
108108
@@ -315,7 +315,7 @@ def run():
315315
output_results(output_format, results)
316316

317317
if options.explore and options.dashboards:
318-
results = engine.explore_dashboards(with_data_details=options.data_details)
318+
results = engine.explore_dashboards(with_data_details=options.data_details, queries_only=options.queries_only)
319319
output_results(output_format, results)
320320

321321
if options.info:

grafana_wtf/core.py

Lines changed: 4 additions & 2 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, with_data_details: bool = False):
450+
def explore_dashboards(self, with_data_details: bool = False, queries_only: 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)
@@ -482,7 +482,9 @@ def explore_dashboards(self, with_data_details: bool = False):
482482
)
483483

484484
# Format results, using only a subset of all the attributes.
485-
result = item.format(with_data_details=with_data_details)
485+
result = item.format(with_data_details=with_data_details, queries_only=queries_only)
486+
if result is None:
487+
continue
486488

487489
# Add information about missing data sources.
488490
if datasources_missing:

grafana_wtf/model.py

Lines changed: 122 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,118 @@ def templating(self) -> List:
4444
return self.dashboard.dashboard.get("templating", {}).get("list", [])
4545

4646

47+
@dataclasses.dataclass
48+
class DashboardDataDetails:
49+
"""
50+
Manage details concerned about "data"-relevant information,
51+
a subset of the complete bunch of dashboard information.
52+
"""
53+
54+
panels: List[Dict]
55+
annotations: List[Dict]
56+
templating: List[Dict]
57+
58+
def to_munch(self):
59+
return Munch(panels=self.panels, annotations=self.annotations, templating=self.templating)
60+
61+
@classmethod
62+
def from_dashboard_details(cls, dbdetails: DashboardDetails):
63+
ds_panels = cls.collect_data_nodes(dbdetails.panels)
64+
ds_annotations = cls.collect_data_nodes(dbdetails.annotations)
65+
ds_templating = cls.collect_data_nodes(dbdetails.templating)
66+
67+
# Inline panel information into data details.
68+
targets = []
69+
for panel in ds_panels:
70+
panel_item = cls._format_panel_compact(panel)
71+
if "targets" in panel:
72+
for target in panel.targets:
73+
target["_panel"] = panel_item
74+
targets.append(target)
75+
76+
return cls(panels=targets, annotations=ds_annotations, templating=ds_templating)
77+
78+
@staticmethod
79+
def collect_data_nodes(element):
80+
"""
81+
Select all element nodes which have a "datasource" attribute.
82+
"""
83+
element = element or []
84+
items = []
85+
86+
def add(item):
87+
if item is not None and item not in items:
88+
items.append(item)
89+
90+
for node in element:
91+
if "datasource" in node and node["datasource"]:
92+
add(node)
93+
94+
return items
95+
96+
@staticmethod
97+
def _format_panel_compact(panel):
98+
"""
99+
Return a compact representation of panel information.
100+
"""
101+
attributes = ["id", "title", "type", "datasource"]
102+
data = OrderedDict()
103+
for attribute in attributes:
104+
data[attribute] = panel.get(attribute)
105+
return data
106+
107+
@staticmethod
108+
def _format_data_node_compact(item: Dict) -> Dict:
109+
"""
110+
Return a compact representation of an element concerned about data.
111+
"""
112+
data = OrderedDict()
113+
data["datasource"] = item.get("datasource")
114+
data["type"] = item.get("type")
115+
for key, value in item.items():
116+
if "query" in key.lower():
117+
data[key] = value
118+
return data
119+
120+
def queries_only(self):
121+
"""
122+
Return a representation of data details information, only where query expressions are present.
123+
"""
124+
# All attributes containing query-likes.
125+
attributes_query_likes = ["expr", "jql", "query", "rawSql", "target"]
126+
127+
attributes = [
128+
# Carry over datasource and panel references for informational purposes.
129+
"datasource",
130+
"_panel",
131+
] + attributes_query_likes
132+
133+
def transform(section):
134+
new_items = []
135+
for item in section:
136+
new_item = OrderedDict()
137+
for key, value in item.items():
138+
if key in attributes:
139+
new_item[key] = value
140+
if any(attr in attributes_query_likes for attr in new_item.keys()):
141+
# Filter annotations without any value.
142+
if "target" in new_item and isinstance(new_item["target"], dict):
143+
if new_item["target"].get("type") == "dashboard":
144+
continue
145+
# Unnest items with nested "query" slot.
146+
for slot in ["query", "target"]:
147+
if slot in new_item and isinstance(new_item[slot], dict) and "query" in new_item[slot]:
148+
new_item["query"] = new_item[slot]["query"]
149+
new_items.append(new_item)
150+
return new_items
151+
152+
return DashboardDataDetails(
153+
panels=transform(self.panels),
154+
annotations=transform(self.annotations),
155+
templating=transform(self.templating),
156+
)
157+
158+
47159
@dataclasses.dataclass
48160
class DatasourceItem:
49161
uid: Optional[str] = None
@@ -94,7 +206,7 @@ class DashboardExplorationItem:
94206
datasources: List[Munch]
95207
grafana_url: str
96208

97-
def format(self, with_data_details: bool = False):
209+
def format(self, with_data_details: bool = False, queries_only: bool = False):
98210
"""
99211
Generate a representation from selected information.
100212
@@ -124,70 +236,19 @@ def format(self, with_data_details: bool = False):
124236

125237
data = Munch(dashboard=dbshort, datasources=dsshort)
126238
if with_data_details:
127-
data.details = self.collect_data_details()
239+
details = self.collect_data_details(queries_only=queries_only)
240+
if not details.panels and not details.annotations and not details.templating:
241+
return None
242+
data.details = details.to_munch()
128243
return data
129244

130-
def collect_data_details(self):
245+
def collect_data_details(self, queries_only: bool = False):
131246
"""
132247
Collect details concerned about data from dashboard information.
133248
"""
134249

135250
dbdetails = DashboardDetails(dashboard=self.dashboard)
136-
137-
ds_panels = self.collect_data_nodes(dbdetails.panels)
138-
ds_annotations = self.collect_data_nodes(dbdetails.annotations)
139-
ds_templating = self.collect_data_nodes(dbdetails.templating)
140-
141-
targets = []
142-
for panel in ds_panels:
143-
panel_item = self._format_panel_compact(panel)
144-
if "targets" in panel:
145-
for target in panel.targets:
146-
target["_panel"] = panel_item
147-
targets.append(target)
148-
149-
response = OrderedDict(targets=targets, annotations=ds_annotations, templating=ds_templating)
150-
151-
return response
152-
153-
@staticmethod
154-
def collect_data_nodes(element):
155-
"""
156-
Select all element nodes which have a "datasource" attribute.
157-
"""
158-
element = element or []
159-
items = []
160-
161-
def add(item):
162-
if item is not None and item not in items:
163-
items.append(item)
164-
165-
for node in element:
166-
if "datasource" in node and node["datasource"]:
167-
add(node)
168-
169-
return items
170-
171-
@staticmethod
172-
def _format_panel_compact(panel):
173-
"""
174-
Return a compact representation of panel information.
175-
"""
176-
attributes = ["id", "title", "type", "datasource"]
177-
data = OrderedDict()
178-
for attribute in attributes:
179-
data[attribute] = panel.get(attribute)
180-
return data
181-
182-
@staticmethod
183-
def _format_data_node_compact(item: Dict) -> Dict:
184-
"""
185-
Return a compact representation of an element concerned about data.
186-
"""
187-
data = OrderedDict()
188-
data["datasource"] = item.get("datasource")
189-
data["type"] = item.get("type")
190-
for key, value in item.items():
191-
if "query" in key.lower():
192-
data[key] = value
193-
return data
251+
dbdatadetails = DashboardDataDetails.from_dashboard_details(dbdetails)
252+
if queries_only:
253+
dbdatadetails = dbdatadetails.queries_only()
254+
return dbdatadetails

tests/test_commands.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -487,11 +487,11 @@ def test_explore_dashboards_data_details(ldi_resources, capsys, caplog):
487487
# Proof the output is correct.
488488
assert len(data) == 1
489489
dashboard = munchify(data[0])
490-
assert dashboard.details.targets[0]._panel.id == 18
491-
assert dashboard.details.targets[0]._panel.type == "graph"
492-
assert dashboard.details.targets[0]._panel.datasource.type == "influxdb"
493-
assert dashboard.details.targets[0]._panel.datasource.uid == "PDF2762CDFF14A314"
494-
assert dashboard.details.targets[0].fields == [{"func": "mean", "name": "P1"}]
490+
assert dashboard.details.panels[0]._panel.id == 18
491+
assert dashboard.details.panels[0]._panel.type == "graph"
492+
assert dashboard.details.panels[0]._panel.datasource.type == "influxdb"
493+
assert dashboard.details.panels[0]._panel.datasource.uid == "PDF2762CDFF14A314"
494+
assert dashboard.details.panels[0].fields == [{"func": "mean", "name": "P1"}]
495495
assert (
496496
dashboard.details.templating[0].query
497497
== "SELECT osm_country_code AS __value, country_and_countrycode AS __text "

0 commit comments

Comments
 (0)