前言:一次”盲飞”式排查 前段时间给 EduMind(一个基于 Spring Boot 4 + Vue 3 的 AI 教学助手)做了一轮生产级差距审计。结果让我后背发凉——整个项目没有任何可观测性 。
唯一和”监控”沾边的配置就一行:
1 management.endpoints.web.exposure.include =health
没有指标、没有追踪、没有结构化日志、没有告警。出问题只能靠 docker compose logs 人肉翻,排查一次线上问题的时间够写三个 feature。
审计结束后,我花了半天补齐了这块。本文记录这个过程。
一、为什么可观测性不是”可选” 很多开发者(包括以前的我)对可观测性的态度是:“等项目大了再说” 。
但可观测性不是锦上添花,它解决三个你绕不开的问题:
问题
没有可观测性
有可观测性
用户说”好慢”
翻代码猜、改完祈祷、等用户反馈
Grafana 一看,p99 延迟在哪段一目了然
凌晨报警
你是报警系统——用户打电话/P0 issue
Alertmanager → 钉钉,起床处理,其他时间安心睡觉
排查线上错误
docker compose logs | grep ERROR,海量文本中找关键信息
Loki 里 {requestId="xxx"} 一条命令看到完整请求链路
可观测性不是成本,是保险。 相当于你花半小时装个刹车,而不是等撞了再修。
二、目标:三根支柱 我给自己定了三个目标,对应 Observability 三支柱:
1 2 3 4 5 6 7 8 9 10 ┌─────────────────────────────────────────────┐ │ 可观测性三支柱 │ ├──────────────┬──────────────┬───────────────┤ │ Metrics │ Logging │ Tracing │ │ (指标) │ (日志) │ (追踪) │ ├──────────────┼──────────────┼───────────────┤ │ Prometheus │ Logback JSON │ RequestId │ │ + Grafana │ + logstash │ + MDC │ │ 看趋势、告警 │ 定位具体问题 │ 串联整个链路 │ └──────────────┴──────────────┴───────────────┘
三、Metrics:从裸奔到一目了然 3.1 为什么选 Prometheus + Grafana
Prometheus 是云原生时代的标配,Spring Boot Actuator 原生支持,一个依赖搞定
Grafana 的 Dashboard 生态太强了,JSON 定义即配置,Git 版本化管理
两者都免费,社区活跃,出了问题 Stack Overflow 一搜就有
3.2 第一步:暴露 Prometheus 端点 pom.xml 加一个依赖:
1 2 3 4 <dependency > <groupId > io.micrometer</groupId > <artifactId > micrometer-registry-prometheus</artifactId > </dependency >
application.properties 三行配置:
1 2 3 management.endpoints.web.exposure.include =health,metrics,prometheus management.metrics.export.prometheus.enabled =true management.metrics.tags.application =edumind
改完重启,访问 http://localhost:8080/actuator/prometheus,出来的就是这样的原始指标:
1 2 3 4 # HELP jvm_memory_used_bytes jvm_memory_used_bytes{area="heap"} 1.2E8 # HELP http_server_requests_seconds_count http_server_requests_seconds_count{uri="/api/chat/stream"} 42.0
3.3 第二步:Prometheus 抓取配置 1 2 3 4 5 6 7 scrape_configs: - job_name: "edumind" metrics_path: "/actuator/prometheus" scrape_interval: 10s static_configs: - targets: ["app:8080" ]
3.4 第三步:Grafana 一键 Dashboard 在 Grafana 里配置好 Prometheus 数据源,然后导入我预制好的 Dashboard。结果就是这张面板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ┌─────────────────────────────────────────────────┐ │ JVM Memory (Heap & Non-Heap) │ GC Pause & Rate │ │ ┌─────────────────────────┐ │ ┌────────────┐ │ │ │ ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ │ │ │ ▁▁▂▃▂▁▁ │ │ │ └─────────────────────────┘ │ └────────────┘ │ │ │ │ │ API Request Rate (by URI) │ API Latency p95 │ │ ┌─────────────────────────┐ │ ┌────────────┐ │ │ │ /api/chat ████████ │ │ │ 1.2s → 0.8s│ │ │ │ /api/auth ██ │ │ │ p95 ↓ 33% │ │ │ └─────────────────────────┘ │ └────────────┘ │ │ │ │ │ 5xx Error Rate │ RAG Search Latency│ │ ┌──────┐ │ ┌────────────┐ │ │ │ 0.1% │ ← 绿色,安心 │ │ p50: 0.8s │ │ │ └──────┘ │ │ p99: 3.2s │ │ │ │ └────────────┘ │ └─────────────────────────────────────────────────┘
这一刻的感受: 之前就像在黑屋子里摸索,现在灯全开了。JVM 用了多少内存、哪个接口最慢、RAG 检索有没有瓶颈——一眼就能看到。
3.5 自定义业务指标 通用指标(JVM、HTTP)是 Spring Boot 自动给的。业务指标 需要自己埋点。用 @Timed 注解一行搞定:
1 2 3 4 5 @Timed(value = "rag.search", description = "RAG检索延迟", histogram = true) public RagResult search (RagSearchRequest request) { }
配合 TimedAspect:
1 2 3 4 5 6 7 @Configuration public class ObservabilityConfig { @Bean public TimedAspect timedAspect (MeterRegistry registry) { return new TimedAspect (registry); } }
之后 RAG 检索的每次调用都会自动记录耗时、次数、分位数,出现在 Grafana 面板和 Prometheus 告警规则里。
3.6 告警:不要在凌晨当人肉 Alertmanager 定义了 6 条告警规则:
1 2 3 4 5 6 - alert: EduMindDown - alert: HighErrorRate - alert: HighLatency - alert: RagSearchSlow - alert: HighHeapUsage
生产环境接入 Alertmanager,配好飞书/钉钉 Webhook,就能在凌晨收到这种消息:
🚨 [EduMind] HighErrorRate — 5xx 比例 8.2%,请立即检查!
凌晨不再是你在值班,是 Prometheus 在值班。
四、Logging:从”找日志”到”搜日志” 4.1 问题:纯文本日志的无助 之前 EduMind 没有 logback-spring.xml,日志是 Spring Boot 默认格式,多行一个请求的信息散布在几十行里:
1 2 3 2026-07-08 10:23:45 INFO c.f.d.Controller - 收到请求 2026-07-08 10:23:45 DEBUG c.f.d.Service - 开始查询 2026-07-08 10:23:46 ERROR c.f.d.Service - 查询失败!!
没有 requestId、没有 userId、没有 JSON 格式。想查”用户 123 刚才那个失败的请求到底发生了什么”,等于大海捞针。
4.2 解决方案:logback-spring.xml + logstash-logback-encoder 加依赖:
1 2 3 4 5 <dependency > <groupId > net.logstash.logback</groupId > <artifactId > logstash-logback-encoder</artifactId > <version > 8.0</version > </dependency >
配置分环境:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <springProfile name ="dev | local" > <root level ="INFO" > <appender-ref ref ="CONSOLE" /> </root > </springProfile > <springProfile name ="prod" > <root level ="INFO" > <appender-ref ref ="ASYNC_JSON" /> <appender-ref ref ="ASYNC_FILE" /> <appender-ref ref ="ERROR_FILE" /> </root > </springProfile >
生产日志输出长这样:
1 2 3 4 5 6 7 8 9 10 { "@timestamp" : "2026-07-08T10:23:45.123Z" , "message" : "RAG检索完成" , "level" : "INFO" , "logger_name" : "c.f.d.rag.RagService" , "thread_name" : "tomcat-handler-3" , "requestId" : "f3d85f5e4a0c40e8" , "userId" : "42" , "application" : "edumind" }
JSON 格式的好处:直接接入 ELK 或 Loki,一个 Lucene 查询 requestId:"xxx" AND level:"ERROR" 就能还原完整请求链路。
4.3 RequestId:给每个请求发身份证 这是最容易被忽视但最实用的一点。实现也很简单——一个 OncePerRequestFilter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class RequestIdFilter extends OncePerRequestFilter { @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain chain) { String requestId = request.getHeader("X-Request-Id" ); if (requestId == null ) { requestId = UUID.randomUUID().toString().replace("-" , "" ); } MDC.put("requestId" , requestId); response.setHeader("X-Request-Id" , requestId); try { chain.doFilter(request, response); } finally { MDC.remove("requestId" ); } } }
配合 JwtAuthenticationFilter 注入 userId:
1 2 MDC.put("userId" , String.valueOf(userId));
效果对比:
1 2 3 4 5 6 7 8 9 # 之前 2026-07-08 10:23:45 INFO - 收到请求 2026-07-08 10:23:45 DEBUG - 查询数据库 2026-07-08 10:23:46 ERROR - 查询失败 # 之后 [f3d85f5e] [user:42] INFO - 收到请求 [f3d85f5e] [user:42] DEBUG - 查询数据库 [f3d85f5e] [user:42] ERROR - 查询失败: Duplicate entry 'xxx'
现在用户报 bug 时,前端拿到报错弹窗里的 X-Request-Id,后端 grep f3d85f5e 就能瞬间定位整条链路。
五、Nginx 也不能落下 应用层的日志已经有 requestId 了,但 Nginx 那层的请求耗时也得能追踪:
1 2 3 4 5 6 7 8 9 10 11 12 log_format trace '$remote_addr [$time_local ] "$request " ' '$status $body_bytes_sent ' '"$http_x_request_id " ' 'rt=$request_time ' 'uct="$upstream_connect_time " ' 'uht="$upstream_header_time "' ; server { access_log /var/log/nginx/access.log trace; }
生产日志里就能看到这样的访问记录:
1 2 192.168.1.1 [08/Jul/2026:10:23:45] "GET /api/chat/stream" 200 1234 "f3d85f5e4a0c40e8" rt=0.045 uct="0.002" uht="0.040"
一个 requestId,从 Nginx → 应用 → 日志文件 → Grafana,全程对得上。排查问题不需要猜”是 Nginx 超时还是应用慢”——rt vs uht 一看就知道。
六、docker-compose 一键到位 所有监控组件都写进 docker-compose.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 prometheus: image: prom/prometheus:v2.55.0 volumes: - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro ports: ["9090:9090" ] grafana: image: grafana/grafana:11.2.0 environment: GF_SECURITY_ADMIN_USER: admin GF_SECURITY_ADMIN_PASSWORD: admin volumes: - ./grafana/dashboards:/etc/grafana/provisioning/dashboards:ro - ./grafana/datasources:/etc/grafana/provisioning/datasources:ro ports: ["3001:3000" ]
开发环境只启监控容器:
1 docker compose up prometheus grafana -d --no-deps
打开 http://localhost:3001,Dashboard 自动加载,数据已经在面板上了。
七、成果清单 改完之后,整个可观测性栈长这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ┌──────────────┐ │ Grafana │ ← 可视化面板 │ :3001 │ └──────┬───────┘ │ 查询 ┌──────▼───────┐ │ Prometheus │ ← 指标存储 + 告警 │ :9090 │ └──────┬───────┘ │ scrape /actuator/prometheus ┌────────────┼────────────┐ │ │ │ ┌─────────▼──┐ ┌──────▼─────┐ ┌──▼──────────┐ │ Spring Boot │ │ Nginx │ │ logback JSON │ │ @Timed/RAG │ │ trace log │ │ + requestId │ └─────────────┘ └────────────┘ └──────────────┘
改动
数量
新增文件
8 个(logback-spring.xml, RequestIdFilter, Prometheus 配置, Grafana Dashboard 等)
修改文件
8 个(pom.xml, application.properties, SecurityConfig, nginx.conf 等)
新增依赖
2 个(micrometer-registry-prometheus, logstash-logback-encoder)
总耗时
~3 小时
八、现在还缺什么 诚实地说,这套方案覆盖了三支柱中的两个半:
1 2 3 ✅ Metrics — Prometheus + Grafana,趋势、告警、仪表盘 ✅ Logging — 结构化 JSON + requestId + userId,可接入 Loki/ELK ⚠️ Tracing — 有 requestId 贯穿,但无 OpenTelemetry Span,内部调用链不可视化
真正的分布式追踪(跨服务 Span 传播、RAG 管线内部 step 级耗时)还需要引入 OpenTelemetry Agent。不过对单体的 Spring Boot 应用来说,当前的 requestId + Metrics 组合已经能覆盖 90% 的排查场景。
另外还有两个”五分钟”配置:
Alertmanager → 钉钉/飞书 Webhook :告警规则写好了,配置好通知渠道就能用
Sentry 接入 :应用错误自动上报,不用等用户告诉你”页面白了”
这两个等上线前补上就行。
结语 可观测性不是大厂的专利。一个 OncePerRequestFilter,三行 Prometheus 配置,一个 JSON 日志格式——投入半天,换来的是:
出问题时不用猜,看面板
排查问题时不用翻,搜 requestId
凌晨不用你是报警系统,Prometheus 替你值夜班
花半天修刹车,比修事故现场便宜一万倍。
相关代码:EduMind — AI 驱动的智能教学助手 ,Spring Boot 4 + Vue 3,Prometheus + Grafana 可观测性栈