本页目录

全栈监控:Prometheus + Grafana + Loki + Tempo + Bark

指标(Prometheus)+ 日志(Loki)+ Trace(Tempo)三条线汇入 Grafana,告警经 Bark 推送到 iPhone。指标分 pull(局域网 node_exporter)和 push(远程 otel-collector fleet agent,经 rathole 隧道 + mTLS)两套;日志走 journald → otelcol-logs → Loki;Trace 走 otelcol-home tail sampling → Tempo。以本机 li-home-0 为实例,覆盖全栈架构、告警规则、采集对比、标签方案、级别判定与踩坑记录。

一、总览

监控架构:指标+日志+Trace → Grafana,告警 → Bark → iPhone 远程 fleet otel-collector host_metrics 30s file_storage 队列 mTLS 客户端证书 rathole TCP 隧道 ↓ Caddy mTLS 终止 路径限定+吊销 otelcol-home OTLP 接收 metrics pipeline traces pipeline tail sampling Prometheus Tempo :4317 node_exporter (pull) journald otelcol-logs Loki :3100 Grafana 指标 · 日志 · Trace Bark APNs → iPhone metrics-fleet:远程 otel-collector(fleet agent)→ OTLP+mTLS → rathole隧道 → Caddy网关 → otelcol-home(metrics+trace pipeline) 本地:node_exporter → Prometheus(pull) · journald → otelcol-logs → Loki · otelcol-home traces → Tempo(gRPC,720h保留) Grafana统一查询指标(Prometheus)+日志(Loki)+Trace(Tempo) · 告警经 Bark webhook → APNs → iPhone(本地 bark-server,不依赖隧道/公网) 所有组件 native systemd · bind 127.0.0.1 · Docker仅跑应用

二、指标采集:两套模型

传统 pull (node_exporter)远程 push (metrics-fleet)
适用局域网节点、本机公网远程服务器
采集器node_exporter / LHM exporterotel-collector-contrib (host_metrics)
指标命名node_*, windows_*, lhm_*system_cpu_*, system_memory_*
网络Prometheus 主动拉,需可达端口远程主动推,零入站端口
传输安全无 (LAN 内裸奔)mTLS + 路径限定(只放行 OTLP 写入)
接入方式手动改 prometheus.yml static_configsadd-server.sh <ssh别名>
Dashboard每台一个独立 JSON单个模板,$host 下拉切换
中转依赖rathole 隧道 → 中转 VPS → 家

Pull 模型:scrape_configs

Prometheus 每 15s 拉取这些目标:本机 node_exporter(:9100)、llama-server(:18080)、Home Assistant(:8123)、Windows 机(9182/9183)、N100 旁路由(:9100)、小米路由器(:9184)、自监控(:9090)。

Push 模型:OTLP 接收

远程服务器通过 otel-collector 采集 host_metrics,经 mTLS 认证 + rathole 加密隧道,推送到家里 Prometheus 的 remote write 端点:

