本页目录
全栈监控: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 为实例,覆盖全栈架构、告警规则、采集对比、标签方案、级别判定与踩坑记录。
一、总览
二、指标采集:两套模型
| 传统 pull (node_exporter) | 远程 push (metrics-fleet) | |
|---|---|---|
| 适用 | 局域网节点、本机 | 公网远程服务器 |
| 采集器 | node_exporter / LHM exporter | otel-collector-contrib (host_metrics) |
| 指标命名 | node_*, windows_*, lhm_* | system_cpu_*, system_memory_* |
| 网络 | Prometheus 主动拉,需可达端口 | 远程主动推,零入站端口 |
| 传输安全 | 无 (LAN 内裸奔) | mTLS + 路径限定(只放行 OTLP 写入) |
| 接入方式 | 手动改 prometheus.yml static_configs | add-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 端点:
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 负责判定和通知。告警链路:
为什么走本地 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: # 同类合并,不同类各发各
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 条)
| 规则 | 级别 | 条件 | 静默 |
|---|---|---|---|
| 旁路由宕机 | critical | up{job="li-home-router"}==0 | 3m |
| 路由器不通 | critical | ping_loss{target="gateway"}>30% | 2m |
| 外网高丢包 | warning | ping_loss{target="223.5.5.5"}>20% | 3m |
| TCP 重传率高 | warning | Tcp_RetransSegs / Tcp_OutSegs > 10% | 5m |
| DNS 全慢 | warning | 三个域名解析均 >100ms | 5m |
| mihomo TUN 失效 | critical | nikki 网卡指标消失 | 2m |
| CN 节点磁盘不足 | warning | 根分区剩余 <5GB | 5m |
| 水泵停转 | critical | fan7 RPM <1500 | 5m |
| VRM 过热 | warning | temp5 >90°C | 2m |
| GPU 热点过热 | warning | 7900XTX 结温 >100°C | 2m |
| N100 CPU 过载 | warning | CPU >90% | 5m |
| 本机 exporter 掉线 | critical | up{job="local-info"}==0 | 2m |
| rathole 服务不可达 | critical | rathole_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)。
数据流
组件
| 组件 | 位置 | 备注 |
|---|---|---|
| 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.yaml | uid loki-local |
标签方案
低基数进索引标签,高基数进structured metadata(可查不进索引):
| 索引标签 | 含义 |
|---|---|
host_name | resource host.name(整机名,对齐 metrics) |
service_name | = host.name,Loki 原生分组字段 |
unit | systemd 服务名;Docker 容器 = 容器名(被 OTTL 从 CONTAINER_NAME 覆盖) |
container | Docker 容器名(仅容器日志) |
level | 见下「级别判定」 |
transport | kernel / stdout / syslog / journal / audit |
级别判定(混合策略)
journald 的 PRIORITY 只在原生源(systemd-journal/syslog/kernel)才是真级别;容器日志和裸 stdout/stderr 的 PRIORITY 只代表流(stdout=6/stderr=3),无意义。
- 基线:容器和非 stdout 源按 PRIORITY 给级别(0–2 critical / 3 error / 4 warn / 5–6 info / 7 debug);容器日志默认
info。 - 正文覆盖:OTTL
IsMatch从消息正文识别[error]/level=info/"level":"warn"以及大写独立词WARNING/ERROR。 - 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"
六、日志踩坑
- Loki 单二进制 ring 自连失败:默认广播网卡 IP,但 gRPC 只听 127.0.0.1 → 连接被拒。必须
common.instance_addr: 127.0.0.1。 - journaldreceiver 把整条日志放 body(Map):字段是
body["PRIORITY"]不是 attributes。OTTL 先从 body 抽属性,最后set(body, body["MESSAGE"])。 - service_name 语义:要的是整机标识,是 resource 级。Loki
discover_service_name只读 resource 属性 → 在 OTTL resource context 设service.name = host.name。 - Docker 容器的 unit:journald driver 下
_SYSTEMD_UNIT=docker.service没用 → OTTL 用CONTAINER_NAME覆盖。 - 级别不能信 PRIORITY:adguard/HA/owntracks 把 info+warning 全写 stderr → PRIORITY 全 3,若信就全变 error。
discover_log_levels别设 false——否则detected_level没了,Grafana 直方图塌成单条。 - 彩色日志 = 字节数组:含 ANSI 的 MESSAGE 被 journald 存成 int-slice,OTTL 无法解码。治本 = 源头关色:容器加
NO_COLOR=1+PY_COLORS=0;Rust 服务加Environment=NO_COLOR=1+RUST_LOG_STYLE=never。 - node_exporter sgcc 刷屏:
sgcc_ts.prom带时间戳(root 所有的孤儿文件),textfile collector 不支持。移出textfile_collector/即根治。 - runlike 重建容器丢数据:
uvx runlike对匿名/命名卷输出错误,需按docker inspect .Mounts显式钉卷。
七、Trace:Tempo + tail sampling
Trace 和指标走同一条 OTLP 入口(otelcol-home :4318),在内部按 pipeline 分流:
采样策略
使用 tail sampling(非 head sampling),保证 span 树完整不被截断:
| 策略 | 条件 | 采样率 |
|---|---|---|
| errors-only | status_code == ERROR | 100% |
| latency-sampling | 耗时 >500ms | 100% |
| 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:
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):
- 远程 otelcol-fleet agent 加 journald receiver + logs pipeline →
otlphttp到otlp.liz6.com:9443 - 家里 otelcol-home 加 logs pipeline →
otlphttp到 Loki/otlp/v1/logs add-server.sh/CA/命名全部沿用,host_name/service_name即各节点名
相关文档
- network-architecture.md — 网络拓扑与守护进程
- rathole-tunnel.md — 隧道架构(承载连接绕 mihomo)
- server-lifecycle.md — 远程服务器接入脚本(add-server)