Grafana tab: - Datasources table (Name, Type, URL, Default) - Open Grafana button (opens browser) - Switch to AlertManager endpoint for real-time active alerts Prometheus tab: - Quick query buttons (up, CPU, Memory) - Metrics browser popup with filter (loads all metric names) - Rules section (recording + alerting rules Treeview) CLI: - --grafana-datasources ALIAS - --prom-rules ALIAS i18n: 28 new keys (EN/RU/ZH) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
187 lines
6.1 KiB
Python
187 lines
6.1 KiB
Python
"""
|
|
Grafana API client for ServerManager.
|
|
|
|
Provides dashboard listing, alert management, datasource queries,
|
|
and annotation creation via Grafana HTTP API.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from core.logger import log
|
|
|
|
|
|
class GrafanaClient:
|
|
"""Client for interacting with a Grafana instance via its HTTP API."""
|
|
|
|
def __init__(self, server: dict) -> None:
|
|
"""
|
|
Initialize the Grafana client.
|
|
|
|
Args:
|
|
server: dict with keys: ip, port, api_token (or user+password), use_ssl
|
|
"""
|
|
self.ip: str = server["ip"]
|
|
self.port: int = int(server["port"])
|
|
self.api_token: str = server.get("api_token", "")
|
|
self.user: str = server.get("user", "")
|
|
self.password: str = server.get("password", "")
|
|
self.use_ssl: bool = bool(server.get("use_ssl", False))
|
|
|
|
scheme = "https" if self.use_ssl else "http"
|
|
self.base_url: str = f"{scheme}://{self.ip}:{self.port}"
|
|
self.headers: dict[str, str] = {"Content-Type": "application/json"}
|
|
self.auth: tuple[str, str] | None = None
|
|
|
|
if self.api_token:
|
|
self.headers["Authorization"] = f"Bearer {self.api_token}"
|
|
elif self.user and self.password:
|
|
self.auth = (self.user, self.password)
|
|
|
|
self.timeout: int = 10
|
|
|
|
def _get(self, path: str, params: dict | None = None) -> Any:
|
|
"""Send a GET request to the Grafana API."""
|
|
import requests
|
|
|
|
url = f"{self.base_url}{path}"
|
|
log.debug("Grafana GET %s", url)
|
|
resp = requests.get(
|
|
url, headers=self.headers, params=params, auth=self.auth, timeout=self.timeout
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
def _post(self, path: str, json_data: dict | None = None) -> Any:
|
|
"""Send a POST request to the Grafana API."""
|
|
import requests
|
|
|
|
url = f"{self.base_url}{path}"
|
|
log.debug("Grafana POST %s", url)
|
|
resp = requests.post(
|
|
url, headers=self.headers, json=json_data, auth=self.auth, timeout=self.timeout
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
def check_connection(self) -> bool:
|
|
"""
|
|
Check connectivity to Grafana via GET /api/health.
|
|
|
|
Returns:
|
|
True if Grafana responds successfully, False otherwise.
|
|
"""
|
|
try:
|
|
result = self._get("/api/health")
|
|
healthy = result.get("database", "") == "ok"
|
|
log.info("Grafana health check: %s", "OK" if healthy else "FAIL")
|
|
return healthy
|
|
except Exception as exc:
|
|
log.error("Grafana health check failed: %s", exc)
|
|
return False
|
|
|
|
def list_dashboards(self) -> list[dict]:
|
|
"""
|
|
List all dashboards via GET /api/search.
|
|
|
|
Returns:
|
|
List of dicts with keys: uid, title, folder, url.
|
|
"""
|
|
try:
|
|
results = self._get("/api/search", params={"type": "dash-db"})
|
|
dashboards = [
|
|
{
|
|
"uid": d.get("uid", ""),
|
|
"title": d.get("title", ""),
|
|
"folder": d.get("folderTitle", ""),
|
|
"url": d.get("url", ""),
|
|
}
|
|
for d in results
|
|
]
|
|
log.info("Grafana: found %d dashboards", len(dashboards))
|
|
return dashboards
|
|
except Exception as exc:
|
|
log.error("Grafana list_dashboards failed: %s", exc)
|
|
return []
|
|
|
|
def get_dashboard(self, uid: str) -> dict:
|
|
"""
|
|
Get a single dashboard by UID via GET /api/dashboards/uid/{uid}.
|
|
|
|
Args:
|
|
uid: Dashboard UID string.
|
|
|
|
Returns:
|
|
Full dashboard JSON dict, or empty dict on error.
|
|
"""
|
|
try:
|
|
result = self._get(f"/api/dashboards/uid/{uid}")
|
|
log.info("Grafana: loaded dashboard '%s'", uid)
|
|
return result
|
|
except Exception as exc:
|
|
log.error("Grafana get_dashboard(%s) failed: %s", uid, exc)
|
|
return {}
|
|
|
|
def list_alerts(self) -> list[dict]:
|
|
"""
|
|
List provisioned alert rules via GET /api/v1/provisioning/alert-rules.
|
|
|
|
Returns:
|
|
List of alert rule dicts, or empty list on error.
|
|
"""
|
|
try:
|
|
results = self._get("/api/v1/provisioning/alert-rules")
|
|
log.info("Grafana: found %d alert rules", len(results))
|
|
return results
|
|
except Exception as exc:
|
|
log.error("Grafana list_alerts failed: %s", exc)
|
|
return []
|
|
|
|
def get_active_alerts(self) -> list[dict]:
|
|
"""List active (firing) alerts via AlertManager endpoint."""
|
|
try:
|
|
results = self._get("/api/alertmanager/grafana/api/v2/alerts")
|
|
log.info("Grafana: %d active alerts", len(results))
|
|
return results
|
|
except Exception as exc:
|
|
log.error("Grafana get_active_alerts failed: %s", exc)
|
|
return []
|
|
|
|
def list_datasources(self) -> list[dict]:
|
|
"""
|
|
List all datasources via GET /api/datasources.
|
|
|
|
Returns:
|
|
List of datasource dicts, or empty list on error.
|
|
"""
|
|
try:
|
|
results = self._get("/api/datasources")
|
|
log.info("Grafana: found %d datasources", len(results))
|
|
return results
|
|
except Exception as exc:
|
|
log.error("Grafana list_datasources failed: %s", exc)
|
|
return []
|
|
|
|
def create_annotation(self, text: str, tags: list[str] | None = None) -> dict:
|
|
"""
|
|
Create a global annotation via POST /api/annotations.
|
|
|
|
Args:
|
|
text: Annotation text/description.
|
|
tags: Optional list of tag strings.
|
|
|
|
Returns:
|
|
API response dict, or empty dict on error.
|
|
"""
|
|
payload: dict[str, Any] = {"text": text}
|
|
if tags:
|
|
payload["tags"] = tags
|
|
try:
|
|
result = self._post("/api/annotations", json_data=payload)
|
|
log.info("Grafana: created annotation id=%s", result.get("id"))
|
|
return result
|
|
except Exception as exc:
|
|
log.error("Grafana create_annotation failed: %s", exc)
|
|
return {}
|