16、日志与监控:Logback配置、结构化日志、Metrics集成、健康检查端点

日志和监控,说白了就是给系统装个“黑匣子”和“仪表盘”。

我见过太多项目,上线后出问题全靠“猜”。服务器CPU飙到100%,没人知道刚才发生了什么。日志里全是无意义的字符串,想查个请求链路得翻半天。嗯,这种体验真的很糟糕。

这一章,我们就来聊聊怎么在Ktor项目里把日志和监控做好。我会从Logback配置讲起,再到结构化日志、Metrics集成,最后加上健康检查端点。这些都是我实际项目中踩过坑、填过土的经验。

核心要点:日志不是写给机器看的,是写给未来的你(或者你的同事)看的。监控不是为了好看,是为了在出问题时能快速定位。

16.1 Logback配置:别再用默认配置了

Ktor默认使用Logback作为日志框架。但默认配置真的不够用。我个人习惯,一上来就改掉默认配置,至少要做到三点:日志滚动、格式规范、级别可控。

先看一个我常用的logback.xml配置:

<configuration>
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%logger{36}) - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

这里有几个关键点:

  • 日志滚动:按天滚动,每天一个文件。如果某天日志特别大,还会按大小拆分(%i)。
  • 保留策略:最多保留30天,总大小不超过1GB。防止磁盘被日志撑爆。
  • 格式:时间、线程、级别、类名、消息。缺一不可。

小技巧:开发环境用CONSOLE,带颜色高亮。生产环境用FILE,不带颜色,减少IO开销。

16.2 结构化日志:让日志变成可查询的数据

传统的日志就是一行字符串。你想想看,如果我想查某个用户的所有操作记录,用grep去匹配字符串?太原始了。

结构化日志,就是把日志变成JSON格式。每个字段都是独立的key-value,方便后续用ELK、Splunk等工具查询。

在Ktor里,我推荐用kotlin-logging + logstash-logback-encoder来实现:

// build.gradle.kts
implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
implementation("net.logstash.logback:logstash-logback-encoder:7.4")

然后在logback.xml里加一个JSON appender:

<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/app.json</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>logs/app.%d{yyyy-MM-dd}.json</fileNamePattern>
    </rollingPolicy>
    <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>

代码里怎么用?看这个例子:

import mu.KotlinLogging

private val logger = KotlinLogging.logger {}

fun processOrder(orderId: String, userId: String) {
    logger.info {
        "订单处理开始"
        // 结构化字段
        mapOf(
            "orderId" to orderId,
            "userId" to userId,
            "action" to "process_order"
        )
    }
    // 业务逻辑...
    logger.info {
        "订单处理完成"
        mapOf(
            "orderId" to orderId,
            "status" to "success"
        )
    }
}

输出的JSON长这样:

{
  "@timestamp": "2024-01-15T10:30:00.123+08:00",
  "level": "INFO",
  "logger": "com.example.OrderService",
  "message": "订单处理开始",
  "orderId": "ORD-20240115-001",
  "userId": "user_12345",
  "action": "process_order"
}

注意:结构化日志不要滥用。每个日志都塞几十个字段,反而影响性能。我一般控制在5-8个关键字段以内。

16.3 Metrics集成:给系统装上仪表盘

日志是“事后诸葛亮”,Metrics是“实时监控”。

Ktor官方提供了Metrics插件,支持Micrometer。Micrometer是一个门面,背后可以对接Prometheus、Graphite、Datadog等。

先加依赖:

implementation("io.ktor:ktor-server-metrics-micrometer")
implementation("io.micrometer:micrometer-registry-prometheus:1.12.0")

然后在Application.kt里配置:

fun Application.module() {
    install(MicrometerMetrics) {
        registry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
        // 自定义标签
        meterBinders = listOf(
            JvmMetrics(),
            ClassLoaderMetrics(),
            UptimeMetrics()
        )
    }

    // 暴露Prometheus端点
    routing {
        get("/metrics") {
            val registry = application.plugin(MicrometerMetrics).registry
            if (registry is PrometheusMeterRegistry) {
                call.respondText(registry.scrape(), ContentType.Text.Plain)
            }
        }
    }
}

这样,访问 /metrics 就能看到Prometheus格式的指标数据了。

我个人习惯,至少监控这几个指标:

指标 说明 告警阈值
http.server.requests HTTP请求数、延迟、状态码 P99 > 500ms 告警
jvm.memory.used JVM堆内存使用量 超过80% 告警
jvm.threads.live 活跃线程数 超过200 告警
system.cpu.usage CPU使用率 持续超过90% 告警

避坑指南:我曾经在生产环境忘了限制Metrics端点的访问权限,结果被外部扫描工具疯狂调用,导致CPU飙升。记得加上认证或内网限制。

16.4 健康检查端点:Kubernetes的“心跳”

如果你的服务部署在Kubernetes里,健康检查是必须的。K8s通过liveness和readiness探针来判断服务是否存活、是否就绪。

Ktor提供了现成的健康检查插件:

implementation("io.ktor:ktor-server-status-pages")
implementation("io.ktor:ktor-server-health-check")

配置起来很简单:

fun Application.module() {
    install(HealthCheck) {
        // 自定义健康检查逻辑
        check("database") {
            // 检查数据库连接
            if (!dbConnection.isValid(5)) {
                throw HealthCheckResult.failure("数据库连接不可用")
            }
        }
        check("redis") {
            // 检查Redis连接
            if (!redisClient.ping()) {
                throw HealthCheckResult.failure("Redis连接不可用")
            }
        }
    }

    routing {
        // 健康检查端点
        get("/health") {
            call.respondText("OK", ContentType.Text.Plain)
        }
        get("/health/ready") {
            // 返回详细健康状态
            val result = application.plugin(HealthCheck).check()
            if (result.all { it.value.isHealthy }) {
                call.respond(mapOf("status" to "UP"))
            } else {
                call.respondText("DOWN", status = HttpStatusCode.ServiceUnavailable)
            }
        }
    }
}

在K8s的Deployment配置里,这样写:

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

注意:liveness探针不要依赖外部服务(比如数据库)。如果数据库挂了,liveness会把Pod重启,但重启后数据库还是挂的,就会陷入无限重启循环。readiness探针可以检查外部依赖。

16.5 本章知识体系

下面这张图,帮你理清日志与监控的整体架构:

日志与监控架构 Ktor 应用 日志系统 Logback 配置 结构化日志 (JSON) 日志滚动与保留 监控系统 Micrometer Metrics Prometheus 端点 健康检查 /health 日志用于事后排查,监控用于实时预警,两者缺一不可

日志和监控,就像飞机的黑匣子和仪表盘。黑匣子记录事故原因,仪表盘让你在飞行中随时知道状态。两者配合,才能让你的服务飞得稳、落得安。

总结:

  • Logback配置要滚动、要格式化、要分环境
  • 结构化日志用JSON,方便查询和分析
  • Metrics集成Micrometer,暴露Prometheus端点
  • 健康检查端点区分liveness和readiness

公众号:蓝海资料掘金营,微信deep3321