mlrun 1.7.0rc13__py3-none-any.whl → 1.7.0rc21__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.

Files changed (156) hide show
  1. mlrun/__init__.py +10 -1
  2. mlrun/__main__.py +23 -111
  3. mlrun/alerts/__init__.py +15 -0
  4. mlrun/alerts/alert.py +144 -0
  5. mlrun/api/schemas/__init__.py +4 -3
  6. mlrun/artifacts/__init__.py +8 -3
  7. mlrun/artifacts/base.py +36 -253
  8. mlrun/artifacts/dataset.py +9 -190
  9. mlrun/artifacts/manager.py +46 -42
  10. mlrun/artifacts/model.py +9 -141
  11. mlrun/artifacts/plots.py +14 -375
  12. mlrun/common/constants.py +65 -3
  13. mlrun/common/formatters/__init__.py +19 -0
  14. mlrun/{runtimes/mpijob/v1alpha1.py → common/formatters/artifact.py} +6 -14
  15. mlrun/common/formatters/base.py +113 -0
  16. mlrun/common/formatters/function.py +46 -0
  17. mlrun/common/formatters/pipeline.py +53 -0
  18. mlrun/common/formatters/project.py +51 -0
  19. mlrun/{runtimes → common/runtimes}/constants.py +32 -4
  20. mlrun/common/schemas/__init__.py +10 -5
  21. mlrun/common/schemas/alert.py +92 -11
  22. mlrun/common/schemas/api_gateway.py +56 -0
  23. mlrun/common/schemas/artifact.py +15 -5
  24. mlrun/common/schemas/auth.py +2 -0
  25. mlrun/common/schemas/client_spec.py +1 -0
  26. mlrun/common/schemas/frontend_spec.py +1 -0
  27. mlrun/common/schemas/function.py +4 -0
  28. mlrun/common/schemas/model_monitoring/__init__.py +15 -3
  29. mlrun/common/schemas/model_monitoring/constants.py +58 -7
  30. mlrun/common/schemas/model_monitoring/grafana.py +9 -5
  31. mlrun/common/schemas/model_monitoring/model_endpoints.py +86 -2
  32. mlrun/common/schemas/pipeline.py +0 -9
  33. mlrun/common/schemas/project.py +6 -11
  34. mlrun/common/types.py +1 -0
  35. mlrun/config.py +36 -8
  36. mlrun/data_types/to_pandas.py +9 -9
  37. mlrun/datastore/base.py +41 -9
  38. mlrun/datastore/datastore.py +6 -2
  39. mlrun/datastore/datastore_profile.py +56 -4
  40. mlrun/datastore/hdfs.py +5 -0
  41. mlrun/datastore/inmem.py +2 -2
  42. mlrun/datastore/redis.py +2 -2
  43. mlrun/datastore/s3.py +5 -0
  44. mlrun/datastore/sources.py +147 -7
  45. mlrun/datastore/store_resources.py +7 -7
  46. mlrun/datastore/targets.py +129 -9
  47. mlrun/datastore/utils.py +42 -0
  48. mlrun/datastore/v3io.py +1 -1
  49. mlrun/db/auth_utils.py +152 -0
  50. mlrun/db/base.py +55 -11
  51. mlrun/db/httpdb.py +346 -107
  52. mlrun/db/nopdb.py +52 -10
  53. mlrun/errors.py +11 -0
  54. mlrun/execution.py +24 -9
  55. mlrun/feature_store/__init__.py +0 -2
  56. mlrun/feature_store/api.py +12 -47
  57. mlrun/feature_store/feature_set.py +9 -0
  58. mlrun/feature_store/feature_vector.py +8 -0
  59. mlrun/feature_store/ingestion.py +7 -6
  60. mlrun/feature_store/retrieval/base.py +9 -4
  61. mlrun/feature_store/retrieval/conversion.py +9 -9
  62. mlrun/feature_store/retrieval/dask_merger.py +2 -0
  63. mlrun/feature_store/retrieval/job.py +9 -3
  64. mlrun/feature_store/retrieval/local_merger.py +2 -0
  65. mlrun/feature_store/retrieval/spark_merger.py +16 -0
  66. mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +7 -12
  67. mlrun/frameworks/parallel_coordinates.py +2 -1
  68. mlrun/frameworks/tf_keras/__init__.py +4 -1
  69. mlrun/k8s_utils.py +10 -11
  70. mlrun/launcher/base.py +4 -3
  71. mlrun/launcher/client.py +5 -3
  72. mlrun/launcher/local.py +8 -2
  73. mlrun/launcher/remote.py +8 -2
  74. mlrun/lists.py +6 -2
  75. mlrun/model.py +62 -20
  76. mlrun/model_monitoring/__init__.py +1 -1
  77. mlrun/model_monitoring/api.py +41 -18
  78. mlrun/model_monitoring/application.py +5 -305
  79. mlrun/model_monitoring/applications/__init__.py +11 -0
  80. mlrun/model_monitoring/applications/_application_steps.py +157 -0
  81. mlrun/model_monitoring/applications/base.py +280 -0
  82. mlrun/model_monitoring/applications/context.py +214 -0
  83. mlrun/model_monitoring/applications/evidently_base.py +211 -0
  84. mlrun/model_monitoring/applications/histogram_data_drift.py +132 -91
  85. mlrun/model_monitoring/applications/results.py +99 -0
  86. mlrun/model_monitoring/controller.py +3 -1
  87. mlrun/model_monitoring/db/__init__.py +2 -0
  88. mlrun/model_monitoring/db/stores/__init__.py +0 -2
  89. mlrun/model_monitoring/db/stores/base/store.py +22 -37
  90. mlrun/model_monitoring/db/stores/sqldb/models/__init__.py +43 -21
  91. mlrun/model_monitoring/db/stores/sqldb/models/base.py +39 -8
  92. mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +27 -7
  93. mlrun/model_monitoring/db/stores/sqldb/models/sqlite.py +5 -0
  94. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +246 -224
  95. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +232 -216
  96. mlrun/model_monitoring/db/tsdb/__init__.py +100 -0
  97. mlrun/model_monitoring/db/tsdb/base.py +329 -0
  98. mlrun/model_monitoring/db/tsdb/helpers.py +30 -0
  99. mlrun/model_monitoring/db/tsdb/tdengine/__init__.py +15 -0
  100. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +240 -0
  101. mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +45 -0
  102. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +397 -0
  103. mlrun/model_monitoring/db/tsdb/v3io/__init__.py +15 -0
  104. mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +117 -0
  105. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +636 -0
  106. mlrun/model_monitoring/evidently_application.py +6 -118
  107. mlrun/model_monitoring/helpers.py +46 -1
  108. mlrun/model_monitoring/model_endpoint.py +3 -2
  109. mlrun/model_monitoring/stream_processing.py +57 -216
  110. mlrun/model_monitoring/writer.py +134 -124
  111. mlrun/package/utils/_formatter.py +2 -2
  112. mlrun/platforms/__init__.py +10 -9
  113. mlrun/platforms/iguazio.py +21 -202
  114. mlrun/projects/operations.py +19 -12
  115. mlrun/projects/pipelines.py +103 -109
  116. mlrun/projects/project.py +377 -137
  117. mlrun/render.py +15 -14
  118. mlrun/run.py +16 -47
  119. mlrun/runtimes/__init__.py +6 -3
  120. mlrun/runtimes/base.py +8 -7
  121. mlrun/runtimes/databricks_job/databricks_wrapper.py +1 -1
  122. mlrun/runtimes/funcdoc.py +0 -28
  123. mlrun/runtimes/kubejob.py +2 -1
  124. mlrun/runtimes/local.py +5 -2
  125. mlrun/runtimes/mpijob/__init__.py +0 -20
  126. mlrun/runtimes/mpijob/v1.py +1 -1
  127. mlrun/runtimes/nuclio/api_gateway.py +440 -208
  128. mlrun/runtimes/nuclio/application/application.py +170 -8
  129. mlrun/runtimes/nuclio/function.py +39 -49
  130. mlrun/runtimes/pod.py +21 -41
  131. mlrun/runtimes/remotesparkjob.py +9 -3
  132. mlrun/runtimes/sparkjob/spark3job.py +1 -1
  133. mlrun/runtimes/utils.py +6 -45
  134. mlrun/serving/server.py +2 -1
  135. mlrun/serving/states.py +53 -2
  136. mlrun/serving/v2_serving.py +5 -1
  137. mlrun/track/tracker.py +2 -1
  138. mlrun/utils/async_http.py +25 -5
  139. mlrun/utils/helpers.py +107 -75
  140. mlrun/utils/logger.py +39 -7
  141. mlrun/utils/notifications/notification/__init__.py +14 -9
  142. mlrun/utils/notifications/notification/base.py +1 -1
  143. mlrun/utils/notifications/notification/slack.py +61 -13
  144. mlrun/utils/notifications/notification/webhook.py +1 -1
  145. mlrun/utils/notifications/notification_pusher.py +147 -16
  146. mlrun/utils/regex.py +9 -0
  147. mlrun/utils/v3io_clients.py +0 -1
  148. mlrun/utils/version/version.json +2 -2
  149. {mlrun-1.7.0rc13.dist-info → mlrun-1.7.0rc21.dist-info}/METADATA +14 -6
  150. {mlrun-1.7.0rc13.dist-info → mlrun-1.7.0rc21.dist-info}/RECORD +154 -133
  151. mlrun/kfpops.py +0 -865
  152. mlrun/platforms/other.py +0 -305
  153. {mlrun-1.7.0rc13.dist-info → mlrun-1.7.0rc21.dist-info}/LICENSE +0 -0
  154. {mlrun-1.7.0rc13.dist-info → mlrun-1.7.0rc21.dist-info}/WHEEL +0 -0
  155. {mlrun-1.7.0rc13.dist-info → mlrun-1.7.0rc21.dist-info}/entry_points.txt +0 -0
  156. {mlrun-1.7.0rc13.dist-info → mlrun-1.7.0rc21.dist-info}/top_level.txt +0 -0
