Reorganize Grafana dashboards by platform domain

This commit is contained in:
Haitao Pan 2026-03-16 23:58:18 +08:00
parent 159a64a934
commit 81dbef313d
65 changed files with 788 additions and 12976 deletions

View File

@ -1,28 +1,31 @@
# Grafana Dashboards
This directory contains Grafana dashboard definitions for Pigsty monitoring system.
This directory contains Grafana dashboard definitions for the observability stack.
## Overview
Pigsty provides **57 built-in dashboards** organized by module:
The repository currently provides **61 domain dashboards + 1 homepage dashboard**.
Dashboards are organized by platform-engineering resource domains:
| Directory | Count | Description |
|-----------------|-------|-------------------------------------------------------------------------|
| [pgsql](pgsql/) | 29 | PostgreSQL cluster, instance, database, and query monitoring |
| [infra](infra/) | 11 | Infrastructure components (VictoriaMetrics, Grafana, Nginx, etcd, etc.) |
| [node](node/) | 8 | Host-level metrics (CPU, memory, disk, network, HAProxy, VIP) |
| [redis](redis/) | 3 | Redis cluster and instance monitoring |
| [app](app/) | 2 | Application dashboards (PostgreSQL logs analysis) |
| [minio](minio/) | 2 | MinIO S3-compatible storage monitoring |
| [mongo](mongo/) | 1 | MongoDB/FerretDB monitoring |
| - | 1 | [pigsty.json](pigsty.json) - Main home dashboard |
| Folder | Count | Description |
|--------|-------|-------------|
| [01-iaas-compute](01-iaas-compute/) | 5 | IAAS compute: node overview, cluster, instance, alert, compatibility summary |
| [02-iaas-storage](02-iaas-storage/) | 4 | IAAS storage: disk, JuiceFS, MinIO overview and instance |
| [03-iaas-network](03-iaas-network/) | 1 | IAAS network: VIP and node-network entry |
| [11-paas-control-plane](11-paas-control-plane/) | 10 | PaaS control plane: Pigsty, Grafana, Victoria stack, Alertmanager, etcd, CMDB |
| [12-paas-cluster](12-paas-cluster/) | 1 | PaaS cluster: Kubernetes overview |
| [13-paas-db](13-paas-db/) | 29 | PaaS DB: PostgreSQL, PGRDS, PGCAT, Mongo/FerretDB |
| [14-paas-cache](14-paas-cache/) | 3 | PaaS cache: Redis overview, cluster, instance |
| [22-bu-proxy](22-bu-proxy/) | 2 | Business unit proxy: Nginx and HAProxy |
| [24-bu-request](24-bu-request/) | 5 | Business unit request: logs, sessions, vector, request-side tooling |
| - | 1 | [homepage.json](homepage.json) - Platform engineering entry dashboard |
## Dashboard Catalog
### Home
- **[pigsty.json](pigsty.json)** - Pigsty home dashboard with global overview
- **[homepage.json](homepage.json)** - Platform engineering entry dashboard with domain summaries and navigation
### PGSQL Dashboards

View File

