mlrun 1.8.0rc18__py3-none-any.whl → 1.8.0rc20__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/__main__.py +5 -0
- mlrun/common/runtimes/constants.py +17 -0
- mlrun/common/schemas/artifact.py +6 -0
- mlrun/common/schemas/model_monitoring/__init__.py +1 -0
- mlrun/common/schemas/model_monitoring/constants.py +16 -0
- mlrun/common/schemas/model_monitoring/model_endpoints.py +4 -2
- mlrun/config.py +2 -2
- mlrun/db/base.py +18 -0
- mlrun/db/httpdb.py +118 -1
- mlrun/db/nopdb.py +9 -0
- mlrun/frameworks/_common/model_handler.py +0 -2
- mlrun/model_monitoring/db/tsdb/base.py +116 -8
- mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +2 -0
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +37 -29
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +46 -26
- mlrun/model_monitoring/helpers.py +2 -2
- mlrun/model_monitoring/stream_processing.py +21 -0
- mlrun/projects/pipelines.py +16 -3
- mlrun/projects/project.py +45 -8
- mlrun/runtimes/nuclio/serving.py +20 -11
- mlrun/serving/v2_serving.py +51 -36
- mlrun/utils/helpers.py +163 -1
- mlrun/utils/notifications/notification/webhook.py +3 -0
- mlrun/utils/notifications/notification_pusher.py +59 -165
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.8.0rc18.dist-info → mlrun-1.8.0rc20.dist-info}/METADATA +1 -1
- {mlrun-1.8.0rc18.dist-info → mlrun-1.8.0rc20.dist-info}/RECORD +31 -31
- {mlrun-1.8.0rc18.dist-info → mlrun-1.8.0rc20.dist-info}/LICENSE +0 -0
- {mlrun-1.8.0rc18.dist-info → mlrun-1.8.0rc20.dist-info}/WHEEL +0 -0
- {mlrun-1.8.0rc18.dist-info → mlrun-1.8.0rc20.dist-info}/entry_points.txt +0 -0
- {mlrun-1.8.0rc18.dist-info → mlrun-1.8.0rc20.dist-info}/top_level.txt +0 -0
mlrun/__main__.py
CHANGED
|
@@ -32,6 +32,7 @@ from tabulate import tabulate
|
|
|
32
32
|
import mlrun
|
|
33
33
|
import mlrun.common.constants as mlrun_constants
|
|
34
34
|
import mlrun.common.schemas
|
|
35
|
+
import mlrun.utils.helpers
|
|
35
36
|
from mlrun.common.helpers import parse_versioned_object_uri
|
|
36
37
|
from mlrun.runtimes.mounts import auto_mount as auto_mount_modifier
|
|
37
38
|
|
|
@@ -304,6 +305,7 @@ def run(
|
|
|
304
305
|
update_in(runtime, "spec.build.code_origin", url_file)
|
|
305
306
|
elif runtime:
|
|
306
307
|
runtime = py_eval(runtime)
|
|
308
|
+
runtime = mlrun.utils.helpers.as_dict(runtime)
|
|
307
309
|
if not isinstance(runtime, dict):
|
|
308
310
|
print(f"Runtime parameter must be a dict, not {type(runtime)}")
|
|
309
311
|
exit(1)
|
|
@@ -515,6 +517,7 @@ def build(
|
|
|
515
517
|
|
|
516
518
|
if runtime:
|
|
517
519
|
runtime = py_eval(runtime)
|
|
520
|
+
runtime = mlrun.utils.helpers.as_dict(runtime)
|
|
518
521
|
if not isinstance(runtime, dict):
|
|
519
522
|
print(f"Runtime parameter must be a dict, not {type(runtime)}")
|
|
520
523
|
exit(1)
|
|
@@ -662,6 +665,8 @@ def deploy(
|
|
|
662
665
|
runtime = py_eval(spec)
|
|
663
666
|
else:
|
|
664
667
|
runtime = {}
|
|
668
|
+
|
|
669
|
+
runtime = mlrun.utils.helpers.as_dict(runtime)
|
|
665
670
|
if not isinstance(runtime, dict):
|
|
666
671
|
print(f"Runtime parameter must be a dict, not {type(runtime)}")
|
|
667
672
|
exit(1)
|
|
@@ -214,6 +214,23 @@ class RunStates:
|
|
|
214
214
|
RunStates.skipped: mlrun_pipelines.common.models.RunStatuses.skipped,
|
|
215
215
|
}[run_state]
|
|
216
216
|
|
|
217
|
+
@staticmethod
|
|
218
|
+
def pipeline_run_status_to_run_state(pipeline_run_status):
|
|
219
|
+
if pipeline_run_status not in mlrun_pipelines.common.models.RunStatuses.all():
|
|
220
|
+
raise ValueError(f"Invalid pipeline run status: {pipeline_run_status}")
|
|
221
|
+
return {
|
|
222
|
+
mlrun_pipelines.common.models.RunStatuses.succeeded: RunStates.completed,
|
|
223
|
+
mlrun_pipelines.common.models.RunStatuses.failed: RunStates.error,
|
|
224
|
+
mlrun_pipelines.common.models.RunStatuses.running: RunStates.running,
|
|
225
|
+
mlrun_pipelines.common.models.RunStatuses.pending: RunStates.pending,
|
|
226
|
+
mlrun_pipelines.common.models.RunStatuses.canceled: RunStates.aborted,
|
|
227
|
+
mlrun_pipelines.common.models.RunStatuses.canceling: RunStates.aborting,
|
|
228
|
+
mlrun_pipelines.common.models.RunStatuses.skipped: RunStates.skipped,
|
|
229
|
+
mlrun_pipelines.common.models.RunStatuses.runtime_state_unspecified: RunStates.unknown,
|
|
230
|
+
mlrun_pipelines.common.models.RunStatuses.error: RunStates.error,
|
|
231
|
+
mlrun_pipelines.common.models.RunStatuses.paused: RunStates.unknown,
|
|
232
|
+
}[pipeline_run_status]
|
|
233
|
+
|
|
217
234
|
|
|
218
235
|
# TODO: remove this class in 1.9.0 - use only MlrunInternalLabels
|
|
219
236
|
class RunLabels(enum.Enum):
|
mlrun/common/schemas/artifact.py
CHANGED
|
@@ -51,6 +51,12 @@ class ArtifactCategories(mlrun.common.types.StrEnum):
|
|
|
51
51
|
True,
|
|
52
52
|
)
|
|
53
53
|
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_kind(cls, kind: str) -> "ArtifactCategories":
|
|
56
|
+
if kind in [cls.model.value, cls.dataset.value, cls.document.value]:
|
|
57
|
+
return cls(kind)
|
|
58
|
+
return cls.other
|
|
59
|
+
|
|
54
60
|
|
|
55
61
|
class ArtifactIdentifier(pydantic.v1.BaseModel):
|
|
56
62
|
# artifact kind
|
|
@@ -61,6 +61,7 @@ class ModelEndpointSchema(MonitoringStrEnum):
|
|
|
61
61
|
STATE = "state"
|
|
62
62
|
MONITORING_MODE = "monitoring_mode"
|
|
63
63
|
FIRST_REQUEST = "first_request"
|
|
64
|
+
SAMPLING_PERCENTAGE = "sampling_percentage"
|
|
64
65
|
|
|
65
66
|
# status - operative
|
|
66
67
|
LAST_REQUEST = "last_request"
|
|
@@ -137,6 +138,10 @@ class EventFieldType:
|
|
|
137
138
|
SAMPLE_PARQUET_PATH = "sample_parquet_path"
|
|
138
139
|
TIME = "time"
|
|
139
140
|
TABLE_COLUMN = "table_column"
|
|
141
|
+
SAMPLING_PERCENTAGE = "sampling_percentage"
|
|
142
|
+
SAMPLING_RATE = "sampling_rate"
|
|
143
|
+
ESTIMATED_PREDICTION_COUNT = "estimated_prediction_count"
|
|
144
|
+
EFFECTIVE_SAMPLE_COUNT = "effective_sample_count"
|
|
140
145
|
|
|
141
146
|
|
|
142
147
|
class FeatureSetFeatures(MonitoringStrEnum):
|
|
@@ -248,6 +253,12 @@ class ProjectSecretKeys:
|
|
|
248
253
|
]
|
|
249
254
|
|
|
250
255
|
|
|
256
|
+
class GetEventsFormat(MonitoringStrEnum):
|
|
257
|
+
SINGLE = "single"
|
|
258
|
+
SEPARATION = "separation"
|
|
259
|
+
INTERSECTION = "intersection"
|
|
260
|
+
|
|
261
|
+
|
|
251
262
|
class ModelEndpointTargetSchemas(MonitoringStrEnum):
|
|
252
263
|
V3IO = "v3io"
|
|
253
264
|
MYSQL = "mysql"
|
|
@@ -448,3 +459,8 @@ FQN_REGEX = re.compile(FQN_PATTERN)
|
|
|
448
459
|
PROJECT_PATTERN = r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"
|
|
449
460
|
|
|
450
461
|
MODEL_ENDPOINT_ID_PATTERN = r"^[a-zA-Z0-9_-]+$"
|
|
462
|
+
|
|
463
|
+
INTERSECT_DICT_KEYS = {
|
|
464
|
+
ModelEndpointMonitoringMetricType.METRIC: "intersect_metrics",
|
|
465
|
+
ModelEndpointMonitoringMetricType.RESULT: "intersect_results",
|
|
466
|
+
}
|
|
@@ -160,6 +160,7 @@ class ModelEndpointStatus(ObjectStatus, ModelEndpointParser):
|
|
|
160
160
|
state: Optional[str] = "unknown" # will be updated according to the function state
|
|
161
161
|
first_request: Optional[datetime] = None
|
|
162
162
|
monitoring_mode: Optional[ModelMonitoringMode] = ModelMonitoringMode.disabled
|
|
163
|
+
sampling_percentage: Optional[float] = 100
|
|
163
164
|
|
|
164
165
|
# operative
|
|
165
166
|
last_request: Optional[datetime] = None
|
|
@@ -177,6 +178,7 @@ class ModelEndpointStatus(ObjectStatus, ModelEndpointParser):
|
|
|
177
178
|
"monitoring_mode",
|
|
178
179
|
"first_request",
|
|
179
180
|
"last_request",
|
|
181
|
+
"sampling_percentage",
|
|
180
182
|
]
|
|
181
183
|
|
|
182
184
|
|
|
@@ -259,12 +261,12 @@ class ModelEndpointMonitoringMetric(BaseModel):
|
|
|
259
261
|
|
|
260
262
|
def __init__(self, **kwargs):
|
|
261
263
|
super().__init__(**kwargs)
|
|
262
|
-
self.full_name =
|
|
264
|
+
self.full_name = compose_full_name(
|
|
263
265
|
project=self.project, app=self.app, name=self.name, type=self.type
|
|
264
266
|
)
|
|
265
267
|
|
|
266
268
|
|
|
267
|
-
def
|
|
269
|
+
def compose_full_name(
|
|
268
270
|
*,
|
|
269
271
|
project: str,
|
|
270
272
|
app: str,
|
mlrun/config.py
CHANGED
|
@@ -83,8 +83,8 @@ default_config = {
|
|
|
83
83
|
"images_to_enrich_registry": "^mlrun/*,python:3.9",
|
|
84
84
|
"kfp_url": "",
|
|
85
85
|
"kfp_ttl": "14400", # KFP ttl in sec, after that completed PODs will be deleted
|
|
86
|
-
"kfp_image": "mlrun/mlrun-kfp", # image to use for KFP runner
|
|
87
|
-
"dask_kfp_image": "mlrun/ml-base", # image to use for dask KFP runner
|
|
86
|
+
"kfp_image": "mlrun/mlrun-kfp", # image to use for KFP runner
|
|
87
|
+
"dask_kfp_image": "mlrun/ml-base", # image to use for dask KFP runner
|
|
88
88
|
"igz_version": "", # the version of the iguazio system the API is running on
|
|
89
89
|
"iguazio_api_url": "", # the url to iguazio api
|
|
90
90
|
"spark_app_image": "", # image to use for spark operator app runtime
|
mlrun/db/base.py
CHANGED
|
@@ -68,6 +68,15 @@ class RunDBInterface(ABC):
|
|
|
68
68
|
):
|
|
69
69
|
pass
|
|
70
70
|
|
|
71
|
+
def push_pipeline_notifications(
|
|
72
|
+
self,
|
|
73
|
+
pipeline_id,
|
|
74
|
+
project="",
|
|
75
|
+
notifications=None,
|
|
76
|
+
timeout=45,
|
|
77
|
+
):
|
|
78
|
+
pass
|
|
79
|
+
|
|
71
80
|
@abstractmethod
|
|
72
81
|
def read_run(
|
|
73
82
|
self,
|
|
@@ -337,6 +346,15 @@ class RunDBInterface(ABC):
|
|
|
337
346
|
) -> list[mm_endpoints.ModelEndpointMonitoringMetric]:
|
|
338
347
|
pass
|
|
339
348
|
|
|
349
|
+
def get_metrics_by_multiple_endpoints(
|
|
350
|
+
self,
|
|
351
|
+
project: str,
|
|
352
|
+
endpoint_ids: Union[str, list[str]],
|
|
353
|
+
type: Literal["results", "metrics", "all"] = "all",
|
|
354
|
+
events_format: mm_constants.GetEventsFormat = mm_constants.GetEventsFormat.SEPARATION,
|
|
355
|
+
) -> dict[str, list[mm_endpoints.ModelEndpointMonitoringMetric]]:
|
|
356
|
+
pass
|
|
357
|
+
|
|
340
358
|
@abstractmethod
|
|
341
359
|
def delete_project(
|
|
342
360
|
self,
|
mlrun/db/httpdb.py
CHANGED
|
@@ -780,9 +780,84 @@ class HTTPRunDB(RunDBInterface):
|
|
|
780
780
|
)
|
|
781
781
|
if response.status_code == http.HTTPStatus.ACCEPTED:
|
|
782
782
|
background_task = mlrun.common.schemas.BackgroundTask(**response.json())
|
|
783
|
-
|
|
783
|
+
background_task = self._wait_for_background_task_to_reach_terminal_state(
|
|
784
|
+
background_task.metadata.name, project=project
|
|
785
|
+
)
|
|
786
|
+
if (
|
|
787
|
+
background_task.status.state
|
|
788
|
+
== mlrun.common.schemas.BackgroundTaskState.succeeded
|
|
789
|
+
):
|
|
790
|
+
logger.info(
|
|
791
|
+
"Notifications for the run have been pushed",
|
|
792
|
+
project=project,
|
|
793
|
+
run_id=uid,
|
|
794
|
+
)
|
|
795
|
+
elif (
|
|
796
|
+
background_task.status.state
|
|
797
|
+
== mlrun.common.schemas.BackgroundTaskState.failed
|
|
798
|
+
):
|
|
799
|
+
logger.error(
|
|
800
|
+
"Failed to push run notifications",
|
|
801
|
+
project=project,
|
|
802
|
+
run_id=uid,
|
|
803
|
+
error=background_task.status.error,
|
|
804
|
+
)
|
|
805
|
+
return None
|
|
806
|
+
|
|
807
|
+
def push_pipeline_notifications(
|
|
808
|
+
self,
|
|
809
|
+
pipeline_id,
|
|
810
|
+
project="",
|
|
811
|
+
notifications=None,
|
|
812
|
+
timeout=45,
|
|
813
|
+
):
|
|
814
|
+
"""
|
|
815
|
+
Push notifications for a pipeline.
|
|
816
|
+
|
|
817
|
+
:param pipeline_id: Unique ID of the pipeline(KFP).
|
|
818
|
+
:param project: Project that the run belongs to.
|
|
819
|
+
:param notifications: List of notifications to push.
|
|
820
|
+
:returns: :py:class:`~mlrun.common.schemas.BackgroundTask`.
|
|
821
|
+
"""
|
|
822
|
+
if notifications is None or type(notifications) is not list:
|
|
823
|
+
raise MLRunInvalidArgumentError(
|
|
824
|
+
"The 'notifications' parameter must be a list."
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
project = project or config.default_project
|
|
828
|
+
|
|
829
|
+
response = self.api_call(
|
|
830
|
+
"POST",
|
|
831
|
+
path=f"projects/{project}/pipelines/{pipeline_id}/push-notifications",
|
|
832
|
+
error="Failed push notifications",
|
|
833
|
+
body=_as_json([notification.to_dict() for notification in notifications]),
|
|
834
|
+
timeout=timeout,
|
|
835
|
+
)
|
|
836
|
+
if response.status_code == http.HTTPStatus.ACCEPTED:
|
|
837
|
+
background_task = mlrun.common.schemas.BackgroundTask(**response.json())
|
|
838
|
+
background_task = self._wait_for_background_task_to_reach_terminal_state(
|
|
784
839
|
background_task.metadata.name, project=project
|
|
785
840
|
)
|
|
841
|
+
if (
|
|
842
|
+
background_task.status.state
|
|
843
|
+
== mlrun.common.schemas.BackgroundTaskState.succeeded
|
|
844
|
+
):
|
|
845
|
+
logger.info(
|
|
846
|
+
"Pipeline notifications have been pushed",
|
|
847
|
+
project=project,
|
|
848
|
+
pipeline_id=pipeline_id,
|
|
849
|
+
)
|
|
850
|
+
elif (
|
|
851
|
+
background_task.status.state
|
|
852
|
+
== mlrun.common.schemas.BackgroundTaskState.failed
|
|
853
|
+
):
|
|
854
|
+
logger.error(
|
|
855
|
+
"Failed to push pipeline notifications",
|
|
856
|
+
project=project,
|
|
857
|
+
pipeline_id=pipeline_id,
|
|
858
|
+
error=background_task.status.error,
|
|
859
|
+
)
|
|
860
|
+
|
|
786
861
|
return None
|
|
787
862
|
|
|
788
863
|
def read_run(
|
|
@@ -3524,6 +3599,48 @@ class HTTPRunDB(RunDBInterface):
|
|
|
3524
3599
|
list[mm_endpoints.ModelEndpointMonitoringMetric], monitoring_metrics
|
|
3525
3600
|
)
|
|
3526
3601
|
|
|
3602
|
+
def get_metrics_by_multiple_endpoints(
|
|
3603
|
+
self,
|
|
3604
|
+
project: str,
|
|
3605
|
+
endpoint_ids: Union[str, list[str]],
|
|
3606
|
+
type: Literal["results", "metrics", "all"] = "all",
|
|
3607
|
+
events_format: mm_constants.GetEventsFormat = mm_constants.GetEventsFormat.SEPARATION,
|
|
3608
|
+
) -> dict[str, list[mm_endpoints.ModelEndpointMonitoringMetric]]:
|
|
3609
|
+
"""Get application metrics/results by endpoint id and project.
|
|
3610
|
+
|
|
3611
|
+
:param project: The name of the project.
|
|
3612
|
+
:param endpoint_ids: The unique id of the model endpoint. Can be a single id or a list of ids.
|
|
3613
|
+
:param type: The type of the metrics to return. "all" means "results" and "metrics".
|
|
3614
|
+
:param events_format: response format:
|
|
3615
|
+
|
|
3616
|
+
separation: {"mep_id1":[...], "mep_id2":[...]}
|
|
3617
|
+
intersection {"intersect_metrics":[], "intersect_results":[]}
|
|
3618
|
+
:return: A dictionary of application metrics and/or results for the model endpoints formatted by events_format.
|
|
3619
|
+
"""
|
|
3620
|
+
path = f"projects/{project}/model-endpoints/metrics"
|
|
3621
|
+
params = {
|
|
3622
|
+
"type": type,
|
|
3623
|
+
"endpoint-id": endpoint_ids,
|
|
3624
|
+
"events_format": events_format,
|
|
3625
|
+
}
|
|
3626
|
+
error_message = (
|
|
3627
|
+
f"Failed to get model monitoring metrics,"
|
|
3628
|
+
f" endpoint_ids: {endpoint_ids}, project: {project}"
|
|
3629
|
+
)
|
|
3630
|
+
response = self.api_call(
|
|
3631
|
+
mlrun.common.types.HTTPMethod.GET,
|
|
3632
|
+
path,
|
|
3633
|
+
error_message,
|
|
3634
|
+
params=params,
|
|
3635
|
+
)
|
|
3636
|
+
monitoring_metrics_by_endpoint = response.json()
|
|
3637
|
+
parsed_metrics_by_endpoint = {}
|
|
3638
|
+
for endpoint, metrics in monitoring_metrics_by_endpoint.items():
|
|
3639
|
+
parsed_metrics_by_endpoint[endpoint] = parse_obj_as(
|
|
3640
|
+
list[mm_endpoints.ModelEndpointMonitoringMetric], metrics
|
|
3641
|
+
)
|
|
3642
|
+
return parsed_metrics_by_endpoint
|
|
3643
|
+
|
|
3527
3644
|
def create_user_secrets(
|
|
3528
3645
|
self,
|
|
3529
3646
|
user: str,
|
mlrun/db/nopdb.py
CHANGED
|
@@ -84,6 +84,15 @@ class NopDB(RunDBInterface):
|
|
|
84
84
|
):
|
|
85
85
|
pass
|
|
86
86
|
|
|
87
|
+
def push_pipeline_notifications(
|
|
88
|
+
self,
|
|
89
|
+
pipeline_id,
|
|
90
|
+
project="",
|
|
91
|
+
notifications=None,
|
|
92
|
+
timeout=45,
|
|
93
|
+
):
|
|
94
|
+
pass
|
|
95
|
+
|
|
87
96
|
def list_runtime_resources(
|
|
88
97
|
self,
|
|
89
98
|
project: Optional[str] = None,
|
|
@@ -976,7 +976,6 @@ class ModelHandler(ABC, Generic[CommonTypes.ModelType, CommonTypes.IOSampleType]
|
|
|
976
976
|
custom_objects_map_json,
|
|
977
977
|
local_path=custom_objects_map_json,
|
|
978
978
|
artifact_path=self._context.artifact_path,
|
|
979
|
-
db_key=False,
|
|
980
979
|
)
|
|
981
980
|
|
|
982
981
|
# Zip the custom objects directory:
|
|
@@ -997,7 +996,6 @@ class ModelHandler(ABC, Generic[CommonTypes.ModelType, CommonTypes.IOSampleType]
|
|
|
997
996
|
custom_objects_zip,
|
|
998
997
|
local_path=custom_objects_zip,
|
|
999
998
|
artifact_path=self._context.artifact_path,
|
|
1000
|
-
db_key=False,
|
|
1001
999
|
)
|
|
1002
1000
|
|
|
1003
1001
|
return artifacts
|
|
@@ -234,14 +234,14 @@ class TSDBConnector(ABC):
|
|
|
234
234
|
@abstractmethod
|
|
235
235
|
def get_metrics_metadata(
|
|
236
236
|
self,
|
|
237
|
-
endpoint_id: str,
|
|
237
|
+
endpoint_id: typing.Union[str, list[str]],
|
|
238
238
|
start: typing.Optional[datetime] = None,
|
|
239
239
|
end: typing.Optional[datetime] = None,
|
|
240
240
|
) -> pd.DataFrame:
|
|
241
241
|
"""
|
|
242
|
-
Fetches distinct metrics metadata from the metrics TSDB table for a specified model
|
|
242
|
+
Fetches distinct metrics metadata from the metrics TSDB table for a specified model endpoints.
|
|
243
243
|
|
|
244
|
-
:param endpoint_id: The model endpoint identifier.
|
|
244
|
+
:param endpoint_id: The model endpoint identifier. Can be a single id or a list of ids.
|
|
245
245
|
:param start: The start time of the query.
|
|
246
246
|
:param end: The end time of the query.
|
|
247
247
|
|
|
@@ -252,14 +252,14 @@ class TSDBConnector(ABC):
|
|
|
252
252
|
@abstractmethod
|
|
253
253
|
def get_results_metadata(
|
|
254
254
|
self,
|
|
255
|
-
endpoint_id: str,
|
|
255
|
+
endpoint_id: typing.Union[str, list[str]],
|
|
256
256
|
start: typing.Optional[datetime] = None,
|
|
257
257
|
end: typing.Optional[datetime] = None,
|
|
258
258
|
) -> pd.DataFrame:
|
|
259
259
|
"""
|
|
260
|
-
Fetches distinct results metadata from the app-results TSDB table for a specified model
|
|
260
|
+
Fetches distinct results metadata from the app-results TSDB table for a specified model endpoints.
|
|
261
261
|
|
|
262
|
-
:param endpoint_id: The model endpoint identifier.
|
|
262
|
+
:param endpoint_id: The model endpoint identifier. Can be a single id or a list of ids.
|
|
263
263
|
:param start: The start time of the query.
|
|
264
264
|
:param end: The end time of the query.
|
|
265
265
|
|
|
@@ -341,7 +341,7 @@ class TSDBConnector(ABC):
|
|
|
341
341
|
logger.debug("No metrics", missing_metrics=metrics_without_data.keys())
|
|
342
342
|
grouped = []
|
|
343
343
|
for (app_name, name), sub_df in grouped:
|
|
344
|
-
full_name =
|
|
344
|
+
full_name = mm_schemas.model_endpoints.compose_full_name(
|
|
345
345
|
project=project,
|
|
346
346
|
app=app_name,
|
|
347
347
|
name=name,
|
|
@@ -410,7 +410,7 @@ class TSDBConnector(ABC):
|
|
|
410
410
|
result_kind = mlrun.model_monitoring.db.tsdb.helpers._get_result_kind(
|
|
411
411
|
sub_df
|
|
412
412
|
)
|
|
413
|
-
full_name =
|
|
413
|
+
full_name = mm_schemas.model_endpoints.compose_full_name(
|
|
414
414
|
project=project, app=app_name, name=name
|
|
415
415
|
)
|
|
416
416
|
try:
|
|
@@ -467,6 +467,7 @@ class TSDBConnector(ABC):
|
|
|
467
467
|
|
|
468
468
|
:return: A list of mm metrics objects.
|
|
469
469
|
"""
|
|
470
|
+
|
|
470
471
|
return list(
|
|
471
472
|
map(
|
|
472
473
|
lambda record: mm_schemas.ModelEndpointMonitoringMetric(
|
|
@@ -481,6 +482,113 @@ class TSDBConnector(ABC):
|
|
|
481
482
|
)
|
|
482
483
|
)
|
|
483
484
|
|
|
485
|
+
@staticmethod
|
|
486
|
+
def df_to_metrics_grouped_dict(
|
|
487
|
+
*,
|
|
488
|
+
df: pd.DataFrame,
|
|
489
|
+
project: str,
|
|
490
|
+
type: str,
|
|
491
|
+
) -> dict[str, list[mm_schemas.ModelEndpointMonitoringMetric]]:
|
|
492
|
+
"""
|
|
493
|
+
Parse a DataFrame of metrics from the TSDB into a grouped mm metrics objects by endpoint_id.
|
|
494
|
+
|
|
495
|
+
:param df: The DataFrame to parse.
|
|
496
|
+
:param project: The project name.
|
|
497
|
+
:param type: The type of the metrics (either "result" or "metric").
|
|
498
|
+
|
|
499
|
+
:return: A grouped dict of mm metrics/results, using model_endpoints_ids as keys.
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
if df.empty:
|
|
503
|
+
return {}
|
|
504
|
+
|
|
505
|
+
grouped_by_fields = [mm_schemas.WriterEvent.APPLICATION_NAME]
|
|
506
|
+
if type == "result":
|
|
507
|
+
name_column = mm_schemas.ResultData.RESULT_NAME
|
|
508
|
+
grouped_by_fields.append(mm_schemas.ResultData.RESULT_KIND)
|
|
509
|
+
else:
|
|
510
|
+
name_column = mm_schemas.MetricData.METRIC_NAME
|
|
511
|
+
|
|
512
|
+
grouped_by_fields.append(name_column)
|
|
513
|
+
# groupby has different behavior for category columns
|
|
514
|
+
df["endpoint_id"] = df["endpoint_id"].astype(str)
|
|
515
|
+
grouped_by_df = df.groupby("endpoint_id")
|
|
516
|
+
grouped_dict = grouped_by_df.apply(
|
|
517
|
+
lambda group: list(
|
|
518
|
+
map(
|
|
519
|
+
lambda record: mm_schemas.ModelEndpointMonitoringMetric(
|
|
520
|
+
project=project,
|
|
521
|
+
type=type,
|
|
522
|
+
app=record.get(mm_schemas.WriterEvent.APPLICATION_NAME),
|
|
523
|
+
name=record.get(name_column),
|
|
524
|
+
**{"kind": record.get(mm_schemas.ResultData.RESULT_KIND)}
|
|
525
|
+
if type == "result"
|
|
526
|
+
else {},
|
|
527
|
+
),
|
|
528
|
+
group[grouped_by_fields].to_dict(orient="records"),
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
).to_dict()
|
|
532
|
+
return grouped_dict
|
|
533
|
+
|
|
534
|
+
@staticmethod
|
|
535
|
+
def df_to_events_intersection_dict(
|
|
536
|
+
*,
|
|
537
|
+
df: pd.DataFrame,
|
|
538
|
+
project: str,
|
|
539
|
+
type: typing.Union[str, mm_schemas.ModelEndpointMonitoringMetricType],
|
|
540
|
+
) -> dict[str, list[mm_schemas.ModelEndpointMonitoringMetric]]:
|
|
541
|
+
"""
|
|
542
|
+
Parse a DataFrame of metrics from the TSDB into a dict of intersection metrics/results by name and application
|
|
543
|
+
(and kind in results).
|
|
544
|
+
|
|
545
|
+
:param df: The DataFrame to parse.
|
|
546
|
+
:param project: The project name.
|
|
547
|
+
:param type: The type of the metrics (either "result" or "metric").
|
|
548
|
+
|
|
549
|
+
:return: A dictionary where the key is event type (as defined by `INTERSECT_DICT_KEYS`),
|
|
550
|
+
and the value is a list containing the intersect metrics or results across all endpoint IDs.
|
|
551
|
+
|
|
552
|
+
For example:
|
|
553
|
+
{
|
|
554
|
+
"intersect_metrics": [...]
|
|
555
|
+
}
|
|
556
|
+
"""
|
|
557
|
+
dict_key = mm_schemas.INTERSECT_DICT_KEYS[type]
|
|
558
|
+
metrics = []
|
|
559
|
+
if df.empty:
|
|
560
|
+
return {dict_key: []}
|
|
561
|
+
|
|
562
|
+
columns_to_zip = [mm_schemas.WriterEvent.APPLICATION_NAME]
|
|
563
|
+
|
|
564
|
+
if type == "result":
|
|
565
|
+
name_column = mm_schemas.ResultData.RESULT_NAME
|
|
566
|
+
columns_to_zip.append(mm_schemas.ResultData.RESULT_KIND)
|
|
567
|
+
else:
|
|
568
|
+
name_column = mm_schemas.MetricData.METRIC_NAME
|
|
569
|
+
columns_to_zip.insert(1, name_column)
|
|
570
|
+
|
|
571
|
+
# groupby has different behavior for category columns
|
|
572
|
+
df["endpoint_id"] = df["endpoint_id"].astype(str)
|
|
573
|
+
df["event_values"] = list(zip(*[df[col] for col in columns_to_zip]))
|
|
574
|
+
grouped_by_event_values = df.groupby("endpoint_id")["event_values"].apply(set)
|
|
575
|
+
common_event_values_combinations = set.intersection(*grouped_by_event_values)
|
|
576
|
+
result_kind = None
|
|
577
|
+
for data in common_event_values_combinations:
|
|
578
|
+
application_name, event_name = data[0], data[1]
|
|
579
|
+
if len(data) > 2: # in result case
|
|
580
|
+
result_kind = data[2]
|
|
581
|
+
metrics.append(
|
|
582
|
+
mm_schemas.ModelEndpointMonitoringMetric(
|
|
583
|
+
project=project,
|
|
584
|
+
type=type,
|
|
585
|
+
app=application_name,
|
|
586
|
+
name=event_name,
|
|
587
|
+
kind=result_kind,
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
return {dict_key: metrics}
|
|
591
|
+
|
|
484
592
|
@staticmethod
|
|
485
593
|
def _get_start_end(
|
|
486
594
|
start: typing.Union[datetime, None],
|
|
@@ -298,6 +298,8 @@ class Predictions(TDEngineSchema):
|
|
|
298
298
|
mm_schemas.EventFieldType.TIME: _TDEngineColumn.TIMESTAMP,
|
|
299
299
|
mm_schemas.EventFieldType.LATENCY: _TDEngineColumn.FLOAT,
|
|
300
300
|
mm_schemas.EventKeyMetrics.CUSTOM_METRICS: _TDEngineColumn.BINARY_1000,
|
|
301
|
+
mm_schemas.EventFieldType.ESTIMATED_PREDICTION_COUNT: _TDEngineColumn.FLOAT,
|
|
302
|
+
mm_schemas.EventFieldType.EFFECTIVE_SAMPLE_COUNT: _TDEngineColumn.INT,
|
|
301
303
|
}
|
|
302
304
|
tags = {
|
|
303
305
|
mm_schemas.WriterEvent.ENDPOINT_ID: _TDEngineColumn.BINARY_64,
|