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 本章知识体系
下面这张图,帮你理清日志与监控的整体架构:
日志和监控,就像飞机的黑匣子和仪表盘。黑匣子记录事故原因,仪表盘让你在飞行中随时知道状态。两者配合,才能让你的服务飞得稳、落得安。
总结:
- Logback配置要滚动、要格式化、要分环境
- 结构化日志用JSON,方便查询和分析
- Metrics集成Micrometer,暴露Prometheus端点
- 健康检查端点区分liveness和readiness
公众号:蓝海资料掘金营,微信deep3321