Skip to content

健康检查

健康检查的职责很单一:把已经注册到 addHealthChecks() 的检查项执行一遍,再把聚合结果暴露成 HTTP 响应。

在 Web 层有两种挂载方式:

  • useHealthChecks(path):按中间件方式挂载
  • mapHealthChecks(pattern):按终结点方式挂载

最小示例

cangjie
import soulsoft_web_hosting.*
import soulsoft_web_healthchecks.*
import soulsoft_extensions_injection.*
import soulsoft_extensions_healthchecks.*

main(args: Array<String>): Int64 {
    let builder = WebHost.createBuilder(args)

    // 注册健康检查服务和检查项
    builder.services.addHealthChecks()
        .addCheck("self", {
            HealthCheckResult.healthy()
        })

    let app = builder.build()

    // 暴露 /health 端点
    app.useHealthChecks("/health")

    app.run()
    return 0
}

默认行为是:

  • Healthy -> 200
  • Degraded -> 200
  • Unhealthy -> 503
  • 响应体默认写入状态名,例如 Healthy
  • 默认附加防缓存响应头

两种挂载方式

useHealthChecks(path)

中间件方式适合直接按固定路径暴露健康检查。

cangjie
import soulsoft_web_hosting.*
import soulsoft_web_healthchecks.*
import soulsoft_extensions_injection.*
import soulsoft_extensions_healthchecks.*

let builder = WebHost.createBuilder(args)

builder.services.addHealthChecks()
    .addCheck("db", {
        HealthCheckResult.healthy()
    })

let app = builder.build()

// 精确匹配 /health,允许尾部 /
app.useHealthChecks("/health")

这里有两个实现细节要记住:

  • 它是按路径精确匹配的,不会把 /health/details 当成 /health
  • 路径比较时会忽略结尾的 /,所以 /health/ 也能命中

mapHealthChecks(pattern)

终结点方式适合需要和路由体系、授权、CORS 等能力一起使用的场景。

cangjie
import soulsoft_web_hosting.*
import soulsoft_web_routing.*
import soulsoft_web_healthchecks.*
import soulsoft_extensions_injection.*
import soulsoft_extensions_healthchecks.*

let builder = WebHost.createBuilder(args)
builder.services.addRouting()

builder.services.addHealthChecks()
    .addCheck("self", {
        HealthCheckResult.healthy()
    })

let app = builder.build()

// 先启用路由
app.useRouting()

// 注册健康检查终结点
app.mapHealthChecks("/health")

mapHealthChecks(...) 返回的是 EndpointConventionBuilder,所以可以继续挂终结点约定。

cangjie
app.mapHealthChecks("/health")
    .requireCors("ops")

过滤检查项

HealthCheckOptions.predicate 决定本次请求要执行哪些检查项。

按名称过滤

cangjie
app.useHealthChecks("/health/db") { options =>
    // 只运行名为 db 的检查项
    options.predicate = { registration =>
        registration.name == "db"
    }
}

按标签过滤

cangjie
builder.services.addHealthChecks()
    .addCheck("db", ["critical"], {
        HealthCheckResult.healthy()
    })
    .addCheck("cache", ["cache"], {
        HealthCheckResult.degraded()
    })

app.useHealthChecks("/health/critical") { options =>
    // 只运行带 critical 标签的检查项
    options.predicate = { registration =>
        var matched = false
        for (tag in registration.tags) {
            if (tag == "critical") {
                matched = true
            }
        }
        matched
    }
}

自定义响应

自定义状态码映射

cangjie
import soulsoft_extensions_healthchecks.*

app.useHealthChecks("/health") { options =>
    // 让 Degraded 也返回 503
    options.resultStatusCodes[HealthStatus.Degraded] = UInt16(503)
}

自定义响应体

cangjie
app.useHealthChecks("/health/detail") { options =>
    options.responseWriter = { context, report =>
        context.response.contentType = "application/json"
        context.response.write("{\"status\":\"${report.status}\"}")
    }
}

默认 responseWriter 会:

  • 设置 Content-Type: text/plain
  • 直接写出 HealthyDegradedUnhealthy

控制缓存头

默认会写入这些防缓存头:

  • Cache-Control: no-store, no-cache
  • Pragma: no-cache
  • Expires: Thu, 01 Jan 1970 00:00:00 GMT

如果你明确允许缓存响应,可以关闭它:

cangjie
app.useHealthChecks("/health/cache") { options =>
    options.allowCachingResponses = true
}

使用自定义检查类型

如果检查逻辑本身需要依赖注入,更合适的方式是实现 IHealthCheck

cangjie
import soulsoft_web_hosting.*
import soulsoft_web_healthchecks.*
import soulsoft_extensions_injection.*
import soulsoft_extensions_healthchecks.*

public class DatabaseHealthCheck <: IHealthCheck {
    public init() {
    }

    public func check(context: HealthCheckContext): HealthCheckResult {
        return HealthCheckResult.healthy()
    }
}

let builder = WebHost.createBuilder(args)

builder.services.addHealthChecks()
    .addCheck<DatabaseHealthCheck>("database")

let app = builder.build()
app.useHealthChecks("/health")

时序图

下面的时序图描述的是一次健康检查请求的执行过程。中间件方式和终结点方式的核心执行逻辑相同,都是进入 HealthCheckMiddleware

执行流程

执行时只要抓住五步:

  1. 入口先把请求导向 HealthCheckMiddleware
  2. 中间件调用 IHealthCheckService.checkHealth(predicate)
  3. predicate 决定哪些检查项参与本次聚合
  4. 聚合状态按 resultStatusCodes 映射成 HTTP 状态码
  5. 最后写缓存头和响应体

可以把默认结果理解成下面这样:

聚合状态默认 HTTP 状态码默认响应体
Healthy200Healthy
Degraded200Degraded
Unhealthy503Unhealthy

一个推荐写法

如果你的目标是“同时暴露存活检查和就绪检查”,推荐直接按标签拆两个端点:

cangjie
import soulsoft_web_hosting.*
import soulsoft_web_healthchecks.*
import soulsoft_extensions_injection.*
import soulsoft_extensions_healthchecks.*

let builder = WebHost.createBuilder(args)

builder.services.addHealthChecks()
    .addCheck("self", ["live"], {
        HealthCheckResult.healthy()
    })
    .addCheck("database", ["ready"], {
        HealthCheckResult.healthy()
    })

let app = builder.build()

// 存活检查:只确认进程自身可响应
app.useHealthChecks("/health/live") { options =>
    options.predicate = { registration =>
        var matched = false
        for (tag in registration.tags) {
            if (tag == "live") {
                matched = true
            }
        }
        matched
    }
}

// 就绪检查:确认依赖也已经准备好
app.useHealthChecks("/health/ready") { options =>
    options.predicate = { registration =>
        var matched = false
        for (tag in registration.tags) {
            if (tag == "ready") {
                matched = true
            }
        }
        matched
    }
}

这个模式的好处很直接:

  • 存活与就绪的语义分开
  • 各端点只执行自己关心的检查项
  • 状态码和响应体仍然走统一的健康检查中间件逻辑