Push 模型:远程指标经 mTLS + rathole 隧道推送到家 远程 otel-collector host_metrics 采集 OTLP+mTLS证书 公网入口 otlp.liz6.com:9443 DNS → 中转 VPS rathole 隧道 TCP 透传 · 只见密文 家 Caddy mTLS 网关 验证书 + 吊销检查 只放行 /api/v1/otlp/* otelcol-home :4318 batch 处理 Prometheus remote write :9090 mTLS 只在 Caddy 终止,rathole 全程只透传密文,自己看不见明文流量。 Prometheus 配置 out_of_order_time_window=30m,兼容跨公网到达顺序抖动。

Prometheus 配置 out_of_order_time_window: 30m 兼容跨公网到达顺序抖动。

Grafana Dashboards

Provisioning 目录:7 个主机面板(CPU / GPU / 内存 / 磁盘 / 散热,按节点分 JSON)+ 1 个服务器状态模板($host 下拉自动发现所有 OTLP 推送来的 host_name)+ 网络监控面板。

面板表达式例:

# CPU: 100*(1-avg by(host_name)(system_cpu_utilization_ratio{host_name="$host",state="idle"}))
# 内存: 100*system_memory_utilization_ratio{host_name="$host",state="used"}
# 存活检测(push 模型无 up 指标,用最近上报时间替代):
time()-max(timestamp(system_memory_usage_bytes{host_name="$host"}))

三、安全:CA 与 mTLS

远程 push 链路的安全由私有 CA 签发客户端证书保证:

~/infra/metrics-fleet/ca/
├── make-ca.sh              # 一次性创建 CA (EC P-256, 10年)
├── issue-server-cert.sh    # 签服务端证书 → /etc/caddy/metrics-fleet/
├── issue-client-cert.sh    # 签客户端证书 → out/clients/<name>/
├── revoke-client-cert.sh   # 吊销 + 更新 Caddy revoked.caddy + reload
└── out/clients/<name>/     # 已签发的客户端证书(每远程机一份)

Caddy 在 127.0.0.1:9443 做 mTLS 网关:验客户端证书(require_and_verify,trust CA)、查吊销名单 → 403、只放行 /api/v1/otlp/* 并 strip_prefix → otelcol-home :4318。

每台远程服务器的公网 IP 需加到本机 + N100 两边 mihomo 的 DIRECT 规则,确保 rathole 承载连接和 SSH 不走代理(承载连接必须走原生路由——见 rathole-tunnel.md 4.3 节)。

四、告警:Grafana + Bark 推送到 iPhone

Prometheus 负责采集,Grafana 负责判定和通知。告警链路:

告警链路:Grafana 规则评估 → bark-server(本地) → APNs → iPhone Grafana 规则评估 每 30s 评估一次 webhook bark-server 127.0.0.1:8085 本地直连,不经隧道 APNs iPhone 锁屏推送 为什么走本地 bark-server:告警通知不能依赖隧道/公网——链路挂了时告警也发不出去,本地直连零外部依赖。

为什么走本地 bark-server,不用公网 webhook:如果告警通知走 rathole 隧道或公网——隧道挂了的时候告警也发不出去。本地 bark-server 直连 iPhone,零外部依赖,不跟基础设施绑定。

联系人与通知策略

# /etc/grafana/provisioning/alerting/contact-points.yaml
contactPoints:
  - name: bark-home
    receivers:
      - type: webhook
        settings:
          url: "http://127.0.0.1:8085/<token>"   # 本地 bark-server,绕开隧道
          maxAlerts: 2                            # APNs 4KB,超 2 条即 PayloadTooLarge
          title: "[{{ .Status }}] {{ .CommonLabels.grafana_folder }}{{ if .CommonLabels.alertname }} · {{ .CommonLabels.alertname }}{{ end }}"
          message: "{{ range .Alerts }}{{ .Annotations.summary }}\n{{ end }}"

policies:
  - receiver: bark-home
    group_by: ["grafana_folder", "alertname"]     # 同类合并,不同类各发各
    group_wait: 30s                               # 首批延迟等同类进来
    group_interval: 5m                            # 同组后续告警间隔
    repeat_interval: 4h                           # 未恢复每 4h 重提醒

maxAlerts: 2 是关键——bark 把整个 webhook JSON 透传给 APNs,每条 alert 约 1.1–1.4KB,超过 2 条就超 APNs 4KB 上限,bark 直接回 500。

告警规则(共 13 条)

规则级别条件静默
旁路由宕机criticalup{job="li-home-router"}==03m
路由器不通criticalping_loss{target="gateway"}>30%2m
外网高丢包warningping_loss{target="223.5.5.5"}>20%3m
TCP 重传率高warningTcp_RetransSegs / Tcp_OutSegs > 10%5m
DNS 全慢warning三个域名解析均 >100ms5m
mihomo TUN 失效criticalnikki 网卡指标消失2m
CN 节点磁盘不足warning根分区剩余 <5GB5m
水泵停转criticalfan7 RPM <15005m
VRM 过热warningtemp5 >90°C2m
GPU 热点过热warning7900XTX 结温 >100°C2m
N100 CPU 过载warningCPU >90%5m
本机 exporter 掉线criticalup{job="local-info"}==02m
rathole 服务不可达criticalrathole_service_up < 1(按 service 分标签)3m

规则用 Grafana provisioning 管理(/etc/grafana/provisioning/alerting/home-network.yaml),git 版本化,改规则不用进 Web UI。每条告警在 annotations.summary 描述现象,annotations.runbook 给出排查步骤——告警通知只推 summary,runbook 留在 Grafana 里展开看。

指标踩坑

  • N100 旁路由接入⁠:Prometheus scrape N100 需过本机代理才通,加 DIRECT 规则绕开。

五、日志监控(Loki + OpenTelemetry)

补齐"只有指标、没有日志"的缺口。本机所有系统日志(内核 + systemd 服务 + Docker 容器)统一进 journald → otelcol-logs → Loki → Grafana Explore 检索。

设计取舍

  • 选 OpenTelemetry,不用 Promtail/Vector:本机已跑 otelcol-home,再引入 Vector 会让采集器技术栈重复。OTel 还让远程主机日志接入几乎免费(复用同一 mTLS/rathole/Caddy 隧道)。
  • Loki 走原生 OTLP 端点 /otlp/v1/logs(旧 lokiexporter 已弃用)。
  • Docker 改 journald 日志驱动⁠:容器日志带 CONTAINER_NAME 进 journald,被同一个 journaldreceiver 收走 → 有真实容器名、单一数据源。
  • native systemd、bind 127.0.0.1、90 天保留、不做日志 dashboard(直接 Explore)。

数据流

日志数据流:journald → otelcol-logs → Loki → Grafana 内核 dmesg systemd 服务 Docker 容器 journald 持久化 /var/log/journal SystemMaxUse=8G otelcol-logs journaldreceiver + OTTL resource_detection+batch User=otelcol-logs Loki 90d Grafana Explore 关键:journaldreceiver 把字段放入 body(Map),OTTL 先从 body 抽属性再 set(body, body["MESSAGE"]) 还原。 Docker 端 /etc/docker/daemon.json: log-driver=journald + tag={{.Name}} + live-restore=true Loki 端:common.instance_addr=127.0.0.1(否则 ring 广播 IP 自连失败),discover_log_levels=true

组件

组件位置备注
Loki 3.7/usr/local/bin/loki,conf /etc/loki:3100,OTLP /otlp/v1/logs,用户 loki
otelcol-logs/usr/local/bin/otelcol-contrib(0.154,复用 fleet 二进制)unit /etc/systemd/system/otelcol-logs.service,无需 root(group systemd-journal)
Grafana 数据源/etc/grafana/provisioning/datasources/loki.yamluid loki-local

标签方案

低基数进索引标签⁠,高基数进structured metadata(可查不进索引):

索引标签含义
host_nameresource host.name(整机名,对齐 metrics)
service_name= host.name,Loki 原生分组字段
unitsystemd 服务名;Docker 容器 = 容器名(被 OTTL 从 CONTAINER_NAME 覆盖)
containerDocker 容器名(仅容器日志)
level见下「级别判定」
transportkernel / stdout / syslog / journal / audit

级别判定(混合策略)

journald 的 PRIORITY 只在原生源(systemd-journal/syslog/kernel)才是真级别;容器日志和裸 stdout/stderr 的 PRIORITY 只代表流(stdout=6/stderr=3),无意义。

  1. 基线⁠:容器和非 stdout 源按 PRIORITY 给级别(0–2 critical / 3 error / 4 warn / 5–6 info / 7 debug);容器日志默认 info
  2. 正文覆盖⁠:OTTL IsMatch 从消息正文识别 [error]/level=info/"level":"warn" 以及大写独立词 WARNING/ERROR
  3. Loki 侧 discover_log_levels: true——Loki 识别 level 属性并据此填 detected_level(Grafana 等级可视化用)。

查询示例(Grafana Explore)

{unit="sshd.service"}
{level="error"}
{container="adguardhome"}
{transport="kernel"}
{service_name="li-home-0"} |= "timeout"

六、日志踩坑

  1. Loki 单二进制 ring 自连失败⁠:默认广播网卡 IP,但 gRPC 只听 127.0.0.1 → 连接被拒。必须 common.instance_addr: 127.0.0.1
  2. journaldreceiver 把整条日志放 body(Map):字段是 body["PRIORITY"] 不是 attributes。OTTL 先从 body 抽属性,最后 set(body, body["MESSAGE"])
  3. service_name 语义⁠:要的是整机标识,是 resource 级。Loki discover_service_name 只读 resource 属性 → 在 OTTL resource context 设 service.name = host.name
  4. Docker 容器的 unit:journald driver 下 _SYSTEMD_UNIT=docker.service 没用 → OTTL 用 CONTAINER_NAME 覆盖。
  5. 级别不能信 PRIORITY:adguard/HA/owntracks 把 info+warning 全写 stderr → PRIORITY 全 3,若信就全变 error。discover_log_levels 别设 false——否则 detected_level 没了,Grafana 直方图塌成单条。
  6. 彩色日志 = 字节数组⁠:含 ANSI 的 MESSAGE 被 journald 存成 int-slice,OTTL 无法解码。⁠治本 = 源头关色⁠:容器加 NO_COLOR=1+PY_COLORS=0;Rust 服务加 Environment=NO_COLOR=1+RUST_LOG_STYLE=never
  7. node_exporter sgcc 刷屏⁠:sgcc_ts.prom 带时间戳(root 所有的孤儿文件),textfile collector 不支持。移出 textfile_collector/ 即根治。
  8. runlike 重建容器丢数据⁠:uvx runlike 对匿名/命名卷输出错误,需按 docker inspect .Mounts 显式钉卷。

七、Trace:Tempo + tail sampling

Trace 和指标走同一条 OTLP 入口(otelcol-home :4318),在内部按 pipeline 分流:

Trace 链路:应用 → otelcol-home → tail sampling → Tempo → Grafana 应用/服务 OTLP trace exporter otelcol-home :4318(同指标入口) pipeline: memory_limiter → tail_sampling → batch Tempo :4317 · gRPC Grafana Explore tempo-local Trace 与指标共用同一条 OTLP 入口(otelcol-home :4318),内部按 pipeline 分流,互不干扰。 tail_sampling 保证 span 树完整,不会出现父子 span 采样不一致的断链。

采样策略

使用 tail sampling(非 head sampling),保证 span 树完整不被截断:

策略条件采样率
errors-onlystatus_code == ERROR100%
latency-sampling耗时 >500ms100%
probabilistic其余正常请求10%

tail sampling 比 head sampling 多 10s 决策延迟(等 span 树收齐再做决定),但不会出现"父 span 采样了、子 span 没采"的断链——这对排查分布式调用链至关重要。

Tempo 后端

# /etc/tempo/config.yaml
distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 127.0.0.1:4317
ingester:
  max_block_duration: 5m
  lifecycler:
    ring:
      kvstore: { store: inmemory }
      replication_factor: 1

单实例 inmemory ring(无集群需求),720h(30 天)保留。与 Loki 相同的模式:native systemd,bind 127.0.0.1,Grafana Explore 直接按 Trace ID 检索。

接入方式

应用侧设 OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318(本机)或 https://otlp.liz6.com:9443(远程,经 fleet 隧道)。SDK 自动产生 trace。远程应用复用现成 fleet collector 的 mTLS 证书——指标和 trace 走同一条管道、同一套安全体系。

八、未来扩展:远程 fleet 日志

复用现成 metrics-fleet 传输(无需改 Caddy,/api/v1/otlp/* 已覆盖 v1/logs):

  1. 远程 otelcol-fleet agent 加 journald receiver + logs pipeline → otlphttpotlp.liz6.com:9443
  2. 家里 otelcol-home 加 logs pipeline → otlphttp 到 Loki /otlp/v1/logs
  3. add-server.sh/CA/命名全部沿用,host_name/service_name 即各节点名

相关文档