Skip to content

Commit 1b681b7

Browse files
authored
feat: Add Prometheus gauges for FeatureStore installation telemetry (#6354)
Signed-off-by: ntkathole <nikhilkathole2683@gmail.com>
1 parent bd01824 commit 1b681b7

5 files changed

Lines changed: 438 additions & 4 deletions

File tree

infra/feast-operator/cmd/main.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import (
4949
routev1 "github.com/openshift/api/route/v1"
5050

5151
"github.com/feast-dev/feast/infra/feast-operator/internal/controller"
52+
feastmetrics "github.com/feast-dev/feast/infra/feast-operator/internal/controller/metrics"
5253
"github.com/feast-dev/feast/infra/feast-operator/internal/controller/services"
5354
// +kubebuilder:scaffold:imports
5455
)
@@ -95,6 +96,7 @@ func main() {
9596
var probeAddr string
9697
var secureMetrics bool
9798
var enableHTTP2 bool
99+
var featureStoreMetrics bool
98100
var tlsOpts []func(*tls.Config)
99101
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
100102
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
@@ -106,6 +108,9 @@ func main() {
106108
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
107109
flag.BoolVar(&enableHTTP2, "enable-http2", false,
108110
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
111+
flag.BoolVar(&featureStoreMetrics, "feature-store-metrics", true,
112+
"Enable Prometheus gauges exposing online/offline store and registry configuration per FeatureStore. "+
113+
"Disable with --feature-store-metrics=false.")
109114
opts := zap.Options{
110115
Development: true,
111116
}
@@ -206,9 +211,19 @@ func main() {
206211

207212
services.SetIsOpenShift(mgr.GetConfig())
208213

214+
var fsMetrics *feastmetrics.FeatureStoreMetrics
215+
if featureStoreMetrics {
216+
fsMetrics = feastmetrics.NewFeatureStoreMetrics()
217+
fsMetrics.Register()
218+
setupLog.Info("FeatureStore installation metrics enabled")
219+
} else {
220+
setupLog.Info("FeatureStore installation metrics disabled (--feature-store-metrics=false)")
221+
}
222+
209223
if err = (&controller.FeatureStoreReconciler{
210-
Client: mgr.GetClient(),
211-
Scheme: mgr.GetScheme(),
224+
Client: mgr.GetClient(),
225+
Scheme: mgr.GetScheme(),
226+
Metrics: fsMetrics,
212227
}).SetupWithManager(mgr); err != nil {
213228
setupLog.Error(err, "unable to create controller", "controller", "FeatureStore")
214229
os.Exit(1)

infra/feast-operator/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require (
1515

1616
require (
1717
github.com/prometheus-operator/prometheus-operator/pkg/client v0.83.0
18+
github.com/prometheus/client_golang v1.22.0
1819
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979
1920
)
2021

@@ -55,7 +56,6 @@ require (
5556
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
5657
github.com/pkg/errors v0.9.1 // indirect
5758
github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.83.0 // indirect
58-
github.com/prometheus/client_golang v1.22.0 // indirect
5959
github.com/prometheus/client_model v0.6.1 // indirect
6060
github.com/prometheus/common v0.62.0 // indirect
6161
github.com/prometheus/procfs v0.15.1 // indirect

infra/feast-operator/internal/controller/featurestore_controller.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343
feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1"
4444
"github.com/feast-dev/feast/infra/feast-operator/internal/controller/authz"
4545
feasthandler "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler"
46+
feastmetrics "github.com/feast-dev/feast/infra/feast-operator/internal/controller/metrics"
4647
"github.com/feast-dev/feast/infra/feast-operator/internal/controller/services"
4748
routev1 "github.com/openshift/api/route/v1"
4849
)
@@ -55,7 +56,8 @@ const (
5556
// FeatureStoreReconciler reconciles a FeatureStore object
5657
type FeatureStoreReconciler struct {
5758
client.Client
58-
Scheme *runtime.Scheme
59+
Scheme *runtime.Scheme
60+
Metrics *feastmetrics.FeatureStoreMetrics
5961
}
6062

6163
// +kubebuilder:rbac:groups=feast.dev,resources=featurestores,verbs=get;list;watch;create;update;patch;delete
@@ -87,6 +89,9 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request
8789
if apierrors.IsNotFound(err) {
8890
// CR deleted since request queued, child objects getting GC'd, no requeue
8991
logger.V(1).Info("FeatureStore CR not found, has been deleted")
92+
if r.Metrics != nil {
93+
r.Metrics.DeleteFeatureStore(req.NamespacedName.Namespace, req.NamespacedName.Name)
94+
}
9095
// Clean up namespace registry entry even if the CR is not found
9196
if err := r.cleanupNamespaceRegistry(ctx, &feastdevv1.FeatureStore{
9297
ObjectMeta: metav1.ObjectMeta{
@@ -107,6 +112,9 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request
107112
// Handle deletion - clean up namespace registry entry
108113
if cr.DeletionTimestamp != nil {
109114
logger.Info("FeatureStore is being deleted, cleaning up namespace registry entry")
115+
if r.Metrics != nil {
116+
r.Metrics.DeleteFeatureStore(cr.Namespace, cr.Name)
117+
}
110118
if err := r.cleanupNamespaceRegistry(ctx, cr); err != nil {
111119
logger.Error(err, "Failed to clean up namespace registry entry")
112120
return ctrl.Result{}, err
@@ -115,6 +123,9 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request
115123
}
116124

117125
result, recErr = r.deployFeast(ctx, cr)
126+
if recErr == nil && r.Metrics != nil {
127+
r.Metrics.RecordFeatureStore(cr)
128+
}
118129
if cr.DeletionTimestamp == nil && !reflect.DeepEqual(currentStatus, cr.Status) {
119130
if err = r.Client.Status().Update(ctx, cr); err != nil {
120131
if apierrors.IsConflict(err) {
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
Copyright 2026 Feast Community.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package metrics provides a Prometheus info gauge that records the store
18+
// types configured for each FeatureStore CR (online store, offline store,
19+
// registry). These operator-level metrics are distinct from the Feast
20+
// feature-server application metrics (feast_feature_server_*) and are useful
21+
// for usage telemetry and assessing the impact of removing store type support.
22+
package metrics
23+
24+
import (
25+
feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1"
26+
"github.com/prometheus/client_golang/prometheus"
27+
ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
28+
)
29+
30+
const typeNone = "none"
31+
32+
// FeatureStoreMetrics holds the Prometheus GaugeVec for feast-operator
33+
// installation telemetry.
34+
type FeatureStoreMetrics struct {
35+
FeatureStoreInfo *prometheus.GaugeVec
36+
}
37+
38+
// NewFeatureStoreMetrics creates a new FeatureStoreMetrics with the GaugeVec
39+
// initialised but not yet registered. Call Register() before starting the manager.
40+
func NewFeatureStoreMetrics() *FeatureStoreMetrics {
41+
return &FeatureStoreMetrics{
42+
FeatureStoreInfo: prometheus.NewGaugeVec(
43+
prometheus.GaugeOpts{
44+
Name: "feast_operator_feature_store_info",
45+
Help: "Information about a deployed FeatureStore. " +
46+
"Value is always 1. Labels carry the configured store types: " +
47+
"'online_store_type', 'offline_store_type', and 'registry_type' " +
48+
"are set to the persistence type (e.g. redis, snowflake.offline, local) " +
49+
"or 'none' when that component is not configured.",
50+
},
51+
[]string{"namespace", "name", "online_store_type", "offline_store_type", "registry_type"},
52+
),
53+
}
54+
}
55+
56+
// Register registers the metric with the controller-runtime metrics registry
57+
// so it is exposed on the manager's /metrics endpoint.
58+
func (m *FeatureStoreMetrics) Register() {
59+
ctrlmetrics.Registry.MustRegister(m.FeatureStoreInfo)
60+
}
61+
62+
// RecordFeatureStore updates the gauge for the given FeatureStore using the
63+
// applied configuration stored in status.Applied (which has operator defaults
64+
// applied). The previous label set for this FeatureStore is deleted first so
65+
// that store type changes are reflected cleanly on the next scrape.
66+
func (m *FeatureStoreMetrics) RecordFeatureStore(fs *feastdevv1.FeatureStore) {
67+
svcs := fs.Status.Applied.Services
68+
m.FeatureStoreInfo.DeletePartialMatch(prometheus.Labels{
69+
"namespace": fs.Namespace,
70+
"name": fs.Name,
71+
})
72+
m.FeatureStoreInfo.WithLabelValues(
73+
fs.Namespace,
74+
fs.Name,
75+
onlineStoreType(svcs),
76+
offlineStoreType(svcs),
77+
registryType(svcs),
78+
).Set(1)
79+
}
80+
81+
// DeleteFeatureStore removes the metric label set for the given FeatureStore.
82+
// Safe to call when the CR has already been deleted from the API server.
83+
func (m *FeatureStoreMetrics) DeleteFeatureStore(namespace, name string) {
84+
m.FeatureStoreInfo.DeletePartialMatch(prometheus.Labels{
85+
"namespace": namespace,
86+
"name": name,
87+
})
88+
}
89+
90+
// onlineStoreType returns the online store persistence type or "none".
91+
func onlineStoreType(svcs *feastdevv1.FeatureStoreServices) string {
92+
if svcs == nil || svcs.OnlineStore == nil {
93+
return typeNone
94+
}
95+
if p := svcs.OnlineStore.Persistence; p != nil && p.DBPersistence != nil {
96+
return p.DBPersistence.Type
97+
}
98+
return "file"
99+
}
100+
101+
// offlineStoreType returns the offline store persistence type or "none".
102+
func offlineStoreType(svcs *feastdevv1.FeatureStoreServices) string {
103+
if svcs == nil || svcs.OfflineStore == nil {
104+
return typeNone
105+
}
106+
if p := svcs.OfflineStore.Persistence; p != nil {
107+
if p.DBPersistence != nil {
108+
return p.DBPersistence.Type
109+
}
110+
if p.FilePersistence != nil && p.FilePersistence.Type != "" {
111+
return p.FilePersistence.Type
112+
}
113+
}
114+
return "file"
115+
}
116+
117+
// registryType returns "local", "remote", "remote_feastref", or "none".
118+
func registryType(svcs *feastdevv1.FeatureStoreServices) string {
119+
if svcs == nil || svcs.Registry == nil {
120+
return typeNone
121+
}
122+
switch {
123+
case svcs.Registry.Local != nil:
124+
return "local"
125+
case svcs.Registry.Remote != nil:
126+
if svcs.Registry.Remote.FeastRef != nil {
127+
return "remote_feastref"
128+
}
129+
return "remote"
130+
default:
131+
return typeNone
132+
}
133+
}

0 commit comments

Comments
 (0)