@@ -19,14 +19,17 @@ import uuid
19
19
 
20
20
  import pandas as pd
21
21
  import sqlalchemy
22
+ import sqlalchemy.exc
23
+ import sqlalchemy.orm
24
+ from sqlalchemy.sql.elements import BinaryExpression
22
25
 
23
26
  import mlrun.common.model_monitoring.helpers
24
- import mlrun.common.schemas.model_monitoring
27
+ import mlrun.common.schemas.model_monitoring as mm_schemas
25
28
  import mlrun.model_monitoring.db
26
29
  import mlrun.model_monitoring.db.stores.sqldb.models
27
30
  import mlrun.model_monitoring.helpers
28
31
  from mlrun.common.db.sql_session import create_session, get_engine
29
- from mlrun.utils import logger
32
+ from mlrun.utils import datetime_now, logger
30
33
 
31
34
 
32
35
  class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
@@ -36,7 +39,6 @@ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
36
39
  data, the user needs to provide a valid connection string for the database.
37
40
  """
38
41
 
39
- _engine = None
40
42
  _tables = {}
41
43
 
42
44
  def __init__(
@@ -64,6 +66,7 @@ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
64
66
  def _init_tables(self):
65
67
  self._init_model_endpoints_table()
66
68
  self._init_application_results_table()
69
+ self._init_application_metrics_table()
67
70
  self._init_monitoring_schedules_table()
68
71
 
69
72
  def _init_model_endpoints_table(self):
@@ -72,83 +75,91 @@ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
72
75
  connection_string=self._sql_connection_string
73
76
  )
74
77
  )
75
- self._tables[
76
- mlrun.common.schemas.model_monitoring.EventFieldType.MODEL_ENDPOINTS
77
- ] = self.ModelEndpointsTable
78
+ self._tables[mm_schemas.EventFieldType.MODEL_ENDPOINTS] = (
79
+ self.ModelEndpointsTable
80
+ )
78
81
 
79
82
  def _init_application_results_table(self):
80
- self.ApplicationResultsTable = (
83
+ self.application_results_table = (
81
84
  mlrun.model_monitoring.db.stores.sqldb.models._get_application_result_table(
82
85
  connection_string=self._sql_connection_string
83
86
  )
84
87
  )
85
- self._tables[
86
- mlrun.common.schemas.model_monitoring.FileTargetKind.APP_RESULTS
87
- ] = self.ApplicationResultsTable
88
+ self._tables[mm_schemas.FileTargetKind.APP_RESULTS] = (
89
+ self.application_results_table
90
+ )
91
+
92
+ def _init_application_metrics_table(self) -> None:
93
+ self.application_metrics_table = mlrun.model_monitoring.db.stores.sqldb.models._get_application_metrics_table(
94
+ connection_string=self._sql_connection_string
95
+ )
96
+ self._tables[mm_schemas.FileTargetKind.APP_METRICS] = (
97
+ self.application_metrics_table
98
+ )
88
99
 
89
100
  def _init_monitoring_schedules_table(self):
90
101
  self.MonitoringSchedulesTable = mlrun.model_monitoring.db.stores.sqldb.models._get_monitoring_schedules_table(
91
102
  connection_string=self._sql_connection_string
92
103
  )
93
- self._tables[
94
- mlrun.common.schemas.model_monitoring.FileTargetKind.MONITORING_SCHEDULES
95
- ] = self.MonitoringSchedulesTable
104
+ self._tables[mm_schemas.FileTargetKind.MONITORING_SCHEDULES] = (
105
+ self.MonitoringSchedulesTable
106
+ )
96
107
 
97
- def _write(self, table: str, event: dict[str, typing.Any]):
108
+ def _write(self, table_name: str, event: dict[str, typing.Any]) -> None:
98
109
  """
