Skip to content

Commit 2bda452

Browse files
committed
History: Add SQL querying capabilities
1 parent 5feff62 commit 2bda452

File tree

5 files changed

+94
-3
lines changed

5 files changed

+94
-3
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ in progress
1414
- History: Unlock YAML export format
1515
- History: Add new options ``--head``, ``--tail``, and ``--reverse``
1616
- Search: Unlock JSON and YAML export formats
17+
- History: Add SQL querying capabilities
1718

1819
2023-03-05 0.14.1
1920
=================

grafana_wtf/commands.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
# (c) 2019-2021 Andreas Motl <[email protected]>
2+
# (c) 2019-2023 Andreas Motl <[email protected]>
33
# License: GNU Affero General Public License, Version 3
44
import logging
55
import os
@@ -15,6 +15,7 @@
1515
from grafana_wtf.report.tabular import TabularSearchReport, get_table_format, TabularEditHistoryReport
1616
from grafana_wtf.util import (
1717
configure_http_logging,
18+
filter_with_sql,
1819
normalize_options,
1920
read_list,
2021
setup_logging,
@@ -31,7 +32,7 @@ def run():
3132
grafana-wtf [options] explore dashboards
3233
grafana-wtf [options] find [<search-expression>]
3334
grafana-wtf [options] replace <search-expression> <replacement> [--dry-run]
34-
grafana-wtf [options] log [<dashboard_uid>] [--number=<count>] [--head=<count>] [--tail=<count>] [--reverse]
35+
grafana-wtf [options] log [<dashboard_uid>] [--number=<count>] [--head=<count>] [--tail=<count>] [--reverse] [--sql=<sql>]
3536
grafana-wtf --version
3637
grafana-wtf (-h | --help)
3738
@@ -129,6 +130,22 @@ def run():
129130
# Output full history table in Markdown format
130131
grafana-wtf log --format=tabular:pipe
131132
133+
# Display dashboards with only a single edit, in JSON format.
134+
grafana-wtf log --sql="
135+
SELECT uid, url, COUNT(version) as number_of_edits
136+
FROM dashboard_versions
137+
GROUP BY uid, url
138+
HAVING number_of_edits=1
139+
"
140+
141+
# Display dashboards with only a single edit, in YAML format, `url` attribute only.
142+
grafana-wtf log --format=yaml --sql="
143+
SELECT url
144+
FROM dashboard_versions
145+
GROUP BY uid, url
146+
HAVING COUNT(version)=1
147+
"
148+
132149
Cache control:
133150
134151
# Use infinite cache expiration time, essentially caching forever.
@@ -231,8 +248,23 @@ def run():
231248

232249
if options.log:
233250

251+
# Sanity checks.
252+
if output_format.startswith("tab") and options.sql:
253+
raise DocoptExit(
254+
f"Options --format={output_format} and --sql can not be used together, only data output is supported."
255+
)
256+
234257
entries = engine.log(dashboard_uid=options.dashboard_uid)
235-
entries = sorted(entries, key=itemgetter("datetime"))
258+
259+
if options.sql is not None:
260+
log.info(f"Filtering result with SQL expression: {options.sql}")
261+
entries = filter_with_sql(
262+
data=entries,
263+
view_name="dashboard_versions",
264+
expression=options.sql,
265+
)
266+
else:
267+
entries = sorted(entries, key=itemgetter("datetime"))
236268

237269
if options.number is not None:
238270
limit = int(options.number)

grafana_wtf/util.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import json
66
import logging
77
import sys
8+
import typing as t
89
from collections import OrderedDict
910

1011
import yaml
@@ -171,3 +172,34 @@ def to_list(value):
171172
if not isinstance(value, list):
172173
value = [value]
173174
return value
175+
176+
177+
trecord = t.List[t.Dict[str, str]]
178+
179+
180+
def filter_with_sql(data: trecord, view_name: str, expression: str) -> trecord:
181+
"""
182+
Filter data in "records" shape by SQL expression, using pandas and DuckDB.
183+
184+
- https://duckdb.org/
185+
- https://pandas.pydata.org/
186+
187+
Example::
188+
189+
SELECT uid, url, COUNT(version) as number_of_edits
190+
FROM dashboard_versions
191+
GROUP BY uid, url
192+
HAVING number_of_edits=1
193+
194+
:param data: Data in "record" shape, aka. list of dictionaries
195+
:param expression: SQL expression
196+
:param view_name: View name the data is registered at, when querying per SQL.
197+
:return:
198+
"""
199+
import pandas as pd
200+
import duckdb
201+
df = pd.DataFrame.from_records(data)
202+
duckdb.register(view_name, df)
203+
results = duckdb.sql(expression)
204+
entries = results.to_df().to_dict(orient="records")
205+
return entries

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
"docopt>=0.6.2,<0.7",
1414
"munch>=2.5.0,<3",
1515
"tqdm>=4.60.0,<5",
16+
# Filtering
17+
"pandas<1.6",
18+
"duckdb<0.8",
1619
# Grafana
1720
"requests>=2.23.0,<3",
1821
"grafana-client>=2.1.0,<4",

tests/test_commands.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,29 @@ def test_log_yaml_success(ldi_resources, capsys, caplog):
317317
assert len(data) == 3
318318

319319

320+
def test_log_filter_sql(ldi_resources, capsys, caplog):
321+
# Only provision specific dashboard(s).
322+
ldi_resources(dashboards=["tests/grafana/dashboards/ldi-v27.json", "tests/grafana/dashboards/ldi-v33.json"])
323+
324+
# Run command and capture output.
325+
set_command("""log --format=yaml --sql='
326+
SELECT url
327+
FROM dashboard_versions
328+
GROUP BY uid, url
329+
HAVING COUNT(version)=1
330+
'
331+
""")
332+
with caplog.at_level(logging.DEBUG):
333+
grafana_wtf.commands.run()
334+
captured = capsys.readouterr()
335+
336+
assert captured.out.strip().split("\n") == [
337+
"- url: http://localhost:33333/d/ioUrPwQiz/luftdaten-info-generic-trend-v27",
338+
"- url: http://localhost:33333/d/jpVsQxRja/luftdaten-info-generic-trend-v33",
339+
"- url: http://localhost:33333/dashboards/f/testdrive/testdrive",
340+
]
341+
342+
320343
def test_explore_datasources_used(create_datasource, create_dashboard, capsys, caplog):
321344
# Create two data sources and a dashboard which uses them.
322345
ds_foo = create_datasource(name="foo")

0 commit comments

Comments
 (0)