Skip to content

Commit 8b5a526

Browse files
ntkatholefranciscojavierarceo
authored andcommitted
feat: Added odfv transformations metrics
Signed-off-by: ntkathole <nikhilkathole2683@gmail.com>
1 parent 5e70cec commit 8b5a526

File tree

11 files changed

+649
-92
lines changed

11 files changed

+649
-92
lines changed

docs/getting-started/components/open-telemetry.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ Once configured, you can monitor various metrics including:
144144
145145
- `feast_feature_server_memory_usage`: Memory utilization of the feature server
146146
- `feast_feature_server_cpu_usage`: CPU usage statistics
147+
- `feast_feature_server_request_latency_seconds`: Request latency with feature count dimensions
148+
- `feast_feature_server_online_store_read_duration_seconds`: Online store read phase duration
149+
- `feast_feature_server_transformation_duration_seconds`: ODFV read-path transformation duration (per ODFV, requires `track_metrics=True`)
150+
- `feast_feature_server_write_transformation_duration_seconds`: ODFV write-path transformation duration (per ODFV, requires `track_metrics=True`)
147151
- Additional custom metrics based on your configuration
148152

153+
For the full list of metrics, see the [Python Feature Server reference](../../reference/feature-servers/python-feature-server.md#available-metrics).
154+
149155
These metrics can be visualized using Prometheus and other compatible monitoring tools.

docs/reference/feature-servers/python-feature-server.md

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -360,18 +360,50 @@ thread from starting). All categories default to `true`.
360360

361361
### Available metrics
362362

363-
| Metric | Type | Labels | Description |
364-
|--------|------|--------|-------------|
365-
| `feast_feature_server_cpu_usage` | Gauge | — | Process CPU usage % |
366-
| `feast_feature_server_memory_usage` | Gauge | — | Process memory usage % |
367-
| `feast_feature_server_request_total` | Counter | `endpoint`, `status` | Total requests per endpoint |
368-
| `feast_feature_server_request_latency_seconds` | Histogram | `endpoint`, `feature_count`, `feature_view_count` | Request latency with p50/p95/p99 support |
369-
| `feast_online_features_request_total` | Counter | — | Total online feature retrieval requests |
370-
| `feast_online_features_entity_count` | Histogram | — | Entity rows per online feature request |
371-
| `feast_push_request_total` | Counter | `push_source`, `mode` | Push requests by source and mode |
372-
| `feast_materialization_total` | Counter | `feature_view`, `status` | Materialization runs (success/failure) |
373-
| `feast_materialization_duration_seconds` | Histogram | `feature_view` | Materialization duration per feature view |
374-
| `feast_feature_freshness_seconds` | Gauge | `feature_view`, `project` | Seconds since last materialization |
363+
| Metric | Type | Labels | Category | Description |
364+
|--------|------|--------|----------|-------------|
365+
| `feast_feature_server_cpu_usage` | Gauge | — | `resource` | Process CPU usage % |
366+
| `feast_feature_server_memory_usage` | Gauge | — | `resource` | Process memory usage % |
367+
| `feast_feature_server_request_total` | Counter | `endpoint`, `status` | `request` | Total requests per endpoint |
368+
| `feast_feature_server_request_latency_seconds` | Histogram | `endpoint`, `feature_count`, `feature_view_count` | `request` | Request latency with p50/p95/p99 support |
369+
| `feast_online_features_request_total` | Counter | — | `online_features` | Total online feature retrieval requests |
370+
| `feast_online_features_entity_count` | Histogram | — | `online_features` | Entity rows per online feature request |
371+
| `feast_feature_server_online_store_read_duration_seconds` | Histogram | — | `online_features` | Online store read phase duration (sync and async) |
372+
| `feast_feature_server_transformation_duration_seconds` | Histogram | `odfv_name`, `mode` | `online_features` | ODFV read-path transformation duration (requires `track_metrics=True` on the ODFV) |
373+
| `feast_feature_server_write_transformation_duration_seconds` | Histogram | `odfv_name`, `mode` | `online_features` | ODFV write-path transformation duration (requires `track_metrics=True` on the ODFV) |
374+
| `feast_push_request_total` | Counter | `push_source`, `mode` | `push` | Push requests by source and mode |
375+
| `feast_materialization_result_total` | Counter | `feature_view`, `status` | `materialization` | Materialization runs (success/failure) |
376+
| `feast_materialization_duration_seconds` | Histogram | `feature_view` | `materialization` | Materialization duration per feature view |
377+
| `feast_feature_freshness_seconds` | Gauge | `feature_view`, `project` | `freshness` | Seconds since last materialization |
378+
379+
### Per-ODFV transformation metrics
380+
381+
The `transformation_duration_seconds` and `write_transformation_duration_seconds`
382+
metrics are gated behind **two** conditions — both must be true for any
383+
instrumentation to run:
384+
385+
1. **Server-level**: the `online_features` category must be enabled in the
386+
metrics configuration.
387+
2. **ODFV-level**: the `OnDemandFeatureView` must have `track_metrics=True`.
388+
389+
This defaults to `False`, so no ODFV incurs timing overhead unless explicitly
390+
opted in:
391+
392+
```python
393+
from feast.on_demand_feature_view import on_demand_feature_view
394+
395+
@on_demand_feature_view(
396+
sources=[my_feature_view, my_request_source],
397+
schema=[Field(name="output", dtype=Float64)],
398+
track_metrics=True, # opt in to transformation timing
399+
)
400+
def my_transform(inputs: pd.DataFrame) -> pd.DataFrame:
401+
...
402+
```
403+
404+
The `odfv_name` label lets you filter or group by individual ODFV,
405+
and the `mode` label (`python`, `pandas`, `substrait`) lets you compare
406+
transformation engines.
375407