99
110
  Create a new record in the SQL table.
100
111
 
101
- :param table: Target table name.
102
- :param event: Event dictionary that will be written into the DB.
112
+ :param table_name: Target table name.
113
+ :param event: Event dictionary that will be written into the DB.
103
114
  """
104
-
105
115
  with self._engine.connect() as connection:
106
116
  # Convert the result into a pandas Dataframe and write it into the database
107
117
  event_df = pd.DataFrame([event])
108
-
109
- event_df.to_sql(table, con=connection, index=False, if_exists="append")
118
+ event_df.to_sql(table_name, con=connection, index=False, if_exists="append")
110
119
 
111
120
  def _update(
112
121
  self,
113
122
  attributes: dict[str, typing.Any],
114
123
  table: sqlalchemy.orm.decl_api.DeclarativeMeta,
115
- **filtered_values,
116
- ):
124
+ criteria: list[BinaryExpression],
125
+ ) -> None:
117
126
  """
118
127
  Update a record in the SQL table.
119
128
 
120
129
  :param attributes: Dictionary of attributes that will be used for update the record. Note that the keys
121
130
  of the attributes dictionary should exist in the SQL table.
122
131
  :param table: SQLAlchemy declarative table.
123
-
132
+ :param criteria: A list of binary expressions that filter the query.
124
133
  """
125
- filter_query_ = []
126
- for _filter in filtered_values:
127
- filter_query_.append(f"{_filter} = '{filtered_values[_filter]}'")
128
-
129
134
  with create_session(dsn=self._sql_connection_string) as session:
130
135
  # Generate and commit the update session query
131
- session.query(table).filter(sqlalchemy.sql.text(*filter_query_)).update(
132
- attributes, synchronize_session=False
133
- )
136
+ session.query(
137
+ table # pyright: ignore[reportOptionalCall]
138
+ ).filter(*criteria).update(attributes, synchronize_session=False)
134
139
  session.commit()
135
140
 
136
- def _get(self, table: sqlalchemy.orm.decl_api.DeclarativeMeta, **filtered_values):
141
+ def _get(
142
+ self,
143
+ table: sqlalchemy.orm.decl_api.DeclarativeMeta,
144
+ criteria: list[BinaryExpression],
145
+ ):
137
146
  """
138
147
  Get a record from the SQL table.
139
148
 
