mlrun 1.10.0rc13__py3-none-any.whl → 1.10.0rc42__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mlrun might be problematic. Click here for more details.
- mlrun/__init__.py +22 -2
- mlrun/artifacts/base.py +0 -31
- mlrun/artifacts/document.py +6 -1
- mlrun/artifacts/llm_prompt.py +123 -25
- mlrun/artifacts/manager.py +0 -5
- mlrun/artifacts/model.py +3 -3
- mlrun/common/constants.py +10 -1
- mlrun/common/formatters/artifact.py +1 -0
- mlrun/common/model_monitoring/helpers.py +86 -0
- mlrun/common/schemas/__init__.py +3 -0
- mlrun/common/schemas/auth.py +2 -0
- mlrun/common/schemas/function.py +10 -0
- mlrun/common/schemas/hub.py +30 -18
- mlrun/common/schemas/model_monitoring/__init__.py +3 -0
- mlrun/common/schemas/model_monitoring/constants.py +30 -6
- mlrun/common/schemas/model_monitoring/functions.py +14 -5
- mlrun/common/schemas/model_monitoring/model_endpoints.py +21 -0
- mlrun/common/schemas/pipeline.py +1 -1
- mlrun/common/schemas/serving.py +3 -0
- mlrun/common/schemas/workflow.py +3 -1
- mlrun/common/secrets.py +22 -1
- mlrun/config.py +33 -11
- mlrun/datastore/__init__.py +11 -3
- mlrun/datastore/azure_blob.py +162 -47
- mlrun/datastore/datastore.py +9 -4
- mlrun/datastore/datastore_profile.py +61 -5
- mlrun/datastore/model_provider/huggingface_provider.py +363 -0
- mlrun/datastore/model_provider/mock_model_provider.py +87 -0
- mlrun/datastore/model_provider/model_provider.py +230 -65
- mlrun/datastore/model_provider/openai_provider.py +295 -42
- mlrun/datastore/s3.py +24 -2
- mlrun/datastore/storeytargets.py +2 -3
- mlrun/datastore/utils.py +15 -3
- mlrun/db/base.py +47 -19
- mlrun/db/httpdb.py +120 -56
- mlrun/db/nopdb.py +38 -10
- mlrun/execution.py +70 -19
- mlrun/hub/__init__.py +15 -0
- mlrun/hub/module.py +181 -0
- mlrun/k8s_utils.py +105 -16
- mlrun/launcher/base.py +13 -6
- mlrun/launcher/local.py +15 -0
- mlrun/model.py +24 -3
- mlrun/model_monitoring/__init__.py +1 -0
- mlrun/model_monitoring/api.py +66 -27
- mlrun/model_monitoring/applications/__init__.py +1 -1
- mlrun/model_monitoring/applications/base.py +509 -117
- mlrun/model_monitoring/applications/context.py +2 -4
- mlrun/model_monitoring/applications/results.py +4 -7
- mlrun/model_monitoring/controller.py +239 -101
- mlrun/model_monitoring/db/_schedules.py +116 -33
- mlrun/model_monitoring/db/_stats.py +4 -3
- mlrun/model_monitoring/db/tsdb/base.py +100 -9
- mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +11 -6
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +191 -50
- mlrun/model_monitoring/db/tsdb/tdengine/writer_graph_steps.py +51 -0
- mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +17 -4
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +259 -40
- mlrun/model_monitoring/helpers.py +54 -9
- mlrun/model_monitoring/stream_processing.py +45 -14
- mlrun/model_monitoring/writer.py +220 -1
- mlrun/platforms/__init__.py +3 -2
- mlrun/platforms/iguazio.py +7 -3
- mlrun/projects/operations.py +6 -1
- mlrun/projects/pipelines.py +46 -26
- mlrun/projects/project.py +166 -58
- mlrun/run.py +94 -17
- mlrun/runtimes/__init__.py +18 -0
- mlrun/runtimes/base.py +14 -6
- mlrun/runtimes/daskjob.py +7 -0
- mlrun/runtimes/local.py +5 -2
- mlrun/runtimes/mounts.py +20 -2
- mlrun/runtimes/mpijob/abstract.py +6 -0
- mlrun/runtimes/mpijob/v1.py +6 -0
- mlrun/runtimes/nuclio/__init__.py +1 -0
- mlrun/runtimes/nuclio/application/application.py +149 -17
- mlrun/runtimes/nuclio/function.py +76 -27
- mlrun/runtimes/nuclio/serving.py +97 -15
- mlrun/runtimes/pod.py +234 -21
- mlrun/runtimes/remotesparkjob.py +6 -0
- mlrun/runtimes/sparkjob/spark3job.py +6 -0
- mlrun/runtimes/utils.py +49 -11
- mlrun/secrets.py +54 -13
- mlrun/serving/__init__.py +2 -0
- mlrun/serving/remote.py +79 -6
- mlrun/serving/routers.py +23 -41
- mlrun/serving/server.py +320 -80
- mlrun/serving/states.py +725 -157
- mlrun/serving/steps.py +62 -0
- mlrun/serving/system_steps.py +200 -119
- mlrun/serving/v2_serving.py +9 -10
- mlrun/utils/helpers.py +288 -88
- mlrun/utils/logger.py +3 -1
- mlrun/utils/notifications/notification/base.py +18 -0
- mlrun/utils/notifications/notification/git.py +2 -4
- mlrun/utils/notifications/notification/slack.py +2 -4
- mlrun/utils/notifications/notification/webhook.py +2 -5
- mlrun/utils/notifications/notification_pusher.py +1 -1
- mlrun/utils/retryer.py +15 -2
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.10.0rc13.dist-info → mlrun-1.10.0rc42.dist-info}/METADATA +45 -51
- {mlrun-1.10.0rc13.dist-info → mlrun-1.10.0rc42.dist-info}/RECORD +106 -101
- mlrun/api/schemas/__init__.py +0 -259
- {mlrun-1.10.0rc13.dist-info → mlrun-1.10.0rc42.dist-info}/WHEEL +0 -0
- {mlrun-1.10.0rc13.dist-info → mlrun-1.10.0rc42.dist-info}/entry_points.txt +0 -0
- {mlrun-1.10.0rc13.dist-info → mlrun-1.10.0rc42.dist-info}/licenses/LICENSE +0 -0
- {mlrun-1.10.0rc13.dist-info → mlrun-1.10.0rc42.dist-info}/top_level.txt +0 -0
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import threading
|
|
16
16
|
from datetime import datetime, timedelta
|
|
17
|
-
from typing import
|
|
17
|
+
from typing import Final, Literal, Optional, Union
|
|
18
18
|
|
|
19
19
|
import pandas as pd
|
|
20
20
|
import taosws
|
|
@@ -22,7 +22,7 @@ import taosws
|
|
|
22
22
|
import mlrun.common.schemas.model_monitoring as mm_schemas
|
|
23
23
|
import mlrun.common.types
|
|
24
24
|
import mlrun.model_monitoring.db.tsdb.tdengine.schemas as tdengine_schemas
|
|
25
|
-
|
|
25
|
+
from mlrun.config import config
|
|
26
26
|
from mlrun.datastore.datastore_profile import DatastoreProfile
|
|
27
27
|
from mlrun.model_monitoring.db import TSDBConnector
|
|
28
28
|
from mlrun.model_monitoring.db.tsdb.tdengine.tdengine_connection import (
|
|
@@ -55,14 +55,12 @@ class TDEngineConnector(TSDBConnector):
|
|
|
55
55
|
"""
|
|
56
56
|
|
|
57
57
|
type: str = mm_schemas.TSDBTarget.TDEngine
|
|
58
|
-
database = f"{tdengine_schemas._MODEL_MONITORING_DATABASE}_{mlrun.mlconf.system_id}"
|
|
59
58
|
|
|
60
59
|
def __init__(
|
|
61
60
|
self,
|
|
62
61
|
project: str,
|
|
63
62
|
profile: DatastoreProfile,
|
|
64
63
|
timestamp_precision: TDEngineTimestampPrecision = TDEngineTimestampPrecision.MICROSECOND,
|
|
65
|
-
**kwargs,
|
|
66
64
|
):
|
|
67
65
|
super().__init__(project=project)
|
|
68
66
|
|
|
@@ -72,6 +70,15 @@ class TDEngineConnector(TSDBConnector):
|
|
|
72
70
|
timestamp_precision
|
|
73
71
|
)
|
|
74
72
|
|
|
73
|
+
if not mlrun.mlconf.system_id:
|
|
74
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
75
|
+
"system_id is not set in mlrun.mlconf. "
|
|
76
|
+
"TDEngineConnector requires system_id to be configured for database name construction. "
|
|
77
|
+
"Please ensure MLRun configuration is properly loaded before creating TDEngineConnector."
|
|
78
|
+
)
|
|
79
|
+
self.database = (
|
|
80
|
+
f"{tdengine_schemas._MODEL_MONITORING_DATABASE}_{mlrun.mlconf.system_id}"
|
|
81
|
+
)
|
|
75
82
|
self._init_super_tables()
|
|
76
83
|
|
|
77
84
|
@property
|
|
@@ -205,7 +212,7 @@ class TDEngineConnector(TSDBConnector):
|
|
|
205
212
|
@staticmethod
|
|
206
213
|
def _generate_filter_query(
|
|
207
214
|
filter_column: str, filter_values: Union[str, list[Union[str, int]]]
|
|
208
|
-
) ->
|
|
215
|
+
) -> str:
|
|
209
216
|
"""
|
|
210
217
|
Generate a filter query for TDEngine based on the provided column and values.
|
|
211
218
|
|
|
@@ -213,15 +220,14 @@ class TDEngineConnector(TSDBConnector):
|
|
|
213
220
|
:param filter_values: A single value or a list of values to filter by.
|
|
214
221
|
|
|
215
222
|
:return: A string representing the filter query.
|
|
216
|
-
:raise:
|
|
223
|
+
:raise: ``MLRunValueError`` if the filter values are not of type string or list.
|
|
217
224
|
"""
|
|
218
|
-
|
|
219
225
|
if isinstance(filter_values, str):
|
|
220
226
|
return f"{filter_column}='{filter_values}'"
|
|
221
227
|
elif isinstance(filter_values, list):
|
|
222
228
|
return f"{filter_column} IN ({', '.join(repr(v) for v in filter_values)}) "
|
|
223
229
|
else:
|
|
224
|
-
raise mlrun.errors.
|
|
230
|
+
raise mlrun.errors.MLRunValueError(
|
|
225
231
|
f"Invalid filter values {filter_values}: must be a string or a list, "
|
|
226
232
|
f"got {type(filter_values).__name__}; filter values: {filter_values}"
|
|
227
233
|
)
|
|
@@ -279,6 +285,65 @@ class TDEngineConnector(TSDBConnector):
|
|
|
279
285
|
after="ProcessBeforeTDEngine",
|
|
280
286
|
)
|
|
281
287
|
|
|
288
|
+
def add_pre_writer_steps(self, graph, after):
|
|
289
|
+
return graph.add_step(
|
|
290
|
+
"mlrun.model_monitoring.db.tsdb.tdengine.writer_graph_steps.ProcessBeforeTDEngine",
|
|
291
|
+
name="ProcessBeforeTDEngine",
|
|
292
|
+
after=after,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def apply_writer_steps(self, graph, after, **kwargs) -> None:
|
|
296
|
+
graph.add_step(
|
|
297
|
+
"mlrun.datastore.storeytargets.TDEngineStoreyTarget",
|
|
298
|
+
name="tsdb_metrics",
|
|
299
|
+
after=after,
|
|
300
|
+
url=f"ds://{self._tdengine_connection_profile.name}",
|
|
301
|
+
supertable=self.tables[mm_schemas.TDEngineSuperTables.METRICS].super_table,
|
|
302
|
+
table_col=mm_schemas.EventFieldType.TABLE_COLUMN,
|
|
303
|
+
time_col=mm_schemas.WriterEvent.END_INFER_TIME,
|
|
304
|
+
database=self.database,
|
|
305
|
+
graph_shape="cylinder",
|
|
306
|
+
columns=[
|
|
307
|
+
mm_schemas.WriterEvent.START_INFER_TIME,
|
|
308
|
+
mm_schemas.MetricData.METRIC_VALUE,
|
|
309
|
+
],
|
|
310
|
+
tag_cols=[
|
|
311
|
+
mm_schemas.WriterEvent.ENDPOINT_ID,
|
|
312
|
+
mm_schemas.WriterEvent.APPLICATION_NAME,
|
|
313
|
+
mm_schemas.MetricData.METRIC_NAME,
|
|
314
|
+
],
|
|
315
|
+
max_events=config.model_endpoint_monitoring.writer_graph.max_events,
|
|
316
|
+
flush_after_seconds=config.model_endpoint_monitoring.writer_graph.flush_after_seconds,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
graph.add_step(
|
|
320
|
+
"mlrun.datastore.storeytargets.TDEngineStoreyTarget",
|
|
321
|
+
name="tsdb_app_results",
|
|
322
|
+
after=after,
|
|
323
|
+
url=f"ds://{self._tdengine_connection_profile.name}",
|
|
324
|
+
supertable=self.tables[
|
|
325
|
+
mm_schemas.TDEngineSuperTables.APP_RESULTS
|
|
326
|
+
].super_table,
|
|
327
|
+
table_col=mm_schemas.EventFieldType.TABLE_COLUMN,
|
|
328
|
+
time_col=mm_schemas.WriterEvent.END_INFER_TIME,
|
|
329
|
+
database=self.database,
|
|
330
|
+
graph_shape="cylinder",
|
|
331
|
+
columns=[
|
|
332
|
+
mm_schemas.WriterEvent.START_INFER_TIME,
|
|
333
|
+
mm_schemas.ResultData.RESULT_VALUE,
|
|
334
|
+
mm_schemas.ResultData.RESULT_STATUS,
|
|
335
|
+
mm_schemas.ResultData.RESULT_EXTRA_DATA,
|
|
336
|
+
],
|
|
337
|
+
tag_cols=[
|
|
338
|
+
mm_schemas.WriterEvent.ENDPOINT_ID,
|
|
339
|
+
mm_schemas.WriterEvent.APPLICATION_NAME,
|
|
340
|
+
mm_schemas.ResultData.RESULT_NAME,
|
|
341
|
+
mm_schemas.ResultData.RESULT_KIND,
|
|
342
|
+
],
|
|
343
|
+
max_events=config.model_endpoint_monitoring.writer_graph.max_events,
|
|
344
|
+
flush_after_seconds=config.model_endpoint_monitoring.writer_graph.flush_after_seconds,
|
|
345
|
+
)
|
|
346
|
+
|
|
282
347
|
def handle_model_error(
|
|
283
348
|
self,
|
|
284
349
|
graph,
|
|
@@ -311,10 +376,7 @@ class TDEngineConnector(TSDBConnector):
|
|
|
311
376
|
flush_after_seconds=tsdb_batching_timeout_secs,
|
|
312
377
|
)
|
|
313
378
|
|
|
314
|
-
def delete_tsdb_records(
|
|
315
|
-
self,
|
|
316
|
-
endpoint_ids: list[str],
|
|
317
|
-
):
|
|
379
|
+
def delete_tsdb_records(self, endpoint_ids: list[str]) -> None:
|
|
318
380
|
"""
|
|
319
381
|
To delete subtables within TDEngine, we first query the subtables names with the provided endpoint_ids.
|
|
320
382
|
Then, we drop each subtable.
|
|
@@ -332,9 +394,7 @@ class TDEngineConnector(TSDBConnector):
|
|
|
332
394
|
get_subtable_query = self.tables[table]._get_subtables_query_by_tag(
|
|
333
395
|
filter_tag="endpoint_id", filter_values=endpoint_ids
|
|
334
396
|
)
|
|
335
|
-
subtables_result = self.connection.run(
|
|
336
|
-
query=get_subtable_query,
|
|
337
|
-
)
|
|
397
|
+
subtables_result = self.connection.run(query=get_subtable_query)
|
|
338
398
|
subtables.extend([subtable[0] for subtable in subtables_result.data])
|
|
339
399
|
except Exception as e:
|
|
340
400
|
logger.warning(
|
|
@@ -346,15 +406,13 @@ class TDEngineConnector(TSDBConnector):
|
|
|
346
406
|
)
|
|
347
407
|
|
|
348
408
|
# Prepare the drop statements
|
|
349
|
-
drop_statements = [
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
)
|
|
409
|
+
drop_statements = [
|
|
410
|
+
self.tables[table].drop_subtable_query(subtable=subtable)
|
|
411
|
+
for subtable in subtables
|
|
412
|
+
]
|
|
354
413
|
try:
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
)
|
|
414
|
+
logger.debug("Dropping subtables", drop_statements=drop_statements)
|
|
415
|
+
self.connection.run(statements=drop_statements)
|
|
358
416
|
except Exception as e:
|
|
359
417
|
logger.warning(
|
|
360
418
|
"Failed to delete model endpoint resources. You may need to delete them manually. "
|
|
@@ -369,6 +427,48 @@ class TDEngineConnector(TSDBConnector):
|
|
|
369
427
|
number_of_endpoints_to_delete=len(endpoint_ids),
|
|
370
428
|
)
|
|
371
429
|
|
|
430
|
+
def delete_application_records(
|
|
431
|
+
self, application_name: str, endpoint_ids: Optional[list[str]] = None
|
|
432
|
+
) -> None:
|
|
433
|
+
"""
|
|
434
|
+
Delete application records from the TSDB for the given model endpoints or all if ``endpoint_ids`` is ``None``.
|
|
435
|
+
"""
|
|
436
|
+
logger.debug(
|
|
437
|
+
"Deleting application records",
|
|
438
|
+
project=self.project,
|
|
439
|
+
application_name=application_name,
|
|
440
|
+
endpoint_ids=endpoint_ids,
|
|
441
|
+
)
|
|
442
|
+
tables = [
|
|
443
|
+
self.tables[mm_schemas.TDEngineSuperTables.APP_RESULTS],
|
|
444
|
+
self.tables[mm_schemas.TDEngineSuperTables.METRICS],
|
|
445
|
+
]
|
|
446
|
+
|
|
447
|
+
filter_query = self._generate_filter_query(
|
|
448
|
+
filter_column=mm_schemas.ApplicationEvent.APPLICATION_NAME,
|
|
449
|
+
filter_values=application_name,
|
|
450
|
+
)
|
|
451
|
+
if endpoint_ids:
|
|
452
|
+
endpoint_ids_filter = self._generate_filter_query(
|
|
453
|
+
filter_column=mm_schemas.EventFieldType.ENDPOINT_ID,
|
|
454
|
+
filter_values=endpoint_ids,
|
|
455
|
+
)
|
|
456
|
+
filter_query += f" AND {endpoint_ids_filter}"
|
|
457
|
+
|
|
458
|
+
drop_statements: list[str] = []
|
|
459
|
+
for table in tables:
|
|
460
|
+
get_subtable_query = table._get_tables_query_by_condition(filter_query)
|
|
461
|
+
subtables_result = self.connection.run(query=get_subtable_query)
|
|
462
|
+
drop_statements.extend(
|
|
463
|
+
[
|
|
464
|
+
table.drop_subtable_query(subtable=subtable[0])
|
|
465
|
+
for subtable in subtables_result.data
|
|
466
|
+
]
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
logger.debug("Dropping application records", drop_statements=drop_statements)
|
|
470
|
+
self.connection.run(statements=drop_statements)
|
|
471
|
+
|
|
372
472
|
def delete_tsdb_resources(self):
|
|
373
473
|
"""
|
|
374
474
|
Delete all project resources in the TSDB connector, such as model endpoints data and drift results.
|
|
@@ -469,6 +569,7 @@ class TDEngineConnector(TSDBConnector):
|
|
|
469
569
|
preform_agg_columns: Optional[list] = None,
|
|
470
570
|
order_by: Optional[str] = None,
|
|
471
571
|
desc: Optional[bool] = None,
|
|
572
|
+
partition_by: Optional[str] = None,
|
|
472
573
|
) -> pd.DataFrame:
|
|
473
574
|
"""
|
|
474
575
|
Getting records from TSDB data collection.
|
|
@@ -496,6 +597,8 @@ class TDEngineConnector(TSDBConnector):
|
|
|
496
597
|
if an empty list was provided The aggregation won't be performed.
|
|
497
598
|
:param order_by: The column or alias to preform ordering on the query.
|
|
498
599
|
:param desc: Whether or not to sort the results in descending order.
|
|
600
|
+
:param partition_by: The column to partition the results by. Note that if interval is provided,
|
|
601
|
+
`agg_funcs` must bg provided as well.
|
|
499
602
|
|
|
500
603
|
:return: DataFrame with the provided attributes from the data collection.
|
|
501
604
|
:raise: MLRunInvalidArgumentError if query the provided table failed.
|
|
@@ -517,6 +620,7 @@ class TDEngineConnector(TSDBConnector):
|
|
|
517
620
|
preform_agg_funcs_columns=preform_agg_columns,
|
|
518
621
|
order_by=order_by,
|
|
519
622
|
desc=desc,
|
|
623
|
+
partition_by=partition_by,
|
|
520
624
|
)
|
|
521
625
|
logger.debug("Querying TDEngine", query=full_query)
|
|
522
626
|
try:
|
|
@@ -684,7 +788,9 @@ class TDEngineConnector(TSDBConnector):
|
|
|
684
788
|
endpoint_ids: Union[str, list[str]],
|
|
685
789
|
start: Optional[datetime] = None,
|
|
686
790
|
end: Optional[datetime] = None,
|
|
687
|
-
) -> pd.DataFrame:
|
|
791
|
+
) -> Union[pd.DataFrame, dict[str, float]]:
|
|
792
|
+
if not endpoint_ids:
|
|
793
|
+
return {}
|
|
688
794
|
filter_query = self._generate_filter_query(
|
|
689
795
|
filter_column=mm_schemas.EventFieldType.ENDPOINT_ID,
|
|
690
796
|
filter_values=endpoint_ids,
|
|
@@ -819,7 +925,7 @@ class TDEngineConnector(TSDBConnector):
|
|
|
819
925
|
# Convert DataFrame to a dictionary
|
|
820
926
|
return {
|
|
821
927
|
(
|
|
822
|
-
row[mm_schemas.WriterEvent.APPLICATION_NAME],
|
|
928
|
+
row[mm_schemas.WriterEvent.APPLICATION_NAME].lower(),
|
|
823
929
|
row[mm_schemas.ResultData.RESULT_STATUS],
|
|
824
930
|
): row["count(result_value)"]
|
|
825
931
|
for _, row in df.iterrows()
|
|
@@ -904,26 +1010,34 @@ class TDEngineConnector(TSDBConnector):
|
|
|
904
1010
|
mm_schemas.WriterEvent.END_INFER_TIME,
|
|
905
1011
|
mm_schemas.WriterEvent.APPLICATION_NAME,
|
|
906
1012
|
]
|
|
1013
|
+
agg_columns = [mm_schemas.WriterEvent.END_INFER_TIME]
|
|
1014
|
+
group_by_columns = [mm_schemas.WriterEvent.APPLICATION_NAME]
|
|
907
1015
|
if record_type == "results":
|
|
908
1016
|
table = self.tables[
|
|
909
1017
|
mm_schemas.TDEngineSuperTables.APP_RESULTS
|
|
910
1018
|
].super_table
|
|
911
1019
|
columns += [
|
|
912
1020
|
mm_schemas.ResultData.RESULT_NAME,
|
|
1021
|
+
mm_schemas.ResultData.RESULT_KIND,
|
|
1022
|
+
mm_schemas.ResultData.RESULT_STATUS,
|
|
1023
|
+
mm_schemas.ResultData.RESULT_VALUE,
|
|
1024
|
+
]
|
|
1025
|
+
agg_columns += [
|
|
913
1026
|
mm_schemas.ResultData.RESULT_VALUE,
|
|
914
1027
|
mm_schemas.ResultData.RESULT_STATUS,
|
|
915
1028
|
mm_schemas.ResultData.RESULT_KIND,
|
|
916
1029
|
]
|
|
917
|
-
|
|
1030
|
+
group_by_columns += [mm_schemas.ResultData.RESULT_NAME]
|
|
918
1031
|
else:
|
|
919
1032
|
table = self.tables[mm_schemas.TDEngineSuperTables.METRICS].super_table
|
|
920
1033
|
columns += [
|
|
921
1034
|
mm_schemas.MetricData.METRIC_NAME,
|
|
922
1035
|
mm_schemas.MetricData.METRIC_VALUE,
|
|
923
1036
|
]
|
|
924
|
-
|
|
1037
|
+
agg_columns += [mm_schemas.MetricData.METRIC_VALUE]
|
|
1038
|
+
group_by_columns += [mm_schemas.MetricData.METRIC_NAME]
|
|
925
1039
|
|
|
926
|
-
|
|
1040
|
+
df = self._get_records(
|
|
927
1041
|
table=table,
|
|
928
1042
|
start=start,
|
|
929
1043
|
end=end,
|
|
@@ -931,10 +1045,17 @@ class TDEngineConnector(TSDBConnector):
|
|
|
931
1045
|
filter_query=filter_query,
|
|
932
1046
|
timestamp_column=mm_schemas.WriterEvent.END_INFER_TIME,
|
|
933
1047
|
# Aggregate per application/metric pair regardless of timestamp
|
|
934
|
-
group_by=
|
|
935
|
-
preform_agg_columns=
|
|
1048
|
+
group_by=group_by_columns,
|
|
1049
|
+
preform_agg_columns=agg_columns,
|
|
936
1050
|
agg_funcs=["last"],
|
|
937
1051
|
)
|
|
1052
|
+
if not df.empty:
|
|
1053
|
+
for column in agg_columns:
|
|
1054
|
+
df.rename(
|
|
1055
|
+
columns={f"last({column})": column},
|
|
1056
|
+
inplace=True,
|
|
1057
|
+
)
|
|
1058
|
+
return df
|
|
938
1059
|
|
|
939
1060
|
df_results = get_latest_metrics_records(record_type="results")
|
|
940
1061
|
df_metrics = get_latest_metrics_records(record_type="metrics")
|
|
@@ -951,19 +1072,14 @@ class TDEngineConnector(TSDBConnector):
|
|
|
951
1072
|
]
|
|
952
1073
|
):
|
|
953
1074
|
metric_objects = []
|
|
954
|
-
|
|
955
1075
|
if not df_results.empty:
|
|
956
|
-
df_results.rename(
|
|
957
|
-
columns={
|
|
958
|
-
f"last({mm_schemas.ResultData.RESULT_VALUE})": mm_schemas.ResultData.RESULT_VALUE,
|
|
959
|
-
},
|
|
960
|
-
inplace=True,
|
|
961
|
-
)
|
|
962
1076
|
for _, row in df_results.iterrows():
|
|
963
1077
|
metric_objects.append(
|
|
964
1078
|
mm_schemas.ApplicationResultRecord(
|
|
965
1079
|
time=datetime.fromisoformat(
|
|
966
|
-
row[mm_schemas.WriterEvent.END_INFER_TIME]
|
|
1080
|
+
row[mm_schemas.WriterEvent.END_INFER_TIME].replace(
|
|
1081
|
+
" +", "+"
|
|
1082
|
+
)
|
|
967
1083
|
),
|
|
968
1084
|
result_name=row[mm_schemas.ResultData.RESULT_NAME],
|
|
969
1085
|
kind=row[mm_schemas.ResultData.RESULT_KIND],
|
|
@@ -973,17 +1089,13 @@ class TDEngineConnector(TSDBConnector):
|
|
|
973
1089
|
)
|
|
974
1090
|
|
|
975
1091
|
if not df_metrics.empty:
|
|
976
|
-
df_metrics.rename(
|
|
977
|
-
columns={
|
|
978
|
-
f"last({mm_schemas.MetricData.METRIC_VALUE})": mm_schemas.MetricData.METRIC_VALUE,
|
|
979
|
-
},
|
|
980
|
-
inplace=True,
|
|
981
|
-
)
|
|
982
1092
|
for _, row in df_metrics.iterrows():
|
|
983
1093
|
metric_objects.append(
|
|
984
1094
|
mm_schemas.ApplicationMetricRecord(
|
|
985
1095
|
time=datetime.fromisoformat(
|
|
986
|
-
row[mm_schemas.WriterEvent.END_INFER_TIME]
|
|
1096
|
+
row[mm_schemas.WriterEvent.END_INFER_TIME].replace(
|
|
1097
|
+
" +", "+"
|
|
1098
|
+
)
|
|
987
1099
|
),
|
|
988
1100
|
metric_name=row[mm_schemas.MetricData.METRIC_NAME],
|
|
989
1101
|
value=row[mm_schemas.MetricData.METRIC_VALUE],
|
|
@@ -1142,11 +1254,9 @@ class TDEngineConnector(TSDBConnector):
|
|
|
1142
1254
|
df.dropna(inplace=True)
|
|
1143
1255
|
return df
|
|
1144
1256
|
|
|
1145
|
-
|
|
1257
|
+
def add_basic_metrics(
|
|
1146
1258
|
self,
|
|
1147
1259
|
model_endpoint_objects: list[mlrun.common.schemas.ModelEndpoint],
|
|
1148
|
-
project: str,
|
|
1149
|
-
run_in_threadpool: Callable,
|
|
1150
1260
|
metric_list: Optional[list[str]] = None,
|
|
1151
1261
|
) -> list[mlrun.common.schemas.ModelEndpoint]:
|
|
1152
1262
|
"""
|
|
@@ -1154,8 +1264,6 @@ class TDEngineConnector(TSDBConnector):
|
|
|
1154
1264
|
|
|
1155
1265
|
:param model_endpoint_objects: A list of `ModelEndpoint` objects that will
|
|
1156
1266
|
be filled with the relevant basic metrics.
|
|
1157
|
-
:param project: The name of the project.
|
|
1158
|
-
:param run_in_threadpool: A function that runs another function in a thread pool.
|
|
1159
1267
|
:param metric_list: List of metrics to include from the time series DB. Defaults to all metrics.
|
|
1160
1268
|
|
|
1161
1269
|
:return: A list of `ModelEndpointMonitoringMetric` objects.
|
|
@@ -1205,6 +1313,39 @@ class TDEngineConnector(TSDBConnector):
|
|
|
1205
1313
|
)
|
|
1206
1314
|
)
|
|
1207
1315
|
|
|
1316
|
+
def get_drift_data(
|
|
1317
|
+
self,
|
|
1318
|
+
start: datetime,
|
|
1319
|
+
end: datetime,
|
|
1320
|
+
) -> mm_schemas.ModelEndpointDriftValues:
|
|
1321
|
+
filter_query = self._generate_filter_query(
|
|
1322
|
+
filter_column=mm_schemas.ResultData.RESULT_STATUS,
|
|
1323
|
+
filter_values=[
|
|
1324
|
+
mm_schemas.ResultStatusApp.potential_detection.value,
|
|
1325
|
+
mm_schemas.ResultStatusApp.detected.value,
|
|
1326
|
+
],
|
|
1327
|
+
)
|
|
1328
|
+
table = self.tables[mm_schemas.TDEngineSuperTables.APP_RESULTS].super_table
|
|
1329
|
+
start, end, interval = self._prepare_aligned_start_end(start, end)
|
|
1330
|
+
|
|
1331
|
+
# get per time-interval x endpoint_id combination the max result status
|
|
1332
|
+
df = self._get_records(
|
|
1333
|
+
table=table,
|
|
1334
|
+
start=start,
|
|
1335
|
+
end=end,
|
|
1336
|
+
interval=interval,
|
|
1337
|
+
columns=[mm_schemas.ResultData.RESULT_STATUS],
|
|
1338
|
+
filter_query=filter_query,
|
|
1339
|
+
timestamp_column=mm_schemas.WriterEvent.END_INFER_TIME,
|
|
1340
|
+
agg_funcs=["max"],
|
|
1341
|
+
partition_by=mm_schemas.WriterEvent.ENDPOINT_ID,
|
|
1342
|
+
)
|
|
1343
|
+
if df.empty:
|
|
1344
|
+
return mm_schemas.ModelEndpointDriftValues(values=[])
|
|
1345
|
+
|
|
1346
|
+
df["_wstart"] = pd.to_datetime(df["_wstart"])
|
|
1347
|
+
return self._df_to_drift_data(df)
|
|
1348
|
+
|
|
1208
1349
|
# Note: this function serves as a reference for checking the TSDB for the existence of a metric.
|
|
1209
1350
|
#
|
|
1210
1351
|
# def read_prediction_metric_for_endpoint_if_exists(
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright 2025 Iguazio
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
import mlrun.common.schemas.model_monitoring as mm_schemas
|
|
18
|
+
import mlrun.feature_store.steps
|
|
19
|
+
from mlrun.utils import logger
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProcessBeforeTDEngine(mlrun.feature_store.steps.MapClass):
|
|
23
|
+
def __init__(self, **kwargs):
|
|
24
|
+
"""
|
|
25
|
+
Process the data before writing to TDEngine. This step create the table name.
|
|
26
|
+
|
|
27
|
+
:returns: Event as a dictionary which will be written into the TDEngine Metrics/Results tables.
|
|
28
|
+
"""
|
|
29
|
+
super().__init__(**kwargs)
|
|
30
|
+
|
|
31
|
+
def do(self, event):
|
|
32
|
+
logger.info("Process event before writing to TDEngine", event=event)
|
|
33
|
+
kind = event.get("kind")
|
|
34
|
+
table_name = (
|
|
35
|
+
f"{event[mm_schemas.WriterEvent.ENDPOINT_ID]}_"
|
|
36
|
+
f"{event[mm_schemas.WriterEvent.APPLICATION_NAME]}"
|
|
37
|
+
)
|
|
38
|
+
if kind == mm_schemas.WriterEventKind.RESULT:
|
|
39
|
+
# Write a new result
|
|
40
|
+
event[mm_schemas.EventFieldType.TABLE_COLUMN] = (
|
|
41
|
+
f"{table_name}_{event[mm_schemas.ResultData.RESULT_NAME]}"
|
|
42
|
+
).replace("-", "_")
|
|
43
|
+
elif kind == mm_schemas.WriterEventKind.METRIC:
|
|
44
|
+
# Write a new metric
|
|
45
|
+
event[mm_schemas.EventFieldType.TABLE_COLUMN] = (
|
|
46
|
+
f"{table_name}_{event[mm_schemas.MetricData.METRIC_NAME]}"
|
|
47
|
+
).replace("-", "_")
|
|
48
|
+
event[mm_schemas.WriterEvent.START_INFER_TIME] = datetime.fromisoformat(
|
|
49
|
+
event[mm_schemas.WriterEvent.START_INFER_TIME]
|
|
50
|
+
)
|
|
51
|
+
return event
|
|
@@ -25,10 +25,12 @@ from mlrun.utils import logger
|
|
|
25
25
|
|
|
26
26
|
def _normalize_dict_for_v3io_frames(event: dict[str, Any]) -> dict[str, Any]:
|
|
27
27
|
"""
|
|
28
|
-
Normalize user
|
|
29
|
-
to a form V3IO frames tolerates.
|
|
28
|
+
Normalize user-defined keys (e.g., model input data and predictions) to a format V3IO Frames tolerates.
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
- Keys must match regex: '^[a-zA-Z_:]([a-zA-Z0-9_:])*$'
|
|
31
|
+
- Replace invalid characters (e.g., '-') with '_'.
|
|
32
|
+
- Prefix keys starting with digits with '_'.
|
|
33
|
+
- Flatten nested dictionaries using dot notation, while normalizing keys recursively.
|
|
32
34
|
"""
|
|
33
35
|
prefix = "_"
|
|
34
36
|
|
|
@@ -38,7 +40,18 @@ def _normalize_dict_for_v3io_frames(event: dict[str, Any]) -> dict[str, Any]:
|
|
|
38
40
|
return prefix + key
|
|
39
41
|
return key
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
def flatten_dict(d: dict[str, Any], parent_key: str = "") -> dict[str, Any]:
|
|
44
|
+
items = {}
|
|
45
|
+
for k, v in d.items():
|
|
46
|
+
new_key = norm_key(k)
|
|
47
|
+
full_key = f"{parent_key}.{new_key}" if parent_key else new_key
|
|
48
|
+
if isinstance(v, dict):
|
|
49
|
+
items.update(flatten_dict(v, full_key))
|
|
50
|
+
else:
|
|
51
|
+
items[full_key] = v
|
|
52
|
+
return items
|
|
53
|
+
|
|
54
|
+
return flatten_dict(event)
|
|
42
55
|
|
|
43
56
|
|
|
44
57
|
class ProcessBeforeTSDB(mlrun.feature_store.steps.MapClass):
|