diff --git a/cmd/orbit/main.go b/cmd/orbit/main.go index d8cbd8a..fd095b5 100644 --- a/cmd/orbit/main.go +++ b/cmd/orbit/main.go @@ -63,6 +63,12 @@ func run() error { } defer metricsListener.Close() metrics = observability.NewRegistry() + metrics.RecordRuntimeConfig(observability.RuntimeConfig{ + WorkflowTickInterval: cfg.WorkflowTickInterval, + ScheduleTickInterval: cfg.ScheduleTickInterval, + RecoveryTickInterval: cfg.RecoveryTickInterval, + BatchLimit: cfg.RuntimeBatchLimit, + }) metrics.RecordDatabase(db.Stats()) } ready := func(ctx context.Context) error { diff --git a/docs/api/observability.md b/docs/api/observability.md index 7ec048a..195fcf4 100644 --- a/docs/api/observability.md +++ b/docs/api/observability.md @@ -44,6 +44,8 @@ curl -fsS http://127.0.0.1:9090/metrics | `orbit_runtime_batch_total` | counter | 后台批次返回次数,标签component及outcome | | `orbit_runtime_batch_duration_seconds` | histogram | 后台批次调用耗时,标签component | | `orbit_runtime_batch_last_success_timestamp_seconds` | gauge | 最后一次无错误批次返回的Unix时间;启动后未成功时为0 | +| `orbit_runtime_tick_interval_seconds{component}` | gauge | 节点实际生效的后台扫描间隔,component为workflow、schedule、execution_recovery | +| `orbit_runtime_batch_limit` | gauge | 节点实际生效的后台批量上限 | | `orbit_db_connections_open` / `orbit_db_connections_in_use` / `orbit_db_connections_idle` | gauge | 本节点MySQL连接池当前连接数 | | `orbit_db_connections_max_open` | gauge | 池配置的最大连接数 | | `orbit_db_wait_count_total` / `orbit_db_wait_duration_seconds_total` | counter | 连接池累计等待次数/秒数,不是数据库锁等待 | diff --git a/internal/observability/catalog.go b/internal/observability/catalog.go index de8f059..98d6cbc 100644 --- a/internal/observability/catalog.go +++ b/internal/observability/catalog.go @@ -62,5 +62,7 @@ func catalog() map[string]descriptor { c["orbit_business_snapshot_duration_seconds"] = descriptor{histogram, "Bounded business-state snapshot duration in seconds", nil} c["orbit_business_snapshot_last_success_timestamp_seconds"] = descriptor{gauge, "Unix time of the last successful bounded business-state snapshot", nil} c["orbit_business_snapshot_truncated"] = descriptor{gauge, "Whether a bounded business-state sample hit its per-state cap", nil} + c["orbit_runtime_tick_interval_seconds"] = descriptor{gauge, "Configured background controller tick interval in seconds", map[string][]string{"component": components}} + c["orbit_runtime_batch_limit"] = descriptor{gauge, "Configured maximum candidates per background batch", nil} return c } diff --git a/internal/observability/metrics_test.go b/internal/observability/metrics_test.go index 1efa2d6..e5654b1 100644 --- a/internal/observability/metrics_test.go +++ b/internal/observability/metrics_test.go @@ -96,6 +96,7 @@ func TestInvalidNumbersAndMonotoneCounters(t *testing.T) { } func TestRuntimeObservationDoesNotExposeErrors(t *testing.T) { r := NewRegistry() + r.RecordRuntimeConfig(RuntimeConfig{WorkflowTickInterval: time.Second, ScheduleTickInterval: 1500 * time.Millisecond, RecoveryTickInterval: 2 * time.Second, BatchLimit: 37}) r.RecordBatch("workflow", time.Now().Add(-time.Millisecond), nil) r.RecordBatch("workflow", time.Now(), errors.New("private-dsn")) r.RecordBatch("schedule", time.Now(), context.Canceled) @@ -106,6 +107,11 @@ func TestRuntimeObservationDoesNotExposeErrors(t *testing.T) { t.Fatal("missing runtime evidence", want) } } + for _, want := range []string{`orbit_runtime_tick_interval_seconds{component="workflow"} 1`, `orbit_runtime_tick_interval_seconds{component="schedule"} 1.5`, `orbit_runtime_tick_interval_seconds{component="execution_recovery"} 2`, "orbit_runtime_batch_limit 37\n"} { + if !strings.Contains(out, want) { + t.Fatal("missing runtime configuration", want) + } + } if !strings.Contains(out, `orbit_build_info{go_version="`+runtime.Version()+`",revision="unknown",version="dev"} 1`) { t.Fatal("missing build identity") } diff --git a/internal/observability/runtime.go b/internal/observability/runtime.go index 7072c27..8e86b7f 100644 --- a/internal/observability/runtime.go +++ b/internal/observability/runtime.go @@ -10,6 +10,26 @@ import ( "git.yuntuo.hk/k2safe/orbit/internal/buildinfo" ) +// RuntimeConfig 是节点启动后实际生效的后台控制参数。 +// 它只用于固定基数观测,不参与业务授权或持久化状态判断。 +type RuntimeConfig struct { + WorkflowTickInterval time.Duration + ScheduleTickInterval time.Duration + RecoveryTickInterval time.Duration + BatchLimit int +} + +// RecordRuntimeConfig records the effective bounded controller settings. +func (r *Registry) RecordRuntimeConfig(config RuntimeConfig) { + if r == nil || config.WorkflowTickInterval <= 0 || config.ScheduleTickInterval <= 0 || config.RecoveryTickInterval <= 0 || config.BatchLimit <= 0 { + return + } + r.SetGauge("orbit_runtime_tick_interval_seconds", config.WorkflowTickInterval.Seconds(), Labels{"component": "workflow"}) + r.SetGauge("orbit_runtime_tick_interval_seconds", config.ScheduleTickInterval.Seconds(), Labels{"component": "schedule"}) + r.SetGauge("orbit_runtime_tick_interval_seconds", config.RecoveryTickInterval.Seconds(), Labels{"component": "execution_recovery"}) + r.SetGauge("orbit_runtime_batch_limit", float64(config.BatchLimit), nil) +} + // initializeProcess is called once by the registry constructor. func (r *Registry) initializeProcess() { if r == nil {