@ -16,6 +16,38 @@ USERNAME = os.environ.get("GRAFANA_USERNAME", 'admin')
PASSWORD = os.environ.get("GRAFANA_PASSWORD", 'pigsty')
CREATE_FOLDERS = True
FOLDER_TITLES = {
'01-iaas-compute': 'IAAS / 计算',
'02-iaas-storage': 'IAAS / 存储',
'03-iaas-network': 'IAAS / 网络',
'11-paas-control-plane': 'PaaS / 平台控制面',
'12-paas-cluster': 'PaaS / 集群',
'13-paas-db': 'PaaS / DB',
'14-paas-cache': 'PaaS / 缓存',
'15-paas-queue': 'PaaS / 队列',
'21-bu-dns': '业务单元 / DNS',
'22-bu-proxy': '业务单元 / 代理',
'23-bu-gateway': '业务单元 / 网关',
'24-bu-request': '业务单元 / 请求',
'25-bu-throughput': '业务单元 / 吞吐',
}
FOLDER_TAGS = {
'01-iaas-compute': ['IAAS', 'IAAS-COMPUTE'],
'02-iaas-storage': ['IAAS', 'IAAS-STORAGE'],
'03-iaas-network': ['IAAS', 'IAAS-NETWORK'],
'11-paas-control-plane': ['PAAS', 'PAAS-CONTROL-PLANE'],
'12-paas-cluster': ['PAAS', 'PAAS-CLUSTER'],
'13-paas-db': ['PAAS', 'PAAS-DB'],
'14-paas-cache': ['PAAS', 'PAAS-CACHE'],
'15-paas-queue': ['PAAS', 'PAAS-QUEUE'],
'21-bu-dns': ['BU', 'BU-DNS'],
'22-bu-proxy': ['BU', 'BU-PROXY'],
'23-bu-gateway': ['BU', 'BU-GATEWAY'],
'24-bu-request': ['BU', 'BU-REQUEST'],
'25-bu-throughput': ['BU', 'BU-THROUGHPUT'],
}
METADB_PASSWORD = 'DBUser.Viewer'
DEFAULT_DATASOURCES = {
'ds-prometheus': {'uid': 'ds-prometheus', 'orgId': 1, 'name': 'Prometheus', 'type': 'prometheus', 'typeName': 'Prometheus', 'typeLogoUrl': 'public/app/plugins/datasource/prometheus/img/prometheus_logo.svg', 'access': 'proxy',
@ -118,7 +150,7 @@ def add_folder(uid, title=""):
if not CREATE_FOLDERS:
return
if title == "":
title = uid.upper()
title = resolve_folder_title(uid)
post('folders', {"uid": uid, "title": title})
return put('folders/%s' % uid, {"title": title, "overwrite": True})
@ -212,6 +244,30 @@ def load_dashboard(path, substitute=False):
else:
return json.load(open(path))
def resolve_folder_title(uid):
return FOLDER_TITLES.get(uid, uid.upper())
def enrich_dashboard(dashboard, folder=None):
if not folder:
return dashboard
extra_tags = FOLDER_TAGS.get(folder, [])
if not extra_tags:
return dashboard
existing_tags = dashboard.get("tags", [])
if not isinstance(existing_tags, list):
existing_tags = []
merged_tags = []
seen = set()
for tag in existing_tags + extra_tags:
if not tag or tag in seen:
continue
seen.add(tag)
merged_tags.append(tag)
dashboard["tags"] = merged_tags
return dashboard
# json serializer: use compact_json if available, fallback to standard json
try:
from compact_json import Formatter
@ -283,13 +339,13 @@ def init_all(dashboard_dir):
# load other second-layer dashboards
for folder_name, folder_path in folders:
print("init folder %s" % folder_name)
add_folder(folder_name, folder_name.upper())
add_folder(folder_name, resolve_folder_title(folder_name))
for f in os.listdir(folder_path):
abs_path = os.path.join(dashboard_dir, folder_name, f)
if os.path.isfile(abs_path) and f.endswith('.json') and not f.startswith('.'):
print("init dashboard: %s / %s" % (folder_name, f))
add_dashboard(load_dashboard(abs_path, True), folder_name)
add_dashboard(enrich_dashboard(load_dashboard(abs_path, True), folder_name), folder_name)
def load_all(dashboard_dir):
@ -305,13 +361,13 @@ def load_all(dashboard_dir):
for folder_name, folder_path in folders:
print("add folder %s" % folder_name)
add_folder(folder_name, folder_name.upper())
add_folder(folder_name, resolve_folder_title(folder_name))
for f in os.listdir(folder_path):
abs_path = os.path.join(dashboard_dir, folder_name, f)
if os.path.isfile(abs_path) and f.endswith('.json') and not f.startswith('.'):
print("load dashboard: %s / %s" % (folder_name, f))
add_dashboard(load_dashboard(abs_path), folder_name)
add_dashboard(enrich_dashboard(load_dashboard(abs_path), folder_name), folder_name)
def dump_all(dashboard_dir):

File diff suppressed because one or more lines are too long

462
merge_dashboards.py Executable file → Normal file
View File

@ -1,6 +1,131 @@
import copy
import json
import re
import os
CONTROL_PLANE_PATH = "files/grafana/11-paas-control-plane/pigsty.json"
OUTPUT_PATH = "files/grafana/homepage.json"
VISIBLE_VARS = [
{
"name": "version",
"type": "constant",
"query": "v4.0.0",
"hide": 2,
},
{
"name": "origin_prometheus",
"label": "数据源",
"type": "query",
"datasource": {"uid": "ds-prometheus"},
"query": "label_values(kube_node_info,origin_prometheus)",
"refresh": 1,
},
{
"name": "interval",
"label": "采样间隔",
"type": "interval",
"query": "3m,5m,10m,30m,1h,6h,12h,1d",
},
]
DOMAIN_SECTIONS = [
{
"title": "IAAS资源",
"items": [
{
"title": "计算",
"description": "主机容量、节点健康、实例告警",
"folder_uid": "01-iaas-compute",
"folder_title": "IAAS / 计算",
"tag": "IAAS-COMPUTE",
"highlights": ["Node Overview", "Node Instance", "Node Alert"],
"dash_height": 9,
},
{
"title": "存储",
"description": "磁盘、卷、对象存储、JuiceFS",
"folder_uid": "02-iaas-storage",
"folder_title": "IAAS / 存储",
"tag": "IAAS-STORAGE",
"highlights": ["Node Disk", "MinIO Overview", "Node JuiceFS"],
"dash_height": 9,
},
{
"title": "网络",
"description": "VIP、节点网络、底层连通性",
"folder_uid": "03-iaas-network",
"folder_title": "IAAS / 网络",
"tag": "IAAS-NETWORK",
"highlights": ["Node VIP"],
"dash_height": 8,
},
],
},
{
"title": "PaaS服务",
"items": [
{
"title": "平台控制面",
"description": "Grafana、Victoria、Alertmanager、Etcd、CMDB",
"folder_uid": "11-paas-control-plane",
"folder_title": "PaaS / 平台控制面",
"tag": "PAAS-CONTROL-PLANE",
"highlights": ["Infra Overview", "Victoria Metrics", "Alert Manager"],
"dash_height": 10,
},
{
"title": "集群",
"description": "K8S 集群资源、命名空间与工作负载入口",
"folder_uid": "12-paas-cluster",
"folder_title": "PaaS / 集群",
"tag": "PAAS-CLUSTER",
"highlights": ["K8S Dashboard"],
"dash_height": 8,
},
{
"title": "DB",
"description": "PGSQL、PGRDS、PGCAT、Ferret",
"folder_uid": "13-paas-db",
"folder_title": "PaaS / DB",
"tag": "PAAS-DB",
"highlights": ["PGSQL Overview", "PGSQL Cluster", "PGCAT Instance"],
"dash_height": 14,
},
{
"title": "缓存",
"description": "Redis 集群、实例与缓存服务运行面",
"folder_uid": "14-paas-cache",
"folder_title": "PaaS / 缓存",
"tag": "PAAS-CACHE",
"highlights": ["Redis Overview", "Redis Cluster"],
"dash_height": 9,
},
],
},
{
"title": "业务单元",
"items": [
{
"title": "代理",
"description": "Nginx、HAProxy 与流量接入层",
"folder_uid": "22-bu-proxy",
"folder_title": "业务单元 / 代理",
"tag": "BU-PROXY",
"highlights": ["Nginx Instance", "Node HAProxy"],
"dash_height": 8,
},
{
"title": "请求",
"description": "请求日志、会话、链路与请求级观测",
"folder_uid": "24-bu-request",
"folder_title": "业务单元 / 请求",
"tag": "BU-REQUEST",
"highlights": ["PGLOG Overview", "Logs Instance", "Node Vector"],
"dash_height": 9,
},
],
},
]
def shift_panel(panel, delta_y):
@ -9,6 +134,17 @@ def shift_panel(panel, delta_y):
shift_panel(nested, delta_y)
def clone_panel(panel, x, y, w=None, h=None):
cloned = copy.deepcopy(panel)
cloned["gridPos"] = {
"x": x,
"y": y,
"w": w if w is not None else panel["gridPos"]["w"],
"h": h if h is not None else panel["gridPos"]["h"],
}
return cloned
def make_text_panel(panel_id, title, html, x, y, w, h, transparent=True):
return {
"id": panel_id,
@ -16,166 +152,214 @@ def make_text_panel(panel_id, title, html, x, y, w, h, transparent=True):
"title": title,
"gridPos": {"h": h, "w": w, "x": x, "y": y},
"transparent": transparent,
"options": {
"content": html,
"mode": "html"
}
"options": {"content": html, "mode": "html"},
}
def make_row_panel(panel_id, title, y):
return {
"id": panel_id,
"type": "row",
"title": title,
"collapsed": False,
"panels": [],
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y},
}
def make_dashlist_panel(panel_id, title, tags, x, y, w, h, max_items=12):
return {
"id": panel_id,
"type": "dashlist",
"title": title,
"pluginVersion": "12.3.0",
"gridPos": {"h": h, "w": w, "x": x, "y": y},
"options": {
"includeVars": True,
"keepTime": True,
"maxItems": max_items,
"query": "",
"showFolderNames": False,
"showHeadings": False,
"showRecentlyViewed": False,
"showSearch": False,
"showStarred": False,
"tags": tags,
},
}
def summary_card_html(item):
highlights = "".join(
f"<li style='margin:0 0 4px 18px;'>{highlight}</li>"
for highlight in item["highlights"]
)
return f"""
<div style="border:1px solid #d1d5db;border-radius:16px;padding:14px 16px;background:#fbfdff;height:100%;">
<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">{item['folder_title']}</div>
<div style="font-size:20px;font-weight:800;color:#111827;margin-bottom:8px;">{item['title']}</div>
<div style="font-size:13px;line-height:1.5;color:#4b5563;">{item['description']}</div>
<ul style="margin:10px 0 12px 0;padding:0;color:#111827;font-size:13px;line-height:1.45;">{highlights}</ul>
<div style="display:inline-block;padding:8px 12px;border-radius:999px;background:#e5e7eb;color:#374151;font-size:12px;font-weight:700;">
右侧保留可跳转目录
</div>
</div>
"""
def homepage_nav_html():
return """
<div style="display:flex;justify-content:space-between;align-items:center;gap:16px;flex-wrap:wrap;padding:8px 4px 2px 4px;">
<div>
<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">Platform Engineering Home</div>
<div style="font-size:28px;font-weight:800;color:#111827;">平台工程总览入口</div>
<div style="font-size:13px;color:#4b5563;margin-top:6px;">首页只保留全局脉搏资源域摘要与跳转详细明细统一下沉到二级 dashboard</div>
</div>
<div style="display:flex;gap:12px;flex-wrap:wrap;">
<span style="padding:10px 16px;border-radius:999px;background:#dbeafe;color:#1d4ed8;font-weight:700;">IAAS资源</span>
<span style="padding:10px 16px;border-radius:999px;background:#ecfdf3;color:#047857;font-weight:700;">PaaS服务</span>
<span style="padding:10px 16px;border-radius:999px;background:#fff7ed;color:#c2410c;font-weight:700;">业务单元</span>
</div>
</div>
"""
def homepage_guide_html():
return """
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;padding:4px 2px 0 2px;">
<div style="border:1px solid #d1d5db;border-radius:12px;padding:12px 14px;background:#fbfdff;">
<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">IAAS资源</div>
<div style="font-size:14px;font-weight:700;color:#111827;">计算 / 存储 / 网络</div>
<div style="font-size:12px;color:#6b7280;margin-top:6px;">先看宿主磁盘VIP 与底层资源是否健康</div>
</div>
<div style="border:1px solid #d1d5db;border-radius:12px;padding:12px 14px;background:#fbfdff;">
<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">PaaS服务</div>
<div style="font-size:14px;font-weight:700;color:#111827;">控制面 / 集群 / DB / 缓存</div>
<div style="font-size:12px;color:#6b7280;margin-top:6px;">平台自身与共享服务按资源域稳定分层</div>
</div>
<div style="border:1px solid #d1d5db;border-radius:12px;padding:12px 14px;background:#fbfdff;">
<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">业务单元</div>
<div style="font-size:14px;font-weight:700;color:#111827;">代理 / 请求</div>
<div style="font-size:12px;color:#6b7280;margin-top:6px;">业务接入面与请求观测单独收口不再混在底层资源里</div>
</div>
</div>
"""
def select_platform_summary_panels(control_plane):
wanted = ["Pigsty ${version}", "Modules", "Instances", "Firing Alerts"]
by_title = {panel.get("title"): panel for panel in control_plane.get("panels", [])}
return [by_title[title] for title in wanted if title in by_title]
def add_domain_section(homepage, start_id, current_y, section):
panel_id = start_id
homepage["panels"].append(make_row_panel(panel_id, section["title"], current_y))
panel_id += 1
current_y += 1
width = 24 // len(section["items"])
summary_height = 5
max_dash_height = max(item["dash_height"] for item in section["items"])
for index, item in enumerate(section["items"]):
x = width * index
homepage["panels"].append(
make_text_panel(
panel_id,
f"{item['title']}摘要",
summary_card_html(item),
x,
current_y,
width,
summary_height,
)
)
panel_id += 1
current_y += summary_height
for index, item in enumerate(section["items"]):
x = width * index
homepage["panels"].append(
make_dashlist_panel(
panel_id,
f"{item['title']}目录",
[item["tag"]],
x,
current_y,
width,
item["dash_height"],
max_items=20,
)
)
panel_id += 1
current_y += max_dash_height
return panel_id, current_y
def merge_dashboards():
# Paths to source dashboards
pig_path = 'files/grafana/pigsty.json'
node_path = 'files/grafana/node.json'
k8s_path = 'files/grafana/k8s.json'
output_path = 'files/grafana/homepage.json'
with open(CONTROL_PLANE_PATH, "r") as handle:
control_plane = json.load(handle)
# Read raw contents
with open(pig_path, 'r') as f:
pig_raw = f.read()
with open(node_path, 'r') as f:
node_raw = f.read()
with open(k8s_path, 'r') as f:
k8s_raw = f.read()
# Perform fixed variable mapping for node.json
# $name -> $hostname, $instance -> $node, $show_name -> $show_hostname
node_raw = re.sub(r'\$name\b', '$hostname', node_raw)
node_raw = re.sub(r'\$\{name\}', '${hostname}', node_raw)
node_raw = re.sub(r'\$instance\b', '$node', node_raw)
node_raw = re.sub(r'\$\{instance\}', '${node}', node_raw)
node_raw = re.sub(r'\$show_name\b', '$show_hostname', node_raw)
node_raw = re.sub(r'\$\{show_name\}', '${show_hostname}', node_raw)
pig = json.loads(pig_raw)
node = json.loads(node_raw)
k8s = json.loads(k8s_raw)
# Base dashboard
homepage = {
"annotations": pig.get("annotations", {"list": []}),
"description": "Pigsty Consolidated Homepage",
"annotations": control_plane.get("annotations", {"list": []}),
"description": "Platform engineering entry dashboard",
"editable": True,
"graphTooltip": 0,
"id": None,
"links": pig.get("links", []),
"links": control_plane.get("links", []),
"panels": [],
"schemaVersion": 39,
"tags": ["HOME", "Pigsty"],
"templating": {"list": []},
"time": pig.get("time", {"from": "now-1h", "to": "now"}),
"timepicker": pig.get("timepicker", {}),
"tags": ["HOME", "Platform"],
"templating": {"list": VISIBLE_VARS},
"time": control_plane.get("time", {"from": "now-1h", "to": "now"}),
"timepicker": control_plane.get("timepicker", {}),
"timezone": "browser",
"title": "Homepage",
"uid": "home",
"version": 1
"version": 1,
}
# Unified Variables
unified_vars = [
{"name": "version", "type": "constant", "query": "v4.0.0", "hide": 2},
{"name": "origin_prometheus", "label": "数据源", "type": "query", "datasource": {"uid": "ds-prometheus"}, "query": "label_values(kube_node_info,origin_prometheus)", "refresh": 1},
{"name": "NameSpace", "label": "命名空间", "type": "query", "datasource": {"uid": "ds-prometheus"}, "query": "label_values(kube_namespace_created{origin_prometheus=~\"$origin_prometheus\"},namespace)"},
{"name": "Container", "label": "服务", "description": "服务(容器)", "type": "query", "datasource": {"uid": "ds-prometheus"}, "query": "label_values(kube_pod_container_info{origin_prometheus=~\"$origin_prometheus\",namespace=~\"$NameSpace\"},container)"},
{"name": "Pod", "label": "Pod", "type": "query", "datasource": {"uid": "ds-prometheus"}, "query": "label_values(kube_pod_container_info{origin_prometheus=~\"$origin_prometheus\",namespace=~\"$NameSpace\",container=~\"$Container\"},pod)"},
{"name": "hostname", "label": "主机名", "type": "query", "datasource": {"uid": "ds-prometheus"}, "query": "label_values(node_uname_info{origin_prometheus=~\"$origin_prometheus\", job=~\"$job\"},nodename)"},
{"name": "node", "label": "实例 IP", "type": "query", "datasource": {"uid": "ds-prometheus"}, "query": "label_values(node_uname_info{origin_prometheus=~\"$origin_prometheus\", job=~\"$job\", nodename=~\"$hostname\"},instance)"},
{"name": "device", "label": "网卡", "type": "query", "datasource": {"uid": "ds-prometheus"}, "query": "label_values(node_network_info{origin_prometheus=~\"$origin_prometheus\", job=~\"$job\", instance=~\"$node\", device!~\"'tap.*|veth.*|br.*|docker.*|virbr.*|lo.*|cni.*'\"},device)"},
{"name": "interval", "label": "采样间隔", "type": "interval", "query": "3m,5m,10m,30m,1h,6h,12h,1d"},
{"name": "job", "label": "JOB高级", "hide": 2, "type": "query", "datasource": {"uid": "ds-prometheus"}, "query": "label_values(node_uname_info{origin_prometheus=~\"$origin_prometheus\"},job)"},
{"name": "Node", "label": "节点池(高级)", "hide": 2, "type": "query", "datasource": {"uid": "ds-prometheus"}, "query": "label_values(kube_node_info{origin_prometheus=~\"$origin_prometheus\"},node)"},
{"name": "maxmount", "hide": 2, "type": "query", "datasource": {"uid": "ds-prometheus"}, "query": "query_result(topk(1,sort_desc(max(node_filesystem_size_bytes{origin_prometheus=~\"$origin_prometheus\",instance=~\"$node\",fstype=~\"ext.?|xfs\",mountpoint!~\".*pods.*\"}) by (mountpoint))))"},
{"name": "show_hostname", "hide": 2, "type": "query", "datasource": {"uid": "ds-prometheus"}, "query": "label_values(node_uname_info{origin_prometheus=~\"$origin_prometheus\", job=~\"$job\", nodename=~\"$hostname\", instance=~\"$node\"},nodename)"},
{"name": "total", "hide": 2, "type": "query", "datasource": {"uid": "ds-prometheus"}, "query": "query_result(count(node_uname_info{origin_prometheus=~\"$origin_prometheus\",job=~\"$job\"}))"}
]
homepage["templating"]["list"] = unified_vars
nav_html = """
<div style="display:flex;justify-content:space-between;align-items:center;gap:16px;flex-wrap:wrap;padding:8px 4px 2px 4px;">
<div style="display:flex;gap:12px;flex-wrap:wrap;">
<a href="/d/infra-overview" style="text-decoration:none;padding:10px 16px;border-radius:999px;background:#1f2937;color:#f9fafb;font-weight:700;">基础设施</a>
<a href="/d/node-overview" style="text-decoration:none;padding:10px 16px;border-radius:999px;background:#e5eefb;color:#1d4ed8;font-weight:700;">主机</a>
<a href="/d/pgsql-overview" style="text-decoration:none;padding:10px 16px;border-radius:999px;background:#ecfdf3;color:#047857;font-weight:700;">数据库</a>
<a href="/dashboards" style="text-decoration:none;padding:10px 16px;border-radius:999px;background:#f4f4f5;color:#27272a;font-weight:700;">更多模块</a>
</div>
<div style="color:#6b7280;font-size:12px;">先选模块再用顶部筛选器缩小范围</div>
</div>
"""
guide_html = """
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;padding:4px 2px 0 2px;">
<div style="border:1px solid #d1d5db;border-radius:12px;padding:12px 14px;background:#fbfdff;">
<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">范围筛选</div>
<div style="font-size:14px;font-weight:700;color:#111827;">数据源 命名空间 服务 Pod</div>
<div style="font-size:12px;color:#6b7280;margin-top:6px;">用于缩小 K8S 资源范围</div>
</div>
<div style="border:1px solid #d1d5db;border-radius:12px;padding:12px 14px;background:#fbfdff;">
<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">当前对象</div>
<div style="font-size:14px;font-weight:700;color:#111827;">主机名 实例 IP 网卡</div>
<div style="font-size:12px;color:#6b7280;margin-top:6px;">用于定位当前分析对象</div>
</div>
<div style="border:1px solid #d1d5db;border-radius:12px;padding:12px 14px;background:#fbfdff;">
<div style="font-size:12px;color:#6b7280;margin-bottom:6px;">视图参数</div>
<div style="font-size:14px;font-weight:700;color:#111827;">采样间隔 + 高级筛选</div>
<div style="font-size:12px;color:#6b7280;margin-top:6px;">JOB 与节点池已折叠为高级项</div>
</div>
</div>
"""
top_panels = [
make_text_panel(1, "模块导航", nav_html, 0, 0, 24, 3),
make_text_panel(2, "筛选说明", guide_html, 0, 3, 24, 5),
]
homepage["panels"].extend(top_panels)
panel_id = 1
homepage["panels"].append(
make_text_panel(panel_id, "总览导航", homepage_nav_html(), 0, 0, 24, 3)
)
panel_id += 1
homepage["panels"].append(
make_text_panel(panel_id, "结构说明", homepage_guide_html(), 0, 3, 24, 5)
)
panel_id += 1
current_y = 8
# 1. Infra
homepage["panels"].append({"collapsed": False, "gridPos": {"h": 1, "w": 24, "x": 0, "y": current_y}, "title": "基础设施总览", "type": "row", "panels": []})
homepage["panels"].append(make_row_panel(panel_id, "平台脉搏", current_y))
panel_id += 1
current_y += 1
infra_max_y = current_y
for p in pig.get("panels", []):
if p.get("type") == "row": continue
# Replace "Apps" panel with "insight Overview" link
if p.get("title") == "Apps":
p["title"] = "insight Overview"
p["type"] = "text"
p["options"] = {
"content": "<div style='text-align: center; padding-top: 10px;'><a href='https://observability.svc.plus/insight/' style='font-size: 18px; color: #58a6ff; font-weight: bold;'>insight Overview</a></div>",
"mode": "html"
}
shift_panel(p, current_y)
homepage["panels"].append(p)
infra_max_y = max(infra_max_y, p["gridPos"]["y"] + p["gridPos"]["h"])
current_y = infra_max_y
# 2. Node
homepage["panels"].append({"collapsed": False, "gridPos": {"h": 1, "w": 24, "x": 0, "y": current_y}, "title": "主机观测", "type": "row", "panels": []})
current_y += 1
node_max_y = current_y
for p in node.get("panels", []):
shift_panel(p, current_y)
homepage["panels"].append(p)
node_max_y = max(node_max_y, p["gridPos"]["y"] + p["gridPos"]["h"])
current_y = node_max_y
summary_layout = [
("Pigsty ${version}", 0, 4, 4, 7),
("Modules", 4, 4, 4, 7),
("Instances", 8, 4, 8, 7),
("Firing Alerts", 16, 4, 8, 7),
]
summary_panels = {panel.get("title"): panel for panel in select_platform_summary_panels(control_plane)}
for title, x, y, w, h in summary_layout:
if title not in summary_panels:
continue
homepage["panels"].append(clone_panel(summary_panels[title], x, y, w, h))
panel_id += 1
current_y += 7
# 3. K8S
homepage["panels"].append({"collapsed": False, "gridPos": {"h": 1, "w": 24, "x": 0, "y": current_y}, "title": "K8S 集群", "type": "row", "panels": []})
current_y += 1
k8s_max_y = current_y
for p in k8s.get("panels", []):
p["gridPos"]["y"] += current_y
homepage["panels"].append(p)
k8s_max_y = max(k8s_max_y, p["gridPos"]["y"] + p["gridPos"]["h"])
current_y = k8s_max_y
for section in DOMAIN_SECTIONS:
panel_id, current_y = add_domain_section(homepage, panel_id, current_y, section)
for i, p in enumerate(homepage["panels"]):
p["id"] = i + 1
for index, panel in enumerate(homepage["panels"], 1):
panel["id"] = index
with open(OUTPUT_PATH, "w") as handle:
json.dump(homepage, handle, indent=2)
with open(output_path, 'w') as f:
json.dump(homepage, f, indent=2)
if __name__ == "__main__":
merge_dashboards()