feat(health): добавить Globalping и мультивыбор источников проб
CD / update-wiki (push) Successful in 8s
quality / commitlint (push) Skipped
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
quality / web (push) Successful in 54s
quality / api (push) Successful in 46s
CD / quality (push) Successful in 1m49s
CD / publish (push) Successful in 1m40s
CD / update-wiki (push) Successful in 8s
quality / commitlint (push) Skipped
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
quality / web (push) Successful in 54s
quality / api (push) Successful in 46s
CD / quality (push) Successful in 1m49s
CD / publish (push) Successful in 1m40s
Несколько источников проб сразу и правило агрегации на сервисе вместо XOR Local/Cloudflare. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
4c4908558b
commit
b9bea44dce
31 files changed
+2671
-271
No files matched your search
@@ -0,0 +1,143 @@
|
||||
export const HEALTH_CHECK_PROVIDERS = [
|
||||
"local",
|
||||
"cloudflare",
|
||||
"globalping",
|
||||
] as const;
|
||||
|
||||
export type HealthCheckProvider = (typeof HEALTH_CHECK_PROVIDERS)[number];
|
||||
|
||||
export const HEALTH_STATUS_PROVIDERS = [
|
||||
...HEALTH_CHECK_PROVIDERS,
|
||||
"aggregate",
|
||||
] as const;
|
||||
|
||||
export type HealthStatusProvider = (typeof HEALTH_STATUS_PROVIDERS)[number];
|
||||
|
||||
export const HEALTH_CHECK_AGGREGATES = ["any", "all", "majority"] as const;
|
||||
|
||||
export type HealthCheckAggregate = (typeof HEALTH_CHECK_AGGREGATES)[number];
|
||||
|
||||
const PROVIDER_SET = new Set<string>(HEALTH_CHECK_PROVIDERS);
|
||||
const STATUS_SET = new Set<string>(HEALTH_STATUS_PROVIDERS);
|
||||
const AGGREGATE_SET = new Set<string>(HEALTH_CHECK_AGGREGATES);
|
||||
|
||||
export function normalizeProbeProvider(value: unknown): HealthCheckProvider {
|
||||
return value === "cloudflare" || value === "globalping" || value === "local"
|
||||
? value
|
||||
: "local";
|
||||
}
|
||||
|
||||
export function normalizeStatusProvider(value: unknown): HealthStatusProvider {
|
||||
if (typeof value === "string" && STATUS_SET.has(value)) {
|
||||
return value as HealthStatusProvider;
|
||||
}
|
||||
return "local";
|
||||
}
|
||||
|
||||
export function uniqueHealthProviders(
|
||||
values: readonly unknown[],
|
||||
): HealthCheckProvider[] {
|
||||
const out: HealthCheckProvider[] = [];
|
||||
for (const value of values) {
|
||||
if (!PROVIDER_SET.has(String(value))) continue;
|
||||
const next = value as HealthCheckProvider;
|
||||
if (!out.includes(next)) out.push(next);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function parseHealthProviders(
|
||||
json: unknown,
|
||||
fallback?: unknown,
|
||||
): HealthCheckProvider[] {
|
||||
if (Array.isArray(json)) {
|
||||
const parsed = uniqueHealthProviders(json);
|
||||
if (parsed.length > 0) return parsed;
|
||||
}
|
||||
if (typeof json === "string" && json.trim()) {
|
||||
const trimmed = json.trim();
|
||||
if (trimmed.startsWith("[")) {
|
||||
try {
|
||||
const parsed = uniqueHealthProviders(JSON.parse(trimmed) as unknown[]);
|
||||
if (parsed.length > 0) return parsed;
|
||||
} catch {
|
||||
// fall through to single-provider
|
||||
}
|
||||
}
|
||||
const one = uniqueHealthProviders(trimmed.split(","));
|
||||
if (one.length > 0) return one;
|
||||
}
|
||||
return [normalizeProbeProvider(fallback)];
|
||||
}
|
||||
|
||||
export function serializeHealthProviders(
|
||||
providers: readonly HealthCheckProvider[],
|
||||
): string {
|
||||
const unique = uniqueHealthProviders(providers);
|
||||
return JSON.stringify(unique.length > 0 ? unique : ["local"]);
|
||||
}
|
||||
|
||||
export function parseHealthAggregate(value: unknown): HealthCheckAggregate {
|
||||
if (typeof value === "string" && AGGREGATE_SET.has(value)) {
|
||||
return value as HealthCheckAggregate;
|
||||
}
|
||||
return "majority";
|
||||
}
|
||||
|
||||
export function derivePrimaryProvider(
|
||||
providers: readonly HealthCheckProvider[],
|
||||
): HealthCheckProvider {
|
||||
return uniqueHealthProviders(providers)[0] ?? "local";
|
||||
}
|
||||
|
||||
export function targetProviders(target: {
|
||||
providers?: readonly HealthCheckProvider[] | null;
|
||||
provider?: HealthCheckProvider | null;
|
||||
}): HealthCheckProvider[] {
|
||||
if (target.providers && target.providers.length > 0) {
|
||||
return uniqueHealthProviders(target.providers);
|
||||
}
|
||||
return [normalizeProbeProvider(target.provider)];
|
||||
}
|
||||
|
||||
export function targetHasProvider(
|
||||
target: {
|
||||
providers?: readonly HealthCheckProvider[] | null;
|
||||
provider?: HealthCheckProvider | null;
|
||||
},
|
||||
provider: HealthCheckProvider,
|
||||
): boolean {
|
||||
return targetProviders(target).includes(provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* any — Down if at least one source is Down (ok only if all ok).
|
||||
* all — Down only if every source is Down (ok if any ok).
|
||||
* majority — Down if a strict majority of sources are Down (2 → both, 3 → ≥2).
|
||||
*/
|
||||
export function aggregateHealthOk(
|
||||
oks: readonly boolean[],
|
||||
policy: HealthCheckAggregate,
|
||||
): boolean {
|
||||
const n = oks.length;
|
||||
if (n === 0) return false;
|
||||
const down = oks.filter((ok) => !ok).length;
|
||||
if (policy === "any") return down === 0;
|
||||
if (policy === "all") return down < n;
|
||||
return down < Math.floor(n / 2) + 1;
|
||||
}
|
||||
|
||||
export function clampGlobalpingLimit(value: unknown, fallback = 3): number {
|
||||
const n = typeof value === "number" ? value : Number(value);
|
||||
if (!Number.isFinite(n)) return fallback;
|
||||
return Math.min(10, Math.max(1, Math.trunc(n)));
|
||||
}
|
||||
|
||||
export function parseGlobalpingLocations(value: unknown): string[] {
|
||||
const raw = typeof value === "string" ? value : "";
|
||||
const parts = raw
|
||||
.split(",")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
return parts.length > 0 ? parts : ["World"];
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export * from "./schemas.js";
|
||||
export * from "./app-switcher.js";
|
||||
export * from "./integration-vps-tracker.js";
|
||||
export * from "./health-probe-mailbox.js";
|
||||
export * from "./health-providers.js";
|
||||
export * from "./audit.js";
|
||||
export type {
|
||||
CfZone,
|
||||
@@ -23,7 +24,6 @@ export type {
|
||||
HealthCheckType,
|
||||
IpHealthState,
|
||||
NodeHealthState,
|
||||
HealthCheckProvider,
|
||||
HealthCheckScope,
|
||||
IpHealthStatus,
|
||||
HealthCheckTarget,
|
||||
|
||||
@@ -37,6 +37,9 @@ export const appSettingsPatchSchema = z.object({
|
||||
healthSuccessRecoveries: z.number().int().min(1).max(20).optional(),
|
||||
healthWorkerUrl: z.string().url().or(z.literal("")).optional(),
|
||||
healthWorkerToken: z.string().optional(),
|
||||
globalpingToken: z.string().optional(),
|
||||
globalpingLocations: z.string().trim().max(200).optional(),
|
||||
globalpingLimit: z.number().int().min(1).max(10).optional(),
|
||||
}).superRefine((data, ctx) => {
|
||||
if (
|
||||
data.healthDegradedFailures != null &&
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
HEALTH_CHECK_AGGREGATES,
|
||||
HEALTH_CHECK_PROVIDERS,
|
||||
HEALTH_STATUS_PROVIDERS,
|
||||
uniqueHealthProviders,
|
||||
} from './health-providers.js'
|
||||
|
||||
export const certMonitoringSchema = z.enum(['auto', 'required', 'skipped'])
|
||||
|
||||
@@ -29,8 +35,19 @@ export const nodeHealthStateSchema = z.enum([
|
||||
])
|
||||
export type NodeHealthState = z.infer<typeof nodeHealthStateSchema>
|
||||
|
||||
export const healthCheckProviderSchema = z.enum(['local', 'cloudflare'])
|
||||
export type HealthCheckProvider = z.infer<typeof healthCheckProviderSchema>
|
||||
export const healthCheckProviderSchema = z.enum(HEALTH_CHECK_PROVIDERS)
|
||||
|
||||
export const healthStatusProviderSchema = z.enum(HEALTH_STATUS_PROVIDERS)
|
||||
|
||||
export const healthCheckAggregateSchema = z.enum(HEALTH_CHECK_AGGREGATES)
|
||||
|
||||
export const healthCheckProvidersSchema = z
|
||||
.array(healthCheckProviderSchema)
|
||||
.min(1)
|
||||
.transform((arr) => {
|
||||
const unique = uniqueHealthProviders(arr)
|
||||
return unique.length > 0 ? unique : (['local'] as const)
|
||||
})
|
||||
|
||||
export const healthCheckScopeSchema = z.enum(['binding', 'group'])
|
||||
export type HealthCheckScope = z.infer<typeof healthCheckScopeSchema>
|
||||
@@ -46,7 +63,7 @@ export const ipHealthStatusSchema = z.object({
|
||||
last_checked_at: z.string().nullable(),
|
||||
last_error: z.string().nullable(),
|
||||
colo: z.string().nullable().optional(),
|
||||
provider: healthCheckProviderSchema.optional(),
|
||||
provider: healthStatusProviderSchema.optional(),
|
||||
})
|
||||
|
||||
export type IpHealthStatus = z.infer<typeof ipHealthStatusSchema>
|
||||
@@ -57,7 +74,7 @@ export const serviceIpHealthSchema = z.object({
|
||||
latency_ms: z.number().nullable(),
|
||||
last_checked_at: z.string().nullable().optional(),
|
||||
last_error: z.string().nullable().optional(),
|
||||
provider: healthCheckProviderSchema.optional(),
|
||||
provider: healthStatusProviderSchema.optional(),
|
||||
colo: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
@@ -116,6 +133,8 @@ export const serviceGroupSchema = z.object({
|
||||
health_check_timeout_ms: z.number().default(3000),
|
||||
health_check_verify_tls: z.coerce.boolean().default(false),
|
||||
health_check_provider: healthCheckProviderSchema.catch('local'),
|
||||
health_check_providers: healthCheckProvidersSchema.catch(['local']),
|
||||
health_check_aggregate: healthCheckAggregateSchema.catch('majority'),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
})
|
||||
@@ -157,6 +176,8 @@ export const serviceDomainBindingSchema = z
|
||||
health_check_timeout_ms: z.number().default(3000),
|
||||
health_check_verify_tls: z.coerce.boolean().default(false),
|
||||
health_check_provider: healthCheckProviderSchema.catch('local'),
|
||||
health_check_providers: healthCheckProvidersSchema.catch(['local']),
|
||||
health_check_aggregate: healthCheckAggregateSchema.catch('majority'),
|
||||
sync_status: z.string().nullable().default(null),
|
||||
})
|
||||
.transform((binding) => ({
|
||||
@@ -329,6 +350,8 @@ const healthCheckConfigFields = {
|
||||
health_check_timeout_ms: z.number().int().min(100).max(30000).optional(),
|
||||
health_check_verify_tls: z.boolean().optional(),
|
||||
health_check_provider: healthCheckProviderSchema.optional(),
|
||||
health_check_providers: healthCheckProvidersSchema.optional(),
|
||||
health_check_aggregate: healthCheckAggregateSchema.optional(),
|
||||
}
|
||||
|
||||
export const healthCheckConfigSchema = z.object(healthCheckConfigFields)
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
import type {
|
||||
HealthCheckProvider,
|
||||
HealthCheckAggregate,
|
||||
HealthStatusProvider,
|
||||
} from "./health-providers.js";
|
||||
|
||||
export type {
|
||||
HealthCheckProvider,
|
||||
HealthCheckAggregate,
|
||||
HealthStatusProvider,
|
||||
} from "./health-providers.js";
|
||||
|
||||
export interface Group {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -23,6 +35,8 @@ export interface ServiceGroup {
|
||||
health_check_timeout_ms: number;
|
||||
health_check_verify_tls: boolean;
|
||||
health_check_provider: HealthCheckProvider;
|
||||
health_check_providers: HealthCheckProvider[];
|
||||
health_check_aggregate: HealthCheckAggregate;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -123,6 +137,8 @@ export interface ServiceBinding {
|
||||
health_check_timeout_ms: number;
|
||||
health_check_verify_tls: boolean;
|
||||
health_check_provider: HealthCheckProvider;
|
||||
health_check_providers: HealthCheckProvider[];
|
||||
health_check_aggregate: HealthCheckAggregate;
|
||||
routing_strategy: LbMode;
|
||||
operation_version: number;
|
||||
created_at: string;
|
||||
@@ -155,6 +171,8 @@ export interface ServiceBindingView {
|
||||
health_check_timeout_ms: number;
|
||||
health_check_verify_tls: boolean;
|
||||
health_check_provider: HealthCheckProvider;
|
||||
health_check_providers: HealthCheckProvider[];
|
||||
health_check_aggregate: HealthCheckAggregate;
|
||||
sync_status: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -181,6 +199,8 @@ export interface ServiceDomainBindingView {
|
||||
health_check_timeout_ms: number;
|
||||
health_check_verify_tls: boolean;
|
||||
health_check_provider: HealthCheckProvider;
|
||||
health_check_providers: HealthCheckProvider[];
|
||||
health_check_aggregate: HealthCheckAggregate;
|
||||
sync_status: string | null;
|
||||
}
|
||||
|
||||
@@ -284,8 +304,6 @@ export type NodeHealthState =
|
||||
| "unhealthy"
|
||||
| "disabled";
|
||||
|
||||
export type HealthCheckProvider = "local" | "cloudflare";
|
||||
|
||||
export type HealthCheckScope = "binding" | "group";
|
||||
|
||||
export interface IpHealthStatus {
|
||||
@@ -299,7 +317,7 @@ export interface IpHealthStatus {
|
||||
last_checked_at: string | null;
|
||||
last_error: string | null;
|
||||
colo?: string | null;
|
||||
provider?: HealthCheckProvider;
|
||||
provider?: HealthStatusProvider;
|
||||
}
|
||||
|
||||
export interface ServiceIpHealth {
|
||||
@@ -308,7 +326,7 @@ export interface ServiceIpHealth {
|
||||
latency_ms: number | null;
|
||||
last_checked_at?: string | null;
|
||||
last_error?: string | null;
|
||||
provider?: HealthCheckProvider;
|
||||
provider?: HealthStatusProvider;
|
||||
colo?: string | null;
|
||||
}
|
||||
|
||||
@@ -394,4 +412,6 @@ export interface HealthCheckTarget {
|
||||
timeout_ms: number;
|
||||
verify_tls: boolean;
|
||||
provider: HealthCheckProvider;
|
||||
providers?: HealthCheckProvider[];
|
||||
aggregate?: HealthCheckAggregate;
|
||||
}
|
||||
Reference in new issue
Block a user