Files
conti-docs/backend/08-observability.md
T
Guangfei.Zhao 1e0cbb86a2 feat: Add comprehensive documentation for integration layer, API design, config governance, observability, build/deploy, and testing strategies
- Introduced integration layer design with Resilience4j for external vendor calls.
- Established API design standards with unified response structures and global exception handling.
- Defined configuration and service governance using Kubernetes native solutions.
- Implemented observability practices including trace ID propagation and structured logging.
- Outlined build and multi-environment deployment strategies using Gradle and GitLab CI/CD.
- Specified testing strategies across different layers, utilizing JUnit, MockK, Testcontainers, and WireMock.
2026-08-12 18:23:11 +08:00

148 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<!-- logback-spring.xml -->
<configuration>
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>traceId</includeMdcKeyName>
<customFields>{"app":"conti-backend"}</customFields>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON" />
</root>
</configuration>
```
```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)