Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add subcommands plugins {list,status}, to inquire plugins - now for real #92

Merged
merged 2 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ in progress
- Grafana 9.5: Use standard UUIDs instead of short UIDs
- Add ``explore dashboards --data-details`` option, to extend the output
by many more details about data inquiry / queries. Thanks, @meyerder.
- Add ``plugins {list,status}`` subcommands, to inquire installed Grafana
plugins. Thanks, @bhks.

2023-07-30 0.15.2
=================
Expand Down
6 changes: 6 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ Explore dashboards and datasources in more detail.
grafana-wtf explore dashboards
grafana-wtf explore datasources

Explore plugins.
::

grafana-wtf plugins list
grafana-wtf plugins status

Run with Docker::

# Access Grafana instance on localhost, without authentication.
Expand Down
25 changes: 25 additions & 0 deletions grafana_wtf/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ def run():
grafana-wtf [options] find [<search-expression>]
grafana-wtf [options] replace <search-expression> <replacement> [--dry-run]
grafana-wtf [options] log [<dashboard_uid>] [--number=<count>] [--head=<count>] [--tail=<count>] [--reverse] [--sql=<sql>]
grafana-wtf [options] plugins list [--id=]
grafana-wtf [options] plugins status [--id=]
grafana-wtf --version
grafana-wtf (-h | --help)

Expand Down Expand Up @@ -156,6 +158,14 @@ def run():
HAVING COUNT(version)=1
"

List plugins:

# Inquire plugin list.
grafana-wtf plugins list

# Inquire plugin health check and metrics endpoints.
grafana-wtf plugins status

Cache control:

# Use infinite cache expiration time, essentially caching forever.
Expand Down Expand Up @@ -311,3 +321,18 @@ def run():
if options.info:
response = engine.info()
output_results(output_format, response)

if options.plugins:
if options.list:
if options.id:
response = engine.plugins_list_by_id(options.id)
else:
response = engine.plugins_list()
elif options.status:
if options.id:
response = engine.plugins_status_by_id(options.id)
else:
response = engine.plugins_status()
else:
raise DocoptExit('Subcommand "plugins" only provides "list" and "status"')
output_results(output_format, response)
45 changes: 45 additions & 0 deletions grafana_wtf/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,51 @@ def explore_dashboards(self, with_data_details: bool = False):

return results

def plugins_list(self):
return self.grafana.plugin.list()

def plugins_status(self):
status = []
plugins = self.grafana.plugin.list()
for plugin in plugins:
item = self.get_plugin_status(plugin)
status.append(item)
return status

def get_plugin_status(self, plugin):
plugin = munchify(plugin)
item = Munch(
name=plugin.name,
type=plugin.type,
id=plugin.id,
enabled=plugin.enabled,
category=plugin.category,
version=plugin.info.version,
signature=plugin.get("signature"),
)

# Status inquiry is not provided by all plugins. Let's filter them.
# Effectively, run it only on non-internal "app" and "datasource" items.
if item.type != "panel" and item.signature != "internal":
try:
item.health = self.grafana.plugin.health(plugin.id)
except Exception as ex:
log.warning(f"Health check failed for plugin {item.id}, type={item.type}: {ex}")
try:
item.metrics = self.grafana.plugin.metrics(plugin.id)
except Exception as ex:
log.warning(f"Metrics inquiry failed for plugin {item.id}, type={item.type}: {ex}")
else:
log.info(f"Skipping status inquiry for plugin {item.id}, type={item.type}")
return item

def plugins_list_by_id(self, plugin_id):
return self.grafana.plugin.by_id(plugin_id=plugin_id)

def plugins_status_by_id(self, plugin_id):
plugin = self.plugins_list_by_id(plugin_id)
return self.get_plugin_status(plugin)


class Indexer:
def __init__(self, engine: GrafanaWtf):
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
f"duckdb<0.9; {no_linux_on_arm}",
# Grafana
"requests>=2.26,<3",
"grafana-client>=2.1.0,<4",
"grafana-client>=3.9.1,<4",
"jsonpath-rw>=1.4.0,<2",
# Caching
"requests-cache>=0.8.0,<2",
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def docker_grafana(docker_services):
"""
docker_services.start("grafana")
public_port = docker_services.wait_for_service("grafana", 3000)
url = "http://{docker_services.docker_ip}:{public_port}".format(**locals())
url = "http://admin:admin@{docker_services.docker_ip}:{public_port}".format(**locals())
return url


Expand Down
125 changes: 125 additions & 0 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import warnings

import grafana_client
from grafana_client.elements.plugin import get_plugin_by_id
from munch import munchify
from packaging import version

Expand Down Expand Up @@ -555,3 +557,126 @@ def test_info(docker_grafana, capsys, caplog):
assert "dashboard_panels" in data["summary"]
assert "dashboard_annotations" in data["summary"]
assert "dashboard_templating" in data["summary"]


def test_plugins_list(docker_grafana, capsys, caplog):
"""
Verify the plugin inquiry API works.
"""
# Which subcommand to test?
set_command("plugins list", "--format=yaml")

# Run command and capture YAML output.
with caplog.at_level(logging.DEBUG):
grafana_wtf.commands.run()
captured = capsys.readouterr()
data = yaml.safe_load(captured.out)

# Grafana 6 has 28 plugins preinstalled.
assert len(data) >= 28

# Proof the output is correct.
plugin = munchify(get_plugin_by_id(plugin_list=data, plugin_id="alertlist"))
assert plugin.name.title() == "Alert List"
assert plugin.type == "panel"
assert plugin.id == "alertlist"
assert plugin.category == ""
assert plugin.enabled is True
assert plugin.info.author.name in ["Grafana Project", "Grafana Labs"]
assert plugin.info.version == ""

assert "metrics" not in plugin
assert "health" not in plugin


def test_plugins_status_datasource(grafana_version, docker_grafana, capsys, caplog):
"""
Verify the plugin status (metrics endpoint) on a 3rd-party "datasource" plugin.
"""
if version.parse(grafana_version) < version.parse("8"):
raise pytest.skip(f"Plugin status inquiry only works on Grafana 8 and newer")

# Before conducting a plugin status test, install a non-internal one.
grafana = grafana_client.GrafanaApi.from_url(url=docker_grafana, timeout=15)
grafana.plugin.install("yesoreyeram-infinity-datasource")

# Which subcommand to test?
set_command("plugins status", "--format=yaml")

# Run command and capture YAML output.
with caplog.at_level(logging.DEBUG):
grafana_wtf.commands.run()
captured = capsys.readouterr()
data = yaml.safe_load(captured.out)

# Grafana 6 has 28 plugins preinstalled.
assert len(data) >= 28

# Proof the output is correct.
plugin = munchify(get_plugin_by_id(plugin_list=data, plugin_id="yesoreyeram-infinity-datasource"))
assert "go_gc_duration_seconds" in plugin.metrics


def test_plugins_status_app(grafana_version, docker_grafana, capsys, caplog):
"""
Verify the plugin status (metrics endpoint and health check) on a 3rd-party "app" plugin.
"""
if version.parse(grafana_version) < version.parse("8"):
raise pytest.skip(f"Plugin status inquiry only works on Grafana 8 and newer")

# Before conducting a plugin status test, install a non-internal one.
grafana = grafana_client.GrafanaApi.from_url(url=docker_grafana, timeout=15)
grafana.plugin.install("aws-datasource-provisioner-app")

# Which subcommand to test?
set_command("plugins status", "--format=yaml")

# Run command and capture YAML output.
with caplog.at_level(logging.DEBUG):
grafana_wtf.commands.run()
captured = capsys.readouterr()
data = yaml.safe_load(captured.out)

# Grafana 6 has 28 plugins preinstalled.
assert len(data) >= 28

# Proof the output is correct.
plugin = munchify(get_plugin_by_id(plugin_list=data, plugin_id="aws-datasource-provisioner-app"))
assert "process_virtual_memory_max_bytes" in plugin.metrics
assert plugin.health == {"message": "", "status": "OK"}


def test_plugins_install_uninstall(grafana_version, docker_grafana, capsys, caplog):
"""
Verify the plugin status when installing/uninstalling a plugin.
"""
if version.parse(grafana_version) < version.parse("8"):
raise pytest.skip(f"Plugin status inquiry only works on Grafana 8 and newer")

plugin_name = "yesoreyeram-infinity-datasource"

# Before conducting a plugin status test, install a non-internal one.
grafana = grafana_client.GrafanaApi.from_url(url=docker_grafana, timeout=15)
grafana.plugin.install(plugin_name)

# Which subcommand to test?
set_command(f"plugins status --id={plugin_name}", "--format=yaml")

# Run command and capture YAML output.
with caplog.at_level(logging.DEBUG):
grafana_wtf.commands.run()
captured = capsys.readouterr()
plugin_status = munchify(yaml.safe_load(captured.out))

# Proof the output is correct.
assert plugin_status.id == plugin_name
assert version.parse(plugin_status.version) >= version.parse("2.0.0")
assert "go_gc_duration_seconds" in plugin_status.metrics

# Uninstall the plugin again.
grafana.plugin.uninstall(plugin_name)

# Verify uninstalling worked.
with pytest.raises(KeyError) as ex:
grafana_wtf.commands.run()
assert ex.match("Plugin not found: yesoreyeram-infinity-datasource")