Skip to content

Commit 4fcf1b5

Browse files
committed
Add plugins {list,status} subcommands, to inquire Grafana plugins
1 parent 9544fa3 commit 4fcf1b5

File tree

7 files changed

+152
-2
lines changed

7 files changed

+152
-2
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ in progress
1212
- Grafana 9.5: Use standard UUIDs instead of short UIDs
1313
- Add ``explore dashboards --data-details`` option, to extend the output
1414
by many more details about data inquiry / queries. Thanks, @meyerder.
15+
- Add ``plugins {list,status}`` subcommands, to inquire installed Grafana
16+
plugins. Thanks, @bhks.
1517

1618
2023-07-30 0.15.2
1719
=================

README.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ Explore dashboards and datasources in more detail.
6060
grafana-wtf explore dashboards
6161
grafana-wtf explore datasources
6262

63+
Explore plugins.
64+
::
65+
66+
grafana-wtf plugins list
67+
grafana-wtf plugins status
68+
6369
Run with Docker::
6470

6571
# Access Grafana instance on localhost, without authentication.

grafana_wtf/commands.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ def run():
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>]
40+
grafana-wtf [options] plugins list
41+
grafana-wtf [options] plugins status
4042
grafana-wtf --version
4143
grafana-wtf (-h | --help)
4244
@@ -156,6 +158,14 @@ def run():
156158
HAVING COUNT(version)=1
157159
"
158160
161+
List plugins:
162+
163+
# Inquire plugin list.
164+
grafana-wtf plugins list
165+
166+
# Inquire plugin health check and metrics endpoints.
167+
grafana-wtf plugins status
168+
159169
Cache control:
160170
161171
# Use infinite cache expiration time, essentially caching forever.
@@ -311,3 +321,12 @@ def run():
311321
if options.info:
312322
response = engine.info()
313323
output_results(output_format, response)
324+
325+
if options.plugins:
326+
if options.list:
327+
response = engine.plugins_list()
328+
elif options.status:
329+
response = engine.plugins_status()
330+
else:
331+
raise DocoptExit('Subcommand "plugins" only provides "list" and "status"')
332+
output_results(output_format, response)

grafana_wtf/core.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,40 @@ def explore_dashboards(self, with_data_details: bool = False):
492492

493493
return results
494494

495+
def plugins_list(self):
496+
return self.grafana.plugin.get_installed_plugins()
497+
498+
def plugins_status(self):
499+
status = []
500+
plugins = self.grafana.plugin.get_installed_plugins()
501+
for plugin in plugins:
502+
plugin = munchify(plugin)
503+
item = Munch(
504+
name=plugin.name,
505+
type=plugin.type,
506+
id=plugin.id,
507+
enabled=plugin.enabled,
508+
category=plugin.category,
509+
version=plugin.info.version,
510+
signature=plugin.get("signature"),
511+
)
512+
513+
# Status inquiry is not provided by all plugins. Let's filter them.
514+
# Effectively, run it only on non-internal "app" and "datasource" items.
515+
if item.type != "panel" and item.signature != "internal":
516+
try:
517+
item.health = self.grafana.plugin.health_check_plugin(plugin.id)
518+
except Exception as ex:
519+
log.warning(f"Health check failed for plugin {item.id}, type={item.type}: {ex}")
520+
try:
521+
item.metrics = self.grafana.plugin.get_plugin_metrics(plugin.id)
522+
except Exception as ex:
523+
log.warning(f"Metrics inquiry failed for plugin {item.id}, type={item.type}: {ex}")
524+
else:
525+
log.info(f"Skipping status inquiry for plugin {item.id}, type={item.type}")
526+
status.append(item)
527+
return status
528+
495529

496530
class Indexer:
497531
def __init__(self, engine: GrafanaWtf):

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
f"duckdb<0.9; {no_linux_on_arm}",
2121
# Grafana
2222
"requests>=2.26,<3",
23-
"grafana-client>=2.1.0,<4",
23+
"grafana-client>=3.8.2,<4",
2424
"jsonpath-rw>=1.4.0,<2",
2525
# Caching
2626
"requests-cache>=0.8.0,<2",

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def docker_grafana(docker_services):
5050
"""
5151
docker_services.start("grafana")
5252
public_port = docker_services.wait_for_service("grafana", 3000)
53-
url = "http://{docker_services.docker_ip}:{public_port}".format(**locals())
53+
url = "http://admin:admin@{docker_services.docker_ip}:{public_port}".format(**locals())
5454
return url
5555

5656

tests/test_commands.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import warnings
22

3+
import grafana_client
4+
from grafana_client.elements.plugin import filter_plugin_by_id
35
from munch import munchify
46
from packaging import version
57

@@ -555,3 +557,90 @@ def test_info(docker_grafana, capsys, caplog):
555557
assert "dashboard_panels" in data["summary"]
556558
assert "dashboard_annotations" in data["summary"]
557559
assert "dashboard_templating" in data["summary"]
560+
561+
562+
def test_plugins_list(docker_grafana, capsys, caplog):
563+
"""
564+
Verify the plugin inquiry API works.
565+
"""
566+
# Which subcommand to test?
567+
set_command("plugins list", "--format=yaml")
568+
569+
# Run command and capture YAML output.
570+
with caplog.at_level(logging.DEBUG):
571+
grafana_wtf.commands.run()
572+
captured = capsys.readouterr()
573+
data = yaml.safe_load(captured.out)
574+
575+
# Grafana 6 has 28 plugins preinstalled.
576+
assert len(data) >= 28
577+
578+
# Proof the output is correct.
579+
plugin = munchify(filter_plugin_by_id(plugin_list=data, plugin_id="alertlist"))
580+
assert plugin.name.title() == "Alert List"
581+
assert plugin.type == "panel"
582+
assert plugin.id == "alertlist"
583+
assert plugin.category == ""
584+
assert plugin.enabled is True
585+
assert plugin.info.author.name in ["Grafana Project", "Grafana Labs"]
586+
assert plugin.info.version == ""
587+
588+
assert "metrics" not in plugin
589+
assert "health" not in plugin
590+
591+
592+
def test_plugins_status_datasource(grafana_version, docker_grafana, capsys, caplog):
593+
"""
594+
Verify the plugin status (metrics endpoint) on a 3rd-party "datasource" plugin.
595+
"""
596+
if version.parse(grafana_version) < version.parse("8"):
597+
raise pytest.skip(f"Plugin status inquiry only works on Grafana 8 and newer")
598+
599+
# Before conducting a plugin status test, install a non-internal one.
600+
grafana = grafana_client.GrafanaApi.from_url(url=docker_grafana, timeout=15)
601+
grafana.plugin.install_plugin("yesoreyeram-infinity-datasource")
602+
603+
# Which subcommand to test?
604+
set_command("plugins status", "--format=yaml")
605+
606+
# Run command and capture YAML output.
607+
with caplog.at_level(logging.DEBUG):
608+
grafana_wtf.commands.run()
609+
captured = capsys.readouterr()
610+
data = yaml.safe_load(captured.out)
611+
612+
# Grafana 6 has 28 plugins preinstalled.
613+
assert len(data) >= 28
614+
615+
# Proof the output is correct.
616+
plugin = munchify(filter_plugin_by_id(plugin_list=data, plugin_id="yesoreyeram-infinity-datasource"))
617+
assert "go_gc_duration_seconds" in plugin.metrics
618+
619+
620+
def test_plugins_status_app(grafana_version, docker_grafana, capsys, caplog):
621+
"""
622+
Verify the plugin status (metrics endpoint and health check) on a 3rd-party "app" plugin.
623+
"""
624+
if version.parse(grafana_version) < version.parse("8"):
625+
raise pytest.skip(f"Plugin status inquiry only works on Grafana 8 and newer")
626+
627+
# Before conducting a plugin status test, install a non-internal one.
628+
grafana = grafana_client.GrafanaApi.from_url(url=docker_grafana, timeout=15)
629+
grafana.plugin.install_plugin("aws-datasource-provisioner-app")
630+
631+
# Which subcommand to test?
632+
set_command("plugins status", "--format=yaml")
633+
634+
# Run command and capture YAML output.
635+
with caplog.at_level(logging.DEBUG):
636+
grafana_wtf.commands.run()
637+
captured = capsys.readouterr()
638+
data = yaml.safe_load(captured.out)
639+
640+
# Grafana 6 has 28 plugins preinstalled.
641+
assert len(data) >= 28
642+
643+
# Proof the output is correct.
644+
plugin = munchify(filter_plugin_by_id(plugin_list=data, plugin_id="aws-datasource-provisioner-app"))
645+
assert "process_virtual_memory_max_bytes" in plugin.metrics
646+
assert plugin.health == {"message": "", "status": "OK"}

0 commit comments

Comments
 (0)