diff --git a/deploy/infra/observability/.env.example b/deploy/infra/observability/.env.example new file mode 100644 index 00000000..fb52f895 --- /dev/null +++ b/deploy/infra/observability/.env.example @@ -0,0 +1,16 @@ +# ============================================================================= +# 观测栈运行时密钥模板 —— 部署时复制为 .env 并填真实值,权威副本见 +# docs/内网凭据与端点.md。.env 只在服务器 chmod 600 存在、不入仓(见 .gitignore)。 +# cp .env.example .env && chmod 600 .env # 然后把占位换成真实值 +# ============================================================================= + +# ── MinIO S3 后端凭据 ── +# Tempo/Loki 的对象存储复用既有 infra-minio(ragflow 共用实例),各存 obs-tempo/obs-loki 桶。 +# ACCESS_KEY 非机密(账号名),SECRET_KEY 是机密、从凭据文档取。 +MINIO_ACCESS_KEY=ragflow +MINIO_SECRET_KEY=__FILL_FROM_docs/内网凭据与端点.md__ + +# ── Grafana 管理员 ── +# 首次进盘用它登录(admin 一键进盘)。口令从凭据文档取,首登后可改。 +GF_ADMIN_USER=admin +GF_ADMIN_PASSWORD=__FILL_FROM_docs/内网凭据与端点.md__ diff --git a/deploy/infra/observability/.gitignore b/deploy/infra/observability/.gitignore new file mode 100644 index 00000000..d0839f94 --- /dev/null +++ b/deploy/infra/observability/.gitignore @@ -0,0 +1,4 @@ +# 运行时密钥文件:.env 只在服务器上 chmod 600 存在,权威副本在 docs/内网凭据与端点.md,不入仓。 +.env +# 若改用 bind-mount 的运行时产物目录(当前用 named volume,一般不落此目录)。 +data/ diff --git a/deploy/infra/observability/README.md b/deploy/infra/observability/README.md new file mode 100644 index 00000000..0ff99612 --- /dev/null +++ b/deploy/infra/observability/README.md @@ -0,0 +1,95 @@ +# 可观测性栈(observability) + +这套栈把五个组件起在 mini-infra:Collector 收信号,Prometheus 存指标,Tempo 存追踪,Loki 存日志,Grafana 看板。所有服务和两条生成线把 OpenTelemetry 信号推到同一个 Collector,Collector 先脱敏、再采样、再分发到三个存储,Grafana 从三个存储读出来,在一块盘上把「用户点一下到模型返回」这条链路串起来看。 + +配置控制面阶段四的第一波只做后端五件加自监控,不碰生产;给 game-cloud 挂 agent、重定向 Python 线的 OTLP、打通三跳 traceparent、落业务埋点,是后续波的事。权威设计见 `docs/agent-specs/2026-07-05-全链路可观测性-OTel三信号全栈-设计.md`。 + +## 起停 + +部署目录假设是 `/opt/infra-observability`(把本目录同步过去)。 + +``` +cd /opt/infra-observability +cp .env.example .env && chmod 600 .env # 首次:填真实密钥,见下"密钥与配置" +docker compose up -d # 起 +docker compose ps # 看状态 +docker compose logs -f otel-collector # 看某件日志 +docker compose down # 停(保留数据卷) +``` + +起的时候 compose 会先跑一个一次性的 `minio-init` 容器,在既有 MinIO 上把 `obs-tempo` 和 `obs-loki` 两个桶建好(幂等,重复起不报错),Tempo 和 Loki 等它成功退出后才启动。 + +## 探活 + +``` +HOST=100.64.0.8 bash health-check.sh +``` + +逐个探五件的就绪端点,全绿退 0,任一没起来退 1。Tempo 和 Loki 刚起的头几十秒 `/ready` 会返 503,属正常就绪过程,等一会儿再探。脚本默认绕过本机 fake-ip 代理直连 Tailscale 地址。 + +## 端口账 + +对外端口都逐个核对过没和 mini-infra 现有占用撞(new-api=3000、gitea=3001、ragflow=9380-9381、postgres=5433、minio=9000-9001、mysql=3306、redis=6379、nacos=8848、rocketmq=9876/10911、nginx=80/443、infinity=23817/23820)。 + +| 组件 | 对外端口 | 用途 | +|---|---|---| +| Grafana | 3002 | Web 看板(默认 3000/3001 已被占,改 3002) | +| Prometheus | 9090 | Web UI / 查询 API | +| Loki | 3100 | HTTP API + OTLP 日志摄入(`/otlp`) | +| Tempo | 3200 | 查询 API | +| Collector | 4317 / 4318 | OTLP gRPC / HTTP 入口(跨机信号源推这里) | +| Collector | 13133 | 健康探针 | + +Collector 自身遥测 8888、业务指标出口 8889 不对外映射,Prometheus 在同网内用服务名 scrape。Tempo 收 Collector 转发的 OTLP 走容器内 4317,不对外映射,不和 Collector 对外的 4317 冲突(不同容器各自监听)。五件之间用 compose 服务名(otel-collector / prometheus / tempo / loki / grafana)在 `infra-shared` 网内互通,连既有 MinIO 用它的容器名 `infra-minio`。 + +## 内存和 CPU 账 + +同箱的是跨项目生产件,失控的 Prometheus 或 Tempo 在 CPU/IO 上挤兑 new-api 就是事故,所以每件都设了内存和 CPU 双上限,`cpu_shares` 压到 512(生产件默认 1024 的一半),CPU 抢起来观测让路。 + +| 组件 | mem_limit | cpus | cpu_shares | +|---|---|---|---| +| Collector | 512m | 0.50 | 512 | +| Prometheus | 1g | 0.75 | 512 | +| Tempo | 768m | 0.50 | 512 | +| Loki | 768m | 0.50 | 512 | +| Grafana | 384m | 0.50 | 512 | + +内存上限合计约 3.4G。mini-infra 扩到 32G 后 available 约 24Gi,装下这套还有大量余量。上限是纪律不是配额,扩了内存也照设。 + +## 存储 + +Tempo 的追踪块和 Loki 的日志块都存在既有 MinIO 上,各用独立的 `obs-tempo` / `obs-loki` 桶,和 ragflow 的桶完全分开。Tempo 的 WAL、Loki 的 index、Prometheus 的 TSDB、Grafana 的状态都落 docker 托管的本地卷(`tempo-data` / `loki-data` / `prometheus-data` / `grafana-data`),用 named volume 而非挂宿主目录,免掉 RocketMQ 那种 uid 权限坑。 + +保留期:Prometheus 指标 15 天(compose 里 `--storage.tsdb.retention.time`),Loki 日志 15 天(`limits_config.retention_period`),Tempo 追踪 7 天(`compactor.block_retention`)。MinIO 连不上的时候,Tempo/Loki 写不进对象存储会重试并在本地缓冲,Collector 那侧的队列满了会丢弃而不是把上游堵死。 + +## 脱敏(安全红线) + +追踪的 span 属性里会带生成的输入输出,日志正文里会有报错原文,里面可能有用户手机号或 token。Collector 上有一道 `redaction` 处理器,在写进 Tempo 和 Loki 之前把手机号、token、密钥、JWT 这类打码,匹配规则在 `otel-collector-config.yaml` 里。这是进存储前的最后一道、也是不能绕过的一道:三条 pipeline(traces/metrics/logs)导出前都过它。验收时要专门造一条带构造手机号和 token 的生成,跑完在 Tempo 和 Loki 里查不到明文才算过。 + +## 密钥与配置 + +MinIO 的 access/secret key 和 Grafana 管理员口令都经 `.env` 的 `${VAR}` 注入,compose 文件和各配置文件里不落明文。`.env` 只在服务器上 `chmod 600` 存在、不入仓,权威副本在 `docs/内网凭据与端点.md`。 + +Tempo 和 Loki 的配置文件里写的是 `${MINIO_ACCESS_KEY}` 这种占位,靠各自启动参数 `-config.expand-env=true` 从容器环境变量展开,所以 compose 给这两件显式注入了环境变量。Collector 不直接连 MinIO,配置里不含密钥。 + +## 出事怎么回滚 + +观测是纯增量的旁路,回滚很干净,主链和业务代码一行不动: + +``` +docker compose down # 停容器,数据卷和 MinIO 桶留着 +docker compose down -v # 连本地数据卷一起删(Prometheus/Grafana/WAL/index) +``` + +MinIO 里的 `obs-tempo` / `obs-loki` 桶要清的话,用 mc 单独删,不会碰到 ragflow 的桶: + +``` +mc rb --force obs/obs-tempo +mc rb --force obs/obs-loki +``` + +停掉整套栈之后,信号源那边推 OTLP 推不上会 best-effort 丢弃,生成主链行为不变。 + +## 后续波要接的 + +第一波之后,按设计 §8 的顺序:给 game-cloud 挂 OTel Java agent 加 actuator,把 Python 便宜档主路的 jsonl 换成真发 OTLP、tier2 的 studio_sink 改发 Collector,打通 Java→worker→本机 Service→回调 的三跳 traceparent,最后落业务埋点、建告警规则、把飞书/钉钉 webhook 接上。`prometheus.yml` 里已经把跨机抓 game-cloud / nacos / rocketmq 的 scrape 目标注释占位,那几件的指标端点就位后取消注释即可。 diff --git a/deploy/infra/observability/docker-compose.yml b/deploy/infra/observability/docker-compose.yml new file mode 100644 index 00000000..720e9558 --- /dev/null +++ b/deploy/infra/observability/docker-compose.yml @@ -0,0 +1,217 @@ +# ============================================================================= +# 全链路可观测性栈 —— OTel Collector + Prometheus + Tempo + Loki + Grafana +# 配置控制面阶段四 · 第一波(纯增量、不碰生产) +# ----------------------------------------------------------------------------- +# 设计权威:docs/agent-specs/2026-07-05-全链路可观测性-OTel三信号全栈-设计.md +# 五件的形状 = 多源汇一口、一口先脱敏再分三库、一板看到底(设计 §4)。 +# Collector 收 OTLP → 脱敏 + 尾部采样 + 批量 → Prometheus(指标)/ Tempo(追踪) +# / Loki(日志);Grafana 统一看板 + 三联下钻。 +# +# 落点 = mini-infra(100.64.0.8,已扩容到 32G / available 24Gi,设计 §10.6)。 +# 同箱的是跨项目生产件(new-api / gitea / nacos / rocketmq / ragflow),所以资源纪律 +# 要比 nacos/rocketmq 那套更严 —— 见下方"资源双上限"。 +# +# ── 部署纪律(全部编进了本文件)────────────────────────────────────────── +# 1) 网络:接既有 infra-shared 网(external、不新建),与 infra-minio / new-api / +# infra-nacos / infra-rmq-namesrv 同网,容器间用 DNS 名互通。观测栈五件之间用 +# compose 服务名(otel-collector / prometheus / tempo / loki / grafana)解析; +# 连既有件用其容器名(infra-minio)。 +# +# 2) 端口避撞(逐个核对过 mini-infra 现有占用:new-api=3000 / gitea=3001 / +# ragflow=9380-9381 / postgres=5433 / minio=9000-9001 / mysql=3306 / redis=6379 / +# nacos=8848 / rocketmq=9876,10911 / nginx=80,443 / infinity=23817,23820): +# Grafana → 3002 (3000/3001 已占,绝不用默认 3000) +# Prometheus → 9090 +# Loki → 3100 +# Tempo → 3200 (query API) +# Collector → 4317(gRPC OTLP)/ 4318(HTTP OTLP)/ 13133(健康探针) +# Collector 自身遥测 8888 与业务指标出口 8889 不对外映射 —— Prometheus 同网 scrape。 +# +# 3) 资源双上限(设计 §5 面三 / §9 硬红线):每件设 mem_limit + cpus 双上限, +# 并把 cpu_shares 压到 512(生产件默认 1024 的一半),CPU 争用时观测让路, +# 绝不让失控的 Prometheus/Tempo 在 CPU/IO 上挤兑 new-api(生成命脉)。 +# 五件 mem_limit 合计约 3.4G,落进 24Gi 余量绰绰有余。 +# +# 4) 存储:Tempo / Loki 的对象存储后端复用既有 infra-minio(不占新端口),各用独立 +# 新桶 obs-tempo / obs-loki(绝不碰 ragflow 现有桶),桶由 minio-init 一次性建。 +# Prometheus 用本地卷、保留 15 天。WAL/index 等本地态落 named volume(避 uid 权限坑)。 +# +# 5) 密钥:S3 凭据与 Grafana 口令经 ${VAR} 从同目录 .env 读(compose 起时自动加载), +# 本文件与各配置文件都不落明文;.env 只在服务器 chmod 600 存在、不入仓,权威副本见 +# docs/内网凭据与端点.md。Tempo/Loki 配置内的 ${VAR} 由各自 -config.expand-env 从 +# 容器环境变量展开(故下面给这两件显式注入 environment)。 +# +# 起停:cd /opt/infra-observability && docker compose up -d;探活见 health-check.sh。 +# 本文件由第一波交付,起容器由主控终审后择窗口做 —— 交付阶段不 up、不 ssh。 +# ============================================================================= +services: + + # ── 一次性:在既有 infra-minio 上建观测专用桶(幂等,建完即退出)──────────── + # 用 mc 客户端连同网 MinIO,--ignore-existing 保证可重复执行不报错。 + # tempo / loki 经 depends_on 等它成功退出后再起,确保桶已就位。 + minio-init: + image: minio/mc:RELEASE.2024-10-29T15-34-59Z + container_name: infra-obs-minio-init + networks: [infra-shared] + environment: + MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} + # $$ 转义:让 $ 原样传给容器内 shell 读环境变量,而非被 compose 提前替换。 + entrypoint: > + /bin/sh -c " + mc alias set obs http://infra-minio:9000 $${MINIO_ACCESS_KEY} $${MINIO_SECRET_KEY} && + mc mb --ignore-existing obs/obs-tempo && + mc mb --ignore-existing obs/obs-loki && + echo 'obs buckets ready: obs-tempo / obs-loki' + " + restart: "no" + + # ── OTel Collector(统一入口)──────────────────────────────────────────── + # 收 OTLP(跨机经 Tailscale 推来)→ 脱敏 + 尾部采样 + 批量 → 分发三库。 + # 脱敏是进库前最后一道安全红线(手机号/token 打码)。健康探针在 13133。 + otel-collector: + image: otel/opentelemetry-collector-contrib:0.116.0 + container_name: infra-otelcol + restart: unless-stopped + networks: [infra-shared] + command: ["--config=/etc/otelcol-contrib/config.yaml"] + volumes: + - ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml:ro + ports: + - "4317:4317" # gRPC OTLP 入口(跨机信号源推送) + - "4318:4318" # HTTP OTLP 入口(跨机信号源推送) + - "13133:13133" # health_check extension(供 health-check.sh 探活) + depends_on: + - tempo + - loki + mem_limit: 512m + cpus: 0.50 + cpu_shares: 512 # 低于生产件默认 1024,CPU 争用时让路 + logging: + driver: json-file + options: { max-size: "10m", max-file: "3" } + + # ── Prometheus(指标存储)────────────────────────────────────────────── + # 本地卷、保留 15 天。scrape 自身 + Collector 出口;跨机抓 game-cloud/nacos/rmq + # 的目标在 prometheus.yml 里注释占位,后续波启用。 + prometheus: + image: prom/prometheus:v3.1.0 + container_name: infra-prometheus + restart: unless-stopped + networks: [infra-shared] + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--storage.tsdb.retention.time=15d" # 保留期 15 天(设计 §7,7-15d 上限) + - "--web.enable-lifecycle" # 允许热加载配置(POST /-/reload) + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus-data:/prometheus + ports: + - "9090:9090" + mem_limit: 1g + cpus: 0.75 + cpu_shares: 512 + logging: + driver: json-file + options: { max-size: "10m", max-file: "3" } + + # ── Tempo(追踪存储,后端 = MinIO/obs-tempo)────────────────────────────── + # 单进程 monolithic。收 Collector 转发的 OTLP(容器内 4317,不对外映射),blocks + # 落 MinIO、WAL 落本地卷。-config.expand-env 让配置里的 ${MINIO_*} 从环境变量展开。 + tempo: + image: grafana/tempo:2.7.1 + container_name: infra-tempo + restart: unless-stopped + networks: [infra-shared] + command: ["-config.file=/etc/tempo.yaml", "-config.expand-env=true"] + environment: + MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} + volumes: + - ./tempo.yaml:/etc/tempo.yaml:ro + - tempo-data:/var/tempo + ports: + - "3200:3200" # query API(Grafana 内网走 tempo:3200,对外便于调试/探活) + depends_on: + minio-init: + condition: service_completed_successfully + mem_limit: 768m + cpus: 0.50 + cpu_shares: 512 + logging: + driver: json-file + options: { max-size: "10m", max-file: "3" } + + # ── Loki(日志存储,后端 = MinIO/obs-loki)──────────────────────────────── + # 单进程 monolithic。只收 WARN/ERROR(在源头控),chunks 落 MinIO、index 落本地卷。 + # OTLP 摄入端点 = :3100/otlp,Collector 用 otlphttp 发到这里。 + loki: + image: grafana/loki:3.3.2 + container_name: infra-loki + restart: unless-stopped + networks: [infra-shared] + command: ["-config.file=/etc/loki/loki-config.yaml", "-config.expand-env=true"] + environment: + MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} + volumes: + - ./loki-config.yaml:/etc/loki/loki-config.yaml:ro + - loki-data:/loki + ports: + - "3100:3100" + depends_on: + minio-init: + condition: service_completed_successfully + mem_limit: 768m + cpus: 0.50 + cpu_shares: 512 + logging: + driver: json-file + options: { max-size: "10m", max-file: "3" } + + # ── Grafana(统一看板 + 告警 + admin 一键进盘)────────────────────────── + # 预配三数据源(Prometheus/Tempo/Loki)+ 三联下钻 + 概览盘。容器内 3000、对外 3002。 + # admin 口令经 .env 注入;首次进盘即见数据源与看板(设计 §10.3 本期做)。 + grafana: + image: grafana/grafana:11.4.0 + container_name: infra-grafana + restart: unless-stopped + networks: [infra-shared] + environment: + GF_SECURITY_ADMIN_USER: ${GF_ADMIN_USER:-admin} + GF_SECURITY_ADMIN_PASSWORD: ${GF_ADMIN_PASSWORD} + GF_USERS_ALLOW_SIGN_UP: "false" + GF_ANALYTICS_REPORTING_ENABLED: "false" + GF_ANALYTICS_CHECK_FOR_UPDATES: "false" + # 三联下钻(trace↔log↔metric)在 Grafana 11 已 GA,靠数据源 provisioning 的 jsonData + # 配跳转即可,无需 feature toggle。 + volumes: + - ./grafana/provisioning:/etc/grafana/provisioning:ro + - ./grafana/dashboards:/var/lib/grafana/dashboards:ro + - grafana-data:/var/lib/grafana + ports: + - "3002:3000" + depends_on: + - prometheus + - tempo + - loki + mem_limit: 384m + cpus: 0.50 + cpu_shares: 512 + logging: + driver: json-file + options: { max-size: "10m", max-file: "3" } + +# ── 既有共享网(external,不新建)────────────────────────────────────────── +networks: + infra-shared: + external: true + +# ── 本地持久卷(docker 托管,避 bind-mount 的 uid 权限坑)──────────────────── +# Prometheus TSDB / Grafana 状态需持久;Tempo WAL / Loki index 是本地热态,块数据在 MinIO。 +volumes: + prometheus-data: + grafana-data: + tempo-data: + loki-data: diff --git a/deploy/infra/observability/grafana/dashboards/observability-overview.json b/deploy/infra/observability/grafana/dashboards/observability-overview.json new file mode 100644 index 00000000..d38d8260 --- /dev/null +++ b/deploy/infra/observability/grafana/dashboards/observability-overview.json @@ -0,0 +1,108 @@ +{ + "annotations": { "list": [] }, + "editable": true, + "graphTooltip": 1, + "time": { "from": "now-6h", "to": "now" }, + "refresh": "30s", + "schemaVersion": 39, + "tags": ["huijing", "observability"], + "title": "绘境AI · 可观测总览", + "uid": "huijing-obs-overview", + "templating": { "list": [] }, + "panels": [ + { + "type": "row", + "title": "观测栈自监控(Collector · 起栈即有数据)", + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, + "collapsed": false, + "panels": [] + }, + { + "type": "timeseries", + "title": "Collector 接收信号速率(spans/logs/metrics per s)", + "description": "OTLP 入口吞吐。三条生成/服务路推来的信号量。", + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 1 }, + "fieldConfig": { "defaults": { "unit": "reqps" }, "overrides": [] }, + "targets": [ + { "refId": "A", "expr": "sum(rate(otelcol_receiver_accepted_spans_total[5m]))", "legendFormat": "spans" }, + { "refId": "B", "expr": "sum(rate(otelcol_receiver_accepted_log_records_total[5m]))", "legendFormat": "logs" }, + { "refId": "C", "expr": "sum(rate(otelcol_receiver_accepted_metric_points_total[5m]))", "legendFormat": "metrics" } + ] + }, + { + "type": "timeseries", + "title": "Collector 导出成功/失败 + 队列(丢弃即后端不可达)", + "description": "验收第 6 条要看的 queue/drop 指标。send_failed 持续 > 0 说明后端抖动或不可达。", + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 1 }, + "fieldConfig": { "defaults": {}, "overrides": [] }, + "targets": [ + { "refId": "A", "expr": "sum(rate(otelcol_exporter_sent_spans_total[5m]))", "legendFormat": "sent spans/s" }, + { "refId": "B", "expr": "sum(rate(otelcol_exporter_send_failed_spans_total[5m]))", "legendFormat": "failed spans/s" }, + { "refId": "C", "expr": "sum(otelcol_exporter_queue_size)", "legendFormat": "queue size" } + ] + }, + { + "type": "stat", + "title": "被监控目标存活(Prometheus up)", + "description": "每个 scrape target 的 up/down。后续波接入 game-cloud/nacos/rmq 后这里会长出更多目标。", + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "gridPos": { "h": 6, "w": 12, "x": 0, "y": 9 }, + "fieldConfig": { + "defaults": { + "mappings": [ + { "type": "value", "options": { "0": { "text": "DOWN", "color": "red" }, "1": { "text": "UP", "color": "green" } } } + ] + }, + "overrides": [] + }, + "options": { "colorMode": "background", "textMode": "value_and_name" }, + "targets": [ + { "refId": "A", "expr": "up", "legendFormat": "{{job}}", "instant": true } + ] + }, + { + "type": "row", + "title": "业务指标(占位 · 后续波埋点落地后生效)", + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 15 }, + "collapsed": false, + "panels": [] + }, + { + "type": "timeseries", + "title": "生成成功率(按状态)", + "description": "占位:待 game-cloud 在任务终态回填处埋 huijing_gen_task_total{status}(设计 §6)。埋点前无数据。", + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "gridPos": { "h": 8, "w": 8, "x": 0, "y": 16 }, + "fieldConfig": { "defaults": { "unit": "percentunit" }, "overrides": [] }, + "targets": [ + { "refId": "A", "expr": "sum(rate(huijing_gen_task_total{status=\"success\"}[30m])) / clamp_min(sum(rate(huijing_gen_task_total[30m])), 1)", "legendFormat": "成功率" } + ] + }, + { + "type": "timeseries", + "title": "生成耗时 P50/P95", + "description": "占位:待 worker 导出 huijing_gen_duration_seconds histogram(设计 §6)。埋点前无数据。", + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "gridPos": { "h": 8, "w": 8, "x": 8, "y": 16 }, + "fieldConfig": { "defaults": { "unit": "s" }, "overrides": [] }, + "targets": [ + { "refId": "A", "expr": "histogram_quantile(0.50, sum(rate(huijing_gen_duration_seconds_bucket[5m])) by (le))", "legendFormat": "P50" }, + { "refId": "B", "expr": "histogram_quantile(0.95, sum(rate(huijing_gen_duration_seconds_bucket[5m])) by (le))", "legendFormat": "P95" } + ] + }, + { + "type": "timeseries", + "title": "队列深度 / 九门失败", + "description": "占位:待埋 huijing_gen_queue_depth(gauge)与 huijing_gen_gate_fail_total{gate}(设计 §6)。埋点前无数据。", + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "gridPos": { "h": 8, "w": 8, "x": 16, "y": 16 }, + "fieldConfig": { "defaults": {}, "overrides": [] }, + "targets": [ + { "refId": "A", "expr": "max(huijing_gen_queue_depth)", "legendFormat": "队列深度" }, + { "refId": "B", "expr": "sum(rate(huijing_gen_gate_fail_total[30m])) by (gate)", "legendFormat": "门失败 {{gate}}" } + ] + } + ] +} diff --git a/deploy/infra/observability/grafana/provisioning/dashboards/dashboards.yaml b/deploy/infra/observability/grafana/provisioning/dashboards/dashboards.yaml new file mode 100644 index 00000000..8334d72c --- /dev/null +++ b/deploy/infra/observability/grafana/provisioning/dashboards/dashboards.yaml @@ -0,0 +1,17 @@ +# ============================================================================= +# Grafana 看板 provisioning —— 从文件加载看板(/var/lib/grafana/dashboards) +# 第一波先放一块概览盘;后续波随业务埋点落地补更多盘,放同目录即自动加载。 +# ============================================================================= +apiVersion: 1 + +providers: + - name: huijing-observability + orgId: 1 + folder: "绘境AI 可观测" + type: file + disableDeletion: false + updateIntervalSeconds: 30 + allowUiUpdates: true + options: + path: /var/lib/grafana/dashboards + foldersFromFilesStructure: false diff --git a/deploy/infra/observability/grafana/provisioning/datasources/datasources.yaml b/deploy/infra/observability/grafana/provisioning/datasources/datasources.yaml new file mode 100644 index 00000000..6cd3008b --- /dev/null +++ b/deploy/infra/observability/grafana/provisioning/datasources/datasources.yaml @@ -0,0 +1,67 @@ +# ============================================================================= +# Grafana 数据源 provisioning —— 三数据源 + 指标↔追踪↔日志三联下钻 +# 设计 §5 面三/§10.3:预配 Prometheus/Tempo/Loki,开箱三联下钻,admin 一键进盘。 +# URL 用 compose 服务名(同 infra-shared 网内解析),内网无鉴权。 +# 注:provisioning 里 Grafana 会展开 ${...} 环境变量,故 Grafana 内部变量(如 +# ${__value.raw} / ${__trace.traceId})一律写成 $${...} 转义,避免被当环境变量吞掉。 +# ============================================================================= +apiVersion: 1 + +datasources: + # ── Prometheus:默认数据源 ────────────────────────────────────────────── + - name: Prometheus + uid: prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + jsonData: + httpMethod: POST + # exemplar 上的 trace_id 直接跳 Tempo(指标 → 追踪 下钻) + exemplarTraceIdDestinations: + - name: trace_id + datasourceUid: tempo + + # ── Tempo:追踪。配 追踪→日志 / 追踪→指标 / 服务图 三向下钻 ────────────────── + - name: Tempo + uid: tempo + type: tempo + access: proxy + url: http://tempo:3200 + jsonData: + # 追踪 → 日志:带 traceID 跳到 Loki 查同一条链路的日志 + tracesToLogsV2: + datasourceUid: loki + spanStartTimeShift: "-1h" + spanEndTimeShift: "1h" + filterByTraceID: true + filterBySpanID: false + # 追踪 → 指标:跳到 Prometheus 看相关指标 + tracesToMetrics: + datasourceUid: prometheus + spanStartTimeShift: "-1h" + spanEndTimeShift: "1h" + # 服务拓扑图与节点图(基于 span 关系) + serviceMap: + datasourceUid: prometheus + nodeGraph: + enabled: true + traceQuery: + timeShiftEnabled: true + spanStartTimeShift: "-1h" + spanEndTimeShift: "1h" + + # ── Loki:日志。配 日志→追踪 下钻(从日志里的 trace_id 跳 Tempo)────────────── + - name: Loki + uid: loki + type: loki + access: proxy + url: http://loki:3100 + jsonData: + # derivedFields:从日志行/结构化元数据里提取 trace_id,一键跳到 Tempo 对应 trace。 + # 后续波真接入日志后,按实际 trace_id 落地形态(structured metadata / 正文 JSON)微调正则。 + derivedFields: + - name: trace_id + matcherRegex: '"trace_id":"(\w+)"' + url: "$${__value.raw}" + datasourceUid: tempo diff --git a/deploy/infra/observability/health-check.sh b/deploy/infra/observability/health-check.sh new file mode 100755 index 00000000..be534b3c --- /dev/null +++ b/deploy/infra/observability/health-check.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# ============================================================================= +# 观测栈健康探针 —— 五件就绪判定(配置控制面阶段四 · 第一波) +# ----------------------------------------------------------------------------- +# 一次探完 Collector / Prometheus / Tempo / Loki / Grafana 五个对外端点, +# 全绿(全部 HTTP 200)整体 PASS 退 0;任一非 200 / 超时则 FAIL 退 1。 +# +# 两处内网纪律(与 nacos/rocketmq 探针一致): +# - `--noproxy '*'`:本机系统代理是 fake-ip(198.18.x);内网直连(Tailscale 100.64.x) +# 必须绕过它,否则连接被代理拦下返 000。这是内网外呼铁律。 +# - curl -w 恒输出 http_code(失败为 000);仅在 curl 二进制本身返空时兜底 000, +# 不用 `|| echo 000`(会与 -w 的 000 叠成 "000000")。 +# +# 各件探的就绪端点: +# Collector :13133 / health_check extension(就绪返 200) +# Prometheus :9090 /-/healthy 自身健康 +# Tempo :3200 /ready 就绪(未就绪期返 503,就绪后 200) +# Loki :3100 /ready 就绪(未就绪期返 503,就绪后 200) +# Grafana :3002 /api/health 自身健康(DB/插件就绪返 200) +# +# 用法:HOST=100.64.0.8 bash deploy/infra/observability/health-check.sh +# ============================================================================= +set -uo pipefail + +HOST="${HOST:-100.64.0.8}" +MAX_TIME="${MAX_TIME:-8}" + +# 探针清单:名称|URL。逐个探,记录结果。 +CHECKS=( + "Collector|http://${HOST}:13133/" + "Prometheus|http://${HOST}:9090/-/healthy" + "Tempo|http://${HOST}:3200/ready" + "Loki|http://${HOST}:3100/ready" + "Grafana|http://${HOST}:3002/api/health" +) + +fail=0 +for item in "${CHECKS[@]}"; do + name="${item%%|*}" + url="${item#*|}" + code="$(curl -s -o /dev/null -w '%{http_code}' --noproxy '*' --max-time "${MAX_TIME}" "${url}" 2>/dev/null)" + code="${code:-000}" + if [[ "$code" == "200" ]]; then + echo "PASS ${name} 200 @ ${url}" + else + echo "FAIL ${name}=${code} @ ${url}" + fail=1 + fi +done + +echo "---" +if [[ "$fail" == "0" ]]; then + echo "PASS 观测栈五件全绿 @ ${HOST}" + exit 0 +fi +echo "FAIL 观测栈存在未就绪组件 @ ${HOST}(见上)" +exit 1 diff --git a/deploy/infra/observability/loki-config.yaml b/deploy/infra/observability/loki-config.yaml new file mode 100644 index 00000000..23530ebe --- /dev/null +++ b/deploy/infra/observability/loki-config.yaml @@ -0,0 +1,75 @@ +# ============================================================================= +# Loki 配置 —— 日志存储 · 单进程 monolithic · 后端 = MinIO(obs-loki 桶) +# 设计 §4/§6:只收 WARN/ERROR(在源头控级别),chunks 落 MinIO、index 落本地卷。 +# Collector 用 otlphttp 发到 :3100/otlp(Loki 3.x 原生 OTLP 摄入端点)。 +# ${MINIO_*} 由启动参数 -config.expand-env=true 从容器环境变量展开(compose 注入)。 +# ============================================================================= + +# 内网单机、不启多租户鉴权(鉴权由所在网络与 nginx 边界兜)。 +auth_enabled: false + +server: + http_listen_port: 3100 + grpc_listen_port: 9096 # 与 Tempo 的 9095 错开(虽不同容器不冲突,显式区分省心) + log_level: info + +common: + instance_addr: 127.0.0.1 + path_prefix: /loki # 本地态根目录(落 named volume loki-data) + replication_factor: 1 + ring: + kvstore: + store: inmemory # 单实例,环状态放内存即可 + +schema_config: + configs: + - from: 2024-01-01 + store: tsdb + object_store: s3 + schema: v13 # 当前稳定 schema + index: + prefix: index_ + period: 24h + +storage_config: + tsdb_shipper: + active_index_directory: /loki/index + cache_location: /loki/index_cache + # S3(MinIO)分字段配置:endpoint 不带 scheme,insecure=true 走 HTTP, + # s3forcepathstyle=true 是 MinIO 必需(AWS SDK 默认 virtual-host 风格,MinIO 用 path 风格)。 + aws: + endpoint: infra-minio:9000 + bucketnames: obs-loki # 观测专用桶,绝不碰 ragflow 现有桶 + access_key_id: ${MINIO_ACCESS_KEY} + secret_access_key: ${MINIO_SECRET_KEY} + s3forcepathstyle: true + insecure: true + +compactor: + working_directory: /loki/retention + compaction_interval: 10m + retention_enabled: true # 开启保留期回收(否则日志只增不删) + retention_delete_delay: 2h + retention_delete_worker_count: 100 + delete_request_store: s3 # 删除请求也落 S3(retention_enabled 时必配) + +limits_config: + retention_period: 360h # 日志保留 15 天(与 Prometheus 保留期看齐) + reject_old_samples: true + reject_old_samples_max_age: 168h + allow_structured_metadata: true # OTLP 摄入把 trace_id 等作为 structured metadata 存 + volume_enabled: true + ingestion_rate_mb: 8 # 只收 WARN/ERROR,8MB/s 足够、且给失控日志封顶 + ingestion_burst_size_mb: 16 + +ruler: + storage: + type: local + local: + directory: /loki/rules + rule_path: /loki/rules-temp + enable_api: true + +# 关掉匿名用量上报(内网、不外呼)。 +analytics: + reporting_enabled: false diff --git a/deploy/infra/observability/otel-collector-config.yaml b/deploy/infra/observability/otel-collector-config.yaml new file mode 100644 index 00000000..aaf330db --- /dev/null +++ b/deploy/infra/observability/otel-collector-config.yaml @@ -0,0 +1,148 @@ +# ============================================================================= +# OTel Collector 配置 —— 统一入口:收 OTLP → 脱敏 + 尾采样 + 批量 → 分发三库 +# 设计 §4/§5 面三/§8:脱敏是进 Tempo/Loki 前的最后一道安全红线;尾采样错误+慢全采、 +# 正常抽样;后端不可达时本地缓冲丢弃而非阻塞主链。 +# +# 组件全部取自 opentelemetry-collector-contrib(redaction / tail_sampling 是 contrib 件, +# core 镜像没有,故 compose 用 -contrib 镜像)。 +# ============================================================================= + +extensions: + # 健康探针:health-check.sh 探 :13133 判就绪。 + health_check: + endpoint: 0.0.0.0:13133 + +receivers: + # OTLP 唯一入口。跨机信号源(game-cloud / 两条生成线,都在别处)经 Tailscale 推到 + # mini-infra 的 4317(gRPC)/4318(HTTP)。监听 0.0.0.0 才能收到容器外/跨机流量。 + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + # ── 内存护栏:软红线,超阈值即拒收/丢弃,防 Collector 自身 OOM 拖累同箱 new-api ── + # 达 limit 时开始拒绝新数据(back-pressure 上游 best-effort 上报会丢而非把 Collector 撑爆)。 + memory_limiter: + check_interval: 1s + limit_percentage: 75 + spike_limit_percentage: 15 + + # ── 尾部采样:一条 trace 的所有 span 齐了再决策(设计 §7)────────────────── + # 多策略取"或":错误全采 + 慢(>2s)全采 + 其余按 10% 概率采。既盯住所有异常/慢链路, + # 又把正常链路的存储量压到一成。decision_wait 是等齐 span 的窗口。 + tail_sampling: + decision_wait: 10s + num_traces: 20000 # 决策期内最多缓存的 trace 数(内存换准确) + expected_new_traces_per_sec: 200 + policies: + - name: errors-always # 错误链路:全采 + type: status_code + status_code: + status_codes: [ERROR] + - name: slow-always # 慢链路(>2000ms):全采 + type: latency + latency: + threshold_ms: 2000 + - name: sample-normal # 其余正常链路:10% 抽样 + type: probabilistic + probabilistic: + sampling_percentage: 10 + + # ── 脱敏:进库前最后一道安全红线(设计 §4/§8 第 7 条)──────────────────────── + # 手机号 / token / 密钥 在写入 Tempo(span 属性)与 Loki(日志正文)前打码。 + # allow_all_keys=true:不删属性(保留可观测性),只把命中键名或命中值的内容 mask。 + # - blocked_key_patterns:键名像密钥的,整值打码(token/secret/password/api_key/…)。 + # - blocked_values:值本身匹配敏感格式的打码(中国大陆手机号 / 常见密钥前缀 / JWT)。 + # redact_all_types=true:非字符串(如 int 型手机号)也按字符串形态检查。 + # summary=info:只记打码计数、不记键名,避免摘要本身泄信息。 + redaction: + allow_all_keys: true + redact_all_types: true + blocked_key_patterns: + - "(?i).*(token|secret|password|passwd|api[_-]?key|access[_-]?key|authorization|cookie|credential).*" + blocked_values: + - "1[3-9]\\d{9}" # 中国大陆手机号 + - "(?i)(sk|pk|api|key|token|bearer|ghp|xox)[-_][A-Za-z0-9]{16,}" # 常见密钥/令牌前缀 + - "eyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{4,}" # JWT + summary: info + + # ── 批量:降导出频次、提吞吐。放在 pipeline 末端、导出前。── + batch: + timeout: 5s + send_batch_size: 512 + send_batch_max_size: 1024 + +exporters: + # ── 追踪 → Tempo(容器内 OTLP gRPC,同网明文 insecure)────────────────────── + # sending_queue = 本地内存缓冲;retry_on_failure = 后端抖动时重试。队列满即丢弃 + # (不阻塞接收)——对应设计 §8"Collector 不可达/后端不可达时本地丢弃而非阻塞"。 + otlp/tempo: + endpoint: tempo:4317 + tls: + insecure: true + sending_queue: + enabled: true + queue_size: 5000 + retry_on_failure: + enabled: true + initial_interval: 5s + max_interval: 30s + max_elapsed_time: 300s + + # ── 指标 → Prometheus 出口(暴露 /metrics 在 8889,由 Prometheus 主动 scrape)── + # namespace=huijing:业务指标统一前缀 huijing_*(如 huijing_gen_task_total)。 + # resource_to_telemetry_conversion:把 resource 属性(service.name 等)落成指标标签。 + prometheus: + endpoint: 0.0.0.0:8889 + namespace: huijing + resource_to_telemetry_conversion: + enabled: true + enable_open_metrics: true + + # ── 日志 → Loki 的 OTLP 摄入端点(:3100/otlp,exporter 自动补 /v1/logs)────── + otlphttp/loki: + endpoint: http://loki:3100/otlp + sending_queue: + enabled: true + queue_size: 5000 + retry_on_failure: + enabled: true + initial_interval: 5s + max_interval: 30s + max_elapsed_time: 300s + +service: + extensions: [health_check] + telemetry: + # Collector 自身遥测暴露在 0.0.0.0:8888,供 Prometheus scrape(队列深度/丢弃数等, + # 用于验收第 6 条"agent 的 queue/drop 指标可见")。默认只在 localhost,须显式放开。 + # 下面是 0.116 的 readers 写法;若该版本起容器时报 telemetry 配置错,退回老式(仍受支持): + # metrics: + # level: detailed + # address: 0.0.0.0:8888 + metrics: + readers: + - pull: + exporter: + prometheus: + host: 0.0.0.0 + port: 8888 + pipelines: + # 追踪:限流 → 尾采样 → 脱敏 → 批量 → Tempo。脱敏在导出前、不可绕过。 + traces: + receivers: [otlp] + processors: [memory_limiter, tail_sampling, redaction, batch] + exporters: [otlp/tempo] + # 指标:限流 → 脱敏(指标标签也可能带敏感值)→ 批量 → Prometheus 出口。 + metrics: + receivers: [otlp] + processors: [memory_limiter, redaction, batch] + exporters: [prometheus] + # 日志:限流 → 脱敏(日志正文里有报错原文)→ 批量 → Loki。 + logs: + receivers: [otlp] + processors: [memory_limiter, redaction, batch] + exporters: [otlphttp/loki] diff --git a/deploy/infra/observability/prometheus.yml b/deploy/infra/observability/prometheus.yml new file mode 100644 index 00000000..e9aadf9f --- /dev/null +++ b/deploy/infra/observability/prometheus.yml @@ -0,0 +1,47 @@ +# ============================================================================= +# Prometheus 配置 —— 指标存储 · 自监控 + Collector 出口抓取 +# 设计 §5 面三:本波先自监控(scrape 自身 + Collector),跨机抓 game-cloud/nacos/rmq +# 的目标注释占位、后续波启用。保留期由 compose 的 --storage.tsdb.retention.time=15d 定。 +# ============================================================================= +global: + scrape_interval: 30s # 30s 一轮,低负载够用、省资源 + evaluation_interval: 30s + external_labels: + cluster: huijing-mvp + site: mini-infra + +scrape_configs: + # ── 自监控:Prometheus 自身 ── + - job_name: prometheus + static_configs: + - targets: ["localhost:9090"] + + # ── Collector 导出的业务指标(namespace=huijing,如 huijing_gen_task_total)── + # Collector 的 prometheus exporter 暴露在 8889;同网用服务名解析,不走对外端口。 + - job_name: otel-collector + static_configs: + - targets: ["otel-collector:8889"] + + # ── Collector 自身遥测(队列深度/丢弃数等,otelcol_* 指标,验收第 6 条要看)── + - job_name: otel-collector-internal + static_configs: + - targets: ["otel-collector:8888"] + + # ========================================================================= + # 预留 · 后续波启用(跨机经 Tailscale 100.64.0.x)。启用前置条件见设计 §5 面一: + # game-cloud 要先挂 actuator/micrometer 让 /actuator/prometheus 端点成立; + # nacos/rocketmq 的 micrometer 端点同样待各自 exporter 就位。端口按各服务实际暴露核对。 + # ========================================================================= + # - job_name: game-cloud + # metrics_path: /actuator/prometheus + # static_configs: + # - targets: ["100.64.0.X:PORT"] # game-cloud 单体 actuator 端口(待定) + # + # - job_name: nacos + # metrics_path: /nacos/actuator/prometheus + # static_configs: + # - targets: ["100.64.0.8:8848"] + # + # - job_name: rocketmq + # static_configs: + # - targets: ["100.64.0.8:PORT"] # rocketmq-exporter 端口(待部署) diff --git a/deploy/infra/observability/tempo.yaml b/deploy/infra/observability/tempo.yaml new file mode 100644 index 00000000..434f7eb9 --- /dev/null +++ b/deploy/infra/observability/tempo.yaml @@ -0,0 +1,55 @@ +# ============================================================================= +# Tempo 配置 —— 追踪存储 · 单进程 monolithic · 后端 = MinIO(obs-tempo 桶) +# 设计 §7:选 Tempo(同 Grafana 体系、三联下钻、对象存储内存友好)。 +# 收 Collector 转发的 OTLP;blocks 落 MinIO、WAL 落本地卷 /var/tempo。 +# ${MINIO_*} 由启动参数 -config.expand-env=true 从容器环境变量展开(compose 注入)。 +# ============================================================================= + +# 允许 Grafana 经 HTTP 流式拉取查询结果(TraceQL 搜索用)。 +stream_over_http_enabled: true + +server: + http_listen_port: 3200 # 对外 query API(compose 映射 3200,Grafana 内网走 tempo:3200) + grpc_listen_port: 9095 + +distributor: + receivers: + # Collector 用 otlp/tempo exporter 发到这里(容器内 4317,不对外映射,不与 + # Collector 对外的 4317 冲突 —— 不同容器各自独立监听)。 + otlp: + protocols: + grpc: + endpoint: "0.0.0.0:4317" + http: + endpoint: "0.0.0.0:4318" + +ingester: + # 尽快切块落对象存储,降低内存里活跃 trace 的驻留(mini-infra 资源纪律)。 + max_block_duration: 5m + +compactor: + compaction: + block_retention: 168h # 追踪保留 7 天(设计 §4:Tempo 追踪 · MinIO bucket) + +storage: + trace: + backend: s3 + wal: + path: /var/tempo/wal # 本地 WAL(落 named volume tempo-data) + s3: + endpoint: infra-minio:9000 # 同网 DNS 名,复用既有 MinIO + bucket: obs-tempo # 观测专用桶,绝不碰 ragflow 现有桶 + access_key: ${MINIO_ACCESS_KEY} + secret_key: ${MINIO_SECRET_KEY} + insecure: true # MinIO 同网走 HTTP、非 TLS(endpoint 为 IP:port, + # minio-go 自动用 path-style,无需额外 forcepathstyle 字段) + +# 关掉匿名用量上报(内网、不外呼)。 +usage_report: + reporting_enabled: false + +# 单租户默认摄入上限:MVP 单机、留足头。 +overrides: + defaults: + ingestion: + max_traces_per_user: 50000