@@ -741,6 +741,82 @@ def test_feature_service(self):
741741 assert fv_count == "2"
742742
743743
744+ class TestBinFeatureCount :
745+ @pytest .mark .parametrize (
746+ ("count" , "expected" ),
747+ [
748+ (0 , "0" ),
749+ (1 , "1-10" ),
750+ (10 , "1-10" ),
751+ (11 , "11-50" ),
752+ (50 , "11-50" ),
753+ (51 , "51-200" ),
754+ (200 , "51-200" ),
755+ (201 , "201+" ),
756+ ],
757+ )
758+ def test_default_boundaries (self , count , expected ):
759+ from feast .feature_server import bin_feature_count
760+
761+ assert bin_feature_count (count , [10 , 50 , 200 ]) == expected
762+
763+ @pytest .mark .parametrize (
764+ ("count" , "expected" ),
765+ [
766+ (1 , "1-5" ),
767+ (5 , "1-5" ),
768+ (6 , "6-20" ),
769+ (20 , "6-20" ),
770+ (21 , "21+" ),
771+ ],
772+ )
773+ def test_custom_boundaries (self , count , expected ):
774+
775+ from feast .feature_server import bin_feature_count
776+
777+ assert bin_feature_count (count , [5 , 20 ]) == expected
778+
779+
780+ class TestMetricsConfig :
781+ def test_feature_count_bins_default (self ):
782+ from feast .infra .feature_servers .base_config import MetricsConfig
783+
784+ config = MetricsConfig ()
785+ assert config .feature_count_bins == [10 , 50 , 200 ]
786+
787+ def test_feature_count_bins_custom (self ):
788+ from feast .infra .feature_servers .base_config import MetricsConfig
789+
790+ config = MetricsConfig (feature_count_bins = [5 , 20 ])
791+ assert config .feature_count_bins == [5 , 20 ]
792+
793+ @pytest .mark .parametrize (
794+ "bins" ,
795+ [
796+ [0 , 10 , 50 ],
797+ [- 1 , 10 , 50 ],
798+ ],
799+ )
800+ def test_feature_count_bins_must_be_positive (self , bins ):
801+ from feast .infra .feature_servers .base_config import MetricsConfig
802+
803+ with pytest .raises (ValueError , match = "positive" ):
804+ MetricsConfig (feature_count_bins = bins )
805+
806+ @pytest .mark .parametrize (
807+ "bins" ,
808+ [
809+ [50 , 10 , 200 ],
810+ [10 , 10 , 200 ],
811+ ],
812+ )
813+ def test_feature_count_bins_must_be_strictly_increasing (self , bins ):
814+ from feast .infra .feature_servers .base_config import MetricsConfig
815+
816+ with pytest .raises (ValueError , match = "strictly increasing" ):
817+ MetricsConfig (feature_count_bins = bins )
818+
819+
744820class TestFeatureServerMetricsIntegration :
745821 """Test that feature server endpoints record metrics."""
746822
@@ -751,6 +827,11 @@ def mock_fs_factory(self):
751827 def builder (** async_support ):
752828 provider = FooProvider .with_async_support (** async_support )
753829 fs = MagicMock ()
830+
831+ from feast .infra .feature_servers .base_config import MetricsConfig
832+
833+ fs .config .feature_server .metrics = MetricsConfig ()
834+
754835 fs ._get_provider .return_value = provider
755836 from feast .online_response import OnlineResponse
756837 from feast .protos .feast .serving .ServingService_pb2 import (
@@ -798,20 +879,30 @@ def test_get_online_features_records_metrics(self, mock_fs_factory):
798879 @pytest .mark .parametrize (
799880 "features,expected_feat_count,expected_fv_count" ,
800881 [
801- (["fv1:a" ], "1" , "1" ),
802- (["fv1:a" , "fv1:b" , "fv2:c" ], "3 " , "2" ),
882+ (["fv1:a" ], "1-10 " , "1" ),
883+ (["fv1:a" , "fv1:b" , "fv2:c" ], "1-10 " , "2" ),
803884 (
804885 ["fv1:a" , "fv1:b" , "fv2:c" , "fv2:d" , "fv3:e" ],
805- "5 " ,
886+ "1-10 " ,
806887 "3" ,
807888 ),
889+ (
890+ [f"fv1:f{ i } " for i in range (11 )],
891+ "11-50" ,
892+ "1" ,
893+ ),
894+ ],
895+ ids = [
896+ "1_feat_1_fv" ,
897+ "3_feats_2_fvs" ,
898+ "5_feats_3_fvs" ,
899+ "11_feats_1_fv" ,
808900 ],
809- ids = ["1_feat_1_fv" , "3_feats_2_fvs" , "5_feats_3_fvs" ],
810901 )
811902 def test_latency_labels_with_varying_request_sizes (
812903 self , mock_fs_factory , features , expected_feat_count , expected_fv_count
813904 ):
814- """Verify feature_count and feature_view_count labels change with request size ."""
905+ """Verify feature_count is bucketed while feature_view_count remains exact ."""
815906 from fastapi .testclient import TestClient
816907
817908 from feast .feature_server import get_app
@@ -837,6 +928,35 @@ def test_latency_labels_with_varying_request_sizes(
837928 after_sum = request_latency .labels (** label_set )._sum .get ()
838929 assert after_sum > before_sum
839930
931+ def test_latency_labels_use_custom_feature_count_bins (self , mock_fs_factory ):
932+ from fastapi .testclient import TestClient
933+
934+ from feast .feature_server import get_app
935+ from feast .infra .feature_servers .base_config import MetricsConfig
936+
937+ fs = mock_fs_factory (online_read = False )
938+ fs .config .feature_server .metrics = MetricsConfig (feature_count_bins = [2 , 4 ])
939+
940+ client = TestClient (get_app (fs ))
941+
942+ label_set = dict (
943+ endpoint = "/get-online-features" ,
944+ feature_count = "3-4" ,
945+ feature_view_count = "1" ,
946+ )
947+ before_sum = request_latency .labels (** label_set )._sum .get ()
948+
949+ client .post (
950+ "/get-online-features" ,
951+ json = {
952+ "features" : ["fv:a" , "fv:b" , "fv:c" ],
953+ "entities" : {"id" : [1 ]},
954+ },
955+ )
956+
957+ after_sum = request_latency .labels (** label_set )._sum .get ()
958+ assert after_sum > before_sum
959+
840960 def test_push_records_metrics (self , mock_fs_factory ):
841961 from fastapi .testclient import TestClient
842962
0 commit comments