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

5.9 KiB
Raw Blame History

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 示例

// 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 透传到下游请求头,方便跨系统关联日志:

webClient.get()
    .uri("/f6/procurement/list")
    .header("X-Trace-Id", TraceIdHolder.current())
    .retrieve()
    // ...

结构化日志配置示例

<!-- 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>
// 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 配置

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health, prometheus, info
  endpoint:
    health:
      probes:
        enabled: true # 暴露 /actuator/health/liveness、/readiness,供 K8s 探针使用
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'

审计日志示例

// 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 { ... }

审计日志走独立 loggerAUDIT),在 logback-spring.xml 里单独配置一个 appender 写到专门的审计日志文件/索引,不和普通业务日志混在一起,方便设置更长的保留期和更严格的访问权限。

关键规则

  • traceId 从入口 filter 生成,贯穿到 f6-integration / mini-clients 调用外部系统,失败时把 traceId 一起返回给前端(已经在 06-api-design.mdApiResult 里),方便排障(对应架构图 Flow 2 的"失败可支持排障"要求)。
  • 审计相关的关键行为(登录、换票、供应商调用失败)走单独的审计日志通道,不和普通业务日志混在一起。
  • 日志/指标最终对接现有 ELK 方案,具体接入方式(Filebeat 采集 stdout,还是直接推 Logstash)待确认。

附录:为什么要在 MDC 里放 traceId,而不是每条日志手动传参

不用 MDC 的话,每个方法打日志都要显式传 traceId 参数:log.info("traceId={} 门店切换成功", traceId),深层调用链里每一层都要多加一个参数,代码侵入性很强,还容易漏传。MDCMapped Diagnostic Context)是日志框架提供的"线程内隐式上下文",在 filter 里设置一次,同一线程内后续所有日志调用(不管调用链多深)都会自动带上这个字段,日志格式配置里声明 includeMdcKeyName 即可,业务代码完全不需要感知 traceId 的传递。

代价和 04-security-auth.md 里提到的 ThreadLocal 类似:MDC 底层也是 ThreadLocal 实现的,异步线程池、协程切换线程的场景需要手动透传(MDC.getCopyOfContextMap() 传给子线程),我们当前同步 Servlet 栈下不需要特殊处理,但如果某个模块引入异步处理要注意这一点。

待补充

  • 具体接入现有 ELK / APM 的方式和字段规范。
  • 审计日志的存储和保留策略。

参考链接