@@ -1732,3 +1732,184 @@ def test_does_not_raise_on_failure(self):
17321732 status = "error" ,
17331733 latency_ms = 5.0 ,
17341734 )
1735+
1736+
1737+ # ---------------------------------------------------------------------------
1738+ # System Metrics endpoint tests
1739+ # ---------------------------------------------------------------------------
1740+
1741+
1742+ class TestParsePrometheusText :
1743+ """Tests for _parse_prometheus_text in system_metrics.py."""
1744+
1745+ def test_parses_counter (self ):
1746+ from feast .api .registry .rest .system_metrics import _parse_prometheus_text
1747+
1748+ text = (
1749+ "# HELP feast_offline_store_request_total Total requests\n "
1750+ "# TYPE feast_offline_store_request_total counter\n "
1751+ 'feast_offline_store_request_total{method="to_arrow",status="success"} 42\n '
1752+ )
1753+ result = _parse_prometheus_text (text )
1754+ assert "feast_offline_store_request_total" in result
1755+ entry = result ["feast_offline_store_request_total" ]
1756+ assert entry ["type" ] == "counter"
1757+ assert len (entry ["samples" ]) == 1
1758+ assert entry ["samples" ][0 ]["value" ] == 42.0
1759+
1760+ def test_preserves_total_suffix (self ):
1761+ """Counter _total suffix must NOT be stripped (was a reported bug)."""
1762+ from feast .api .registry .rest .system_metrics import _parse_prometheus_text
1763+
1764+ text = (
1765+ "# TYPE feast_offline_store_request_total counter\n "
1766+ "feast_offline_store_request_total 10\n "
1767+ )
1768+ result = _parse_prometheus_text (text )
1769+ assert "feast_offline_store_request_total" in result
1770+ assert "feast_offline_store_request" not in result
1771+
1772+ def test_groups_histogram_samples (self ):
1773+ from feast .api .registry .rest .system_metrics import _parse_prometheus_text
1774+
1775+ text = (
1776+ "# TYPE feast_offline_store_request_latency_seconds histogram\n "
1777+ 'feast_offline_store_request_latency_seconds_bucket{le="0.1"} 5\n '
1778+ 'feast_offline_store_request_latency_seconds_bucket{le="1.0"} 15\n '
1779+ "feast_offline_store_request_latency_seconds_sum 12.5\n "
1780+ "feast_offline_store_request_latency_seconds_count 15\n "
1781+ )
1782+ result = _parse_prometheus_text (text )
1783+ assert "feast_offline_store_request_latency_seconds" in result
1784+ entry = result ["feast_offline_store_request_latency_seconds" ]
1785+ assert entry ["type" ] == "histogram"
1786+ assert len (entry ["samples" ]) == 4
1787+
1788+ def test_empty_input (self ):
1789+ from feast .api .registry .rest .system_metrics import _parse_prometheus_text
1790+
1791+ assert _parse_prometheus_text ("" ) == {}
1792+
1793+ def test_skips_comment_and_help_lines (self ):
1794+ from feast .api .registry .rest .system_metrics import _parse_prometheus_text
1795+
1796+ text = (
1797+ "# HELP some_metric A help string\n "
1798+ "# This is a random comment\n "
1799+ "# TYPE some_metric gauge\n "
1800+ "some_metric 3.14\n "
1801+ )
1802+ result = _parse_prometheus_text (text )
1803+ assert "some_metric" in result
1804+ assert result ["some_metric" ]["samples" ][0 ]["value" ] == 3.14
1805+
1806+ def test_non_numeric_value (self ):
1807+ from feast .api .registry .rest .system_metrics import _parse_prometheus_text
1808+
1809+ text = "# TYPE info_metric gauge\n info_metric NaN\n "
1810+ result = _parse_prometheus_text (text )
1811+ assert len (result ["info_metric" ]["samples" ]) == 1
1812+
1813+
1814+ class TestSystemMetricsRouter :
1815+ """Tests for the FastAPI system-metrics router endpoints."""
1816+
1817+ def _make_test_client (self , store = None ):
1818+ from fastapi import FastAPI
1819+ from fastapi .testclient import TestClient
1820+
1821+ from feast .api .registry .rest .system_metrics import get_system_metrics_router
1822+
1823+ app = FastAPI ()
1824+ app .include_router (get_system_metrics_router (grpc_handler = None , store = store ))
1825+ return TestClient (app )
1826+
1827+ @patch ("feast.api.registry.rest.system_metrics.http_requests.get" )
1828+ def test_promql_instant_success (self , mock_get ):
1829+ mock_resp = MagicMock ()
1830+ mock_resp .status_code = 200
1831+ mock_resp .json .return_value = {
1832+ "status" : "success" ,
1833+ "data" : {"resultType" : "vector" },
1834+ }
1835+ mock_resp .raise_for_status = MagicMock ()
1836+ mock_get .return_value = mock_resp
1837+
1838+ client = self ._make_test_client ()
1839+ resp = client .get ("/system-metrics/query" , params = {"query" : "up" })
1840+ assert resp .status_code == 200
1841+ assert resp .json ()["status" ] == "success"
1842+
1843+ @patch ("feast.api.registry.rest.system_metrics.http_requests.get" )
1844+ def test_promql_instant_connection_error (self , mock_get ):
1845+ import requests
1846+
1847+ mock_get .side_effect = requests .exceptions .ConnectionError ("refused" )
1848+
1849+ client = self ._make_test_client ()
1850+ resp = client .get ("/system-metrics/query" , params = {"query" : "up" })
1851+ assert resp .status_code == 503
1852+ assert "Failed to connect" in resp .json ()["detail" ]
1853+
1854+ @patch ("feast.api.registry.rest.system_metrics.http_requests.get" )
1855+ def test_promql_instant_timeout (self , mock_get ):
1856+ import requests
1857+
1858+ mock_get .side_effect = requests .exceptions .Timeout ("timed out" )
1859+
1860+ client = self ._make_test_client ()
1861+ resp = client .get ("/system-metrics/query" , params = {"query" : "up" })
1862+ assert resp .status_code == 504
1863+ assert "timed out" in resp .json ()["detail" ]
1864+
1865+ @patch ("feast.api.registry.rest.system_metrics.http_requests.get" )
1866+ def test_promql_range_success (self , mock_get ):
1867+ mock_resp = MagicMock ()
1868+ mock_resp .json .return_value = {"status" : "success" }
1869+ mock_resp .raise_for_status = MagicMock ()
1870+ mock_get .return_value = mock_resp
1871+
1872+ client = self ._make_test_client ()
1873+ resp = client .get (
1874+ "/system-metrics/query_range" ,
1875+ params = {
1876+ "query" : "up" ,
1877+ "start" : "2026-01-01T00:00:00Z" ,
1878+ "end" : "2026-01-02T00:00:00Z" ,
1879+ },
1880+ )
1881+ assert resp .status_code == 200
1882+
1883+ @patch ("feast.api.registry.rest.system_metrics.http_requests.get" )
1884+ def test_scrape_success (self , mock_get ):
1885+ mock_resp = MagicMock ()
1886+ mock_resp .status_code = 200
1887+ mock_resp .text = "# TYPE up gauge\n up 1\n "
1888+ mock_resp .raise_for_status = MagicMock ()
1889+ mock_get .return_value = mock_resp
1890+
1891+ client = self ._make_test_client ()
1892+ resp = client .get ("/system-metrics/scrape" )
1893+ assert resp .status_code == 200
1894+ body = resp .json ()
1895+ assert "up" in body
1896+
1897+ @patch ("feast.api.registry.rest.system_metrics.http_requests.get" )
1898+ def test_scrape_failure (self , mock_get ):
1899+ import requests
1900+
1901+ mock_get .side_effect = requests .exceptions .ConnectionError ("refused" )
1902+
1903+ client = self ._make_test_client ()
1904+ resp = client .get ("/system-metrics/scrape" )
1905+ assert resp .status_code == 503
1906+ assert "Failed to scrape" in resp .json ()["detail" ]
1907+
1908+ def test_prometheus_url_from_store (self ):
1909+ from feast .api .registry .rest .system_metrics import get_system_metrics_router
1910+
1911+ store = MagicMock ()
1912+ store .config .feature_server .metrics .prometheus_url = "http://custom:9090"
1913+
1914+ router = get_system_metrics_router (grpc_handler = None , store = store )
1915+ assert router is not None
0 commit comments