observability/grafana/grafana.py (158 lines of code) (raw):
from base64 import b64decode
import json
import os
import subprocess
import tempfile
def run_command(command):
if os.getenv("PRINT_COMMANDS"):
print(f"calling '{command}'")
result = subprocess.run(command, shell=True, capture_output=True, text=True)
if result.returncode != 0:
print(f"Command failed: {command}\nError: {result.stderr}")
exit(result.returncode)
return result.stdout.strip()
def yq_to_json(yaml_file: str) -> str:
command = f"yq -o=json '.' {yaml_file}"
result = subprocess.run(command, shell=True, capture_output=True, text=True)
if result.returncode != 0:
print(f"Command failed: {command}\nError: {result.stderr}")
exit(result.returncode)
return result.stdout.strip()
class GrafanaRunner:
def __init__(self, rg: str, grafana: str, dry_run: bool):
self.rg = rg
self.grafana = grafana
self.dry_run = dry_run
def list_existing_folders(self) -> dict[str, any]:
return json.loads(
run_command(f'az grafana folder list -g "{self.rg}" -n "{self.grafana}"')
)
def create_folder(self, name: str) -> dict[str, any]:
command_to_run = f'az grafana folder create --only-show-errors -g "{self.rg}" -n "{self.grafana}" --title "{name}"'
if self.dry_run:
print(f"DRY_RUN: {command_to_run}")
return {}
return json.loads(run_command(command_to_run))
def create_dashboard(self, dashboard_file: str) -> dict[str, any]:
command_to_run = f'az grafana dashboard update --overwrite true -g "{self.rg}" -n "{self.grafana}" --definition "{dashboard_file}"'
if self.dry_run:
print(f"DRY_RUN: {command_to_run}")
return {}
return json.loads(run_command(command_to_run))
def delete_dashboard(self, uid: str):
command_to_run = f'az grafana dashboard delete -g "{self.rg}" -n "{self.grafana}" --dashboard "{uid}"'
if self.dry_run:
print(f"DRY_RUN: {command_to_run}")
else:
command_to_run
def list_existing_dashboards(self) -> dict[str, any]:
return json.loads(
run_command(f'az grafana dashboard list -g "{self.rg}" -n "{self.grafana}"')
)
def show_existing_dashboard(self, uid: str) -> dict[str, any]:
return json.loads(
run_command(
f'az grafana dashboard show --dashboard "{uid}" -g "{self.rg}" -n "{self.grafana}"'
)
)
def get_folder_uid(name: str, grafana_folders: list[dict[str, any]]) -> str:
found = [n for n in grafana_folders if n["title"] == name]
return found[0]["uid"] if found else ""
def fs_get_dashboards(folder: str) -> list[dict[str, any]]:
print(f"reading dashboards in {folder}")
return_array = []
files = [f for f in os.listdir(folder) if f.endswith(".json")]
for f in files:
with open(os.path.join(folder, f)) as dashboard_file:
dashboard = json.load(dashboard_file)
if "dashboard" in dashboard:
return_array.append(dashboard)
else:
return_array.append({"dashboard": dashboard})
return return_array
def get_or_create_folder(
name: str, g: GrafanaRunner, existing_folders: list[dict[str, any]]
) -> str:
existing_uid = get_folder_uid(name, existing_folders)
return existing_uid if existing_uid != "" else g.create_folder(name).get("uid", "")
def create_dashboard(
temp_file: str,
dashboard: dict[str, any],
folder_uid: str,
existing_dashboards: list[dict[str, any]],
g: GrafanaRunner,
) -> None:
with open(temp_file, "w") as f:
dashboard["folderUid"] = folder_uid
json.dump(dashboard, f)
dashboard_found = [
e
for e in existing_dashboards
if e["title"] == dashboard["dashboard"]["title"]
if e["folderUid"] == folder_uid
]
create_or_update = True
if dashboard_found:
assert len(dashboard_found) == 1
existing_dashboard = g.show_existing_dashboard(dashboard_found[0]["uid"])
# Deleting info that might change
# This script uses override and will always create new versions/ids
if existing_dashboard["dashboard"].get("uid", None):
del existing_dashboard["dashboard"]["uid"]
if existing_dashboard["dashboard"].get("id", None):
del existing_dashboard["dashboard"]["id"]
if existing_dashboard["dashboard"].get("version", None):
del existing_dashboard["dashboard"]["version"]
if dashboard["dashboard"].get("id", None):
del dashboard["dashboard"]["id"]
if dashboard["dashboard"].get("version", None):
del dashboard["dashboard"]["version"]
if existing_dashboard["dashboard"] == dashboard["dashboard"]:
print("Dashboard matches, no update needed")
create_or_update = False
if create_or_update:
print("Dashboard differs or does not exist update needed")
g.create_dashboard(temp_file)
def delete_stale_dashboard(
d: str,
dashboards_visited: set[str],
existing_folders: list[dict[str, any]],
g: GrafanaRunner,
azure_managed_folders: list[str],
) -> None:
if f"{d['folderUid']}_{d['title']}" not in dashboards_visited:
for amf in azure_managed_folders:
uid = get_folder_uid(amf, existing_folders)
if uid and uid == d["folderUid"]:
return
g.delete_dashboard(d["title"])
def main():
RESOURCEGROUP = os.getenv("GLOBAL_RESOURCEGROUP", "global")
DRY_RUN = os.getenv("DRY_RUN", "false").lower() == "true"
GRAFANA_NAME = os.getenv("GRAFANA_NAME")
OBSERVABILITY_CONFIG = os.getenv("OBSERVABILITY_CONFIG", "observability.yaml")
WORK_DIR = os.path.join(os.path.dirname(__file__), "..")
config = json.loads(yq_to_json(os.path.join(WORK_DIR, OBSERVABILITY_CONFIG)))
g = GrafanaRunner(RESOURCEGROUP, GRAFANA_NAME, DRY_RUN)
existing_folders = g.list_existing_folders()
existing_dashboards = g.list_existing_dashboards()
dashboards_visited = set()
for local_folder in config["grafana-dashboards"]["dashboardFolders"]:
folder_uid = get_or_create_folder(local_folder["name"], g, existing_folders)
for dashboard in fs_get_dashboards(
os.path.join(WORK_DIR, local_folder["path"])
):
temp_file = tempfile.NamedTemporaryFile()
create_dashboard(
temp_file.name, dashboard, folder_uid, existing_dashboards, g
)
dashboards_visited.add(f"{folder_uid}_{dashboard['dashboard']['title']}")
os.remove(temp_file.name)
for d in existing_dashboards:
delete_stale_dashboard(
d,
dashboards_visited,
existing_dashboards,
g,
config["grafana-dashboards"]["azureManagedFolders"],
)
if __name__ == "__main__":
main()