mlrun 1.5.0rc11__py3-none-any.whl → 1.5.0rc13__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 +31 -2
- mlrun/api/api/endpoints/functions.py +110 -52
- mlrun/api/api/endpoints/model_endpoints.py +0 -56
- mlrun/api/crud/model_monitoring/deployment.py +208 -38
- mlrun/api/crud/model_monitoring/helpers.py +19 -6
- mlrun/api/crud/model_monitoring/model_endpoints.py +14 -31
- mlrun/api/db/sqldb/db.py +3 -1
- mlrun/api/utils/builder.py +2 -4
- mlrun/common/model_monitoring/helpers.py +19 -5
- mlrun/common/schemas/model_monitoring/constants.py +69 -0
- mlrun/common/schemas/model_monitoring/model_endpoints.py +22 -1
- mlrun/config.py +30 -12
- mlrun/datastore/__init__.py +1 -0
- mlrun/datastore/datastore_profile.py +2 -2
- mlrun/datastore/sources.py +4 -30
- mlrun/datastore/targets.py +106 -55
- mlrun/db/httpdb.py +20 -6
- mlrun/feature_store/__init__.py +2 -0
- mlrun/feature_store/api.py +3 -31
- mlrun/feature_store/feature_vector.py +1 -1
- mlrun/feature_store/retrieval/base.py +8 -3
- mlrun/launcher/remote.py +3 -3
- mlrun/lists.py +11 -0
- mlrun/model_monitoring/__init__.py +0 -1
- mlrun/model_monitoring/api.py +1 -1
- mlrun/model_monitoring/application.py +313 -0
- mlrun/model_monitoring/batch_application.py +526 -0
- mlrun/model_monitoring/batch_application_handler.py +32 -0
- mlrun/model_monitoring/evidently_application.py +89 -0
- mlrun/model_monitoring/helpers.py +39 -3
- mlrun/model_monitoring/stores/kv_model_endpoint_store.py +38 -7
- mlrun/model_monitoring/tracking_policy.py +4 -4
- mlrun/model_monitoring/writer.py +37 -0
- mlrun/projects/pipelines.py +38 -4
- mlrun/projects/project.py +257 -43
- mlrun/run.py +5 -2
- mlrun/runtimes/__init__.py +2 -0
- mlrun/runtimes/function.py +2 -1
- mlrun/utils/helpers.py +12 -0
- mlrun/utils/http.py +3 -0
- mlrun/utils/notifications/notification_pusher.py +22 -8
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.5.0rc11.dist-info → mlrun-1.5.0rc13.dist-info}/METADATA +5 -5
- {mlrun-1.5.0rc11.dist-info → mlrun-1.5.0rc13.dist-info}/RECORD +49 -44
- /mlrun/model_monitoring/{model_monitoring_batch.py → batch.py} +0 -0
- {mlrun-1.5.0rc11.dist-info → mlrun-1.5.0rc13.dist-info}/LICENSE +0 -0
- {mlrun-1.5.0rc11.dist-info → mlrun-1.5.0rc13.dist-info}/WHEEL +0 -0
- {mlrun-1.5.0rc11.dist-info → mlrun-1.5.0rc13.dist-info}/entry_points.txt +0 -0
- {mlrun-1.5.0rc11.dist-info → mlrun-1.5.0rc13.dist-info}/top_level.txt +0 -0
mlrun/feature_store/api.py
CHANGED
|
@@ -975,37 +975,9 @@ def _ingest_with_spark(
|
|
|
975
975
|
)
|
|
976
976
|
|
|
977
977
|
df_to_write = df
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
from pyspark.sql.functions import (
|
|
982
|
-
dayofmonth,
|
|
983
|
-
hour,
|
|
984
|
-
minute,
|
|
985
|
-
month,
|
|
986
|
-
second,
|
|
987
|
-
year,
|
|
988
|
-
)
|
|
989
|
-
|
|
990
|
-
time_unit_to_op = {
|
|
991
|
-
"year": year,
|
|
992
|
-
"month": month,
|
|
993
|
-
"day": dayofmonth,
|
|
994
|
-
"hour": hour,
|
|
995
|
-
"minute": minute,
|
|
996
|
-
"second": second,
|
|
997
|
-
}
|
|
998
|
-
timestamp_col = df_to_write[timestamp_key]
|
|
999
|
-
for partition in spark_options["partitionBy"]:
|
|
1000
|
-
if (
|
|
1001
|
-
partition not in df_to_write.columns
|
|
1002
|
-
and partition in time_unit_to_op
|
|
1003
|
-
):
|
|
1004
|
-
op = time_unit_to_op[partition]
|
|
1005
|
-
df_to_write = df_to_write.withColumn(
|
|
1006
|
-
partition, op(timestamp_col)
|
|
1007
|
-
)
|
|
1008
|
-
df_to_write = target.prepare_spark_df(df_to_write, key_columns)
|
|
978
|
+
df_to_write = target.prepare_spark_df(
|
|
979
|
+
df_to_write, key_columns, timestamp_key, spark_options
|
|
980
|
+
)
|
|
1009
981
|
if overwrite:
|
|
1010
982
|
df_to_write.write.mode("overwrite").save(**spark_options)
|
|
1011
983
|
else:
|
|
@@ -631,7 +631,7 @@ class FeatureVector(ModelObj):
|
|
|
631
631
|
feature_set_fields: list of field (name, alias) per featureset
|
|
632
632
|
"""
|
|
633
633
|
processed_features = {} # dict of name to (featureset, feature object)
|
|
634
|
-
feature_set_objects = {}
|
|
634
|
+
feature_set_objects = self.feature_set_objects or {}
|
|
635
635
|
index_keys = []
|
|
636
636
|
feature_set_fields = collections.defaultdict(list)
|
|
637
637
|
features = copy(self.spec.features)
|
|
@@ -136,7 +136,7 @@ class BaseMerger(abc.ABC):
|
|
|
136
136
|
order_by=order_by,
|
|
137
137
|
)
|
|
138
138
|
|
|
139
|
-
def _write_to_offline_target(self):
|
|
139
|
+
def _write_to_offline_target(self, timestamp_key=None):
|
|
140
140
|
if self._target:
|
|
141
141
|
is_persistent_vector = self.vector.metadata.name is not None
|
|
142
142
|
if not self._target.path and not is_persistent_vector:
|
|
@@ -144,7 +144,12 @@ class BaseMerger(abc.ABC):
|
|
|
144
144
|
"target path was not specified"
|
|
145
145
|
)
|
|
146
146
|
self._target.set_resource(self.vector)
|
|
147
|
-
size = self._target.write_dataframe(
|
|
147
|
+
size = self._target.write_dataframe(
|
|
148
|
+
self._result_df,
|
|
149
|
+
timestamp_key=timestamp_key
|
|
150
|
+
if not self._drop_indexes and timestamp_key not in self._drop_columns
|
|
151
|
+
else None,
|
|
152
|
+
)
|
|
148
153
|
if is_persistent_vector:
|
|
149
154
|
target_status = self._target.update_resource_status("ready", size=size)
|
|
150
155
|
logger.info(f"wrote target: {target_status}")
|
|
@@ -361,7 +366,7 @@ class BaseMerger(abc.ABC):
|
|
|
361
366
|
)
|
|
362
367
|
self._order_by(order_by_active)
|
|
363
368
|
|
|
364
|
-
self._write_to_offline_target()
|
|
369
|
+
self._write_to_offline_target(timestamp_key=result_timestamp)
|
|
365
370
|
return OfflineVectorResponse(self)
|
|
366
371
|
|
|
367
372
|
def init_online_vector_service(
|
mlrun/launcher/remote.py
CHANGED
|
@@ -89,7 +89,7 @@ class ClientRemoteLauncher(launcher.ClientBaseLauncher):
|
|
|
89
89
|
|
|
90
90
|
else:
|
|
91
91
|
raise mlrun.errors.MLRunRuntimeError(
|
|
92
|
-
"
|
|
92
|
+
"Function image is not built/ready, set auto_build=True or use .deploy() method first"
|
|
93
93
|
)
|
|
94
94
|
|
|
95
95
|
if runtime.verbose:
|
|
@@ -122,11 +122,11 @@ class ClientRemoteLauncher(launcher.ClientBaseLauncher):
|
|
|
122
122
|
resp = db.submit_job(run, schedule=schedule)
|
|
123
123
|
if schedule:
|
|
124
124
|
action = resp.pop("action", "created")
|
|
125
|
-
logger.info(f"
|
|
125
|
+
logger.info(f"Task schedule {action}", **resp)
|
|
126
126
|
return
|
|
127
127
|
|
|
128
128
|
except (requests.HTTPError, Exception) as err:
|
|
129
|
-
logger.error(
|
|
129
|
+
logger.error("Failed remote run", error=mlrun.errors.err_to_str(err))
|
|
130
130
|
|
|
131
131
|
if isinstance(err, requests.HTTPError):
|
|
132
132
|
runtime._handle_submit_job_http_error(err)
|
mlrun/lists.py
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
+
import warnings
|
|
14
15
|
from copy import copy
|
|
15
16
|
from typing import List
|
|
16
17
|
|
|
@@ -219,6 +220,16 @@ class ArtifactList(list):
|
|
|
219
220
|
"""return as a list of artifact objects"""
|
|
220
221
|
return [dict_to_artifact(artifact) for artifact in self]
|
|
221
222
|
|
|
223
|
+
def objects(self) -> List[Artifact]:
|
|
224
|
+
"""return as a list of artifact objects"""
|
|
225
|
+
warnings.warn(
|
|
226
|
+
"'objects' is deprecated in 1.3.0 and will be removed in 1.6.0. "
|
|
227
|
+
"Use 'to_objects' instead.",
|
|
228
|
+
# TODO: remove in 1.6.0
|
|
229
|
+
FutureWarning,
|
|
230
|
+
)
|
|
231
|
+
return [dict_to_artifact(artifact) for artifact in self]
|
|
232
|
+
|
|
222
233
|
def dataitems(self) -> List["mlrun.DataItem"]:
|
|
223
234
|
"""return as a list of DataItem objects"""
|
|
224
235
|
dataitems = []
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
# flake8: noqa - this is until we take care of the F401 violations with respect to __all__ & sphinx
|
|
16
16
|
# for backwards compatibility
|
|
17
17
|
|
|
18
|
-
|
|
19
18
|
from .helpers import get_stream_path
|
|
20
19
|
from .model_endpoint import ModelEndpoint
|
|
21
20
|
from .stores import ModelEndpointStore, ModelEndpointStoreType, get_model_endpoint_store
|
mlrun/model_monitoring/api.py
CHANGED
|
@@ -28,9 +28,9 @@ from mlrun.common.schemas.model_monitoring import EventFieldType, ModelMonitorin
|
|
|
28
28
|
from mlrun.data_types.infer import InferOptions, get_df_stats
|
|
29
29
|
from mlrun.utils import logger
|
|
30
30
|
|
|
31
|
+
from .batch import VirtualDrift
|
|
31
32
|
from .features_drift_table import FeaturesDriftTablePlot
|
|
32
33
|
from .model_endpoint import ModelEndpoint
|
|
33
|
-
from .model_monitoring_batch import VirtualDrift
|
|
34
34
|
|
|
35
35
|
# A union of all supported dataset types:
|
|
36
36
|
DatasetType = typing.Union[
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# Copyright 2023 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
|
+
|
|
16
|
+
import dataclasses
|
|
17
|
+
import json
|
|
18
|
+
from typing import Any, Dict, List, Tuple, Union
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import pandas as pd
|
|
22
|
+
|
|
23
|
+
import mlrun.common.helpers
|
|
24
|
+
import mlrun.common.schemas.model_monitoring
|
|
25
|
+
import mlrun.utils.v3io_clients
|
|
26
|
+
from mlrun.datastore import get_stream_pusher
|
|
27
|
+
from mlrun.datastore.targets import ParquetTarget
|
|
28
|
+
from mlrun.model_monitoring.helpers import get_stream_path
|
|
29
|
+
from mlrun.serving.utils import StepToDict
|
|
30
|
+
from mlrun.utils import logger
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclasses.dataclass
|
|
34
|
+
class ModelMonitoringApplicationResult:
|
|
35
|
+
"""
|
|
36
|
+
Class representing the result of a custom model monitoring application.
|
|
37
|
+
|
|
38
|
+
:param application_name: (str) Name of the model monitoring application.
|
|
39
|
+
:param endpoint_id: (str) ID of the monitored model endpoint.
|
|
40
|
+
:param schedule_time: (pd.Timestamp)Timestamp of the monitoring schedule.
|
|
41
|
+
:param result_name: (str) Name of the application result.
|
|
42
|
+
:param result_value: (float) Value of the application result.
|
|
43
|
+
:param result_kind: (ResultKindApp) Kind of application result.
|
|
44
|
+
:param result_status: (ResultStatusApp) Status of the application result.
|
|
45
|
+
:param result_extra_data: (dict) Extra data associated with the application result.
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
application_name: str
|
|
50
|
+
endpoint_id: str
|
|
51
|
+
schedule_time: pd.Timestamp
|
|
52
|
+
result_name: str
|
|
53
|
+
result_value: float
|
|
54
|
+
result_kind: mlrun.common.schemas.model_monitoring.constants.ResultKindApp
|
|
55
|
+
result_status: mlrun.common.schemas.model_monitoring.constants.ResultStatusApp
|
|
56
|
+
result_extra_data: dict
|
|
57
|
+
|
|
58
|
+
def to_dict(self):
|
|
59
|
+
"""
|
|
60
|
+
Convert the object to a dictionary format suitable for writing.
|
|
61
|
+
|
|
62
|
+
:returns: (dict) Dictionary representation of the result.
|
|
63
|
+
"""
|
|
64
|
+
return {
|
|
65
|
+
mlrun.common.schemas.model_monitoring.constants.WriterEvent.APPLICATION_NAME: self.application_name,
|
|
66
|
+
mlrun.common.schemas.model_monitoring.constants.WriterEvent.ENDPOINT_ID: self.endpoint_id,
|
|
67
|
+
mlrun.common.schemas.model_monitoring.constants.WriterEvent.SCHEDULE_TIME: self.schedule_time.isoformat(
|
|
68
|
+
sep=" ", timespec="microseconds"
|
|
69
|
+
),
|
|
70
|
+
mlrun.common.schemas.model_monitoring.constants.WriterEvent.RESULT_NAME: self.result_name,
|
|
71
|
+
mlrun.common.schemas.model_monitoring.constants.WriterEvent.RESULT_VALUE: self.result_value,
|
|
72
|
+
mlrun.common.schemas.model_monitoring.constants.WriterEvent.RESULT_KIND: self.result_kind.value,
|
|
73
|
+
mlrun.common.schemas.model_monitoring.constants.WriterEvent.RESULT_STATUS: self.result_status.value,
|
|
74
|
+
mlrun.common.schemas.model_monitoring.constants.WriterEvent.RESULT_EXTRA_DATA: json.dumps(
|
|
75
|
+
self.result_extra_data
|
|
76
|
+
),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ModelMonitoringApplication(StepToDict):
|
|
81
|
+
"""
|
|
82
|
+
Class representing a model monitoring application. Subclass this to create custom monitoring logic.
|
|
83
|
+
|
|
84
|
+
example for very simple costume application::
|
|
85
|
+
# mlrun: start-code
|
|
86
|
+
class MyApp(ModelMonitoringApplication):
|
|
87
|
+
|
|
88
|
+
def run_application(
|
|
89
|
+
self,
|
|
90
|
+
sample_df_stats: pd.DataFrame,
|
|
91
|
+
feature_stats: pd.DataFrame,
|
|
92
|
+
sample_df: pd.DataFrame,
|
|
93
|
+
schedule_time: pd.Timestamp,
|
|
94
|
+
latest_request: pd.Timestamp,
|
|
95
|
+
endpoint_id: str,
|
|
96
|
+
output_stream_uri: str,
|
|
97
|
+
) -> typing.Union[ModelMonitoringApplicationResult, typing.List[ModelMonitoringApplicationResult]
|
|
98
|
+
]:
|
|
99
|
+
self.context.log_artifact(TableArtifact("sample_df_stats", df=sample_df_stats))
|
|
100
|
+
return ModelMonitoringApplicationResult(
|
|
101
|
+
self.name,
|
|
102
|
+
endpoint_id,
|
|
103
|
+
schedule_time,
|
|
104
|
+
result_name="data_drift_test",
|
|
105
|
+
result_value=0.5,
|
|
106
|
+
result_kind=mlrun.common.schemas.model_monitoring.constants.ResultKindApp.data_drift,
|
|
107
|
+
result_status = mlrun.common.schemas.model_monitoring.constants.ResultStatusApp.detected,
|
|
108
|
+
result_extra_data={})
|
|
109
|
+
|
|
110
|
+
# mlrun: end-code
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
kind = "monitoring_application"
|
|
114
|
+
|
|
115
|
+
def do(self, event: Dict[str, Any]):
|
|
116
|
+
"""
|
|
117
|
+
Process the monitoring event and return application results.
|
|
118
|
+
|
|
119
|
+
:param event: (dict) The monitoring event to process.
|
|
120
|
+
:returns: (List[ModelMonitoringApplicationResult]) The application results.
|
|
121
|
+
"""
|
|
122
|
+
resolved_event = self._resolve_event(event)
|
|
123
|
+
if not (
|
|
124
|
+
hasattr(self, "context") and isinstance(self.context, mlrun.MLClientCtx)
|
|
125
|
+
):
|
|
126
|
+
self._lazy_init(app_name=resolved_event[0])
|
|
127
|
+
return self.run_application(*resolved_event)
|
|
128
|
+
|
|
129
|
+
def _lazy_init(self, app_name: str):
|
|
130
|
+
self.context = self._create_context_for_logging(app_name=app_name)
|
|
131
|
+
|
|
132
|
+
def run_application(
|
|
133
|
+
self,
|
|
134
|
+
application_name: str,
|
|
135
|
+
sample_df_stats: pd.DataFrame,
|
|
136
|
+
feature_stats: pd.DataFrame,
|
|
137
|
+
sample_df: pd.DataFrame,
|
|
138
|
+
schedule_time: pd.Timestamp,
|
|
139
|
+
latest_request: pd.Timestamp,
|
|
140
|
+
endpoint_id: str,
|
|
141
|
+
output_stream_uri: str,
|
|
142
|
+
) -> Union[
|
|
143
|
+
ModelMonitoringApplicationResult, List[ModelMonitoringApplicationResult]
|
|
144
|
+
]:
|
|
145
|
+
"""
|
|
146
|
+
Implement this method with your custom monitoring logic.
|
|
147
|
+
|
|
148
|
+
:param application_name (str) the app name
|
|
149
|
+
:param sample_df_stats: (pd.DataFrame) The new sample distribution DataFrame.
|
|
150
|
+
:param feature_stats: (pd.DataFrame) The train sample distribution DataFrame.
|
|
151
|
+
:param sample_df: (pd.DataFrame) The new sample DataFrame.
|
|
152
|
+
:param schedule_time: (pd.Timestamp) Timestamp of the monitoring schedule.
|
|
153
|
+
:param latest_request: (pd.Timestamp) Timestamp of the latest request on this endpoint_id.
|
|
154
|
+
:param endpoint_id: (str) ID of the monitored model endpoint
|
|
155
|
+
:param output_stream_uri: (str) URI of the output stream for results
|
|
156
|
+
|
|
157
|
+
:returns: (ModelMonitoringApplicationResult) or
|
|
158
|
+
(List[ModelMonitoringApplicationResult]) of the application results.
|
|
159
|
+
"""
|
|
160
|
+
raise NotImplementedError
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def _resolve_event(
|
|
164
|
+
event: Dict[str, Any],
|
|
165
|
+
) -> Tuple[
|
|
166
|
+
str,
|
|
167
|
+
pd.DataFrame,
|
|
168
|
+
pd.DataFrame,
|
|
169
|
+
pd.DataFrame,
|
|
170
|
+
pd.Timestamp,
|
|
171
|
+
pd.Timestamp,
|
|
172
|
+
str,
|
|
173
|
+
str,
|
|
174
|
+
]:
|
|
175
|
+
"""
|
|
176
|
+
Converting the event into a single tuple that will be be used for passing the event arguments to the running
|
|
177
|
+
application
|
|
178
|
+
|
|
179
|
+
:param event: dictionary with all the incoming data
|
|
180
|
+
|
|
181
|
+
:return: A tuple of:
|
|
182
|
+
[0] = (str) application name
|
|
183
|
+
[1] = (pd.DataFrame) current input statistics
|
|
184
|
+
[2] = (pd.DataFrame) train statistics
|
|
185
|
+
[3] = (pd.DataFrame) current input data
|
|
186
|
+
[4] = (pd.Timestamp) timestamp of batch schedule time
|
|
187
|
+
[5] = (pd.Timestamp) timestamp of the latest request
|
|
188
|
+
[6] = (str) endpoint id
|
|
189
|
+
[7] = (str) output stream uri
|
|
190
|
+
"""
|
|
191
|
+
return (
|
|
192
|
+
event[
|
|
193
|
+
mlrun.common.schemas.model_monitoring.constants.ApplicationEvent.APPLICATION_NAME
|
|
194
|
+
],
|
|
195
|
+
ModelMonitoringApplication._dict_to_histogram(
|
|
196
|
+
json.loads(
|
|
197
|
+
event[
|
|
198
|
+
mlrun.common.schemas.model_monitoring.constants.ApplicationEvent.CURRENT_STATS
|
|
199
|
+
]
|
|
200
|
+
)
|
|
201
|
+
),
|
|
202
|
+
ModelMonitoringApplication._dict_to_histogram(
|
|
203
|
+
json.loads(
|
|
204
|
+
event[
|
|
205
|
+
mlrun.common.schemas.model_monitoring.constants.ApplicationEvent.FEATURE_STATS
|
|
206
|
+
]
|
|
207
|
+
)
|
|
208
|
+
),
|
|
209
|
+
ParquetTarget(
|
|
210
|
+
path=event[
|
|
211
|
+
mlrun.common.schemas.model_monitoring.constants.ApplicationEvent.SAMPLE_PARQUET_PATH
|
|
212
|
+
]
|
|
213
|
+
).as_df(),
|
|
214
|
+
pd.Timestamp(
|
|
215
|
+
event[
|
|
216
|
+
mlrun.common.schemas.model_monitoring.constants.ApplicationEvent.SCHEDULE_TIME
|
|
217
|
+
]
|
|
218
|
+
),
|
|
219
|
+
pd.Timestamp(
|
|
220
|
+
event[
|
|
221
|
+
mlrun.common.schemas.model_monitoring.constants.ApplicationEvent.LAST_REQUEST
|
|
222
|
+
]
|
|
223
|
+
),
|
|
224
|
+
event[
|
|
225
|
+
mlrun.common.schemas.model_monitoring.constants.ApplicationEvent.ENDPOINT_ID
|
|
226
|
+
],
|
|
227
|
+
event[
|
|
228
|
+
mlrun.common.schemas.model_monitoring.constants.ApplicationEvent.OUTPUT_STREAM_URI
|
|
229
|
+
],
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
def _create_context_for_logging(app_name: str):
|
|
234
|
+
context = mlrun.get_or_create_ctx(
|
|
235
|
+
f"{app_name}-logger",
|
|
236
|
+
upload_artifacts=True,
|
|
237
|
+
labels={"workflow": "model-monitoring-app-logger"},
|
|
238
|
+
)
|
|
239
|
+
return context
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def _dict_to_histogram(histogram_dict: Dict[str, Dict[str, Any]]) -> pd.DataFrame:
|
|
243
|
+
"""
|
|
244
|
+
Convert histogram dictionary to pandas DataFrame with feature histograms as columns
|
|
245
|
+
|
|
246
|
+
:param histogram_dict: Histogram dictionary
|
|
247
|
+
|
|
248
|
+
:returns: Histogram dataframe
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
# Create a dictionary with feature histograms as values
|
|
252
|
+
histograms = {}
|
|
253
|
+
for feature, stats in histogram_dict.items():
|
|
254
|
+
if "hist" in stats:
|
|
255
|
+
# Normalize to probability distribution of each feature
|
|
256
|
+
histograms[feature] = np.array(stats["hist"][0]) / stats["count"]
|
|
257
|
+
|
|
258
|
+
# Convert the dictionary to pandas DataFrame
|
|
259
|
+
histograms = pd.DataFrame(histograms)
|
|
260
|
+
|
|
261
|
+
return histograms
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class PushToMonitoringWriter(StepToDict):
|
|
265
|
+
kind = "monitoring_application_stream_pusher"
|
|
266
|
+
|
|
267
|
+
def __init__(
|
|
268
|
+
self,
|
|
269
|
+
project: str = None,
|
|
270
|
+
writer_application_name: str = None,
|
|
271
|
+
stream_uri: str = None,
|
|
272
|
+
name: str = None,
|
|
273
|
+
):
|
|
274
|
+
"""
|
|
275
|
+
Class for pushing application results to the monitoring writer stream.
|
|
276
|
+
|
|
277
|
+
:param project: Project name.
|
|
278
|
+
:param writer_application_name: Writer application name.
|
|
279
|
+
:param stream_uri: Stream URI for pushing results.
|
|
280
|
+
:param name: Name of the PushToMonitoringWriter
|
|
281
|
+
instance default to PushToMonitoringWriter.
|
|
282
|
+
"""
|
|
283
|
+
self.project = project
|
|
284
|
+
self.application_name_to_push = writer_application_name
|
|
285
|
+
self.stream_uri = stream_uri or get_stream_path(
|
|
286
|
+
project=self.project, application_name=self.application_name_to_push
|
|
287
|
+
)
|
|
288
|
+
self.output_stream = None
|
|
289
|
+
self.name = name or "PushToMonitoringWriter"
|
|
290
|
+
|
|
291
|
+
def do(
|
|
292
|
+
self,
|
|
293
|
+
event: Union[
|
|
294
|
+
ModelMonitoringApplicationResult, List[ModelMonitoringApplicationResult]
|
|
295
|
+
],
|
|
296
|
+
):
|
|
297
|
+
"""
|
|
298
|
+
Push application results to the monitoring writer stream.
|
|
299
|
+
|
|
300
|
+
:param event: Monitoring result(s) to push.
|
|
301
|
+
"""
|
|
302
|
+
self._lazy_init()
|
|
303
|
+
event = event if isinstance(event, List) else [event]
|
|
304
|
+
for result in event:
|
|
305
|
+
data = result.to_dict()
|
|
306
|
+
logger.info(f"Pushing data = {data} \n to stream = {self.stream_uri}")
|
|
307
|
+
self.output_stream.push([data])
|
|
308
|
+
|
|
309
|
+
def _lazy_init(self):
|
|
310
|
+
if self.output_stream is None:
|
|
311
|
+
self.output_stream = get_stream_pusher(
|
|
312
|
+
self.stream_uri,
|
|
313
|
+
)
|