Higress AI 网关可观测性体系:从 0 到 1 搭建 Promtail + Loki + Grafana 全链路监控
AI 网关上线后,如何监控它的运行状态?如何按用户、按模型、按路由统计 Token 消耗?如何在数据丢失时快速发现并回灌?本文分享了一套完整的生产级可观测性体系搭建过程,涵盖 Envoy 结构化日志、Promtail 两阶段解析、Loki 存储与查询、Grafana 多维看板,以及 Prometheus 指标采集的全链路设计。
一、背景与目标
1.1 为什么 AI 网关需要专门的可观测性
传统 API 网关的可观测性主要关注 QPS、延迟、错误率。但 AI 网关多了一个关键维度——Token 消耗。
Token 是 LLM 调用的计费单位,也是成本管控的核心指标。企业需要回答以下问题:
| 今天一共消耗了多少 Token? | 成本核算 |
| 哪个团队消耗最多? | 内部计费 |
| 哪个模型用得最多? | 模型选型优化 |
| 某个用户的 Token 趋势是否正常? | 异常检测 |
| 首 Token 延迟是多少? | 用户体验监控 |
这些问题,传统的 Prometheus + Grafana 方案无法直接回答——因为 Token 信息在 LLM 的响应体中,不在 HTTP 指标里。
1.2 我们的目标
┌─────────────────────────────────────────────────────────────┐
│ 可观测性体系目标 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 网关运行状态监控 │
│ – QPS、延迟、错误率、TCP 连接数 │
│ – 容器 CPU、内存使用率 │
│ │
│ 2. AI 业务指标监控 │
│ – 按模型统计 Token 消耗(Input / Output) │
│ – 按用户/团队统计 Token 消耗 │
│ – 按路由 + 用户 + 模型的细粒度统计 │
│ – 首 Token 延迟、请求处理耗时 │
│ │
│ 3. 日志采集与查询 │
│ – 全量审计日志结构化存储 │
│ – 支持按用户、模型、路由的日志查询 │
│ – 日志保留 180 天 │
│ │
│ 4. 异常检测与自愈 │
│ – 采集链路断流告警 │
│ – positions offset 异常自动修复 │
│ │
└─────────────────────────────────────────────────────────────┘
1.3 技术选型
| Envoy Access Log | 日志源 | Higress 基于 Envoy,原生支持结构化 JSON 日志 |
| ai-statistics 插件 | Token 采集 | Higress 官方插件,从 LLM 响应中提取 Token 统计 |
| Promtail | 日志采集 | Loki 官方日志采集器,支持 pipeline 解析 |
| Loki | 日志存储 | 轻量级日志聚合,与 Grafana 深度集成 |
| Prometheus | 指标采集 | 采集 Envoy 暴露的运行时指标 |
| cAdvisor | 容器指标 | 采集容器 CPU、内存、网络指标 |
| Grafana | 可视化 | 统一看板,同时展示 Prometheus 指标和 Loki 日志 |
二、整体架构
┌─────────────────────────────────────────────────────────────────────┐
│ Higress AI Gateway │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ key-auth │───▶│ ai-proxy │───▶│ ai-statistics │ │
│ │ (认证) │ │ (协议转换) │ │ (Token 统计采集) │ │
│ └──────────┘ └──────────────┘ └───────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ Envoy Access Log │ │
│ │ (JSON 结构化日志) │ │
│ │ 含 consumer, ai_log, │ │
│ │ route_name 等字段 │ │
│ └────────────┬─────────────┘ │
└─────────────────────────────────────────────────┼───────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 可观测性数据链路 │
│ │
│ ┌────────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Promtail │────▶│ Loki │◀────│ Grafana │ │Prometheus│ │
│ │ (日志采集) │ │ (日志存储)│ │ (可视化) │ │(指标采集) │ │
│ └────────────┘ └──────────┘ └──────────┘ └────┬─────┘ │
│ │ │
│ ┌────────────┐ ┌──────────┐ │ │
│ │ cAdvisor │────▶│Prometheus│◀───────────────────────────┘ │
│ │ (容器指标) │ │ │ │
│ └────────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────────┘
两条数据链路:
| 日志链路 | Envoy Access Log → Promtail → Loki → Grafana | 日志文件 tail | Loki 文件系统存储 | LogQL 查询 |
| 指标链路 | Envoy metrics + cAdvisor → Prometheus → Grafana | HTTP pull | Prometheus TSDB | PromQL 查询 |
三、Envoy 结构化日志 — 数据源头
3.1 日志格式设计
Higress 基于 Envoy,通过 higress-config ConfigMap 配置 access log 格式。我们设计了一套包含所有必要字段的 JSON 格式:
# higress-config ConfigMap
mesh: |–
accessLogEncoding: TEXT
accessLogFile: /dev/stdout
accessLogFormat: |
{
"consumer": "%FILTER_STATE(wasm.consumer:PLAIN)%",
"api_key": "%REQ(X-CONSUMER-NAME)%",
"ai_log": "%FILTER_STATE(wasm.ai_log:PLAIN)%",
"authority": "%REQ(X-ENVOY-ORIGINAL-HOST?:AUTHORITY)%",
"bytes_received": "%BYTES_RECEIVED%",
"bytes_sent": "%BYTES_SENT%",
"downstream_local_address": "%DOWNSTREAM_LOCAL_ADDRESS%",
"downstream_remote_address": "%DOWNSTREAM_REMOTE_ADDRESS%",
"duration": "%DURATION%",
"method": "%REQ(:METHOD)%",
"path": "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%",
"protocol": "%PROTOCOL%",
"request_id": "%REQ(X-REQUEST-ID)%",
"response_code": "%RESPONSE_CODE%",
"response_flags": "%RESPONSE_FLAGS%",
"route_name": "%ROUTE_NAME%",
"start_time": "%START_TIME%",
"trace_id": "%REQ(X-B3-TRACEID)%",
"upstream_cluster": "%UPSTREAM_CLUSTER%",
"upstream_host": "%UPSTREAM_HOST%",
"user_agent": "%REQ(USER-AGENT)%",
"x_forwarded_for": "%REQ(X-FORWARDED-FOR)%",
"response_code_details": "%RESPONSE_CODE_DETAILS%"
}
3.2 关键字段说明
| consumer | FILTER_STATE(wasm.consumer) | ai-statistics 插件写入的消费者名称 |
| api_key | REQ(X-CONSUMER-NAME) | Lua 脚本注入的用户名 Header |
| ai_log | FILTER_STATE(wasm.ai_log) | ai-statistics 插件写入的 Token 统计 JSON |
| route_name | ROUTE_NAME | Envoy 匹配到的路由名称 |
| start_time | START_TIME | 请求开始时间(RFC3339 格式) |
| response_code | RESPONSE_CODE | HTTP 响应码 |
| duration | DURATION | 请求处理耗时(毫秒) |
| response_flags | RESPONSE_FLAGS | Envoy 响应标志(如 UF=上游连接失败) |
3.3 ai_log 字段结构
ai_log 是一个嵌套 JSON 字符串,由 ai-statistics 插件写入:
{
"model": "qwen3.8-max",
"input_token": 156,
"output_token": 892,
"total_token": 1048,
"input_token_cost": 0,
"output_token_cost": 0,
"total_token_cost": 0,
"first_token_latency": 230,
"request_process_duration": 1580
}
| model | 实际使用的模型名 |
| input_token | 输入 Token 数 |
| output_token | 输出 Token 数 |
| total_token | 总 Token 数 |
| first_token_latency | 首 Token 延迟(毫秒) |
| request_process_duration | 请求处理总耗时(毫秒) |
3.4 日志输出示例
一条完整的 Envoy access log:
{
"consumer": "user-a",
"api_key": "user-a",
"ai_log": "{\\"model\\":\\"qwen3.8-max\\",\\"input_token\\":156,\\"output_token\\":892,\\"total_token\\":1048,\\"first_token_latency\\":230,\\"request_process_duration\\":1580}",
"authority": "api.example.com",
"bytes_received": 2048,
"bytes_sent": 15360,
"duration": 1620,
"method": "POST",
"path": "/v1/chat/completions",
"response_code": "200",
"response_flags": "-",
"route_name": "ai-route-aliyun.internal",
"start_time": "2026-09-01T10:30:45.123Z",
"upstream_host": "10.0.1.5:443"
}
四、ai-statistics 插件 — Token 采集
4.1 插件配置
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: ai–statistics–2.0.1
namespace: higress–system
spec:
priority: 900
defaultConfig:
# 定义要采集的属性
attributes:
# 从请求 Header 读取消费者名称
– key: consumer
value_source: request_header
value: x–mse–consumer
apply_to_log: true # 写入 access log
as_separate_log_field: true # 作为独立字段(而非嵌套在 ai_log 中)
# 以下属性从 LLM 响应体中提取
– key: model
value: "$response.body.choices.@first.model|$response.body.model"
apply_to_log: true
as_separate_log_field: true
– key: input_token
value: "$response.body.usage.prompt_tokens"
apply_to_log: true
as_separate_log_field: true
– key: output_token
value: "$response.body.usage.completion_tokens"
apply_to_log: true
as_separate_log_field: true
– key: total_token
value: "$response.body.usage.total_tokens"
apply_to_log: true
as_separate_log_field: true
– key: first_token_latency
value: "$first_token_latency"
apply_to_log: true
as_separate_log_field: true
– key: request_process_duration
value: "$request_process_duration"
apply_to_log: true
as_separate_log_field: true
4.2 工作原理
LLM 响应返回
│
▼ ai-statistics 插件 (priority: 900)
│
│ 1. 解析响应体 JSON
│ 2. 提取 usage.prompt_tokens → input_token
│ 3. 提取 usage.completion_tokens → output_token
│ 4. 提取 usage.total_tokens → total_token
│ 5. 读取首 Token 时间戳,计算 first_token_latency
│ 6. 计算 request_process_duration
│ 7. 从 x-mse-consumer Header 读取消费者名称
│ 8. 将所有属性写入 FILTER_STATE
│
▼ Envoy Access Log
│
│ ai_log 字段 = 上述属性的 JSON 序列化
│ consumer 字段 = 消费者名称(独立字段)
4.3 关键设计决策
| as_separate_log_field | true | 每个属性作为独立字段,便于 Promtail 分别提取为 label |
| consumer 来源 | request_header: x-mse-consumer | 从 Lua 脚本注入的 Header 读取,而非从响应体提取 |
| model 提取路径 | 双路径 | 分隔 | 兼容不同 Provider 的响应格式(choices.model vs body.model) |
五、Promtail — 日志采集与解析
5.1 采集架构
Envoy access.log(容器内 /dev/stdout)
│
▼ 日志文件挂载
/var/log/higress/proxy/access.log*(宿主机)
│
▼ Promtail 容器挂载
/var/log/higress/proxy/access.log*(Promtail 容器内)
│
▼ Pipeline 解析
Loki(结构化日志 + labels)
5.2 完整配置
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions/positions.yaml
clients:
– url: http://loki:3100/loki/api/v1/push
scrape_configs:
# ===== 审计日志(核心)=====
– job_name: higress–audit–logs
static_configs:
– targets:
– localhost
labels:
job: higress–audit–logs
__path__: /var/log/higress/proxy/access.log*
pipeline_stages:
# 第 1 次解析:外层 Envoy JSON
– json:
expressions:
start_time: start_time
ai_log: ai_log
route: route_name
consumer: consumer
api_key: api_key
# 使用 start_time 替换日志时间戳
– timestamp:
source: start_time
format: RFC3339Nano
# 第 2 次解析:内层 ai_log JSON 字符串
– json:
source: ai_log
expressions:
model: model
# 提取为 Loki labels
– labels:
model:
route:
consumer:
api_key:
# ===== 网关运行日志 =====
– job_name: higress–gateway–logs
static_configs:
– targets:
– localhost
labels:
job: higress–gateway–logs
__path__: /var/log/higress/gateway.log
# ===== 控制面日志 =====
– job_name: higress–pilot–logs
static_configs:
– targets:
– localhost
labels:
job: higress–pilot–logs
__path__: /var/log/higress/pilot.log
5.3 Pipeline 详解
Pipeline 是 Promtail 的核心,负责将原始日志转换为结构化数据。我们的审计日志需要两阶段解析:
原始日志(单行 JSON)
│
▼ 第 1 阶段:解析外层 JSON
│
│ json:
│ start_time → "2026-09-01T10:30:45.123Z"
│ ai_log → "{\\"model\\":\\"qwen3.8-max\\",…}" (字符串)
│ route → "ai-route-aliyun.internal"
│ consumer → "user-a"
│ api_key → "user-a"
│
▼ 时间戳替换
│
│ timestamp:
│ source: start_time
│ format: RFC3339Nano
│
│ → 日志时间戳从"采集时间"变为"请求实际发生时间"
│
▼ 第 2 阶段:解析内层 ai_log JSON
│
│ json:
│ source: ai_log
│ model → "qwen3.8-max"
│
▼ Labels 提取
│
│ labels:
│ model: "qwen3.8-max"
│ route: "ai-route-aliyun.internal"
│ consumer: "user-a"
│ api_key: "user-a"
│
▼ 写入 Loki(带 labels 索引)
5.4 关键设计决策
| 两阶段 JSON 解析 | ai_log 是嵌套 JSON 字符串,需要先用 source 字段指定来源,再解析内部字段 |
| 时间戳替换 | 使用 timestamp 插件将日志时间戳替换为 start_time,确保回灌历史日志时时间正确 |
| labels 选择 | 只提取 model、route、consumer、api_key 四个 labels,避免 labels 基数过高影响 Loki 性能 |
| 通配符路径 | access.log* 匹配所有轮转文件(access.log、access.log.1、access.log-20260831 等) |
5.5 Labels 基数控制
Loki 的 labels 会创建索引,labels 组合越多,索引越大。我们只提取了 4 个 labels:
| model | ~10 | 模型数量有限,适合做 label |
| route | ~6 | 路由数量有限 |
| consumer | ~10 | 用户数量有限 |
| api_key | ~10 | 与 consumer 基本一致 |
不提取为 label 的字段:request_id、upstream_host、response_code 等,这些字段基数高或查询频率低,用日志内容过滤(|= "xxx")更合适。
六、Loki — 日志存储与查询
6.1 存储配置
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
instance_addr: 127.0.0.1
kvstore:
store: inmemory
ingester:
wal:
enabled: false # 关闭 WAL,避免竞态丢数据(详见系列第二篇)
schema_config:
configs:
– from: 2020-10-24
store: boltdb–shipper
object_store: filesystem
schema: v11
index:
prefix: index_
period: 24h
limits_config:
retention_period: 180d # 日志保留 180 天
reject_old_samples: true
reject_old_samples_max_age: 4320h
ingestion_rate_mb: 100
ingestion_burst_size_mb: 200
per_stream_rate_limit: 5MB
per_stream_rate_limit_burst: 10MB
allow_structured_metadata: false # Loki 3.x 适配
max_line_size: 4194304 # 4MB,防止长行被丢弃
query_scheduler:
max_outstanding_requests_per_tenant: 1024
6.2 关键配置说明
| wal.enabled | false | 关闭 WAL,避免 2.9 版本竞态丢数据 |
| retention_period | 180d | 日志保留 6 个月 |
| max_line_size | 4MB | Loki 3.x 默认 256KB,Envoy 日志含嵌套 JSON 可能超长 |
| allow_structured_metadata | false | boltdb-shipper 不支持 TSDB 索引,必须关闭 |
| query_scheduler.max_outstanding_requests_per_tenant | 1024 | 默认 100,Grafana 并发查询容易被拒 |
6.3 LogQL 查询示例
查询某用户的所有 AI 请求日志
{job="higress-audit-logs", consumer="user-a"} |= "ai_log"
查询某模型的所有请求
{job="higress-audit-logs", model="qwen3.8-max"}
统计某路由的请求数
count_over_time({job="higress-audit-logs", route="ai-route-aliyun.internal"} |= "ai_log" [1h])
统计某用户的 Token 总量
sum by(api_key) (
sum_over_time(
{job="higress-audit-logs", api_key="user-a"}
|= "ai_log"
| json total_token="total_token"
| unwrap total_token [24h]
)
)
七、Prometheus — 指标采集
7.1 采集配置
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
# Prometheus 自身
– job_name: 'prometheus'
static_configs:
– targets: ['localhost:9090']
# Higress 网关(Envoy metrics)
– job_name: 'higress-gateway'
static_configs:
– targets: ['higress:15090']
metrics_path: /stats/prometheus
scrape_interval: 15s
scrape_timeout: 10s
# 容器指标
– job_name: 'cadvisor'
static_configs:
– targets: ['cadvisor:8080']
scrape_interval: 15s
scrape_timeout: 10s
7.2 采集的指标类型
| Envoy | envoy_http_downstream_rq_total | 下游请求总量 |
| Envoy | envoy_http_downstream_rq_xx | 按响应码分类的请求数 |
| Envoy | envoy_http_downstream_cx_active | 活跃连接数 |
| Envoy | envoy_cluster_upstream_rq_time | 上游请求延迟 |
| cAdvisor | container_cpu_usage_seconds_total | 容器 CPU 使用率 |
| cAdvisor | container_memory_usage_bytes | 容器内存使用量 |
| cAdvisor | container_network_receive_bytes_total | 容器网络接收字节 |
八、Grafana — 多维看板
8.1 看板结构
我们构建了一个综合看板 Higress AI Gateway Dashboard,包含以下面板组:
┌─────────────────────────────────────────────────────────────┐
│ General(网关总览) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Downstream│ │Downstream│ │Downstream│ │TCP Recv │ │
│ │Request │ │Success │ │Request │ │Bytes │ │
│ │Volume │ │Rate │ │Duration │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Upstream │ │Upstream │ │Upstream │ │TCP Sent │ │
│ │Request │ │Success │ │Request │ │Bytes │ │
│ │Volume │ │Rate │ │Duration │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Workload(容器资源) │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ CPU │ │ Memory │ │
│ └──────────────────────┘ └──────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Request(请求分析) │
│ ┌──────────┐ ┌──────────┐ │
│ │Downstream│ │Upstream │ │
│ │QPS │ │QPS │ │
│ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Token(AI 业务指标) │
│ ┌──────────────────────────────────────────────┐ │
│ │ Token Usage Detail(明细表格) │ │
│ │ 路由 | 用户 | 模型 | 请求数 | Token 总量 │ │
│ └──────────────────────────────────────────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Token Per │ │Input Tps │ │Output Tps│ │First Tkn │ │
│ │Second │ │(Model) │ │(Model) │ │Latency │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Model Token Usage │ │ Consumer Token Usage │ │
│ │ (按模型饼图) │ │ (按用户饼图) │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Token Usage by Route & Consumer │ │
│ │ (路由+用户+模型 三维聚合表格) │ │
│ └──────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ WAF(安全防护) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │WAF │ │WAF Denied│ │WAF Denied│ │
│ │Processed │ │by Phase │ │by RuleID │ │
│ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────────┤
│ XDS(配置同步) │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ XDS Active Conn │ │ XDS Requests Size │ │
│ └──────────────────────┘ └──────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Service Top(服务排行) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │QPS Top 10│ │Failure │ │Active Cx │ │RT Top 10 │ │
│ │ │ │Top 10 │ │Top 10 │ │(Slow) │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
8.2 核心面板查询
面板 1:Downstream QPS(Prometheus)
round(sum(irate(envoy_http_downstream_rq_total{higress="$gateway"}[5m])), 0.001)
面板 2:Token Per Second(Loki)
sum by(model) (
sum_over_time(
{job="higress-audit-logs", model!=""}
|= "ai_log"
| json total_token="total_token"
| unwrap total_token [$__range]
)
)
面板 3:Model Token Usage(按模型饼图)
sum by(model) (
sum_over_time(
{job="higress-audit-logs", model!=""}
|= "ai_log"
| json total_token="total_token"
| unwrap total_token [$__range]
)
)
面板 4:Consumer Token Usage(按用户饼图)
sum by(route) (
sum_over_time(
{job="higress-audit-logs", model!=""}
|= "ai_log"
| json total_token="total_token"
| unwrap total_token [$__range]
)
)
面板 5:Token Usage by Route & Consumer(三维聚合表格)
请求数查询:
sum by(route, api_key, model) (
count_over_time(
{job="higress-audit-logs", model!=""}
|= "ai_log"
| json
| ai_log!="-"
[$__range]
)
)
Token 总量查询:
sum by(route, api_key, model) (
sum_over_time(
{job="higress-audit-logs", model!=""}
|= "ai_log"
| json total_token="total_token"
| unwrap total_token
[$__range]
)
)
Grafana Transformations:
- Group By: route, api_key, model
- 列重命名:route → 路由, api_key → 用户, model → 模型
- 正则提取:从路由名中提取可读名称(如 ai-route-aliyun.internal → aliyun)
面板 6:First Token Latency(首 Token 延迟)
avg by(model) (
avg_over_time(
{job="higress-audit-logs", model!=""}
|= "ai_log"
| json first_token_latency="first_token_latency"
| unwrap first_token_latency
[$__range]
)
)
8.3 Prometheus vs Loki 数据源选择
| QPS、延迟、错误率 | Prometheus | Envoy 原生暴露指标,适合实时聚合 |
| Token 消耗统计 | Loki | Token 信息在日志中,需要 LogQL 解析 |
| 容器 CPU/内存 | Prometheus | cAdvisor 暴露指标 |
| 审计日志明细 | Loki | 日志查询是 Loki 的强项 |
| 按用户/模型聚合 | Loki | 需要从日志中提取维度 |
九、Docker Compose 一键部署
9.1 完整编排
services:
# Higress AI Gateway
higress:
image: higress–registry.cn–hangzhou.cr.aliyuncs.com/higress/all–in–one:latest
container_name: higress–ai–gateway
restart: unless–stopped
ports:
– "8001:8001" # 控制台
– "80:80" # HTTP
– "443:8443" # HTTPS
– "15090:15090" # Metrics
environment:
– MODE=full
– CONFIG_TEMPLATE=ai–gateway
– O11Y=on # 开启可观测性
volumes:
– ./data:/data
– ./logs:/var/log/higress
networks:
– higress–net
# Prometheus – 指标采集
prometheus:
image: prom/prometheus:latest
container_name: higress–prometheus
restart: unless–stopped
ports:
– "9095:9090"
volumes:
– ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
– prometheus–data:/prometheus
command:
– '–storage.tsdb.retention.time=180d'
– '–web.enable-lifecycle'
networks:
– higress–net
# Grafana – 可视化
grafana:
image: grafana/grafana:latest
container_name: higress–grafana
restart: unless–stopped
ports:
– "3005:3000"
environment:
– GF_SECURITY_ADMIN_USER=admin
– GF_SECURITY_ADMIN_PASSWORD=admin
– GF_SECURITY_ALLOW_EMBEDDING=true
volumes:
– ./grafana/provisioning:/etc/grafana/provisioning:ro
– ./grafana/dashboards:/var/lib/grafana/dashboards:ro
– grafana–data:/var/lib/grafana
networks:
– higress–net
# Loki – 日志存储
loki:
image: grafana/loki:3.7.1
container_name: higress–loki
restart: unless–stopped
ports:
– "3100:3100"
volumes:
– ./loki/loki–config.yaml:/etc/loki/local–config.yaml:ro
– loki–data:/loki
networks:
– higress–net
# Promtail – 日志采集
promtail:
image: grafana/promtail:2.9.0
container_name: higress–promtail
restart: unless–stopped
volumes:
– ./loki/promtail–config.yaml:/etc/promtail/config.yml:ro
– ./logs:/var/log/higress:ro
– ./loki/positions:/tmp/positions
networks:
– higress–net
# cAdvisor – 容器指标
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: higress–cadvisor
restart: unless–stopped
ports:
– "8081:8080"
volumes:
– /:/rootfs:ro
– /var/run:/var/run:ro
– /sys:/sys:ro
– /var/lib/docker/:/var/lib/docker:ro
networks:
– higress–net
networks:
higress-net:
driver: bridge
volumes:
prometheus-data:
grafana-data:
loki-data:
9.2 启动顺序
1. docker compose up -d higress # 先启动网关
2. docker compose up -d loki # 启动日志存储
3. docker compose up -d promtail # 启动日志采集
4. docker compose up -d prometheus # 启动指标采集
5. docker compose up -d cadvisor # 启动容器指标
6. docker compose up -d grafana # 最后启动可视化
9.3 验证部署
# 检查所有容器状态
docker compose ps
# 验证 Loki 就绪
curl -s http://localhost:3100/ready
# 输出: ready
# 验证 Prometheus 就绪
curl -s http://localhost:9095/-/ready
# 验证 Grafana 就绪
curl -s http://localhost:3005/api/health
# 输出: {"commit":"…","database":"ok","version":"…"}
# 验证日志正在采集
curl -sG 'http://localhost:3100/loki/api/v1/query' \\
–data-urlencode 'query=count_over_time({job="higress-audit-logs"}[5m])'
十、运维实践
10.1 日志轮转与保留
# 配置 logrotate(/etc/logrotate.d/higress)
/var/log/higress/proxy/access.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
copytruncate
}
| daily | 每天轮转 |
| rotate 30 | 保留 30 天 |
| copytruncate | 复制后截断,不中断 Promtail |
10.2 Promtail 自愈脚本
positions offset 异常时自动修复(详见系列第二篇):
#!/bin/bash
# promtail-watchdog.sh
# 检测 offset > 文件大小 → 重置为 0 → 重启 Promtail
POSITIONS_FILE="/path/to/positions.yaml"
LOG_DIR="/path/to/logs/proxy"
while IFS= read -r line; do
# 提取文件路径和 offset
CONTAINER_PATH=$(echo "$line" | sed 's/^[[:space:]]*//;s/:.*//')
OFFSET=$(echo "$line" | grep -oP '"\\K[0-9]+(?=")' || echo "0")
HOST_PATH=$(echo "$CONTAINER_PATH" | sed 's|^/var/log/higress/proxy/|'"${LOG_DIR}"'/|')
[ -f "$HOST_PATH" ] || continue
FILE_SIZE=$(stat -c%s "$HOST_PATH" 2>/dev/null || echo "0")
if [ "$FILE_SIZE" -lt "$OFFSET" ] && [ "$OFFSET" -gt 0 ]; then
# offset 异常,重置为 0
sed -i "s|${CONTAINER_PATH}: \\"${OFFSET}\\"|${CONTAINER_PATH}: \\"0\\"|g" "$POSITIONS_FILE"
docker compose restart promtail
fi
done < "$POSITIONS_FILE"
10.3 常见问题排查
| Grafana 无数据 | curl localhost:3100/ready | Loki 未就绪 |
| Token 曲线为零 | curl localhost:3100/metrics | grep lines_received | 日志未采集或 WAL 丢数据 |
| QPS 有数据但 Token 为零 | 检查 Promtail positions | offset 异常或日志轮转 |
| 日志查询为空 | docker logs higress-promtail | Promtail 配置错误或连接失败 |
| 看板延迟高 | curl localhost:3100/metrics | grep query | Loki 查询并发过高 |
10.4 性能调优建议
| Loki ingestion_rate_mb | 100 | 提高写入速率限制 |
| Loki max_line_size | 4MB | 防止长日志行被丢弃 |
| Loki query_scheduler.max_outstanding_requests | 1024 | 提高并发查询上限 |
| Promtail positions 持久化 | 挂载 volume | 避免容器重启后从头采集 |
| Prometheus scrape_interval | 15s | 平衡实时性和资源消耗 |
| Grafana dashboard 刷新 | 30s | 避免频繁查询压垮 Loki |
十一、总结
本文展示了一套完整的 Higress AI 网关可观测性体系:
| 数据源 | Envoy Access Log + ai-statistics | 结构化日志 + Token 统计 |
| 日志采集 | Promtail | 两阶段 JSON 解析 + 时间戳替换 + labels 提取 |
| 日志存储 | Loki | 结构化日志存储 + LogQL 查询 |
| 指标采集 | Prometheus + cAdvisor | Envoy 指标 + 容器指标 |
| 可视化 | Grafana | 多维看板(QPS/Token/延迟/资源) |
核心设计思路:
日志即指标:Token 统计不单独暴露 metrics,而是嵌入日志,通过 LogQL 聚合。简化了架构,同时保留了日志明细查询能力。
两阶段解析:Envoy 日志是嵌套 JSON(外层 Envoy 字段 + 内层 ai_log),Promtail 用两次 json stage 逐层解析。
时间戳替换:用 start_time 替换采集时间,确保回灌历史日志时时间正确。
Labels 基数控制:只提取 4 个低基数 labels(model/route/consumer/api_key),避免索引膨胀。
双数据源看板:Prometheus 负责实时指标(QPS/延迟),Loki 负责业务指标(Token),Grafana 统一展示。
如果你也在搭建 AI 网关的可观测性体系,希望这篇从零到一的实践分享能给你一些参考。欢迎在评论区交流讨论!


-171主机测评](https://www.171host.com/wp-content/uploads/2026/09/20260909052429-6aa0ed8d3dff8-220x150.jpg)