140
- param table: SQLAlchemy declarative table.
149
+ param table: SQLAlchemy declarative table.
150
+ :param criteria: A list of binary expressions that filter the query.
141
151
  """
142
-
143
- filter_query_ = []
144
- for _filter in filtered_values:
145
- filter_query_.append(f"{_filter} = '{filtered_values[_filter]}'")
146
152
  with create_session(dsn=self._sql_connection_string) as session:
147
153
  try:
154
+ logger.debug(
155
+ "Querying the DB",
156
+ table=table.__name__,
157
+ criteria=[str(criterion) for criterion in criteria],
158
+ )
148
159
  # Generate the get query
149
160
  return (
150
- session.query(table)
151
- .filter(sqlalchemy.sql.text(*filter_query_))
161
+ session.query(table) # pyright: ignore[reportOptionalCall]
162
+ .filter(*criteria)
152
163
  .one_or_none()
153
164
  )
154
165
  except sqlalchemy.exc.ProgrammingError:
@@ -157,21 +168,21 @@ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
157
168
  return
158
169
 
159
170
  def _delete(
160
- self, table: sqlalchemy.orm.decl_api.DeclarativeMeta, **filtered_values
161
- ):
171
+ self,
172
+ table: sqlalchemy.orm.decl_api.DeclarativeMeta,
173
+ criteria: list[BinaryExpression],
174
+ ) -> None:
162
175
  """
163
176
  Delete records from the SQL table.
164
177
 
165
- param table: SQLAlchemy declarative table.
178
+ param table: SQLAlchemy declarative table.
179
+ :param criteria: A list of binary expressions that filter the query.
166
180
  """
167
- filter_query_ = []
168
- for _filter in filtered_values:
169
- filter_query_.append(f"{_filter} = '{filtered_values[_filter]}'")
170
181
  with create_session(dsn=self._sql_connection_string) as session:
171
182
  # Generate and commit the delete query
172
- session.query(table).filter(sqlalchemy.sql.text(*filter_query_)).delete(
173
- synchronize_session=False
174
- )
183
+ session.query(
184
+ table # pyright: ignore[reportOptionalCall]
185
+ ).filter(*criteria).delete(synchronize_session=False)
175
186
  session.commit()
176
187
 
177
188
  def write_model_endpoint(self, endpoint: dict[str, typing.Any]):
@@ -183,15 +194,12 @@ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
183
194
  """
184
195
 
185
196
  # Adjust timestamps fields
186
- endpoint[mlrun.common.schemas.model_monitoring.EventFieldType.FIRST_REQUEST] = (
187
- endpoint
188
- )[
189
- mlrun.common.schemas.model_monitoring.EventFieldType.LAST_REQUEST
190
- ] = mlrun.utils.datetime_now()
197
+ endpoint[mm_schemas.EventFieldType.FIRST_REQUEST] = (endpoint)[
198
+ mm_schemas.EventFieldType.LAST_REQUEST
199
+ ] = datetime_now()
191
200
 
192
201
  self._write(
193
- table=mlrun.common.schemas.model_monitoring.EventFieldType.MODEL_ENDPOINTS,
194
- event=endpoint,
202
+ table_name=mm_schemas.EventFieldType.MODEL_ENDPOINTS, event=endpoint
195
203
  )
196
204
 
