Nhost Grafana 可观测性配置实战:仪表盘供给、Go 模板安全与核心告警规则
【免费下载链接】nhostThe Open Source Firebase Alternative with GraphQL.项目地址: https://gitcode.com/GitHub_Trending/nh/nhost
本文以 Nhost 仓库中 observability/grafana/README.md 为蓝本,系统讲解 Nhost 云平台为每个项目预置的 Grafana 可观测性套件:如何向仓库贡献/修改仪表盘、为什么这些配置文件必须遵守 Go 模板安全规则、数据源与仪表盘如何在 Kubernetes 中被自动供给,以及内置的 5 条核心告警规则的判定逻辑。读完本文,你可以安全地为该项目新增或修改 Grafana 仪表盘,并理解其告警与通知路由的完整链路。
目录文件全景:一份配置,多种角色
Nhost 云端的 Grafana 实例完全由 observability/grafana/ 目录下的文件驱动,各文件的职责如下:
| 文件 | 角色 |
|---|---|
| grafana.ini | Grafana 主配置,含路径、SMTP、OAuth 登录(Go 模板) |
| dashboards_providers.yaml | 仪表盘文件供给源,指定仪表盘挂载目录与所属文件夹 |
| datasources.yaml.tmpl | Prometheus 数据源模板,运行时由脚本渲染 |
| setup_config.sh | 容器启动脚本:渲染数据源并轮询刷新 |
| rules_nhost.yaml | 内置告警规则(Go 模板) |
| contact_points.yaml | 告警通知接收人配置(Go 模板) |
| notification_policies.yaml | 告警路由策略 |
| oauth_client.json | 动态注册风格的 OIDC 客户端元数据(Go 模板) |
| dashboard_project_metrics.json、dashboard_graphql.json、dashboard_ingress_metrics.json、dashboard_functions_metrics.json | 四张内置仪表盘(Go 模板) |
一个关键事实贯穿全部文件:其中绝大多数不是普通 YAML/JSON/INI,而是 Go 模板。这是理解后文所有规则的起点。
仪表盘贡献流程(继承自 README 官方步骤)
README 中明确给出了向该 Grafana 配置贡献仪表盘的标准流程,共 6 步:
- 在 Nhost 的 Grafana 中创建或修改仪表盘;
- 点击左上角标题旁边的Share按钮;
- 切换到Export标签页;
- 确认“Export for sharing externally”复选框未被勾选(这是最容易出错的一步);
- 点击Save to file按钮;
- 将文件保存到仪表盘目录中(当前仓库的仪表盘 JSON 位于 observability/grafana/ 下,如
dashboard_graphql.json)。
为什么要特别强调第 4 步?因为 Nhost 采用Grafana 文件供给(File Provisioning)模式装载仪表盘。从 dashboards_providers.yaml 可以看到供给源配置:
apiVersion: 1 providers: - disableDeletion: false editable: false # 仪表盘在 UI 中只读 folder: "Nhost - {{ .Subdomain }} ({{ .ProjectName }})" # Go 模板:按项目动态命名 name: default options: path: /var/lib/grafana/dashboards/default # 容器内挂载的仪表盘目录 orgId: 1 type: file这意味着:
- 仪表盘不能在 UI 中编辑(
editable: false),任何变更都必须走“导出 → 修改 → 提交仓库”的流程; - 文件夹名是 Go 模板表达式,渲染时按项目的 Subdomain 与 ProjectName 展开,因此每个 Nhost 项目拥有自己独立的仪表盘文件夹,这正是仪表盘 JSON 中允许出现
{{ .Subdomain }}等占位符的原因; - 运行时仪表盘文件挂载到
/var/lib/grafana/dashboards/default,与仓库内observability/grafana/dashboard_*.json文件一一对应。
因此贡献的仪表盘 JSON 必须与仓库现有文件的格式保持一致——即未经外部分享处理的内部导出格式,并且要能兼容仓库的 Go 模板渲染管线(见下一节)。
模板安全:为什么不能跑格式化器
README 的 “Template safety” 一节给出了本目录最重要的约束:
rules_nhost.yaml和仪表盘 JSON 文件都是 Go 模板。请原样保留其中的{{ ... }}动作;不要运行会改写模板定界符、或在花括号之间插入空格的格式化器。
这条规则看似简单,背后有两层模板嵌套的工程事实:
第一层:Go 模板占位符。grafana.ini 展示了运行时注入的变量,例如:
[server] root_url = '{{ .RootURL }}' [auth.generic_oauth] name = Nhost enabled = true client_id = {{ .RootURL }}/public/oauth-client.json auth_url = {{ .AuthURL }}/oauth2/authorize token_url = {{ .AuthURL }}/oauth2/token api_url = {{ .AuthURL }}/oauth2/userinfo use_pkce = true allow_sign_up = true scopes = openid email profile graphql groups_attribute_path = "https://hasura.io/jwt/claims"."x-hasura-organization-ids" allowed_groups = {{ .OrganizationID }} org_mapping = *:1:{{ .Role }}{{ .RootURL }}、{{ .AuthURL }}、{{ .OrganizationID }}、{{ .Role }}都是 Go 模板动作,由 Nhost 平台在为每个项目实例化 Grafana 容器时执行渲染。dashboards_providers.yaml 中的{{ .Subdomain }}/{{ .ProjectName }}、rules_nhost.yaml 中的{{ .Subdomain }}、contact_points.yaml 中的{{ .Contacts.Emails }}同理。若格式化器把{{改写为{{或其他变体,Go 模板解析将直接失败,整个 Grafana 实例的供给配置作废。
第二层:Grafana 模板语法被“转义”在 Go 模板之内。rules_nhost.yaml 中告警的 summary 字段写法很有代表性:
summary: | The service replica {{ print "{{ index $labels \"pod\" }}" }} is experiencing, or has experienced, high CPU usage. Current usage is at {{ print "{{ index $values \"A\" }}" }}%.这里{{ print "..." }}是 Go 动作,其内部字符串里的{{ index $labels "pod" }}是留给 Grafana 告警消息渲染的模板表达式。Nhost 正是借助这种“Go 模板内嵌字符串、Grafana 模板字符串”的双层结构,让同一份规则文件既能被平台按项目渲染(注入 Subdomain 等),又能被 Grafana 在触发告警时二次渲染(注入标签值)。两层模板各自对花括号极其敏感:任何在{{与标识符之间插入空格、或改写引号转义的操作都会破坏其中一层。这就是 README 警告“do not run formatters that rewrite template delimiters or insert spaces between their braces”的底层原因。
数据源供给链:从 Kubernetes 令牌到 Prometheus 数据源
Grafana 的数据源配置由 setup_config.sh 在容器启动时动态生成,其完整逻辑值得逐段拆解:
DATASOURCES=/var/lib/grafana/provisioning/datasources/datasources.yaml mkdir -p /var/lib/grafana/provisioning/datasources generate_datasources() { # 1. 读取当前 Pod 的 Kubernetes ServiceAccount 令牌 TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) # 2. 从命名空间名推导 App ID(命名空间形如 nhost-<app_id>) APP_ID=$(sed "s/nhost-//g" /var/run/secrets/kubernetes.io/serviceaccount/namespace) # 3. 用令牌与 App ID 渲染数据源模板 sed "s/\${TOKEN}/$TOKEN/g; s/\${APP_ID}/$APP_ID/g" \ < /datasources.yaml.tmpl \ > "${DATASOURCES}.tmp" }渲染目标 datasources.yaml.tmpl 定义了一个名为Nhost、uid 为nhost的 Prometheus 数据源:
apiVersion: 1 datasources: - access: proxy isDefault: true name: Nhost type: prometheus url: http://amp-signer.nhost-services:8080 # 集群内指标代理服务 uid: nhost jsonData: customQueryParameters: app_id=${APP_ID} # 每次查询附带 app_id 参数,实现按项目隔离 httpHeaderName1: 'Authorization' manageAlerts: false cacheLevel: 'High' disableRecordingRules: true timeInterval: '60s' # 最低查询间隔 60s secureJsonData: httpHeaderValue1: 'Bearer ${TOKEN}' # 每次查询携带 SA 令牌从脚本与模板的结构看,这套设计的意图很清晰:
- 租户隔离:Grafana 不直连 Prometheus,而是经由集群内的
amp-signer代理,并以app_id自定义查询参数圈定本项目可见的指标范围; - 凭证时效:ServiceAccount 令牌会轮转,因此脚本在初始生成后进入死循环,每 600 秒重新渲染一次,并用
cmp比对新旧文件; - 变更热加载:仅当文件真正变化时,才通过 Grafana 管理 API 触发重新供给:
while true; do sleep 600 generate_datasources if ! cmp -s "${DATASOURCES}.tmp" "${DATASOURCES}"; then mv "${DATASOURCES}.tmp" "${DATASOURCES}" curl -sf -X POST \ -u "${GF_SECURITY_ADMIN_USER}:${GF_SECURITY_ADMIN_PASSWORD}" \ http://localhost:3000/api/admin/provisioning/datasources/reload else rm "${DATASOURCES}.tmp" fi done这一“模板 + 渲染循环 + 按需 reload”的模式,与上文仪表盘供给(dashboards_providers.yaml)共同构成了 Nhost 每项目一套 Grafana 的运行时供给链。
内置告警规则:5 条覆盖核心资源与请求质量
rules_nhost.yaml 定义了一个名为core的告警组,文件夹与仪表盘一致(“Nhost - Subdomain (ProjectName)”),评估间隔interval: 5m,共 5 条规则。每条规则都是“查询(A)→阈值判断(B)”的 Grafana Unified Alerting 结构,并统一处理无数据与执行错误状态:
| UID | 标题 | 判定表达式(核心) | 阈值 | 持续时间 |
|---|---|---|---|---|
nhosthighcpuusage | High CPU usage | 各 Pod CPU 使用率:irate 容器 CPU 用量 ÷ CPU 配额(quota/period),排除 grafana 与 POD 容器 | > 75% | 持续 15 分钟 |
nhostlowdiskspace | Low disk space | PVC 已用字节 ÷ 容量字节 × 100 | > 75% | 持续 15 分钟 |
nhostlowmemory | Low free memory | 容器 working set 内存 ÷ memory limit × 100,排除 grafana 容器 | > 75% | 持续 15 分钟 |
nhostoom | Service restarted due to lack of memory | increase(pod_terminated_total{reason="OOMKilled", pod!="grafana"}[...]) | > 0(即发生过) | 0s(立即) |
nhosthigherrorrate | High request error rate | Nginx Ingress 10 分钟窗口 4xx/5xx 占比,且该窗口请求量 ≥ 100 | > 25% | 持续 15 分钟 |
几个值得注意的工程细节:
- 最小样本量约束:错误率规则在表达式末尾用
and on(ingress, method) (... >= 100)保证只有 10 分钟内请求量达到 100 的 ingress 才参与判定,避免小流量项目因个别 4xx 误报——规则注解中也明确写道“该告警仅在服务方法 10 分钟内收到至少 100 个请求后才被评估”; - OOM 规则不设持续时间:
for: 0s且execErrState: OK,即只要观测到一次 OOMKilled 就立即告警,因为 OOM 本身就是已发生的事实,无需等待; - 每条规则自带排障指引:注解中的
description以结构化列表给出可能原因(高流量、低效代码/查询、资源不足、网络问题、权限问题等)与处置建议(优化代码、增加副本数、提升 CPU/内存配额、观察服务日志等),并通过runbook_url指向 Nhost 官方文档对应的章节(如 compute-resources、configuring-postgres),让告警消息本身就是可读的 runbook; - 注解中的双层模板:
summary使用{{ print "{{ index $labels \"pod\" }}" }}这类写法(见前文“模板安全”一节),description之外的 Subdomain/Project Name 字段则使用{{ .Subdomain }}/{{ .ProjectName }}由平台渲染。
通知路由:从告警到邮件、Slack、PagerDuty 与 Webhook
告警触发后的投递路径由两个文件定义:
notification_policies.yaml 设置全局路由:
apiVersion: 1 policies: - orgId: 1 receiver: Nhost Managed Contacts group_by: - grafana_folder - alertname所有告警都路由到Nhost Managed Contacts这个接收人,并按grafana_folder+alertname分组——同一项目的同一条告警在重复触发时会被合并,避免通知风暴。
contact_points.yaml 定义了接收人的具体渠道,全部是 Go 模板,由平台按用户/组织配置渲染:
- email:
addresses由{{ join .Contacts.Emails "," }}展开,sendReminder: true开启重发提醒; - pagerduty:
uid以 100 为基数递增({{ add 100 $i }}),携带integrationKey、severity、class、component、group; - discord:
uid以 200 为基数递增,启用use_discord_username: true; - slack:
uid以 300 为基数递增,支持 recipient、token、@mention 用户/群组/频道等完整参数; - webhook:
uid以 400 为基数递增,支持自定义 HTTP 方法、基本认证、授权头与maxAlerts上限。
从 uid 的编码方式(100/200/300/400 分段)可以推断,Nhost 有意让各渠道接收人在 Grafana 内部拥有稳定且互不冲突的标识,便于告警规则跨渠道引用同一组通知目标。
登录集成:通过 Nhost OAuth 保护 Grafana
grafana.ini 的认证配置将 Grafana 登录完全托管给 Nhost 自身:
[users] allow_sign_up = false [auth] disable_login_form = true本地注册与登录表单被彻底关闭,用户必须走auth.generic_oauth(见前文配置摘录):
- OIDC 元数据自描述:
client_id直接指向{{ .RootURL }}/public/oauth-client.json,该文件即 oauth_client.json 的渲染产物——一份动态客户端注册风格的元数据文档,声明了redirect_uris(回到/login/generic_oauth)、token_endpoint_auth_method: none、authorization_code授权类型与openid email profile graphql范围; - PKCE 开启(
use_pkce = true),符合公共客户端的安全要求; - 组织级访问控制:
groups_attribute_path从 JWT 的 Hasura claims 中提取x-hasura-organization-ids,allowed_groups = {{ .OrganizationID }}确保只有本组织的成员可以登录该项目的 Grafana,org_mapping = *:1:{{ .Role }}再把 Nhost 角色映射为 Grafana 组织角色; - 此外
[smtp]段也是条件模板({{ if .SMTP }}),仅在部署配置了邮件服务器时启用,用于告警邮件发送。
内置仪表盘:项目、GraphQL、Ingress 与 Functions 四个视角
供给链最终呈现给用户的,是四张覆盖 Nhost 全栈的仪表盘:
- Project Metrics(dashboard_project_metrics.json):“Resources utilized by an Nhost project”,含 CPU usage by Service Replica 等面板,面板说明中特意提醒 CPU 使用率是相邻数据点间的平均值,在长时间区间内粒度会下降,突刺可能不易被察觉;
- GraphQL Metrics(dashboard_graphql.json):请求速率、订阅数、响应时长与失败率,配合按副本的 CPU/内存利用率(Resource Utilization 分组);
- Ingress Metrics(dashboard_ingress_metrics.json):按方法与响应状态维度的请求数、平均响应大小、平均/P95 响应时间、错误率(失败请求数 ÷ 总请求数)与总错误数;
- Functions Metrics(dashboard_functions_metrics.json):Serverless 函数调用总数、总字节发送量、总耗时、按方法与按状态码的调用分布、P95/P75 响应时间(面板描述明确解释:“P95 响应时间指 95% 的响应时间都低于该值”)、平均响应时间、错误率与总错误数。
这些仪表盘 JSON 与告警规则共享同一套模板安全约束:在修改或新增面板后,必须按“贡献流程”一节导出文件,并保证导出文件中任何{{ ... }}定界符原样保留、花括号之间不出现多余空格,然后提交到仓库的仪表盘目录,方可通过供给链下发到所有项目。
小结
Nhost 的可观测性方案是一套“仓库即配置”的供给体系:仪表盘 JSON、告警规则、通知渠道与 Grafana 主配置全部以 Go 模板形式存放在 observability/grafana/,由 setup_config.sh 与 Kubernetes 环境信息在运行时渲染生效。对贡献者而言,有两条不可逾越的纪律:一是在 Grafana 中导出仪表盘时不勾选“Export for sharing externally”;二是绝不允许格式化器触碰{{ ... }}模板定界符。遵守这两点,你的仪表盘与告警变更就能安全地进入 Nhost 每项目一套 Grafana 的完整供给与告警链路。
【免费下载链接】nhostThe Open Source Firebase Alternative with GraphQL.项目地址: https://gitcode.com/GitHub_Trending/nh/nhost
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考