376408
### Scraping with Prometheus
377409

docs/reference/feature-store-yaml.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ feature_server:
4040
enabled: true # Enable Prometheus metrics server on port 8000
4141
resource: true # CPU / memory gauges
4242
request: true # endpoint latency histograms & request counters
43-
online_features: true # online feature retrieval counters
43+
online_features: true # online feature retrieval counters + store read & ODFV transform timing
4444
push: true # push request counters
4545
materialization: true # materialization counters & duration histograms
4646
freshness: true # per-feature-view freshness gauges

sdk/python/feast/feature_store.py

Lines changed: 88 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2097,76 +2097,104 @@ def _transform_on_demand_feature_view_df(
20972097
Raises:
20982098
Exception: For unsupported OnDemandFeatureView modes
20992099
"""
2100-
if feature_view.mode == "python" and isinstance(
2101-
feature_view.feature_transformation, PythonTransformation
2102-
):
2103-
input_dict = (
2104-
df.to_dict(orient="records")[0]
2105-
if feature_view.singleton
2106-
else df.to_dict(orient="list")
2100+
_should_track = False
2101+
try:
2102+
from feast.metrics import _config as _metrics_config
2103+
2104+
_should_track = _metrics_config.online_features and getattr(
2105+
feature_view, "track_metrics", False
21072106
)
2107+
except Exception:
2108+
pass
21082109

2109-
if feature_view.singleton:
2110-
transformed_rows = []
2110+
if _should_track:
2111+
import time as _time
21112112

2112-
for i, row in df.iterrows():
2113-
output = feature_view.feature_transformation.udf(row.to_dict())
2114-
if i == 0:
2115-
transformed_rows = output
2116-
else:
2117-
for k in output:
2118-
if isinstance(output[k], list):
2119-
transformed_rows[k].extend(output[k])
2120-
else:
2121-
transformed_rows[k].append(output[k])
2122-
2123-
transformed_data = pd.DataFrame(transformed_rows)
2124-
else:
2125-
transformed_data = feature_view.feature_transformation.udf(input_dict)
2113+
_t0 = _time.monotonic()
21262114

2127-
if feature_view.write_to_online_store:
2128-
entities = [
2129-
self.get_entity(entity) for entity in (feature_view.entities or [])
2130-
]
2131-
join_keys = [entity.join_key for entity in entities if entity]
2132-
join_keys = [k for k in join_keys if k in input_dict.keys()]
2133-
transformed_df = (
2134-
pd.DataFrame(transformed_data)
2135-
if not isinstance(transformed_data, pd.DataFrame)
2136-
else transformed_data
2137-
)
2138-
input_df = pd.DataFrame(
2139-
[input_dict] if feature_view.singleton else input_dict
2115+
try:
2116+
if feature_view.mode == "python" and isinstance(
2117+
feature_view.feature_transformation, PythonTransformation
2118+
):
2119+
input_dict = (
2120+
df.to_dict(orient="records")[0]
2121+
if feature_view.singleton
2122+
else df.to_dict(orient="list")
21402123
)
2141-
if input_df.shape[0] == transformed_df.shape[0]:
2124+
2125+
if feature_view.singleton:
2126+
transformed_rows = []
2127+
2128+
for i, row in df.iterrows():
2129+
output = feature_view.feature_transformation.udf(row.to_dict())
2130+
if i == 0:
2131+
transformed_rows = output
2132+
else:
2133+
for k in output:
2134+
if isinstance(output[k], list):
2135+
transformed_rows[k].extend(output[k])
2136+
else:
2137+
transformed_rows[k].append(output[k])
2138+
2139+
transformed_data = pd.DataFrame(transformed_rows)
2140+
else:
2141+
transformed_data = feature_view.feature_transformation.udf(
2142+
input_dict
2143+
)
2144+
2145+
if feature_view.write_to_online_store:
2146+
entities = [
2147+
self.get_entity(entity)
2148+
for entity in (feature_view.entities or [])
2149+
]
2150+
join_keys = [entity.join_key for entity in entities if entity]
2151+
join_keys = [k for k in join_keys if k in input_dict.keys()]
2152+
transformed_df = (
2153+
pd.DataFrame(transformed_data)
2154+
if not isinstance(transformed_data, pd.DataFrame)
2155+
else transformed_data
2156+
)
2157+
input_df = pd.DataFrame(
2158+
[input_dict] if feature_view.singleton else input_dict
2159+
)
2160+
if input_df.shape[0] == transformed_df.shape[0]:
2161+
for k in input_dict:
2162+
if k not in transformed_data:
2163+
transformed_data[k] = input_dict[k]
2164+
transformed_df = pd.DataFrame(transformed_data)
2165+
else:
2166+
transformed_df = pd.merge(
2167+
transformed_df,
2168+
input_df,
2169+
how="left",
2170+
on=join_keys,
2171+
)
2172+
else:
2173+
# overwrite any transformed features and update the dictionary
21422174
for k in input_dict:
21432175
if k not in transformed_data:
21442176
transformed_data[k] = input_dict[k]
2145-
transformed_df = pd.DataFrame(transformed_data)
2146-
else:
2147-
transformed_df = pd.merge(
2148-
transformed_df,
2149-
input_df,
2150-
how="left",
2151-
on=join_keys,
2152-
)
2153-
else:
2154-
# overwrite any transformed features and update the dictionary
2155-
for k in input_dict:
2156-
if k not in transformed_data:
2157-
transformed_data[k] = input_dict[k]
21582177

2159-
return pd.DataFrame(transformed_data)
2178+
return pd.DataFrame(transformed_data)
21602179

2161-
elif feature_view.mode == "pandas" and isinstance(
2162-
feature_view.feature_transformation, PandasTransformation
2163-
):
2164-
transformed_df = feature_view.feature_transformation.udf(df)
2165-
for col in df.columns:
2166-
transformed_df[col] = df[col]
2167-
return transformed_df
2168-
else:
2169-
raise Exception("Unsupported OnDemandFeatureView mode")
2180+
elif feature_view.mode == "pandas" and isinstance(
2181+
feature_view.feature_transformation, PandasTransformation
2182+
):
2183+
transformed_df = feature_view.feature_transformation.udf(df)
2184+
for col in df.columns:
2185+
transformed_df[col] = df[col]
2186+
return transformed_df
2187+
else:
2188+
raise Exception("Unsupported OnDemandFeatureView mode")
2189+
finally:
2190+
if _should_track:
2191+
from feast.metrics import track_write_transformation
2192+
2193+
track_write_transformation(
2194+
feature_view.name,
2195+
feature_view.mode,
2196+
_time.monotonic() - _t0,
2197+
)
21702198

21712199
def _validate_vector_features(self, feature_view, df: pd.DataFrame) -> None:
21722200
"""

sdk/python/feast/infra/feature_servers/base_config.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,20 @@ class MetricsConfig(FeastConfigBaseModel):
6262
online_features: StrictBool = True
6363
"""Emit online feature retrieval metrics
6464
(feast_online_features_request_total,
65-
feast_online_features_entity_count)."""
65+
feast_online_features_entity_count,
66+
feast_feature_server_online_store_read_duration_seconds,
67+
feast_feature_server_transformation_duration_seconds,
68+
feast_feature_server_write_transformation_duration_seconds).
69+
ODFV transformation metrics additionally require track_metrics=True
70+
on the OnDemandFeatureView definition."""
6671

6772
push: StrictBool = True
6873
"""Emit push/write request counters
6974
(feast_push_request_total)."""
7075

7176
materialization: StrictBool = True
7277
"""Emit materialization success/failure counters and duration histograms
73-
(feast_materialization_total,
78+
(feast_materialization_result_total,
7479
feast_materialization_duration_seconds)."""
7580

7681
freshness: StrictBool = True

sdk/python/feast/infra/online_stores/online_store.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,19 @@ def get_online_features(
185185
native_entity_values=True,
186186
)
187187

188+
_track_read = False
189+
try:
190+
from feast.metrics import _config as _metrics_config
191+
192+
_track_read = _metrics_config.online_features
193+
except Exception:
194+
pass
195+
196+
if _track_read:
197+
import time as _time
198+
199+
_read_start = _time.monotonic()
200+
188201
for table, requested_features in grouped_refs:
189202
# Get the correct set of entity values with the correct join keys.
190203
table_entity_values, idxs, output_len = utils._get_unique_entities(
@@ -218,6 +231,11 @@ def get_online_features(
218231
output_len,
219232
)
220233

234+
if _track_read:
235+
from feast.metrics import track_online_store_read
236+
237+
track_online_store_read(_time.monotonic() - _read_start)
238+
221239
if requested_on_demand_feature_views:
222240
utils._augment_response_with_on_demand_transforms(
223241
online_features_response,
@@ -293,6 +311,19 @@ async def query_table(table, requested_features):
293311

294312
return idxs, read_rows, output_len
295313

314+
_track_read = False
315+
try:
316+
from feast.metrics import _config as _metrics_config
317+
318+
_track_read = _metrics_config.online_features
319+
except Exception:
320+
pass
321+
322+
if _track_read:
323+
import time as _time
324+
325+
_read_start = _time.monotonic()
326+
296327
all_responses = await asyncio.gather(
297328
*[
298329
query_table(table, requested_features)
@@ -318,6 +349,11 @@ async def query_table(table, requested_features):
318349
output_len,
319350
)
320351

352+
if _track_read:
353+
from feast.metrics import track_online_store_read
354+
355+
track_online_store_read(_time.monotonic() - _read_start)
356+
321357
if requested_on_demand_feature_views:
322358
utils._augment_response_with_on_demand_transforms(
323359
online_features_response,

0 commit comments

Comments
 (0)