197
205
  def update_model_endpoint(
@@ -207,31 +215,26 @@ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
207
215
  """
208
216
  self._init_model_endpoints_table()
209
217
 
210
- attributes.pop(
211
- mlrun.common.schemas.model_monitoring.EventFieldType.ENDPOINT_ID, None
212
- )
213
-
214
- filter_endpoint = {
215
- mlrun.common.schemas.model_monitoring.EventFieldType.UID: endpoint_id
216
- }
218
+ attributes.pop(mm_schemas.EventFieldType.ENDPOINT_ID, None)
217
219
 
218
220
  self._update(
219
- attributes=attributes, table=self.ModelEndpointsTable, **filter_endpoint
221
+ attributes=attributes,
222
+ table=self.ModelEndpointsTable,
223
+ criteria=[self.ModelEndpointsTable.uid == endpoint_id],
220
224
  )
221
225
 
222
- def delete_model_endpoint(self, endpoint_id: str):
226
+ def delete_model_endpoint(self, endpoint_id: str) -> None:
223
227
  """
224
228
  Deletes the SQL record of a given model endpoint id.
225
229
 
226
230
  :param endpoint_id: The unique id of the model endpoint.
227
231
  """
228
232
  self._init_model_endpoints_table()
229
-
230
- filter_endpoint = {
231
- mlrun.common.schemas.model_monitoring.EventFieldType.UID: endpoint_id
232
- }
233
233
  # Delete the model endpoint record using sqlalchemy ORM
234
- self._delete(table=self.ModelEndpointsTable, **filter_endpoint)
234
+ self._delete(
235
+ table=self.ModelEndpointsTable,
236
+ criteria=[self.ModelEndpointsTable.uid == endpoint_id],
237
+ )
235
238
 
236
239
  def get_model_endpoint(
237
240
  self,
@@ -248,11 +251,11 @@ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
248
251
  """
249
252
  self._init_model_endpoints_table()
250
253
 
251
- # Get the model endpoint record using sqlalchemy ORM
252
- filter_endpoint = {
253
- mlrun.common.schemas.model_monitoring.EventFieldType.UID: endpoint_id
254
- }
255
- endpoint_record = self._get(table=self.ModelEndpointsTable, **filter_endpoint)
254
+ # Get the model endpoint record
255
+ endpoint_record = self._get(
256
+ table=self.ModelEndpointsTable,
257
+ criteria=[self.ModelEndpointsTable.uid == endpoint_id],
258
+ )
256
259
 
257
260
  if not endpoint_record:
258
261
  raise mlrun.errors.MLRunNotFoundError(f"Endpoint {endpoint_id} not found")
@@ -288,7 +291,7 @@ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
288
291
  endpoint_list = []
289
292
 
290
293
  model_endpoints_table = (
291
- self.ModelEndpointsTable.__table__ # pyright: ignore[reportGeneralTypeIssues]
294
+ self.ModelEndpointsTable.__table__ # pyright: ignore[reportAttributeAccessIssue]
292
295
  )
293
296
 
294
297
  # Get the model endpoints records using sqlalchemy ORM
@@ -303,36 +306,32 @@ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
303
306
  query = self._filter_values(
304
307
  query=query,
305
308
  model_endpoints_table=model_endpoints_table,
306
- key_filter=mlrun.common.schemas.model_monitoring.EventFieldType.MODEL,
309
+ key_filter=mm_schemas.EventFieldType.MODEL,
307
310
  filtered_values=[model],
308
311
  )
309
312
  if function:
310
313
  query = self._filter_values(
311
314
  query=query,
312
315
  model_endpoints_table=model_endpoints_table,
313
- key_filter=mlrun.common.schemas.model_monitoring.EventFieldType.FUNCTION,
316
+ key_filter=mm_schemas.EventFieldType.FUNCTION,
314
317
  filtered_values=[function],
315
318
  )
316
319
  if uids:
317
320
  query = self._filter_values(
318
321
  query=query,
319
322
  model_endpoints_table=model_endpoints_table,
320
- key_filter=mlrun.common.schemas.model_monitoring.EventFieldType.UID,
323
+ key_filter=mm_schemas.EventFieldType.UID,
321
324
  filtered_values=uids,
322
325
  combined=False,
323
326
  )
324
327
  if top_level:
325
- node_ep = str(
326
- mlrun.common.schemas.model_monitoring.EndpointType.NODE_EP.value
327
- )
328
- router_ep = str(
329
- mlrun.common.schemas.model_monitoring.EndpointType.ROUTER.value
330
- )
328
+ node_ep = str(mm_schemas.EndpointType.NODE_EP.value)
329
+ router_ep = str(mm_schemas.EndpointType.ROUTER.value)
331
330
  endpoint_types = [node_ep, router_ep]
332
331
  query = self._filter_values(
333
332
  query=query,
334
333
  model_endpoints_table=model_endpoints_table,
335
- key_filter=mlrun.common.schemas.model_monitoring.EventFieldType.ENDPOINT_TYPE,
334
+ key_filter=mm_schemas.EventFieldType.ENDPOINT_TYPE,
336
335
  filtered_values=endpoint_types,
337
336
  combined=False,
338
337
  )
@@ -350,68 +349,89 @@ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
350
349
 
351
350
  return endpoint_list
352
351
 
353
- def write_application_result(self, event: dict[str, typing.Any]):
354
- """
355
- Write a new application result event in the target table.
356
-
357
- :param event: An event dictionary that represents the application result, should be corresponded to the
358
- schema defined in the :py:class:`~mlrun.common.schemas.model_monitoring.constants.WriterEvent`
359
- object.
360
- """
361
- self._init_application_results_table()
352
+ def write_application_event(
353
+ self,
354
+ event: dict[str, typing.Any],
355
+ kind: mm_schemas.WriterEventKind = mm_schemas.WriterEventKind.RESULT,
356
+ ) -> None:
357
+ """
358
+ Write a new application event in the target table.
359
+
360
+ :param event: An event dictionary that represents the application result or metric,
361
+ should be corresponded to the schema defined in the
362
+ :py:class:`~mm_constants.constants.WriterEvent` object.
363
+ :param kind: The type of the event, can be either "result" or "metric".
364
+ """
365
+
366
+ if kind == mm_schemas.WriterEventKind.METRIC:
367
+ self._init_application_metrics_table()
368
+ table = self.application_metrics_table
369
+ table_name = mm_schemas.FileTargetKind.APP_METRICS
370
+ elif kind == mm_schemas.WriterEventKind.RESULT:
371
+ self._init_application_results_table()
372
+ table = self.application_results_table
373
+ table_name = mm_schemas.FileTargetKind.APP_RESULTS
374
+ else:
375
+ raise ValueError(f"Invalid {kind = }")
362
376
 
363
- application_filter_dict = {
364
- mlrun.common.schemas.model_monitoring.EventFieldType.UID: self._generate_application_result_uid(
365
- event
366
- )
367
- }
377
+ application_result_uid = self._generate_application_result_uid(event, kind=kind)
378
+ criteria = [table.uid == application_result_uid]
368
379
 
369
- application_record = self._get(
370
- table=self.ApplicationResultsTable, **application_filter_dict
371
- )
380
+ application_record = self._get(table=table, criteria=criteria)
372
381
  if application_record:
373
382
  self._convert_to_datetime(
374
- event=event,
375
- key=mlrun.common.schemas.model_monitoring.WriterEvent.START_INFER_TIME,
383
+ event=event, key=mm_schemas.WriterEvent.START_INFER_TIME
376
384
  )
377
385
  self._convert_to_datetime(
378
- event=event,
379
- key=mlrun.common.schemas.model_monitoring.WriterEvent.END_INFER_TIME,
386
+ event=event, key=mm_schemas.WriterEvent.END_INFER_TIME
380
387
  )
381
388
  # Update an existing application result
382
- self._update(
383
- attributes=event,
384
- table=self.ApplicationResultsTable,
385
- **application_filter_dict,
386
- )
389
+ self._update(attributes=event, table=table, criteria=criteria)
387
390
  else:
388
391
  # Write a new application result
389
- event[mlrun.common.schemas.model_monitoring.EventFieldType.UID] = (
390
- application_filter_dict[
391
- mlrun.common.schemas.model_monitoring.EventFieldType.UID
392
- ]
393
- )
394
-
395
- self._write(
396
- table=mlrun.common.schemas.model_monitoring.FileTargetKind.APP_RESULTS,
397
- event=event,
398
- )
392
+ event[mm_schemas.EventFieldType.UID] = application_result_uid
393
+ self._write(table_name=table_name, event=event)
399
394
 
400
395
  @staticmethod
401
- def _convert_to_datetime(event: dict[str, typing.Any], key: str):
396
+ def _convert_to_datetime(event: dict[str, typing.Any], key: str) -> None:
402
397
  if isinstance(event[key], str):
403
398
  event[key] = datetime.datetime.fromisoformat(event[key])
399
+ event[key] = event[key].astimezone(tz=datetime.timezone.utc)
404
400
 
405
401
  @staticmethod
406
- def _generate_application_result_uid(event: dict[str, typing.Any]) -> str:
407
- return (
408
- event[mlrun.common.schemas.model_monitoring.WriterEvent.ENDPOINT_ID]
409
- + "_"
410
- + event[mlrun.common.schemas.model_monitoring.WriterEvent.APPLICATION_NAME]
411
- + "_"
412
- + event[mlrun.common.schemas.model_monitoring.WriterEvent.RESULT_NAME]
402
+ def _generate_application_result_uid(
403
+ event: dict[str, typing.Any],
404
+ kind: mm_schemas.WriterEventKind = mm_schemas.WriterEventKind.RESULT,
405
+ ) -> str:
406
+ if kind == mm_schemas.WriterEventKind.RESULT:
407
+ name = event[mm_schemas.ResultData.RESULT_NAME]
408
+ else:
409
+ name = event[mm_schemas.MetricData.METRIC_NAME]
410
+ return "_".join(
411
+ [
412
+ event[mm_schemas.WriterEvent.ENDPOINT_ID],
413
+ event[mm_schemas.WriterEvent.APPLICATION_NAME],
414
+ name,
415
+ ]
413
416
  )
414
417
 
418
+ @staticmethod
419
+ def _get_filter_criteria(
420
+ *,
421
+ table: sqlalchemy.orm.decl_api.DeclarativeMeta,
422
+ endpoint_id: str,
423
+ application_name: typing.Optional[str] = None,
424
+ ) -> list[BinaryExpression]:
425
+ """
426
+ Return the filter criteria for the given endpoint_id and application_name.
427
+ Note: the table object must include the relevant columns:
428
+ `endpoint_id` and `application_name`.
429
+ """
430
+ criteria = [table.endpoint_id == endpoint_id]
431
+ if application_name is not None:
432
+ criteria.append(table.application_name == application_name)
433
+ return criteria
434
+
415
435
  def get_last_analyzed(self, endpoint_id: str, application_name: str) -> int:
416
436
  """
417
437
  Get the last analyzed time for the provided model endpoint and application.
@@ -421,14 +441,15 @@ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
421
441
 
422
442
  :return: Timestamp as a Unix time.
423
443
  :raise: MLRunNotFoundError if last analyzed value is not found.
424
-
425
444
  """
426
445
  self._init_monitoring_schedules_table()
427
- application_filter_dict = self.filter_endpoint_and_application_name(
428
- endpoint_id=endpoint_id, application_name=application_name
429
- )
430
446
  monitoring_schedule_record = self._get(
431
- table=self.MonitoringSchedulesTable, **application_filter_dict
447
+ table=self.MonitoringSchedulesTable,
448
+ criteria=self._get_filter_criteria(
449
+ table=self.MonitoringSchedulesTable,
450
+ endpoint_id=endpoint_id,
451
+ application_name=application_name,
452
+ ),
432
453
  )
433
454
  if not monitoring_schedule_record:
434
455
  raise mlrun.errors.MLRunNotFoundError(
@@ -450,53 +471,67 @@ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
450
471
  """
451
472
  self._init_monitoring_schedules_table()
452
473
 
453
- application_filter_dict = self.filter_endpoint_and_application_name(
454
- endpoint_id=endpoint_id, application_name=application_name
474
+ criteria = self._get_filter_criteria(
475
+ table=self.MonitoringSchedulesTable,
476
+ endpoint_id=endpoint_id,
477
+ application_name=application_name,
455
478
  )
456
479
  monitoring_schedule_record = self._get(
457
- table=self.MonitoringSchedulesTable, **application_filter_dict
480
+ table=self.MonitoringSchedulesTable, criteria=criteria
458
481
  )
459
482
  if not monitoring_schedule_record:
460
- # Add a new record with empty last analyzed value
483
+ # Add a new record with last analyzed value
461
484
  self._write(
462
- table=mlrun.common.schemas.model_monitoring.FileTargetKind.MONITORING_SCHEDULES,
485
+ table_name=mm_schemas.FileTargetKind.MONITORING_SCHEDULES,
463
486
  event={
464
- mlrun.common.schemas.model_monitoring.SchedulingKeys.UID: uuid.uuid4().hex,
465
- mlrun.common.schemas.model_monitoring.SchedulingKeys.APPLICATION_NAME: application_name,
466
- mlrun.common.schemas.model_monitoring.SchedulingKeys.ENDPOINT_ID: endpoint_id,
467
- mlrun.common.schemas.model_monitoring.SchedulingKeys.LAST_ANALYZED: last_analyzed,
487
+ mm_schemas.SchedulingKeys.UID: uuid.uuid4().hex,
488
+ mm_schemas.SchedulingKeys.APPLICATION_NAME: application_name,
489
+ mm_schemas.SchedulingKeys.ENDPOINT_ID: endpoint_id,
490
+ mm_schemas.SchedulingKeys.LAST_ANALYZED: last_analyzed,
468
491
  },
469
492
  )
470
493
 
471
494
  self._update(
472
- attributes={
473
- mlrun.common.schemas.model_monitoring.SchedulingKeys.LAST_ANALYZED: last_analyzed
474
- },
495
+ attributes={mm_schemas.SchedulingKeys.LAST_ANALYZED: last_analyzed},
475
496
  table=self.MonitoringSchedulesTable,
476
- **application_filter_dict,
497
+ criteria=criteria,
477
498
  )
478
499
 
479
- def _delete_last_analyzed(self, endpoint_id: str = "", application_name: str = ""):
500
+ def _delete_last_analyzed(
501
+ self, endpoint_id: str, application_name: typing.Optional[str] = None
502
+ ) -> None:
480
503
  self._init_monitoring_schedules_table()
481
-
482
- application_filter_dict = self.filter_endpoint_and_application_name(
483
- endpoint_id=endpoint_id, application_name=application_name
504
+ criteria = self._get_filter_criteria(
505
+ table=self.MonitoringSchedulesTable,
506
+ endpoint_id=endpoint_id,
507
+ application_name=application_name,
484
508
  )
485
-
486
509
  # Delete the model endpoint record using sqlalchemy ORM
487
- self._delete(table=self.MonitoringSchedulesTable, **application_filter_dict)
510
+ self._delete(table=self.MonitoringSchedulesTable, criteria=criteria)
488
511
 
489
512
  def _delete_application_result(
490
- self, endpoint_id: str = "", application_name: str = ""
491
- ):
513
+ self, endpoint_id: str, application_name: typing.Optional[str] = None
514
+ ) -> None:
492
515
  self._init_application_results_table()
493
-
494
- application_filter_dict = self.filter_endpoint_and_application_name(
495
- endpoint_id=endpoint_id, application_name=application_name
516
+ criteria = self._get_filter_criteria(
517
+ table=self.application_results_table,
518
+ endpoint_id=endpoint_id,
519
+ application_name=application_name,
496
520
  )
497
-
498
- # Delete the model endpoint record using sqlalchemy ORM
499
- self._delete(table=self.ApplicationResultsTable, **application_filter_dict)
521
+ # Delete the relevant records from the results table
522
+ self._delete(table=self.application_results_table, criteria=criteria)
523
+
524
+ def _delete_application_metrics(
525
+ self, endpoint_id: str, application_name: typing.Optional[str] = None
526
+ ) -> None:
527
+ self._init_application_metrics_table()
528
+ criteria = self._get_filter_criteria(
529
+ table=self.application_metrics_table,
530
+ endpoint_id=endpoint_id,
531
+ application_name=application_name,
532
+ )
533
+ # Delete the relevant records from the metrics table
534
+ self._delete(table=self.application_metrics_table, criteria=criteria)
500
535
 
501
536
  def _create_tables_if_not_exist(self):
502
537
  self._init_tables()
@@ -504,9 +539,7 @@ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
504
539
  for table in self._tables:
505
540
  # Create table if not exist. The `metadata` contains the `ModelEndpointsTable`
506
541
  if not self._engine.has_table(table):
507
- self._tables[table].metadata.create_all( # pyright: ignore[reportGeneralTypeIssues]
508
- bind=self._engine
509
- )
542
+ self._tables[table].metadata.create_all(bind=self._engine)
510
543
 
511
544
  @staticmethod
512
545
  def _filter_values(
@@ -567,9 +600,7 @@ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
567
600
 
568
601
  # Convert endpoint labels into dictionary
569
602
  endpoint_labels = json.loads(
570
- endpoint_dict.get(
571
- mlrun.common.schemas.model_monitoring.EventFieldType.LABELS
572
- )
603
+ endpoint_dict.get(mm_schemas.EventFieldType.LABELS)
573
604
  )
574
605
 
575
606
  for label in labels:
@@ -585,78 +616,69 @@ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
585
616
 
586
617
  return True
587
618
 
588
- @staticmethod
589
- def filter_endpoint_and_application_name(
590
- endpoint_id: str, application_name: str
591
- ) -> dict[str, str]:
592
- """Generate a dictionary filter for endpoint id and application name"""
593
- if not endpoint_id and not application_name:
594
- raise mlrun.errors.MLRunBadRequestError(
595
- "Please provide a valid endpoint_id and/or application_name"
596
- )
597
- application_filter_dict = {}
598
- if endpoint_id:
599
- application_filter_dict[
600
- mlrun.common.schemas.model_monitoring.SchedulingKeys.ENDPOINT_ID
601
- ] = endpoint_id
602
- if application_name:
603
- application_filter_dict[
604
- mlrun.common.schemas.model_monitoring.SchedulingKeys.APPLICATION_NAME
605
- ] = application_name
606
- return application_filter_dict
607
-
608
- def delete_model_endpoints_resources(self, endpoints: list[dict[str, typing.Any]]):
619
+ def delete_model_endpoints_resources(self) -> None:
609
620
  """
610
- Delete all model endpoints resources in both SQL and the time series DB.
611
-
612
- :param endpoints: A list of model endpoints flattened dictionaries.
621
+ Delete all the model monitoring resources of the project in the SQL tables.
613
622
  """
623
+ endpoints = self.list_model_endpoints()
624
+ logger.debug("Deleting model monitoring resources", project=self.project)
614
625
 
615
626
  for endpoint_dict in endpoints:
616
- endpoint_id = endpoint_dict[
617
- mlrun.common.schemas.model_monitoring.EventFieldType.UID
618
- ]
627
+ endpoint_id = endpoint_dict[mm_schemas.EventFieldType.UID]
619
628
 
620
629
  # Delete last analyzed records
621
630
  self._delete_last_analyzed(endpoint_id=endpoint_id)
622
631
 
623
- # Delete application results records
632
+ # Delete application results and metrics records
624
633
  self._delete_application_result(endpoint_id=endpoint_id)
634
+ self._delete_application_metrics(endpoint_id=endpoint_id)
625
635
 
626
636
  # Delete model endpoint record
627
637
  self.delete_model_endpoint(endpoint_id=endpoint_id)
628
638
 
629
- def get_endpoint_real_time_metrics(
630
- self,
631
- endpoint_id: str,
632
- metrics: list[str],
633
- start: str = "now-1h",
634
- end: str = "now",
635
- access_key: str = None,
636
- ) -> dict[str, list[tuple[str, float]]]:
639
+ def get_model_endpoint_metrics(
640
+ self, endpoint_id: str, type: mm_schemas.ModelEndpointMonitoringMetricType
641
+ ) -> list[mm_schemas.ModelEndpointMonitoringMetric]:
637
642
  """
638
- Getting metrics from the time series DB. There are pre-defined metrics for model endpoints such as
639
- `predictions_per_second` and `latency_avg_5m` but also custom metrics defined by the user.
640
-
641
- :param endpoint_id: The unique id of the model endpoint.
642
- :param metrics: A list of real-time metrics to return for the model endpoint.
643
- :param start: The start time of the metrics. Can be represented by a string containing an RFC 3339
644
- time, a Unix timestamp in milliseconds, a relative time (`'now'` or
645
- `'now-[0-9]+[mhd]'`, where `m` = minutes, `h` = hours, and `'d'` = days), or 0 for the
646
- earliest time.
647
- :param end: The end time of the metrics. Can be represented by a string containing an RFC 3339
648
- time, a Unix timestamp in milliseconds, a relative time (`'now'` or
649
- `'now-[0-9]+[mhd]'`, where `m` = minutes, `h` = hours, and `'d'` = days), or 0 for the
650
- earliest time.
651
- :param access_key: V3IO access key that will be used for generating Frames client object. If not
652
- provided, the access key will be retrieved from the environment variables.
653
-
654
- :return: A dictionary of metrics in which the key is a metric name and the value is a list of tuples that
655
- includes timestamps and the values.
656
- """
657
- # # TODO : Implement this method once Perometheus is supported
658
- logger.warning(
659
- "Real time metrics service using Prometheus will be implemented in 1.4.0"
643
+ Fetch the model endpoint metrics or results (according to `type`) for the
644
+ requested endpoint.
645
+ """
646
+ logger.debug(
647
+ "Fetching metrics for model endpoint",
648
+ project=self.project,
649
+ endpoint_id=endpoint_id,
650
+ type=type,
660
651
  )
652
+ if type == mm_schemas.ModelEndpointMonitoringMetricType.METRIC:
653
+ self._init_application_metrics_table()
654
+ table = self.application_metrics_table
655
+ name_col = mm_schemas.MetricData.METRIC_NAME
656
+ else:
657
+ self._init_application_results_table()
658
+ table = self.application_results_table
659
+ name_col = mm_schemas.ResultData.RESULT_NAME
660
+
661
+ # Note: the block below does not use self._get, as we need here all the
662
+ # results, not only `one_or_none`.
663
+ with sqlalchemy.orm.Session(self._engine) as session:
664
+ metric_rows = (
665
+ session.query(table) # pyright: ignore[reportOptionalCall]
666
+ .filter(table.endpoint_id == endpoint_id)
667
+ .all()
668
+ )
661
669
 
662
- return {}
670
+ return [
671
+ mm_schemas.ModelEndpointMonitoringMetric(
672
+ project=self.project,
673
+ app=metric_row.application_name,
674
+ type=type,
675
+ name=getattr(metric_row, name_col),
676
+ full_name=mlrun.model_monitoring.helpers._compose_full_name(
677
+ project=self.project,
678
+ app=metric_row.application_name,
679
+ type=type,
680
+ name=getattr(metric_row, name_col),
681
+ ),
682
+ )
683
+ for metric_row in metric_rows
684
+ ]