fix(system): retired heartbeat rows no longer alarm on status page
- GET /system/health splits processes into current instances (hosts with
a beat in the last 10 min, newest per host+kind, not stopped) and
retiredProcesses[]; 45s stale and level only consider current ones
- heartbeat writer marks stopped_at on graceful exit (migration 0043),
upsert clears it; pure api role now stops its writer on SIGTERM
- scheduled job system.heartbeat_cleanup (maintain, 1h) deletes rows
without a beat for 24h and samples older than 7 days
- status page: per-role alarms ("io worker 45 秒没有心跳"), collapsible
retired instances; glossary/runbook updated
- bump all members to 0.2.2, CHANGELOG [0.2.2]
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
fa261eb6cd
commit
b4c9b1ead2
32 files changed
+747
-91
No files matched your search
@@ -2167,6 +2167,31 @@ export const Api = {
|
||||
query: browser.BrowserProvidersQuery,
|
||||
response: z.object({ items: z.array(browser.BrowserProviderView) }),
|
||||
}),
|
||||
createProvider: ep({
|
||||
method: "post",
|
||||
path: "/browser/providers",
|
||||
scope: "admin",
|
||||
summary: "登记浏览器服务(camofox 地址、访问凭据存凭证库、启用)",
|
||||
body: browser.BrowserProviderCreate,
|
||||
response: browser.BrowserProviderView,
|
||||
}),
|
||||
updateProvider: ep({
|
||||
method: "patch",
|
||||
path: "/browser/providers/{id}",
|
||||
scope: "admin",
|
||||
summary: "修改浏览器服务地址、访问凭据或启用(环境变量登记的只能改启用)",
|
||||
params: browser.BrowserProviderParams,
|
||||
body: browser.BrowserProviderUpdate,
|
||||
response: browser.BrowserProviderView,
|
||||
}),
|
||||
probeProvider: ep({
|
||||
method: "post",
|
||||
path: "/browser/providers/{id}/probe",
|
||||
scope: "admin",
|
||||
summary: "测试连通:探测该浏览器服务的 /health 与 /openapi.json 并写回能力",
|
||||
params: browser.BrowserProviderParams,
|
||||
response: browser.BrowserProviderView,
|
||||
}),
|
||||
sessions: ep({
|
||||
method: "get",
|
||||
path: "/browser/sessions",
|
||||
|
||||
@@ -240,8 +240,12 @@ export const BrowserProviderView = z.object({
|
||||
baseUrl: z.string().nullable(),
|
||||
/** 访问凭据的 secret 引用(明文永不出 API) */
|
||||
secretRef: Id.nullable(),
|
||||
/** env = 由 BROWSER_PROVIDER_URL / BROWSER_ACCESS_KEY 登记(地址与凭据只能改部署配置);null = 设置页登记 */
|
||||
managedBy: z.enum(["env"]).nullable(),
|
||||
/** 是否配置了访问凭据(secret 或环境变量) */
|
||||
hasAccessKey: z.boolean(),
|
||||
enabled: z.boolean(),
|
||||
/** 最近一次声明/探测到的能力;verified 只由人工核验写入 config.verified */
|
||||
/** 最近一次声明/探测到的能力;verified 只由人工核验写入(config.verified 或适配器内置的已核验版本表) */
|
||||
capabilities: BrowserCapabilities.nullable(),
|
||||
verifiedAt: Timestamp.nullable(),
|
||||
healthStatus: InstanceHealth,
|
||||
@@ -251,6 +255,30 @@ export const BrowserProviderView = z.object({
|
||||
});
|
||||
export type BrowserProviderView = z.infer<typeof BrowserProviderView>;
|
||||
|
||||
const HttpUrl = z
|
||||
.string()
|
||||
.url()
|
||||
.refine((u) => /^https?:\/\//i.test(u), "只支持 http(s) 地址");
|
||||
|
||||
/** 设置页登记浏览器服务(camofox);accessKey 明文只在请求里出现一次,落库为 secret */
|
||||
export const BrowserProviderCreate = z.object({
|
||||
name: z.string().min(1).max(64),
|
||||
baseUrl: HttpUrl,
|
||||
accessKey: z.string().min(1).max(512).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
});
|
||||
export type BrowserProviderCreate = z.infer<typeof BrowserProviderCreate>;
|
||||
|
||||
/** 改地址 / 凭据 / 启用;accessKey = null 清除凭据 */
|
||||
export const BrowserProviderUpdate = z.object({
|
||||
baseUrl: HttpUrl.optional(),
|
||||
accessKey: z.string().min(1).max(512).nullable().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
});
|
||||
export type BrowserProviderUpdate = z.infer<typeof BrowserProviderUpdate>;
|
||||
|
||||
export const BrowserProviderParams = z.object({ id: Id });
|
||||
|
||||
export const BrowserProvidersQuery = z.object({
|
||||
/** true = 现场探测 /health 与 /openapi.json 并写回 */
|
||||
probe: QueryBool,
|
||||
|
||||
@@ -381,8 +381,10 @@ export const SystemProcess = z.object({
|
||||
lastBeatAt: Timestamp,
|
||||
/** 距上次心跳秒数 */
|
||||
beatAgeSec: z.number().nonnegative(),
|
||||
/** 超过 45s 没心跳 */
|
||||
/** 超过 45s 没心跳(只对当前实例告警) */
|
||||
stale: z.boolean(),
|
||||
/** 进程正常退出时写入;非 null 即已下线 */
|
||||
stoppedAt: Timestamp.nullable(),
|
||||
/** 最近一次采样的事件循环延迟 */
|
||||
eventLoopLagMs: NonNegInt,
|
||||
/** 最近一个心跳周期(15s)内的最大延迟 */
|
||||
@@ -434,7 +436,10 @@ export const SystemHealth = z.object({
|
||||
checkedAt: Timestamp,
|
||||
/** 处理本请求的 api 进程 */
|
||||
api: z.object({ version: z.string(), startedAt: Timestamp, uptimeSec: z.number().nonnegative() }),
|
||||
/** 当前实例:最近 10 分钟内有心跳的 host 上、同 host 同 kind 最新启动且未退出的进程;stale 与告警只看这里 */
|
||||
processes: z.array(SystemProcess),
|
||||
/** 已下线实例:已正常退出、所在 host 10 分钟没有心跳(容器已重建)、或被同 host 新进程替换;不计入 level */
|
||||
retiredProcesses: z.array(SystemProcess),
|
||||
sse: z.object({ connections: NonNegInt }),
|
||||
db: z.object({
|
||||
level: HealthLevel,
|
||||
|
||||
Reference in new issue
Block a user