# 08. 可观测性 ## 决策 统一 Trace ID + 结构化(JSON)日志 + Micrometer 指标,对应架构图 `Cross-Cutting` 里的 `Observability` 要求;关键行为单独走审计日志通道,对应 `Audit / Security`。 ## 结构约定 ``` platform-observability/ TraceIdFilter # 入口生成/透传 traceId,写入 MDC logback-spring.xml # 结构化日志格式配置 MetricsConfig # Micrometer 基础配置,暴露 /actuator/prometheus AuditLogAspect # AOP 切面,标注 @Audited 的方法自动记录审计日志 ``` ## `TraceIdFilter` 示例 ```kotlin // platform-observability/.../TraceIdFilter.kt class TraceIdFilter : OncePerRequestFilter() { override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) { val traceId = request.getHeader("X-Trace-Id") ?: UUID.randomUUID().toString() MDC.put("traceId", traceId) response.setHeader("X-Trace-Id", traceId) try { chain.doFilter(request, response) } finally { MDC.clear() // 必须清理,否则线程池复用线程会带出上一个请求的 traceId } } } object TraceIdHolder { fun current(): String = MDC.get("traceId") ?: "unknown" } ``` 调用 F6/Mini 域时,把当前 `traceId` 透传到下游请求头,方便跨系统关联日志: ```kotlin webClient.get() .uri("/f6/procurement/list") .header("X-Trace-Id", TraceIdHolder.current()) .retrieve() // ... ``` ## 结构化日志配置示例 ```xml traceId {"app":"conti-backend"} ``` ```groovy // build.gradle implementation 'net.logstash.logback:logstash-logback-encoder:7.4' ``` 输出的每条日志会带上 `traceId` 字段,直接对接现有 ELK 方案(见 `Architecture-Diagram/ODP ELK Logging Solution Project - Overview.pdf`)时可以直接按 `traceId` 过滤出一次请求的完整链路日志。 ## Micrometer / Actuator 配置 ```yaml # application.yml management: endpoints: web: exposure: include: health, prometheus, info endpoint: health: probes: enabled: true # 暴露 /actuator/health/liveness、/readiness,供 K8s 探针使用 ``` ```groovy implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' ``` ## 审计日志示例 ```kotlin // platform-observability/.../Audited.kt @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) annotation class Audited(val action: String) // platform-observability/.../AuditLogAspect.kt @Aspect @Component class AuditLogAspect(private val storeContextHolder: StoreContextHolder) { private val auditLog = LoggerFactory.getLogger("AUDIT") @Around("@annotation(audited)") fun logAudit(joinPoint: ProceedingJoinPoint, audited: Audited): Any? { val result = runCatching { joinPoint.proceed() } auditLog.info( "action={} userId={} storeId={} traceId={} success={}", audited.action, storeContextHolder.userId, storeContextHolder.storeId, TraceIdHolder.current(), result.isSuccess, ) return result.getOrThrow() } } // 使用方式 @Audited(action = "WEBVIEW_TICKET_ISSUE") fun issueTicket(userId: Long, storeId: Long): WebviewTicket { ... } ``` 审计日志走独立 logger(`AUDIT`),在 `logback-spring.xml` 里单独配置一个 appender 写到专门的审计日志文件/索引,不和普通业务日志混在一起,方便设置更长的保留期和更严格的访问权限。 ## 关键规则 - `traceId` 从入口 filter 生成,贯穿到 `f6-integration` / `mini-clients` 调用外部系统,失败时把 `traceId` 一起返回给前端(已经在 [06-api-design.md](./06-api-design.md) 的 `ApiResult` 里),方便排障(对应架构图 Flow 2 的"失败可支持排障"要求)。 - 审计相关的关键行为(登录、换票、供应商调用失败)走单独的审计日志通道,不和普通业务日志混在一起。 - 日志/指标最终对接现有 ELK 方案,具体接入方式(Filebeat 采集 stdout,还是直接推 Logstash)待确认。 ## 附录:为什么要在 MDC 里放 traceId,而不是每条日志手动传参 不用 `MDC` 的话,每个方法打日志都要显式传 `traceId` 参数:`log.info("traceId={} 门店切换成功", traceId)`,深层调用链里每一层都要多加一个参数,代码侵入性很强,还容易漏传。`MDC`(Mapped Diagnostic Context)是日志框架提供的"线程内隐式上下文",在 filter 里设置一次,同一线程内后续所有日志调用(不管调用链多深)都会自动带上这个字段,日志格式配置里声明 `includeMdcKeyName` 即可,业务代码完全不需要感知 `traceId` 的传递。 代价和 [04-security-auth.md](./04-security-auth.md) 里提到的 `ThreadLocal` 类似:`MDC` 底层也是 `ThreadLocal` 实现的,异步线程池、协程切换线程的场景需要手动透传(`MDC.getCopyOfContextMap()` 传给子线程),我们当前同步 Servlet 栈下不需要特殊处理,但如果某个模块引入异步处理要注意这一点。 ## 待补充 - 具体接入现有 ELK / APM 的方式和字段规范。 - 审计日志的存储和保留策略。 ## 参考链接 - [SLF4J MDC 官方文档](https://www.slf4j.org/manual.html#mdc) - [Micrometer 官方文档](https://docs.micrometer.io/micrometer/reference/) - [Spring Boot Actuator 官方文档](https://docs.spring.io/spring-boot/reference/actuator/index.html)