feat(monitoring): generate daily tasks and optimize heartbeat writes

Adds a MonitoringDailyTaskWorker that materializes and dispatches
daily collect tasks from active subscription/desktop-client state,
with per-run atomic metrics and a Prometheus collector. Dashboard
and question-detail APIs now surface a platform_authorization_status
so the UI can distinguish no-desktop-client, no-authorized-platforms,
and authorized states; quota/access-state lookups resolve by request
workspace instead of tenant.

Hardens heartbeat: a desktop_client_primary_leases table gives
per-(tenant, workspace) sticky primary selection, read-mostly lease
resolution avoids a per-heartbeat transaction, and unchanged platform
access reports skip snapshot upserts outside a 10-minute refresh
window. Adds heartbeat primary/snapshot metrics exposed via
token-protected tenant-api /api/internal/metrics endpoints plus
Prometheus. Monitoring quota seed is removed from dev-seed since
daily plans now derive from desktop bindings, and the projection
uses the canonical six-platform catalog so platforms with zero
samples still render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 22:19:57 +08:00
parent ffe4d335aa
commit ff86933c24
26 changed files with 3546 additions and 277 deletions
+17
View File
@@ -105,6 +105,9 @@ type RabbitMQConfig struct {
}
type SchedulerConfig struct {
HTTPHost string `mapstructure:"http_host"`
HTTPPort int `mapstructure:"http_port"`
InternalMetricsToken string `mapstructure:"internal_metrics_token"`
DispatchInterval time.Duration `mapstructure:"dispatch_interval"`
DispatchTimeout time.Duration `mapstructure:"dispatch_timeout"`
DispatchBatchSize int `mapstructure:"dispatch_batch_size"`
@@ -427,6 +430,12 @@ func applyEnvOverrides(cfg *Config) {
if model, ok := lookupFirstNonEmptyEnv("SILICONFLOW_RERANKER_MODEL", "RETRIEVAL_RERANKER_MODEL"); ok {
cfg.Retrieval.RerankerModel = model
}
if host, ok := lookupNonEmptyEnv("SCHEDULER_HTTP_HOST"); ok {
cfg.Scheduler.HTTPHost = host
}
if token, ok := lookupFirstNonEmptyEnv("SCHEDULER_INTERNAL_METRICS_TOKEN", "SCHEDULER_METRICS_TOKEN"); ok {
cfg.Scheduler.InternalMetricsToken = token
}
}
func normalizeRabbitMQConfig(cfg *RabbitMQConfig) {
@@ -569,6 +578,14 @@ func normalizeSchedulerConfig(cfg *SchedulerConfig) {
return
}
if cfg.HTTPPort == 0 {
cfg.HTTPPort = 8081
}
cfg.HTTPHost = strings.TrimSpace(cfg.HTTPHost)
if cfg.HTTPHost == "" {
cfg.HTTPHost = "127.0.0.1"
}
cfg.InternalMetricsToken = strings.TrimSpace(cfg.InternalMetricsToken)
if cfg.DispatchInterval <= 0 {
cfg.DispatchInterval = 15 * time.Second
}
@@ -114,6 +114,58 @@ brand_library: {}
}
}
func TestLoadAppliesSchedulerHTTPDefaultsAndDisableSentinel(t *testing.T) {
defaultConfigPath := writeTestConfig(t, `
scheduler: {}
`)
cfg, err := Load(defaultConfigPath)
if err != nil {
t.Fatalf("Load() default error = %v", err)
}
if cfg.Scheduler.HTTPHost != "127.0.0.1" {
t.Fatalf("expected default scheduler http host 127.0.0.1, got %q", cfg.Scheduler.HTTPHost)
}
if cfg.Scheduler.HTTPPort != 8081 {
t.Fatalf("expected default scheduler http port 8081, got %d", cfg.Scheduler.HTTPPort)
}
disabledConfigPath := writeTestConfig(t, `
scheduler:
http_port: -1
`)
cfg, err = Load(disabledConfigPath)
if err != nil {
t.Fatalf("Load() disabled error = %v", err)
}
if cfg.Scheduler.HTTPPort != -1 {
t.Fatalf("expected scheduler http port -1 to disable server, got %d", cfg.Scheduler.HTTPPort)
}
}
func TestLoadPrefersEnvSchedulerMetricsSettings(t *testing.T) {
t.Setenv("SCHEDULER_HTTP_HOST", "0.0.0.0")
t.Setenv("SCHEDULER_INTERNAL_METRICS_TOKEN", "env-metrics-token")
configPath := writeTestConfig(t, `
scheduler:
http_host: 127.0.0.1
internal_metrics_token: config-token
`)
cfg, err := Load(configPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if cfg.Scheduler.HTTPHost != "0.0.0.0" {
t.Fatalf("expected env scheduler http host, got %q", cfg.Scheduler.HTTPHost)
}
if cfg.Scheduler.InternalMetricsToken != "env-metrics-token" {
t.Fatalf("expected env scheduler metrics token, got %q", cfg.Scheduler.InternalMetricsToken)
}
}
func TestLoadAppliesMembershipDefaults(t *testing.T) {
configPath := writeTestConfig(t, `
membership: {}