| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent bd01824 commit 1b681b7
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -49,6 +49,7 @@ import ( | |||
| 49 | 49 | routev1 "github.com/openshift/api/route/v1" | |
| 50 | 50 | ||
| 51 | 51 | "github.com/feast-dev/feast/infra/feast-operator/internal/controller" | |
| 52 | + feastmetrics "github.com/feast-dev/feast/infra/feast-operator/internal/controller/metrics" | ||
| 52 | 53 | "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" | |
| 53 | 54 | // +kubebuilder:scaffold:imports | |
| 54 | 55 | ) | |
@@ -95,6 +96,7 @@ func main() { | |||
| 95 | 96 | var probeAddr string | |
| 96 | 97 | var secureMetrics bool | |
| 97 | 98 | var enableHTTP2 bool | |
| 99 | + var featureStoreMetrics bool | ||
| 98 | 100 | var tlsOpts []func(*tls.Config) | |
| 99 | 101 | flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ | |
| 100 | 102 | "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") | |
@@ -106,6 +108,9 @@ func main() { | |||
| 106 | 108 | "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") | |
| 107 | 109 | flag.BoolVar(&enableHTTP2, "enable-http2", false, | |
| 108 | 110 | "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.") | ||
| 109 | 114 | opts := zap.Options{ | |
| 110 | 115 | Development: true, | |
| 111 | 116 | } | |
@@ -206,9 +211,19 @@ func main() { | |||
| 206 | 211 | ||
| 207 | 212 | services.SetIsOpenShift(mgr.GetConfig()) | |
| 208 | 213 | ||
| 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 | + | ||
| 209 | 223 | if err = (&controller.FeatureStoreReconciler{ | |
| 210 | - Client: mgr.GetClient(), | ||
| 211 | - Scheme: mgr.GetScheme(), | ||
| 224 | + Client: mgr.GetClient(), | ||
| 225 | + Scheme: mgr.GetScheme(), | ||
| 226 | + Metrics: fsMetrics, | ||
| 212 | 227 | }).SetupWithManager(mgr); err != nil { | |
| 213 | 228 | setupLog.Error(err, "unable to create controller", "controller", "FeatureStore") | |
| 214 | 229 | os.Exit(1) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,6 +15,7 @@ require ( | |||
| 15 | 15 | ||
| 16 | 16 | require ( | |
| 17 | 17 | github.com/prometheus-operator/prometheus-operator/pkg/client v0.83.0 | |
| 18 | + github.com/prometheus/client_golang v1.22.0 | ||
| 18 | 19 | k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 | |
| 19 | 20 | ) | |
| 20 | 21 | ||
@@ -55,7 +56,6 @@ require ( | |||
| 55 | 56 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect | |
| 56 | 57 | github.com/pkg/errors v0.9.1 // indirect | |
| 57 | 58 | github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.83.0 // indirect | |
| 58 | - github.com/prometheus/client_golang v1.22.0 // indirect | ||
| 59 | 59 | github.com/prometheus/client_model v0.6.1 // indirect | |
| 60 | 60 | github.com/prometheus/common v0.62.0 // indirect | |
| 61 | 61 | github.com/prometheus/procfs v0.15.1 // indirect | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -43,6 +43,7 @@ import ( | |||
| 43 | 43 | feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" | |
| 44 | 44 | "github.com/feast-dev/feast/infra/feast-operator/internal/controller/authz" | |
| 45 | 45 | 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" | ||
| 46 | 47 | "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" | |
| 47 | 48 | routev1 "github.com/openshift/api/route/v1" | |
| 48 | 49 | ) | |
@@ -55,7 +56,8 @@ const ( | |||
| 55 | 56 | // FeatureStoreReconciler reconciles a FeatureStore object | |
| 56 | 57 | type FeatureStoreReconciler struct { | |
| 57 | 58 | client.Client | |
| 58 | - Scheme *runtime.Scheme | ||
| 59 | + Scheme *runtime.Scheme | ||
| 60 | + Metrics *feastmetrics.FeatureStoreMetrics | ||
| 59 | 61 | } | |
| 60 | 62 | ||
| 61 | 63 | // +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 | |||
| 87 | 89 | if apierrors.IsNotFound(err) { | |
| 88 | 90 | // CR deleted since request queued, child objects getting GC'd, no requeue | |
| 89 | 91 | 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 | + } | ||
| 90 | 95 | // Clean up namespace registry entry even if the CR is not found | |
| 91 | 96 | if err := r.cleanupNamespaceRegistry(ctx, &feastdevv1.FeatureStore{ | |
| 92 | 97 | ObjectMeta: metav1.ObjectMeta{ | |
@@ -107,6 +112,9 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request | |||
| 107 | 112 | // Handle deletion - clean up namespace registry entry | |
| 108 | 113 | if cr.DeletionTimestamp != nil { | |
| 109 | 114 | 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 | + } | ||
| 110 | 118 | if err := r.cleanupNamespaceRegistry(ctx, cr); err != nil { | |
| 111 | 119 | logger.Error(err, "Failed to clean up namespace registry entry") | |
| 112 | 120 | return ctrl.Result{}, err | |
@@ -115,6 +123,9 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request | |||
| 115 | 123 | } | |
| 116 | 124 | ||
| 117 | 125 | result, recErr = r.deployFeast(ctx, cr) | |
| 126 | + if recErr == nil && r.Metrics != nil { | ||
| 127 | + r.Metrics.RecordFeatureStore(cr) | ||
| 128 | + } | ||
| 118 | 129 | if cr.DeletionTimestamp == nil && !reflect.DeepEqual(currentStatus, cr.Status) { | |
| 119 | 130 | if err = r.Client.Status().Update(ctx, cr); err != nil { | |
| 120 | 131 | if apierrors.IsConflict(err) { | |
| Original file line number | Diff line number | Diff 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 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments