diff --git a/cmd/api_helpers.go b/cmd/api_helpers.go index c664f396..52f4762b 100644 --- a/cmd/api_helpers.go +++ b/cmd/api_helpers.go @@ -77,7 +77,6 @@ type natsBundle struct { jobsKV jetstream.KeyValue registryKV jetstream.KeyValue factsKV jetstream.KeyValue - stateKV jetstream.KeyValue objStore file.ObjectStoreManager } @@ -115,10 +114,10 @@ func setupAPIServer( ) checker := newHealthChecker(b.nc, b.jobsKV) - auditStore, auditKV, serverOpts := createAuditStore(ctx, log, b.nc, namespace) - metricsProvider := newMetricsProvider( - b.nc, b.jobsKV, b.registryKV, b.factsKV, b.stateKV, auditKV, streamName, b.jobClient, - ) + auditStore, serverOpts := createAuditStore(ctx, log, b.nc, namespace) + kvBuckets := configuredKVBuckets(namespace) + objBuckets := configuredObjectBuckets(namespace) + metricsProvider := newMetricsProvider(b.nc, streamName, kvBuckets, objBuckets, b.jobClient) sm := api.New(appConfig, log, serverOpts...) registerAPIHandlers( @@ -208,7 +207,6 @@ func connectNATSBundle( jobsKV: jobsKV, registryKV: registryKV, factsKV: factsKV, - stateKV: stateKV, objStore: objStore, } } @@ -241,14 +239,46 @@ func newHealthChecker( } } +// configuredKVBuckets returns the namespaced names of all KV buckets +// declared in osapi.yaml. Only non-empty bucket configs are included. +func configuredKVBuckets(namespace string) []string { + var buckets []string + add := func(name string) { + if name != "" { + buckets = append(buckets, job.ApplyNamespaceToInfraName(namespace, name)) + } + } + + add(appConfig.NATS.KV.Bucket) + add(appConfig.NATS.KV.ResponseBucket) + add(appConfig.NATS.Audit.Bucket) + add(appConfig.NATS.Registry.Bucket) + add(appConfig.NATS.Facts.Bucket) + add(appConfig.NATS.State.Bucket) + add(appConfig.NATS.FileState.Bucket) + + return buckets +} + +// configuredObjectBuckets returns the namespaced names of all Object Store +// buckets declared in osapi.yaml. +func configuredObjectBuckets(namespace string) []string { + var buckets []string + if appConfig.NATS.Objects.Bucket != "" { + buckets = append( + buckets, + job.ApplyNamespaceToInfraName(namespace, appConfig.NATS.Objects.Bucket), + ) + } + + return buckets +} + func newMetricsProvider( nc messaging.NATSClient, - jobsKV jetstream.KeyValue, - registryKV jetstream.KeyValue, - factsKV jetstream.KeyValue, - stateKV jetstream.KeyValue, - auditKV jetstream.KeyValue, streamName string, + kvBuckets []string, + objBuckets []string, jc jobclient.JobClient, ) *health.ClosureMetricsProvider { return &health.ClosureMetricsProvider{ @@ -285,19 +315,21 @@ func newMetricsProvider( }, nil }, KVInfoFn: func(fnCtx context.Context) ([]health.KVMetrics, error) { - buckets := []jetstream.KeyValue{jobsKV, registryKV, factsKV, stateKV, auditKV} - results := make([]health.KVMetrics, 0, len(buckets)) + natsConn, ok := nc.(*natsclient.Client) + if !ok || natsConn.ExtJS == nil { + return nil, fmt.Errorf("jetstream client unavailable") + } - for _, kv := range buckets { - if kv == nil { + results := make([]health.KVMetrics, 0, len(kvBuckets)) + for _, name := range kvBuckets { + kv, err := natsConn.ExtJS.KeyValue(fnCtx, name) + if err != nil { continue } - status, err := kv.Status(fnCtx) if err != nil { continue } - results = append(results, health.KVMetrics{ Name: status.Bucket(), Keys: int(status.Values()), @@ -307,6 +339,30 @@ func newMetricsProvider( return results, nil }, + ObjectStoreInfoFn: func(fnCtx context.Context) ([]health.ObjectStoreMetrics, error) { + natsConn, ok := nc.(*natsclient.Client) + if !ok || natsConn.ExtJS == nil { + return nil, fmt.Errorf("jetstream client unavailable") + } + + results := make([]health.ObjectStoreMetrics, 0, len(objBuckets)) + for _, name := range objBuckets { + obj, err := natsConn.ExtJS.ObjectStore(fnCtx, name) + if err != nil { + continue + } + status, err := obj.Status(fnCtx) + if err != nil { + continue + } + results = append(results, health.ObjectStoreMetrics{ + Name: status.Bucket(), + Size: status.Size(), + }) + } + + return results, nil + }, ConsumerStatsFn: func(fnCtx context.Context) (*health.ConsumerMetrics, error) { natsConn, ok := nc.(*natsclient.Client) if !ok || natsConn.ExtJS == nil { @@ -389,9 +445,9 @@ func createAuditStore( log *slog.Logger, nc messaging.NATSClient, namespace string, -) (audit.Store, jetstream.KeyValue, []api.Option) { +) (audit.Store, []api.Option) { if appConfig.NATS.Audit.Bucket == "" { - return nil, nil, nil + return nil, nil } auditKVConfig := cli.BuildAuditKVConfig(namespace, appConfig.NATS.Audit) @@ -402,7 +458,7 @@ func createAuditStore( store := audit.NewKVStore(log, auditKV) - return store, auditKV, []api.Option{api.WithAuditStore(store)} + return store, []api.Option{api.WithAuditStore(store)} } func registerAPIHandlers( diff --git a/cmd/client_health_status.go b/cmd/client_health_status.go index fa1702f3..33b2cd0e 100644 --- a/cmd/client_health_status.go +++ b/cmd/client_health_status.go @@ -143,6 +143,14 @@ func displayStatusHealth( )) } + // Object Stores + for _, o := range data.ObjectStores { + cli.PrintKV("Object Store", fmt.Sprintf( + "%s "+cli.DimStyle.Render("(%s)"), + o.Name, cli.FormatBytes(o.Size), + )) + } + // Consumers last — the table can be long with many agents if data.Consumers != nil { fmt.Println() diff --git a/docs/docs/gen/api/get-health-status.api.mdx b/docs/docs/gen/api/get-health-status.api.mdx index 631aa310..0978488c 100644 --- a/docs/docs/gen/api/get-health-status.api.mdx +++ b/docs/docs/gen/api/get-health-status.api.mdx @@ -5,7 +5,7 @@ description: "Returns per-component health status with system metrics. Requires sidebar_label: "System status and component health" hide_title: true hide_table_of_contents: true -api: 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 +api: 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 sidebar_class_name: "get api-method" info_path: gen/api/agent-management-api custom_edit_url: null @@ -429,6 +429,74 @@ Returns per-component health status with system metrics. Requires authentication + + + + + object_stores + + object[] + + + + + + + Object Store statistics. + + + + + Array [ + + + + + + + + ] + + + + + @@ -1341,6 +1409,74 @@ Returns per-component health status with system metrics. Requires authentication + + + + + object_stores + + object[] + + + + + + + Object Store statistics. + + + + + Array [ + + + + + + + + ] + + + + + diff --git a/go.mod b/go.mod index 915686bf..9232de7c 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/oapi-codegen/runtime v1.2.0 github.com/osapi-io/nats-client v0.0.0-20260306210421-d68b2a0f287b github.com/osapi-io/nats-server v0.0.0-20260216201410-1f33dfc63848 - github.com/osapi-io/osapi-sdk v0.0.0-20260307055727-ba9d92f92610 + github.com/osapi-io/osapi-sdk v0.0.0-20260307073158-439e543a3013 github.com/prometheus-community/pro-bing v0.8.0 github.com/prometheus/client_golang v1.23.2 github.com/samber/slog-echo v1.21.0 diff --git a/go.sum b/go.sum index c3bd1a96..4298d6a0 100644 --- a/go.sum +++ b/go.sum @@ -755,8 +755,8 @@ github.com/osapi-io/nats-client v0.0.0-20260306210421-d68b2a0f287b h1:d68ZLQLxJW github.com/osapi-io/nats-client v0.0.0-20260306210421-d68b2a0f287b/go.mod h1:66M9jRN03gZezKNttR17FCRZyLdF7E0BvBLitfrJl38= github.com/osapi-io/nats-server v0.0.0-20260216201410-1f33dfc63848 h1:ELW1sTVBn5JIc17mHgd5fhpO3/7btaxJpxykG2Fe0U4= github.com/osapi-io/nats-server v0.0.0-20260216201410-1f33dfc63848/go.mod h1:4rzeY9jiJF/+Ej4WNwqK5HQ2sflZrEs60GxQpg3Iya8= -github.com/osapi-io/osapi-sdk v0.0.0-20260307055727-ba9d92f92610 h1:79ExRL8H8JsmIqi178benv+jwH28EU12N/HjZ+hiO3c= -github.com/osapi-io/osapi-sdk v0.0.0-20260307055727-ba9d92f92610/go.mod h1:i9g4jaIL6NVo9MRpz33lAEnY4L7u6aO97/5hN4W3hGE= +github.com/osapi-io/osapi-sdk v0.0.0-20260307073158-439e543a3013 h1:kcP1brAYrbrETk+8jgJKyGE8NI0zIvSg3hT5Y1oviT4= +github.com/osapi-io/osapi-sdk v0.0.0-20260307073158-439e543a3013/go.mod h1:i9g4jaIL6NVo9MRpz33lAEnY4L7u6aO97/5hN4W3hGE= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= diff --git a/internal/api/gen/api.yaml b/internal/api/gen/api.yaml index c51c0678..36c5a29e 100644 --- a/internal/api/gen/api.yaml +++ b/internal/api/gen/api.yaml @@ -2274,6 +2274,20 @@ components: - name - keys - bytes + ObjectStoreInfo: + type: object + properties: + name: + type: string + description: Object Store bucket name. + example: file-objects + size: + type: integer + description: Total bytes in the store. + example: 5242880 + required: + - name + - size JobStats: type: object properties: @@ -2415,6 +2429,11 @@ components: items: $ref: '#/components/schemas/KVBucketInfo' description: KV bucket statistics. + object_stores: + type: array + items: + $ref: '#/components/schemas/ObjectStoreInfo' + description: Object Store statistics. jobs: $ref: '#/components/schemas/JobStats' agents: diff --git a/internal/api/health/gen/api.yaml b/internal/api/health/gen/api.yaml index b7d05fb4..82f29c46 100644 --- a/internal/api/health/gen/api.yaml +++ b/internal/api/health/gen/api.yaml @@ -205,6 +205,21 @@ components: - keys - bytes + ObjectStoreInfo: + type: object + properties: + name: + type: string + description: Object Store bucket name. + example: "file-objects" + size: + type: integer + description: Total bytes in the store. + example: 5242880 + required: + - name + - size + JobStats: type: object properties: @@ -353,6 +368,11 @@ components: items: $ref: '#/components/schemas/KVBucketInfo' description: KV bucket statistics. + object_stores: + type: array + items: + $ref: '#/components/schemas/ObjectStoreInfo' + description: Object Store statistics. jobs: $ref: '#/components/schemas/JobStats' agents: diff --git a/internal/api/health/gen/health.gen.go b/internal/api/health/gen/health.gen.go index a11186ce..d31355be 100644 --- a/internal/api/health/gen/health.gen.go +++ b/internal/api/health/gen/health.gen.go @@ -126,6 +126,15 @@ type NATSInfo struct { Version string `json:"version"` } +// ObjectStoreInfo defines model for ObjectStoreInfo. +type ObjectStoreInfo struct { + // Name Object Store bucket name. + Name string `json:"name"` + + // Size Total bytes in the store. + Size int `json:"size"` +} + // ReadyResponse defines model for ReadyResponse. type ReadyResponse struct { // Error Error message when not ready. @@ -148,6 +157,9 @@ type StatusResponse struct { KvBuckets *[]KVBucketInfo `json:"kv_buckets,omitempty"` Nats *NATSInfo `json:"nats,omitempty"` + // ObjectStores Object Store statistics. + ObjectStores *[]ObjectStoreInfo `json:"object_stores,omitempty"` + // Status Overall health status. Status string `json:"status"` diff --git a/internal/api/health/health_status_get.go b/internal/api/health/health_status_get.go index f6e4b4e2..a31d05dd 100644 --- a/internal/api/health/health_status_get.go +++ b/internal/api/health/health_status_get.go @@ -104,6 +104,7 @@ func (h *Health) populateMetrics( natsInfo *NATSMetrics streams []StreamMetrics kvBuckets []KVMetrics + objectStores []ObjectStoreMetrics jobStats *JobMetrics agentStats *AgentMetrics consumerStats *ConsumerMetrics @@ -154,6 +155,17 @@ func (h *Health) populateMetrics( mu.Unlock() }) + collect("object-stores", func() { + o, err := h.Metrics.GetObjectStoreInfo(ctx) + if err != nil { + h.logger.Warn("failed to get Object Store info for status", "error", err) + return + } + mu.Lock() + objectStores = o + mu.Unlock() + }) + collect("jobs", func() { j, err := h.Metrics.GetJobStats(ctx) if err != nil { @@ -222,6 +234,17 @@ func (h *Health) populateMetrics( resp.KvBuckets = &bucketInfos } + if objectStores != nil { + infos := make([]gen.ObjectStoreInfo, 0, len(objectStores)) + for _, o := range objectStores { + infos = append(infos, gen.ObjectStoreInfo{ + Name: o.Name, + Size: int(o.Size), + }) + } + resp.ObjectStores = &infos + } + if jobStats != nil { resp.Jobs = &gen.JobStats{ Total: jobStats.Total, diff --git a/internal/api/health/health_status_get_public_test.go b/internal/api/health/health_status_get_public_test.go index f585cfc0..1b3f4b52 100644 --- a/internal/api/health/health_status_get_public_test.go +++ b/internal/api/health/health_status_get_public_test.go @@ -192,6 +192,11 @@ func (s *HealthStatusGetPublicTestSuite) TestGetHealthStatus() { {Name: "job-queue", Keys: 10, Bytes: 2048}, }, nil }, + ObjectStoreInfoFn: func(_ context.Context) ([]health.ObjectStoreMetrics, error) { + return []health.ObjectStoreMetrics{ + {Name: "file-objects", Size: 5242880}, + }, nil + }, ConsumerStatsFn: func(_ context.Context) (*health.ConsumerMetrics, error) { return &health.ConsumerMetrics{ Total: 2, @@ -237,6 +242,11 @@ func (s *HealthStatusGetPublicTestSuite) TestGetHealthStatus() { s.Equal("job-queue", (*r.KvBuckets)[0].Name) s.Equal(10, (*r.KvBuckets)[0].Keys) + s.Require().NotNil(r.ObjectStores) + s.Len(*r.ObjectStores, 1) + s.Equal("file-objects", (*r.ObjectStores)[0].Name) + s.Equal(5242880, (*r.ObjectStores)[0].Size) + s.Require().NotNil(r.Consumers) s.Equal(2, r.Consumers.Total) s.Require().NotNil(r.Consumers.Consumers) @@ -284,6 +294,9 @@ func (s *HealthStatusGetPublicTestSuite) TestGetHealthStatus() { {Name: "job-queue", Keys: 5, Bytes: 512}, }, nil }, + ObjectStoreInfoFn: func(_ context.Context) ([]health.ObjectStoreMetrics, error) { + return nil, fmt.Errorf("object store info unavailable") + }, ConsumerStatsFn: func(_ context.Context) (*health.ConsumerMetrics, error) { return nil, fmt.Errorf("consumer stats unavailable") }, @@ -302,6 +315,7 @@ func (s *HealthStatusGetPublicTestSuite) TestGetHealthStatus() { s.Nil(r.Streams) s.Require().NotNil(r.KvBuckets) s.Len(*r.KvBuckets, 1) + s.Nil(r.ObjectStores) s.Nil(r.Consumers) s.Nil(r.Jobs) s.Nil(r.Agents) @@ -323,6 +337,9 @@ func (s *HealthStatusGetPublicTestSuite) TestGetHealthStatus() { KVInfoFn: func(_ context.Context) ([]health.KVMetrics, error) { return nil, fmt.Errorf("kv info unavailable") }, + ObjectStoreInfoFn: func(_ context.Context) ([]health.ObjectStoreMetrics, error) { + return nil, fmt.Errorf("object store info unavailable") + }, ConsumerStatsFn: func(_ context.Context) (*health.ConsumerMetrics, error) { return nil, fmt.Errorf("consumer stats unavailable") }, @@ -340,6 +357,7 @@ func (s *HealthStatusGetPublicTestSuite) TestGetHealthStatus() { s.Nil(r.Nats) s.Nil(r.Streams) s.Nil(r.KvBuckets) + s.Nil(r.ObjectStores) s.Nil(r.Consumers) s.Nil(r.Jobs) s.Nil(r.Agents) @@ -395,6 +413,13 @@ func (s *HealthStatusGetPublicTestSuite) TestGetHealthStatusHTTP() { {Name: "job-queue", Keys: 10, Bytes: 2048}, }, nil }, + ObjectStoreInfoFn: func( + _ context.Context, + ) ([]health.ObjectStoreMetrics, error) { + return []health.ObjectStoreMetrics{ + {Name: "file-objects", Size: 5242880}, + }, nil + }, ConsumerStatsFn: func( _ context.Context, ) (*health.ConsumerMetrics, error) { @@ -433,12 +458,14 @@ func (s *HealthStatusGetPublicTestSuite) TestGetHealthStatusHTTP() { `"nats"`, `"streams"`, `"kv_buckets"`, + `"object_stores"`, `"consumers"`, `"jobs"`, `"agents"`, `"web-01"`, `"group=web.prod"`, `"query_any_web_01"`, + `"file-objects"`, }, }, { @@ -557,6 +584,11 @@ func (s *HealthStatusGetPublicTestSuite) TestGetHealthStatusRBACHTTP() { ) ([]health.KVMetrics, error) { return []health.KVMetrics{}, nil }, + ObjectStoreInfoFn: func( + _ context.Context, + ) ([]health.ObjectStoreMetrics, error) { + return []health.ObjectStoreMetrics{}, nil + }, JobStatsFn: func( _ context.Context, ) (*health.JobMetrics, error) { diff --git a/internal/api/health/types.go b/internal/api/health/types.go index e29da7c6..76235e86 100644 --- a/internal/api/health/types.go +++ b/internal/api/health/types.go @@ -36,6 +36,7 @@ type MetricsProvider interface { GetNATSInfo(ctx context.Context) (*NATSMetrics, error) GetStreamInfo(ctx context.Context) ([]StreamMetrics, error) GetKVInfo(ctx context.Context) ([]KVMetrics, error) + GetObjectStoreInfo(ctx context.Context) ([]ObjectStoreMetrics, error) GetConsumerStats(ctx context.Context) (*ConsumerMetrics, error) GetJobStats(ctx context.Context) (*JobMetrics, error) GetAgentStats(ctx context.Context) (*AgentMetrics, error) @@ -62,6 +63,12 @@ type KVMetrics struct { Bytes uint64 } +// ObjectStoreMetrics holds Object Store bucket statistics. +type ObjectStoreMetrics struct { + Name string + Size uint64 +} + // ConsumerMetrics holds JetStream consumer statistics. type ConsumerMetrics struct { Total int @@ -102,12 +109,13 @@ type AgentDetail struct { // ClosureMetricsProvider implements MetricsProvider using function closures. type ClosureMetricsProvider struct { - NATSInfoFn func(ctx context.Context) (*NATSMetrics, error) - StreamInfoFn func(ctx context.Context) ([]StreamMetrics, error) - KVInfoFn func(ctx context.Context) ([]KVMetrics, error) - ConsumerStatsFn func(ctx context.Context) (*ConsumerMetrics, error) - JobStatsFn func(ctx context.Context) (*JobMetrics, error) - AgentStatsFn func(ctx context.Context) (*AgentMetrics, error) + NATSInfoFn func(ctx context.Context) (*NATSMetrics, error) + StreamInfoFn func(ctx context.Context) ([]StreamMetrics, error) + KVInfoFn func(ctx context.Context) ([]KVMetrics, error) + ObjectStoreInfoFn func(ctx context.Context) ([]ObjectStoreMetrics, error) + ConsumerStatsFn func(ctx context.Context) (*ConsumerMetrics, error) + JobStatsFn func(ctx context.Context) (*JobMetrics, error) + AgentStatsFn func(ctx context.Context) (*AgentMetrics, error) } // GetNATSInfo delegates to the NATSInfoFn closure. @@ -131,6 +139,13 @@ func (p *ClosureMetricsProvider) GetKVInfo( return p.KVInfoFn(ctx) } +// GetObjectStoreInfo delegates to the ObjectStoreInfoFn closure. +func (p *ClosureMetricsProvider) GetObjectStoreInfo( + ctx context.Context, +) ([]ObjectStoreMetrics, error) { + return p.ObjectStoreInfoFn(ctx) +} + // GetConsumerStats delegates to the ConsumerStatsFn closure. func (p *ClosureMetricsProvider) GetConsumerStats( ctx context.Context,