mlrun 1.7.0rc26__py3-none-any.whl → 1.7.0rc31__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 +7 -7
- mlrun/alerts/alert.py +13 -1
- mlrun/artifacts/manager.py +5 -0
- mlrun/common/constants.py +3 -3
- mlrun/common/formatters/artifact.py +1 -0
- mlrun/common/formatters/base.py +9 -9
- mlrun/common/schemas/alert.py +4 -8
- mlrun/common/schemas/api_gateway.py +7 -0
- mlrun/common/schemas/constants.py +3 -0
- mlrun/common/schemas/model_monitoring/__init__.py +1 -0
- mlrun/common/schemas/model_monitoring/constants.py +32 -13
- mlrun/common/schemas/model_monitoring/model_endpoints.py +0 -12
- mlrun/common/schemas/project.py +10 -9
- mlrun/common/schemas/schedule.py +1 -1
- mlrun/config.py +37 -11
- mlrun/data_types/spark.py +2 -2
- mlrun/data_types/to_pandas.py +48 -16
- mlrun/datastore/__init__.py +1 -0
- mlrun/datastore/azure_blob.py +2 -1
- mlrun/datastore/base.py +21 -13
- mlrun/datastore/datastore.py +7 -5
- mlrun/datastore/datastore_profile.py +1 -1
- mlrun/datastore/google_cloud_storage.py +1 -0
- mlrun/datastore/inmem.py +4 -1
- mlrun/datastore/s3.py +2 -0
- mlrun/datastore/snowflake_utils.py +3 -1
- mlrun/datastore/sources.py +40 -11
- mlrun/datastore/store_resources.py +2 -0
- mlrun/datastore/targets.py +71 -26
- mlrun/db/base.py +11 -0
- mlrun/db/httpdb.py +50 -31
- mlrun/db/nopdb.py +11 -1
- mlrun/errors.py +4 -0
- mlrun/execution.py +18 -10
- mlrun/feature_store/retrieval/spark_merger.py +4 -32
- mlrun/launcher/local.py +2 -2
- mlrun/model.py +27 -1
- mlrun/model_monitoring/api.py +9 -55
- mlrun/model_monitoring/applications/histogram_data_drift.py +4 -1
- mlrun/model_monitoring/controller.py +57 -73
- mlrun/model_monitoring/db/stores/__init__.py +21 -9
- mlrun/model_monitoring/db/stores/base/store.py +39 -1
- mlrun/model_monitoring/db/stores/sqldb/models/base.py +9 -7
- mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +4 -2
- mlrun/model_monitoring/db/stores/sqldb/sql_store.py +41 -80
- mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +22 -27
- mlrun/model_monitoring/db/tsdb/__init__.py +19 -14
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +4 -2
- mlrun/model_monitoring/helpers.py +15 -17
- mlrun/model_monitoring/writer.py +2 -7
- mlrun/projects/operations.py +1 -0
- mlrun/projects/project.py +87 -75
- mlrun/render.py +10 -5
- mlrun/run.py +7 -7
- mlrun/runtimes/base.py +1 -1
- mlrun/runtimes/daskjob.py +7 -1
- mlrun/runtimes/local.py +24 -7
- mlrun/runtimes/nuclio/function.py +20 -0
- mlrun/runtimes/pod.py +5 -29
- mlrun/serving/routers.py +75 -59
- mlrun/serving/server.py +1 -0
- mlrun/serving/v2_serving.py +8 -1
- mlrun/utils/helpers.py +46 -2
- mlrun/utils/logger.py +36 -2
- mlrun/utils/notifications/notification/base.py +4 -0
- mlrun/utils/notifications/notification/git.py +21 -0
- mlrun/utils/notifications/notification/slack.py +8 -0
- mlrun/utils/notifications/notification/webhook.py +41 -1
- mlrun/utils/notifications/notification_pusher.py +2 -2
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc31.dist-info}/METADATA +13 -8
- {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc31.dist-info}/RECORD +76 -78
- {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc31.dist-info}/WHEEL +1 -1
- mlrun/feature_store/retrieval/conversion.py +0 -271
- mlrun/model_monitoring/controller_handler.py +0 -37
- {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc31.dist-info}/LICENSE +0 -0
- {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc31.dist-info}/entry_points.txt +0 -0
- {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc31.dist-info}/top_level.txt +0 -0
|
@@ -63,7 +63,7 @@ class ObjectStoreFactory(enum.Enum):
|
|
|
63
63
|
:param value: Provided enum (invalid) value.
|
|
64
64
|
"""
|
|
65
65
|
valid_values = list(cls.__members__.keys())
|
|
66
|
-
raise mlrun.errors.
|
|
66
|
+
raise mlrun.errors.MLRunInvalidMMStoreType(
|
|
67
67
|
f"{value} is not a valid endpoint store, please choose a valid value: %{valid_values}."
|
|
68
68
|
)
|
|
69
69
|
|
|
@@ -88,21 +88,28 @@ def get_model_endpoint_store(
|
|
|
88
88
|
def get_store_object(
|
|
89
89
|
project: str,
|
|
90
90
|
secret_provider: typing.Optional[typing.Callable[[str], str]] = None,
|
|
91
|
+
store_connection_string: typing.Optional[str] = None,
|
|
91
92
|
**kwargs,
|
|
92
93
|
) -> StoreBase:
|
|
93
94
|
"""
|
|
94
95
|
Generate a store object. If a connection string is provided, the store type will be updated according to the
|
|
95
96
|
connection string. Currently, the supported store types are SQL and v3io-nosql.
|
|
96
97
|
|
|
97
|
-
:param project:
|
|
98
|
-
:param secret_provider:
|
|
98
|
+
:param project: The name of the project.
|
|
99
|
+
:param secret_provider: An optional secret provider to get the connection string secret.
|
|
100
|
+
:param store_connection_string: Optional explicit connection string of the store.
|
|
99
101
|
|
|
100
102
|
:return: `StoreBase` object. Using this object, the user can apply different operations such as write, update, get
|
|
101
|
-
|
|
103
|
+
and delete a model endpoint record.
|
|
104
|
+
:raise: `MLRunInvalidMMStoreType` if the user didn't provide store connection
|
|
105
|
+
or the provided store connection is invalid.
|
|
102
106
|
"""
|
|
103
107
|
|
|
104
|
-
store_connection_string =
|
|
105
|
-
|
|
108
|
+
store_connection_string = (
|
|
109
|
+
store_connection_string
|
|
110
|
+
or mlrun.model_monitoring.helpers.get_connection_string(
|
|
111
|
+
secret_provider=secret_provider
|
|
112
|
+
)
|
|
106
113
|
)
|
|
107
114
|
|
|
108
115
|
if store_connection_string and (
|
|
@@ -111,10 +118,15 @@ def get_store_object(
|
|
|
111
118
|
):
|
|
112
119
|
store_type = mlrun.common.schemas.model_monitoring.ModelEndpointTarget.SQL
|
|
113
120
|
kwargs["store_connection_string"] = store_connection_string
|
|
121
|
+
elif store_connection_string and store_connection_string == "v3io":
|
|
122
|
+
store_type = (
|
|
123
|
+
mlrun.common.schemas.model_monitoring.ModelEndpointTarget.V3IO_NOSQL
|
|
124
|
+
)
|
|
114
125
|
else:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
raise mlrun.errors.MLRunInvalidMMStoreType(
|
|
127
|
+
"You must provide a valid store connection by using "
|
|
128
|
+
"set_model_monitoring_credentials API."
|
|
129
|
+
)
|
|
118
130
|
# Get store type value from ObjectStoreFactory enum class
|
|
119
131
|
store_type_fact = ObjectStoreFactory(store_type)
|
|
120
132
|
|
|
@@ -11,7 +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
|
-
|
|
14
|
+
import json
|
|
15
15
|
import typing
|
|
16
16
|
from abc import ABC, abstractmethod
|
|
17
17
|
|
|
@@ -170,3 +170,41 @@ class StoreBase(ABC):
|
|
|
170
170
|
|
|
171
171
|
:return: A list of the available metrics.
|
|
172
172
|
"""
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def _validate_labels(
|
|
176
|
+
endpoint_dict: dict,
|
|
177
|
+
labels: list,
|
|
178
|
+
) -> bool:
|
|
179
|
+
"""Validate that the model endpoint dictionary has the provided labels. There are 2 possible cases:
|
|
180
|
+
1 - Labels were provided as a list of key-values pairs (e.g. ['label_1=value_1', 'label_2=value_2']): Validate
|
|
181
|
+
that each pair exist in the endpoint dictionary.
|
|
182
|
+
2 - Labels were provided as a list of key labels (e.g. ['label_1', 'label_2']): Validate that each key exist in
|
|
183
|
+
the endpoint labels dictionary.
|
|
184
|
+
|
|
185
|
+
:param endpoint_dict: Dictionary of the model endpoint records.
|
|
186
|
+
:param labels: List of dictionary of required labels.
|
|
187
|
+
|
|
188
|
+
:return: True if the labels exist in the endpoint labels dictionary, otherwise False.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
# Convert endpoint labels into dictionary
|
|
192
|
+
endpoint_labels = json.loads(
|
|
193
|
+
endpoint_dict.get(mm_schemas.EventFieldType.LABELS)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
for label in labels:
|
|
197
|
+
# Case 1 - label is a key=value pair
|
|
198
|
+
if "=" in label:
|
|
199
|
+
lbl, value = list(map(lambda x: x.strip(), label.split("=")))
|
|
200
|
+
if lbl not in endpoint_labels or str(endpoint_labels[lbl]) != value:
|
|
201
|
+
return False
|
|
202
|
+
# Case 2 - label is just a key
|
|
203
|
+
else:
|
|
204
|
+
if label not in endpoint_labels:
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
def create_tables(self):
|
|
210
|
+
pass
|
|
@@ -11,8 +11,10 @@
|
|
|
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
|
+
|
|
14
15
|
from sqlalchemy import (
|
|
15
|
-
|
|
16
|
+
DATETIME,
|
|
17
|
+
TIMESTAMP, # TODO: migrate to DATETIME, see ML-6921
|
|
16
18
|
Boolean,
|
|
17
19
|
Column,
|
|
18
20
|
Float,
|
|
@@ -90,11 +92,11 @@ class ModelEndpointsBaseTable(BaseModel):
|
|
|
90
92
|
metrics = Column(EventFieldType.METRICS, Text)
|
|
91
93
|
first_request = Column(
|
|
92
94
|
EventFieldType.FIRST_REQUEST,
|
|
93
|
-
TIMESTAMP(timezone=True),
|
|
95
|
+
TIMESTAMP(timezone=True), # TODO: migrate to DATETIME, see ML-6921
|
|
94
96
|
)
|
|
95
97
|
last_request = Column(
|
|
96
98
|
EventFieldType.LAST_REQUEST,
|
|
97
|
-
TIMESTAMP(timezone=True),
|
|
99
|
+
TIMESTAMP(timezone=True), # TODO: migrate to DATETIME, see ML-6921
|
|
98
100
|
)
|
|
99
101
|
|
|
100
102
|
|
|
@@ -122,11 +124,11 @@ class ApplicationResultBaseTable(BaseModel):
|
|
|
122
124
|
|
|
123
125
|
start_infer_time = Column(
|
|
124
126
|
WriterEvent.START_INFER_TIME,
|
|
125
|
-
|
|
127
|
+
DATETIME(timezone=True),
|
|
126
128
|
)
|
|
127
129
|
end_infer_time = Column(
|
|
128
130
|
WriterEvent.END_INFER_TIME,
|
|
129
|
-
|
|
131
|
+
DATETIME(timezone=True),
|
|
130
132
|
)
|
|
131
133
|
|
|
132
134
|
result_status = Column(ResultData.RESULT_STATUS, String(10))
|
|
@@ -152,11 +154,11 @@ class ApplicationMetricsBaseTable(BaseModel):
|
|
|
152
154
|
)
|
|
153
155
|
start_infer_time = Column(
|
|
154
156
|
WriterEvent.START_INFER_TIME,
|
|
155
|
-
|
|
157
|
+
DATETIME(timezone=True),
|
|
156
158
|
)
|
|
157
159
|
end_infer_time = Column(
|
|
158
160
|
WriterEvent.END_INFER_TIME,
|
|
159
|
-
|
|
161
|
+
DATETIME(timezone=True),
|
|
160
162
|
)
|
|
161
163
|
metric_name = Column(
|
|
162
164
|
MetricData.METRIC_NAME,
|
|
@@ -34,10 +34,12 @@ Base = declarative_base()
|
|
|
34
34
|
class ModelEndpointsTable(Base, ModelEndpointsBaseTable):
|
|
35
35
|
first_request = Column(
|
|
36
36
|
EventFieldType.FIRST_REQUEST,
|
|
37
|
+
# TODO: migrate to DATETIME, see ML-6921
|
|
37
38
|
sqlalchemy.dialects.mysql.TIMESTAMP(fsp=3, timezone=True),
|
|
38
39
|
)
|
|
39
40
|
last_request = Column(
|
|
40
41
|
EventFieldType.LAST_REQUEST,
|
|
42
|
+
# TODO: migrate to DATETIME, see ML-6921
|
|
41
43
|
sqlalchemy.dialects.mysql.TIMESTAMP(fsp=3, timezone=True),
|
|
42
44
|
)
|
|
43
45
|
|
|
@@ -52,11 +54,11 @@ class _ApplicationResultOrMetric:
|
|
|
52
54
|
|
|
53
55
|
start_infer_time = Column(
|
|
54
56
|
WriterEvent.START_INFER_TIME,
|
|
55
|
-
sqlalchemy.dialects.mysql.
|
|
57
|
+
sqlalchemy.dialects.mysql.DATETIME(fsp=3, timezone=True),
|
|
56
58
|
)
|
|
57
59
|
end_infer_time = Column(
|
|
58
60
|
WriterEvent.END_INFER_TIME,
|
|
59
|
-
sqlalchemy.dialects.mysql.
|
|
61
|
+
sqlalchemy.dialects.mysql.DATETIME(fsp=3, timezone=True),
|
|
60
62
|
)
|
|
61
63
|
|
|
62
64
|
@declared_attr
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
import datetime
|
|
16
|
-
import json
|
|
17
16
|
import typing
|
|
18
17
|
import uuid
|
|
19
18
|
|
|
@@ -21,6 +20,7 @@ import pandas as pd
|
|
|
21
20
|
import sqlalchemy
|
|
22
21
|
import sqlalchemy.exc
|
|
23
22
|
import sqlalchemy.orm
|
|
23
|
+
from sqlalchemy.engine import make_url
|
|
24
24
|
from sqlalchemy.sql.elements import BinaryExpression
|
|
25
25
|
|
|
26
26
|
import mlrun.common.model_monitoring.helpers
|
|
@@ -62,6 +62,10 @@ class SQLStoreBase(StoreBase):
|
|
|
62
62
|
|
|
63
63
|
self._sql_connection_string = kwargs.get("store_connection_string")
|
|
64
64
|
self._engine = get_engine(dsn=self._sql_connection_string)
|
|
65
|
+
self._init_tables()
|
|
66
|
+
|
|
67
|
+
def create_tables(self):
|
|
68
|
+
self._create_tables_if_not_exist()
|
|
65
69
|
|
|
66
70
|
def _init_tables(self):
|
|
67
71
|
self._init_model_endpoints_table()
|
|
@@ -70,13 +74,13 @@ class SQLStoreBase(StoreBase):
|
|
|
70
74
|
self._init_monitoring_schedules_table()
|
|
71
75
|
|
|
72
76
|
def _init_model_endpoints_table(self):
|
|
73
|
-
self.
|
|
77
|
+
self.model_endpoints_table = (
|
|
74
78
|
mlrun.model_monitoring.db.stores.sqldb.models._get_model_endpoints_table(
|
|
75
79
|
connection_string=self._sql_connection_string
|
|
76
80
|
)
|
|
77
81
|
)
|
|
78
82
|
self._tables[mm_schemas.EventFieldType.MODEL_ENDPOINTS] = (
|
|
79
|
-
self.
|
|
83
|
+
self.model_endpoints_table
|
|
80
84
|
)
|
|
81
85
|
|
|
82
86
|
def _init_application_results_table(self):
|
|
@@ -150,22 +154,17 @@ class SQLStoreBase(StoreBase):
|
|
|
150
154
|
:param criteria: A list of binary expressions that filter the query.
|
|
151
155
|
"""
|
|
152
156
|
with create_session(dsn=self._sql_connection_string) as session:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
)
|
|
165
|
-
except sqlalchemy.exc.ProgrammingError:
|
|
166
|
-
# Probably table doesn't exist, try to create tables
|
|
167
|
-
self._create_tables_if_not_exist()
|
|
168
|
-
return
|
|
157
|
+
logger.debug(
|
|
158
|
+
"Querying the DB",
|
|
159
|
+
table=table.__name__,
|
|
160
|
+
criteria=[str(criterion) for criterion in criteria],
|
|
161
|
+
)
|
|
162
|
+
# Generate the get query
|
|
163
|
+
return (
|
|
164
|
+
session.query(table) # pyright: ignore[reportOptionalCall]
|
|
165
|
+
.filter(*criteria)
|
|
166
|
+
.one_or_none()
|
|
167
|
+
)
|
|
169
168
|
|
|
170
169
|
def _delete(
|
|
171
170
|
self,
|
|
@@ -213,14 +212,13 @@ class SQLStoreBase(StoreBase):
|
|
|
213
212
|
of the attributes dictionary should exist in the SQL table.
|
|
214
213
|
|
|
215
214
|
"""
|
|
216
|
-
self._init_model_endpoints_table()
|
|
217
215
|
|
|
218
216
|
attributes.pop(mm_schemas.EventFieldType.ENDPOINT_ID, None)
|
|
219
217
|
|
|
220
218
|
self._update(
|
|
221
219
|
attributes=attributes,
|
|
222
|
-
table=self.
|
|
223
|
-
criteria=[self.
|
|
220
|
+
table=self.model_endpoints_table,
|
|
221
|
+
criteria=[self.model_endpoints_table.uid == endpoint_id],
|
|
224
222
|
)
|
|
225
223
|
|
|
226
224
|
def delete_model_endpoint(self, endpoint_id: str) -> None:
|
|
@@ -229,11 +227,10 @@ class SQLStoreBase(StoreBase):
|
|
|
229
227
|
|
|
230
228
|
:param endpoint_id: The unique id of the model endpoint.
|
|
231
229
|
"""
|
|
232
|
-
self._init_model_endpoints_table()
|
|
233
230
|
# Delete the model endpoint record using sqlalchemy ORM
|
|
234
231
|
self._delete(
|
|
235
|
-
table=self.
|
|
236
|
-
criteria=[self.
|
|
232
|
+
table=self.model_endpoints_table,
|
|
233
|
+
criteria=[self.model_endpoints_table.uid == endpoint_id],
|
|
237
234
|
)
|
|
238
235
|
|
|
239
236
|
def get_model_endpoint(
|
|
@@ -249,12 +246,11 @@ class SQLStoreBase(StoreBase):
|
|
|
249
246
|
|
|
250
247
|
:raise MLRunNotFoundError: If the model endpoints table was not found or the model endpoint id was not found.
|
|
251
248
|
"""
|
|
252
|
-
self._init_model_endpoints_table()
|
|
253
249
|
|
|
254
250
|
# Get the model endpoint record
|
|
255
251
|
endpoint_record = self._get(
|
|
256
|
-
table=self.
|
|
257
|
-
criteria=[self.
|
|
252
|
+
table=self.model_endpoints_table,
|
|
253
|
+
criteria=[self.model_endpoints_table.uid == endpoint_id],
|
|
258
254
|
)
|
|
259
255
|
|
|
260
256
|
if not endpoint_record:
|
|
@@ -286,23 +282,28 @@ class SQLStoreBase(StoreBase):
|
|
|
286
282
|
|
|
287
283
|
:return: A list of model endpoint dictionaries.
|
|
288
284
|
"""
|
|
289
|
-
self._init_model_endpoints_table()
|
|
290
285
|
# Generate an empty model endpoints that will be filled afterwards with model endpoint dictionaries
|
|
291
286
|
endpoint_list = []
|
|
292
287
|
|
|
293
288
|
model_endpoints_table = (
|
|
294
|
-
self.
|
|
289
|
+
self.model_endpoints_table.__table__ # pyright: ignore[reportAttributeAccessIssue]
|
|
295
290
|
)
|
|
296
|
-
|
|
297
291
|
# Get the model endpoints records using sqlalchemy ORM
|
|
298
292
|
with create_session(dsn=self._sql_connection_string) as session:
|
|
299
293
|
# Generate the list query
|
|
300
|
-
query =
|
|
301
|
-
|
|
294
|
+
query = (
|
|
295
|
+
session.query(self.model_endpoints_table)
|
|
296
|
+
.options(
|
|
297
|
+
# Exclude these fields when listing model endpoints to avoid returning too much data (ML-6594)
|
|
298
|
+
sqlalchemy.orm.defer(mm_schemas.EventFieldType.FEATURE_STATS),
|
|
299
|
+
sqlalchemy.orm.defer(mm_schemas.EventFieldType.CURRENT_STATS),
|
|
300
|
+
)
|
|
301
|
+
.filter_by(project=self.project)
|
|
302
302
|
)
|
|
303
303
|
|
|
304
304
|
# Apply filters
|
|
305
305
|
if model:
|
|
306
|
+
model = model if ":" in model else f"{model}:latest"
|
|
306
307
|
query = self._filter_values(
|
|
307
308
|
query=query,
|
|
308
309
|
model_endpoints_table=model_endpoints_table,
|
|
@@ -310,11 +311,12 @@ class SQLStoreBase(StoreBase):
|
|
|
310
311
|
filtered_values=[model],
|
|
311
312
|
)
|
|
312
313
|
if function:
|
|
314
|
+
function_uri = f"{self.project}/{function}"
|
|
313
315
|
query = self._filter_values(
|
|
314
316
|
query=query,
|
|
315
317
|
model_endpoints_table=model_endpoints_table,
|
|
316
|
-
key_filter=mm_schemas.EventFieldType.
|
|
317
|
-
filtered_values=[
|
|
318
|
+
key_filter=mm_schemas.EventFieldType.FUNCTION_URI,
|
|
319
|
+
filtered_values=[function_uri],
|
|
318
320
|
)
|
|
319
321
|
if uids:
|
|
320
322
|
query = self._filter_values(
|
|
@@ -364,11 +366,9 @@ class SQLStoreBase(StoreBase):
|
|
|
364
366
|
"""
|
|
365
367
|
|
|
366
368
|
if kind == mm_schemas.WriterEventKind.METRIC:
|
|
367
|
-
self._init_application_metrics_table()
|
|
368
369
|
table = self.application_metrics_table
|
|
369
370
|
table_name = mm_schemas.FileTargetKind.APP_METRICS
|
|
370
371
|
elif kind == mm_schemas.WriterEventKind.RESULT:
|
|
371
|
-
self._init_application_results_table()
|
|
372
372
|
table = self.application_results_table
|
|
373
373
|
table_name = mm_schemas.FileTargetKind.APP_RESULTS
|
|
374
374
|
else:
|
|
@@ -442,7 +442,6 @@ class SQLStoreBase(StoreBase):
|
|
|
442
442
|
:return: Timestamp as a Unix time.
|
|
443
443
|
:raise: MLRunNotFoundError if last analyzed value is not found.
|
|
444
444
|
"""
|
|
445
|
-
self._init_monitoring_schedules_table()
|
|
446
445
|
monitoring_schedule_record = self._get(
|
|
447
446
|
table=self.MonitoringSchedulesTable,
|
|
448
447
|
criteria=self._get_filter_criteria(
|
|
@@ -469,8 +468,6 @@ class SQLStoreBase(StoreBase):
|
|
|
469
468
|
:param last_analyzed: Timestamp as a Unix time that represents the last analyzed time of a certain
|
|
470
469
|
application and model endpoint.
|
|
471
470
|
"""
|
|
472
|
-
self._init_monitoring_schedules_table()
|
|
473
|
-
|
|
474
471
|
criteria = self._get_filter_criteria(
|
|
475
472
|
table=self.MonitoringSchedulesTable,
|
|
476
473
|
endpoint_id=endpoint_id,
|
|
@@ -500,7 +497,6 @@ class SQLStoreBase(StoreBase):
|
|
|
500
497
|
def _delete_last_analyzed(
|
|
501
498
|
self, endpoint_id: str, application_name: typing.Optional[str] = None
|
|
502
499
|
) -> None:
|
|
503
|
-
self._init_monitoring_schedules_table()
|
|
504
500
|
criteria = self._get_filter_criteria(
|
|
505
501
|
table=self.MonitoringSchedulesTable,
|
|
506
502
|
endpoint_id=endpoint_id,
|
|
@@ -512,7 +508,6 @@ class SQLStoreBase(StoreBase):
|
|
|
512
508
|
def _delete_application_result(
|
|
513
509
|
self, endpoint_id: str, application_name: typing.Optional[str] = None
|
|
514
510
|
) -> None:
|
|
515
|
-
self._init_application_results_table()
|
|
516
511
|
criteria = self._get_filter_criteria(
|
|
517
512
|
table=self.application_results_table,
|
|
518
513
|
endpoint_id=endpoint_id,
|
|
@@ -524,7 +519,6 @@ class SQLStoreBase(StoreBase):
|
|
|
524
519
|
def _delete_application_metrics(
|
|
525
520
|
self, endpoint_id: str, application_name: typing.Optional[str] = None
|
|
526
521
|
) -> None:
|
|
527
|
-
self._init_application_metrics_table()
|
|
528
522
|
criteria = self._get_filter_criteria(
|
|
529
523
|
table=self.application_metrics_table,
|
|
530
524
|
endpoint_id=endpoint_id,
|
|
@@ -538,8 +532,12 @@ class SQLStoreBase(StoreBase):
|
|
|
538
532
|
|
|
539
533
|
for table in self._tables:
|
|
540
534
|
# Create table if not exist. The `metadata` contains the `ModelEndpointsTable`
|
|
535
|
+
db_name = make_url(self._sql_connection_string).database
|
|
541
536
|
if not self._engine.has_table(table):
|
|
537
|
+
logger.info(f"Creating table {table} on {db_name} db.")
|
|
542
538
|
self._tables[table].metadata.create_all(bind=self._engine)
|
|
539
|
+
else:
|
|
540
|
+
logger.info(f"Table {table} already exists on {db_name} db.")
|
|
543
541
|
|
|
544
542
|
@staticmethod
|
|
545
543
|
def _filter_values(
|
|
@@ -581,41 +579,6 @@ class SQLStoreBase(StoreBase):
|
|
|
581
579
|
# Apply AND operator on the SQL query object with the filters tuple
|
|
582
580
|
return query.filter(sqlalchemy.and_(*filter_query))
|
|
583
581
|
|
|
584
|
-
@staticmethod
|
|
585
|
-
def _validate_labels(
|
|
586
|
-
endpoint_dict: dict,
|
|
587
|
-
labels: list,
|
|
588
|
-
) -> bool:
|
|
589
|
-
"""Validate that the model endpoint dictionary has the provided labels. There are 2 possible cases:
|
|
590
|
-
1 - Labels were provided as a list of key-values pairs (e.g. ['label_1=value_1', 'label_2=value_2']): Validate
|
|
591
|
-
that each pair exist in the endpoint dictionary.
|
|
592
|
-
2 - Labels were provided as a list of key labels (e.g. ['label_1', 'label_2']): Validate that each key exist in
|
|
593
|
-
the endpoint labels dictionary.
|
|
594
|
-
|
|
595
|
-
:param endpoint_dict: Dictionary of the model endpoint records.
|
|
596
|
-
:param labels: List of dictionary of required labels.
|
|
597
|
-
|
|
598
|
-
:return: True if the labels exist in the endpoint labels dictionary, otherwise False.
|
|
599
|
-
"""
|
|
600
|
-
|
|
601
|
-
# Convert endpoint labels into dictionary
|
|
602
|
-
endpoint_labels = json.loads(
|
|
603
|
-
endpoint_dict.get(mm_schemas.EventFieldType.LABELS)
|
|
604
|
-
)
|
|
605
|
-
|
|
606
|
-
for label in labels:
|
|
607
|
-
# Case 1 - label is a key=value pair
|
|
608
|
-
if "=" in label:
|
|
609
|
-
lbl, value = list(map(lambda x: x.strip(), label.split("=")))
|
|
610
|
-
if lbl not in endpoint_labels or str(endpoint_labels[lbl]) != value:
|
|
611
|
-
return False
|
|
612
|
-
# Case 2 - label is just a key
|
|
613
|
-
else:
|
|
614
|
-
if label not in endpoint_labels:
|
|
615
|
-
return False
|
|
616
|
-
|
|
617
|
-
return True
|
|
618
|
-
|
|
619
582
|
def delete_model_endpoints_resources(self) -> None:
|
|
620
583
|
"""
|
|
621
584
|
Delete all the model monitoring resources of the project in the SQL tables.
|
|
@@ -650,11 +613,9 @@ class SQLStoreBase(StoreBase):
|
|
|
650
613
|
type=type,
|
|
651
614
|
)
|
|
652
615
|
if type == mm_schemas.ModelEndpointMonitoringMetricType.METRIC:
|
|
653
|
-
self._init_application_metrics_table()
|
|
654
616
|
table = self.application_metrics_table
|
|
655
617
|
name_col = mm_schemas.MetricData.METRIC_NAME
|
|
656
618
|
else:
|
|
657
|
-
self._init_application_results_table()
|
|
658
619
|
table = self.application_results_table
|
|
659
620
|
name_col = mm_schemas.ResultData.RESULT_NAME
|
|
660
621
|
|
|
@@ -256,7 +256,6 @@ class KVStoreBase(StoreBase):
|
|
|
256
256
|
self.project,
|
|
257
257
|
function,
|
|
258
258
|
model,
|
|
259
|
-
labels,
|
|
260
259
|
top_level,
|
|
261
260
|
),
|
|
262
261
|
raise_for_status=v3io.dataplane.RaiseForStatus.never,
|
|
@@ -269,7 +268,6 @@ class KVStoreBase(StoreBase):
|
|
|
269
268
|
exc=mlrun.errors.err_to_str(exc),
|
|
270
269
|
)
|
|
271
270
|
return endpoint_list
|
|
272
|
-
|
|
273
271
|
# Create a list of model endpoints unique ids
|
|
274
272
|
if uids is None:
|
|
275
273
|
uids = []
|
|
@@ -282,10 +280,19 @@ class KVStoreBase(StoreBase):
|
|
|
282
280
|
|
|
283
281
|
# Add each relevant model endpoint to the model endpoints list
|
|
284
282
|
for endpoint_id in uids:
|
|
285
|
-
|
|
283
|
+
endpoint_dict = self.get_model_endpoint(
|
|
286
284
|
endpoint_id=endpoint_id,
|
|
287
285
|
)
|
|
288
|
-
|
|
286
|
+
# Exclude these fields when listing model endpoints to avoid returning too much data (ML-6594)
|
|
287
|
+
endpoint_dict.pop(mm_schemas.EventFieldType.FEATURE_STATS)
|
|
288
|
+
endpoint_dict.pop(mm_schemas.EventFieldType.CURRENT_STATS)
|
|
289
|
+
|
|
290
|
+
if labels and not self._validate_labels(
|
|
291
|
+
endpoint_dict=endpoint_dict, labels=labels
|
|
292
|
+
):
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
endpoint_list.append(endpoint_dict)
|
|
289
296
|
|
|
290
297
|
return endpoint_list
|
|
291
298
|
|
|
@@ -509,20 +516,16 @@ class KVStoreBase(StoreBase):
|
|
|
509
516
|
project: str,
|
|
510
517
|
function: str = None,
|
|
511
518
|
model: str = None,
|
|
512
|
-
labels: list[str] = None,
|
|
513
519
|
top_level: bool = False,
|
|
514
520
|
) -> str:
|
|
515
521
|
"""
|
|
516
522
|
Convert the provided filters into a valid filter expression. The expected filter expression includes different
|
|
517
523
|
conditions, divided by ' AND '.
|
|
518
524
|
|
|
519
|
-
:param project:
|
|
520
|
-
:param model:
|
|
521
|
-
:param function:
|
|
522
|
-
:param
|
|
523
|
-
a label (i.e. list("key=value")) or by looking for the existence of a given
|
|
524
|
-
key (i.e. "key").
|
|
525
|
-
:param top_level: If True will return only routers and endpoint that are NOT children of any router.
|
|
525
|
+
:param project: The name of the project.
|
|
526
|
+
:param model: The name of the model to filter by.
|
|
527
|
+
:param function: The name of the function to filter by.
|
|
528
|
+
:param top_level: If True will return only routers and endpoint that are NOT children of any router.
|
|
526
529
|
|
|
527
530
|
:return: A valid filter expression as a string.
|
|
528
531
|
|
|
@@ -533,25 +536,17 @@ class KVStoreBase(StoreBase):
|
|
|
533
536
|
raise mlrun.errors.MLRunInvalidArgumentError("project can't be empty")
|
|
534
537
|
|
|
535
538
|
# Add project filter
|
|
536
|
-
filter_expression = [f"
|
|
539
|
+
filter_expression = [f"{mm_schemas.EventFieldType.PROJECT}=='{project}'"]
|
|
537
540
|
|
|
538
541
|
# Add function and model filters
|
|
539
542
|
if function:
|
|
540
|
-
|
|
543
|
+
function_uri = f"{project}/{function}" if function else None
|
|
544
|
+
filter_expression.append(
|
|
545
|
+
f"{mm_schemas.EventFieldType.FUNCTION_URI}=='{function_uri}'"
|
|
546
|
+
)
|
|
541
547
|
if model:
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
# Add labels filters
|
|
545
|
-
if labels:
|
|
546
|
-
for label in labels:
|
|
547
|
-
if not label.startswith("_"):
|
|
548
|
-
label = f"_{label}"
|
|
549
|
-
|
|
550
|
-
if "=" in label:
|
|
551
|
-
lbl, value = list(map(lambda x: x.strip(), label.split("=")))
|
|
552
|
-
filter_expression.append(f"{lbl}=='{value}'")
|
|
553
|
-
else:
|
|
554
|
-
filter_expression.append(f"exists({label})")
|
|
548
|
+
model = model if ":" in model else f"{model}:latest"
|
|
549
|
+
filter_expression.append(f"{mm_schemas.EventFieldType.MODEL}=='{model}'")
|
|
555
550
|
|
|
556
551
|
# Apply top_level filter (remove endpoints that considered a child of a router)
|
|
557
552
|
if top_level:
|
|
@@ -57,41 +57,46 @@ class ObjectTSDBFactory(enum.Enum):
|
|
|
57
57
|
:param value: Provided enum (invalid) value.
|
|
58
58
|
"""
|
|
59
59
|
valid_values = list(cls.__members__.keys())
|
|
60
|
-
raise mlrun.errors.
|
|
60
|
+
raise mlrun.errors.MLRunInvalidMMStoreType(
|
|
61
61
|
f"{value} is not a valid tsdb, please choose a valid value: %{valid_values}."
|
|
62
62
|
)
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def get_tsdb_connector(
|
|
66
66
|
project: str,
|
|
67
|
-
tsdb_connector_type: str = "",
|
|
68
67
|
secret_provider: typing.Optional[typing.Callable[[str], str]] = None,
|
|
68
|
+
tsdb_connection_string: typing.Optional[str] = None,
|
|
69
69
|
**kwargs,
|
|
70
70
|
) -> TSDBConnector:
|
|
71
71
|
"""
|
|
72
72
|
Get TSDB connector object.
|
|
73
|
-
:param project:
|
|
74
|
-
:param
|
|
75
|
-
|
|
76
|
-
:param secret_provider: An optional secret provider to get the connection string secret.
|
|
73
|
+
:param project: The name of the project.
|
|
74
|
+
:param secret_provider: An optional secret provider to get the connection string secret.
|
|
75
|
+
:param tsdb_connection_string: An optional explicit connection string to the TSDB.
|
|
77
76
|
|
|
78
77
|
:return: `TSDBConnector` object. The main goal of this object is to handle different operations on the
|
|
79
78
|
TSDB connector such as updating drift metrics or write application record result.
|
|
79
|
+
:raise: `MLRunInvalidMMStoreType` if the user didn't provide TSDB connection
|
|
80
|
+
or the provided TSDB connection is invalid.
|
|
80
81
|
"""
|
|
81
82
|
|
|
82
|
-
tsdb_connection_string =
|
|
83
|
-
|
|
83
|
+
tsdb_connection_string = (
|
|
84
|
+
tsdb_connection_string
|
|
85
|
+
or mlrun.model_monitoring.helpers.get_tsdb_connection_string(
|
|
86
|
+
secret_provider=secret_provider
|
|
87
|
+
)
|
|
84
88
|
)
|
|
85
89
|
|
|
86
90
|
if tsdb_connection_string and tsdb_connection_string.startswith("taosws"):
|
|
87
91
|
tsdb_connector_type = mlrun.common.schemas.model_monitoring.TSDBTarget.TDEngine
|
|
88
92
|
kwargs["connection_string"] = tsdb_connection_string
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
elif tsdb_connection_string and tsdb_connection_string == "v3io":
|
|
94
|
+
tsdb_connector_type = mlrun.common.schemas.model_monitoring.TSDBTarget.V3IO_TSDB
|
|
95
|
+
else:
|
|
96
|
+
raise mlrun.errors.MLRunInvalidMMStoreType(
|
|
97
|
+
"You must provide a valid tsdb store connection by using "
|
|
98
|
+
"set_model_monitoring_credentials API."
|
|
99
|
+
)
|
|
95
100
|
|
|
96
101
|
# Get connector type value from ObjectTSDBFactory enum class
|
|
97
102
|
tsdb_connector_factory = ObjectTSDBFactory(tsdb_connector_type)
|
|
@@ -38,7 +38,10 @@ def _is_no_schema_error(exc: v3io_frames.ReadError) -> bool:
|
|
|
38
38
|
In case of a nonexistent TSDB table - a `v3io_frames.ReadError` error is raised.
|
|
39
39
|
Check if the error message contains the relevant string to verify the cause.
|
|
40
40
|
"""
|
|
41
|
-
|
|
41
|
+
msg = str(exc)
|
|
42
|
+
# https://github.com/v3io/v3io-tsdb/blob/v0.14.1/pkg/tsdb/v3iotsdb.go#L205
|
|
43
|
+
# https://github.com/v3io/v3io-tsdb/blob/v0.14.1/pkg/partmgr/partmgr.go#L238
|
|
44
|
+
return "No TSDB schema file found" in msg or "Failed to read schema at path" in msg
|
|
42
45
|
|
|
43
46
|
|
|
44
47
|
class V3IOTSDBConnector(TSDBConnector):
|
|
@@ -596,7 +599,6 @@ class V3IOTSDBConnector(TSDBConnector):
|
|
|
596
599
|
end=end,
|
|
597
600
|
columns=[mm_schemas.EventFieldType.LATENCY],
|
|
598
601
|
filter_query=f"endpoint_id=='{endpoint_id}'",
|
|
599
|
-
interval=aggregation_window,
|
|
600
602
|
agg_funcs=agg_funcs,
|
|
601
603
|
sliding_window_step=aggregation_window,
|
|
602
604
|
)
|