mlrun 1.7.0rc14__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 (152) 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 +5 -11
  34. mlrun/common/types.py +1 -0
  35. mlrun/config.py +27 -9
  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/inmem.py +2 -2
  41. mlrun/datastore/redis.py +2 -2
  42. mlrun/datastore/s3.py +5 -0
  43. mlrun/datastore/sources.py +147 -7
  44. mlrun/datastore/store_resources.py +7 -7
  45. mlrun/datastore/targets.py +110 -42
  46. mlrun/datastore/utils.py +42 -0
  47. mlrun/db/base.py +54 -10
  48. mlrun/db/httpdb.py +282 -79
  49. mlrun/db/nopdb.py +52 -10
  50. mlrun/errors.py +11 -0
  51. mlrun/execution.py +24 -9
  52. mlrun/feature_store/__init__.py +0 -2
  53. mlrun/feature_store/api.py +12 -47
  54. mlrun/feature_store/feature_set.py +9 -0
  55. mlrun/feature_store/feature_vector.py +8 -0
  56. mlrun/feature_store/ingestion.py +7 -6
  57. mlrun/feature_store/retrieval/base.py +9 -4
  58. mlrun/feature_store/retrieval/conversion.py +9 -9
  59. mlrun/feature_store/retrieval/dask_merger.py +2 -0
  60. mlrun/feature_store/retrieval/job.py +9 -3
  61. mlrun/feature_store/retrieval/local_merger.py +2 -0
  62. mlrun/feature_store/retrieval/spark_merger.py +16 -0
  63. mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +7 -12
  64. mlrun/frameworks/parallel_coordinates.py +2 -1
  65. mlrun/frameworks/tf_keras/__init__.py +4 -1
  66. mlrun/k8s_utils.py +10 -11
  67. mlrun/launcher/base.py +4 -3
  68. mlrun/launcher/client.py +5 -3
  69. mlrun/launcher/local.py +8 -2
  70. mlrun/launcher/remote.py +8 -2
  71. mlrun/lists.py +6 -2
  72. mlrun/model.py +45 -21
  73. mlrun/model_monitoring/__init__.py +1 -1
  74. mlrun/model_monitoring/api.py +41 -18
  75. mlrun/model_monitoring/application.py +5 -305
  76. mlrun/model_monitoring/applications/__init__.py +11 -0
  77. mlrun/model_monitoring/applications/_application_steps.py +157 -0
  78. mlrun/model_monitoring/applications/base.py +280 -0
  79. mlrun/model_monitoring/applications/context.py +214 -0
  80. mlrun/model_monitoring/applications/evidently_base.py +211 -0
  81. mlrun/model_monitoring/applications/histogram_data_drift.py +132 -91
  82. mlrun/model_monitoring/applications/results.py +99 -0
  83. mlrun/model_monitoring/controller.py +3 -1
  84. mlrun/model_monitoring/db/__init__.py +2 -0
  85. mlrun/model_monitoring/db/stores/__init__.py +0 -2
  86. mlrun/model_monitoring/db/stores/base/store.py +22 -37
  87. mlrun/model_monitoring/db/stores/sqldb/models/__init__.py +43 -21
  88. mlrun/model_monitoring/db/stores/sqldb/models/base.py +39 -8
  89. mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +27 -7
  90. mlrun/model_monitoring/db/stores/sqldb/models/sqlite.py +5 -0
  91. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +246 -224
  92. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +232 -216
  93. mlrun/model_monitoring/db/tsdb/__init__.py +100 -0
  94. mlrun/model_monitoring/db/tsdb/base.py +329 -0
  95. mlrun/model_monitoring/db/tsdb/helpers.py +30 -0
  96. mlrun/model_monitoring/db/tsdb/tdengine/__init__.py +15 -0
  97. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +240 -0
  98. mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +45 -0
  99. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +397 -0
  100. mlrun/model_monitoring/db/tsdb/v3io/__init__.py +15 -0
  101. mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +117 -0
  102. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +636 -0
  103. mlrun/model_monitoring/evidently_application.py +6 -118
  104. mlrun/model_monitoring/helpers.py +46 -1
  105. mlrun/model_monitoring/model_endpoint.py +3 -2
  106. mlrun/model_monitoring/stream_processing.py +57 -216
  107. mlrun/model_monitoring/writer.py +134 -124
  108. mlrun/package/utils/_formatter.py +2 -2
  109. mlrun/platforms/__init__.py +10 -9
  110. mlrun/platforms/iguazio.py +21 -202
  111. mlrun/projects/operations.py +19 -12
  112. mlrun/projects/pipelines.py +79 -102
  113. mlrun/projects/project.py +265 -103
  114. mlrun/render.py +15 -14
  115. mlrun/run.py +16 -46
  116. mlrun/runtimes/__init__.py +6 -3
  117. mlrun/runtimes/base.py +8 -7
  118. mlrun/runtimes/databricks_job/databricks_wrapper.py +1 -1
  119. mlrun/runtimes/funcdoc.py +0 -28
  120. mlrun/runtimes/kubejob.py +2 -1
  121. mlrun/runtimes/local.py +5 -2
  122. mlrun/runtimes/mpijob/__init__.py +0 -20
  123. mlrun/runtimes/mpijob/v1.py +1 -1
  124. mlrun/runtimes/nuclio/api_gateway.py +194 -84
  125. mlrun/runtimes/nuclio/application/application.py +170 -8
  126. mlrun/runtimes/nuclio/function.py +39 -49
  127. mlrun/runtimes/pod.py +16 -36
  128. mlrun/runtimes/remotesparkjob.py +9 -3
  129. mlrun/runtimes/sparkjob/spark3job.py +1 -1
  130. mlrun/runtimes/utils.py +6 -45
  131. mlrun/serving/server.py +2 -1
  132. mlrun/serving/v2_serving.py +5 -1
  133. mlrun/track/tracker.py +2 -1
  134. mlrun/utils/async_http.py +25 -5
  135. mlrun/utils/helpers.py +107 -75
  136. mlrun/utils/logger.py +39 -7
  137. mlrun/utils/notifications/notification/__init__.py +14 -9
  138. mlrun/utils/notifications/notification/base.py +1 -1
  139. mlrun/utils/notifications/notification/slack.py +34 -7
  140. mlrun/utils/notifications/notification/webhook.py +1 -1
  141. mlrun/utils/notifications/notification_pusher.py +147 -16
  142. mlrun/utils/regex.py +9 -0
  143. mlrun/utils/v3io_clients.py +0 -1
  144. mlrun/utils/version/version.json +2 -2
  145. {mlrun-1.7.0rc14.dist-info → mlrun-1.7.0rc21.dist-info}/METADATA +14 -6
  146. {mlrun-1.7.0rc14.dist-info → mlrun-1.7.0rc21.dist-info}/RECORD +150 -130
  147. mlrun/kfpops.py +0 -865
  148. mlrun/platforms/other.py +0 -305
  149. {mlrun-1.7.0rc14.dist-info → mlrun-1.7.0rc21.dist-info}/LICENSE +0 -0
  150. {mlrun-1.7.0rc14.dist-info → mlrun-1.7.0rc21.dist-info}/WHEEL +0 -0
  151. {mlrun-1.7.0rc14.dist-info → mlrun-1.7.0rc21.dist-info}/entry_points.txt +0 -0
  152. {mlrun-1.7.0rc14.dist-info → mlrun-1.7.0rc21.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,636 @@
1
+ # Copyright 2024 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
+ import typing
16
+ from datetime import datetime
17
+ from io import StringIO
18
+ from typing import Literal, Optional, Union
19
+
20
+ import pandas as pd
21
+ import v3io_frames.client
22
+ import v3io_frames.errors
23
+ from v3io_frames.frames_pb2 import IGNORE
24
+
25
+ import mlrun.common.model_monitoring
26
+ import mlrun.common.schemas.model_monitoring as mm_schemas
27
+ import mlrun.feature_store.steps
28
+ import mlrun.utils.v3io_clients
29
+ from mlrun.model_monitoring.db import TSDBConnector
30
+ from mlrun.model_monitoring.helpers import get_invocations_fqn
31
+ from mlrun.utils import logger
32
+
33
+ _TSDB_BE = "tsdb"
34
+ _TSDB_RATE = "1/s"
35
+ _CONTAINER = "users"
36
+
37
+
38
+ class V3IOTSDBConnector(TSDBConnector):
39
+ """
40
+ Handles the TSDB operations when the TSDB connector is of type V3IO. To manage these operations we use V3IO Frames
41
+ Client that provides API for executing commands on the V3IO TSDB table.
42
+ """
43
+
44
+ type: str = mm_schemas.TSDBTarget.V3IO_TSDB
45
+
46
+ def __init__(
47
+ self,
48
+ project: str,
49
+ container: str = _CONTAINER,
50
+ v3io_framesd: typing.Optional[str] = None,
51
+ create_table: bool = False,
52
+ ) -> None:
53
+ super().__init__(project=project)
54
+
55
+ self.container = container
56
+
57
+ self.v3io_framesd = v3io_framesd or mlrun.mlconf.v3io_framesd
58
+ self._frames_client: v3io_frames.client.ClientBase = (
59
+ self._get_v3io_frames_client(self.container)
60
+ )
61
+
62
+ self._init_tables_path()
63
+
64
+ if create_table:
65
+ self.create_tables()
66
+
67
+ def _init_tables_path(self):
68
+ self.tables = {}
69
+
70
+ events_table_full_path = mlrun.mlconf.get_model_monitoring_file_target_path(
71
+ project=self.project,
72
+ kind=mm_schemas.FileTargetKind.EVENTS,
73
+ )
74
+ (
75
+ _,
76
+ _,
77
+ events_path,
78
+ ) = mlrun.common.model_monitoring.helpers.parse_model_endpoint_store_prefix(
79
+ events_table_full_path
80
+ )
81
+ self.tables[mm_schemas.V3IOTSDBTables.EVENTS] = events_path
82
+
83
+ monitoring_application_full_path = (
84
+ mlrun.mlconf.get_model_monitoring_file_target_path(
85
+ project=self.project,
86
+ kind=mm_schemas.FileTargetKind.MONITORING_APPLICATION,
87
+ )
88
+ )
89
+ (
90
+ _,
91
+ _,
92
+ monitoring_application_path,
93
+ ) = mlrun.common.model_monitoring.helpers.parse_model_endpoint_store_prefix(
94
+ monitoring_application_full_path
95
+ )
96
+ self.tables[mm_schemas.V3IOTSDBTables.APP_RESULTS] = (
97
+ monitoring_application_path + mm_schemas.V3IOTSDBTables.APP_RESULTS
98
+ )
99
+ self.tables[mm_schemas.V3IOTSDBTables.METRICS] = (
100
+ monitoring_application_path + mm_schemas.V3IOTSDBTables.METRICS
101
+ )
102
+
103
+ monitoring_predictions_full_path = (
104
+ mlrun.mlconf.get_model_monitoring_file_target_path(
105
+ project=self.project,
106
+ kind=mm_schemas.FileTargetKind.PREDICTIONS,
107
+ )
108
+ )
109
+ (
110
+ _,
111
+ _,
112
+ monitoring_predictions_path,
113
+ ) = mlrun.common.model_monitoring.helpers.parse_model_endpoint_store_prefix(
114
+ monitoring_predictions_full_path
115
+ )
116
+ self.tables[mm_schemas.FileTargetKind.PREDICTIONS] = monitoring_predictions_path
117
+
118
+ def create_tables(self) -> None:
119
+ """
120
+ Create the tables using the TSDB connector. The tables are being created in the V3IO TSDB and include:
121
+ - app_results: a detailed result that includes status, kind, extra data, etc.
122
+ - metrics: a basic key value that represents a single numeric metric.
123
+ Note that the predictions table is automatically created by the model monitoring stream pod.
124
+ """
125
+ application_tables = [
126
+ mm_schemas.V3IOTSDBTables.APP_RESULTS,
127
+ mm_schemas.V3IOTSDBTables.METRICS,
128
+ ]
129
+ for table_name in application_tables:
130
+ logger.info("Creating table in V3IO TSDB", table_name=table_name)
131
+ table = self.tables[table_name]
132
+ self._frames_client.create(
133
+ backend=_TSDB_BE,
134
+ table=table,
135
+ if_exists=IGNORE,
136
+ rate=_TSDB_RATE,
137
+ )
138
+
139
+ def apply_monitoring_stream_steps(
140
+ self,
141
+ graph,
142
+ tsdb_batching_max_events: int = 10,
143
+ tsdb_batching_timeout_secs: int = 300,
144
+ ):
145
+ """
146
+ Apply TSDB steps on the provided monitoring graph. Throughout these steps, the graph stores live data of
147
+ different key metric dictionaries.This data is being used by the monitoring dashboards in
148
+ grafana. Results can be found under v3io:///users/pipelines/project-name/model-endpoints/events/.
149
+ In that case, we generate 3 different key metric dictionaries:
150
+ - base_metrics (average latency and predictions over time)
151
+ - endpoint_features (Prediction and feature names and values)
152
+ - custom_metrics (user-defined metrics)
153
+ """
154
+
155
+ # Write latency per prediction, labeled by endpoint ID only
156
+ graph.add_step(
157
+ "storey.TSDBTarget",
158
+ name="tsdb_predictions",
159
+ after="MapFeatureNames",
160
+ path=f"{self.container}/{self.tables[mm_schemas.FileTargetKind.PREDICTIONS]}",
161
+ rate="1/s",
162
+ time_col=mm_schemas.EventFieldType.TIMESTAMP,
163
+ container=self.container,
164
+ v3io_frames=self.v3io_framesd,
165
+ columns=["latency"],
166
+ index_cols=[
167
+ mm_schemas.EventFieldType.ENDPOINT_ID,
168
+ ],
169
+ aggr="count,avg",
170
+ aggr_granularity="1m",
171
+ max_events=tsdb_batching_max_events,
172
+ flush_after_seconds=tsdb_batching_timeout_secs,
173
+ key=mm_schemas.EventFieldType.ENDPOINT_ID,
174
+ )
175
+
176
+ # Before writing data to TSDB, create dictionary of 2-3 dictionaries that contains
177
+ # stats and details about the events
178
+
179
+ def apply_process_before_tsdb():
180
+ graph.add_step(
181
+ "mlrun.model_monitoring.db.tsdb.v3io.stream_graph_steps.ProcessBeforeTSDB",
182
+ name="ProcessBeforeTSDB",
183
+ after="sample",
184
+ )
185
+
186
+ apply_process_before_tsdb()
187
+
188
+ # Unpacked keys from each dictionary and write to TSDB target
189
+ def apply_filter_and_unpacked_keys(name, keys):
190
+ graph.add_step(
191
+ "mlrun.model_monitoring.db.tsdb.v3io.stream_graph_steps.FilterAndUnpackKeys",
192
+ name=name,
193
+ after="ProcessBeforeTSDB",
194
+ keys=[keys],
195
+ )
196
+
197
+ def apply_tsdb_target(name, after):
198
+ graph.add_step(
199
+ "storey.TSDBTarget",
200
+ name=name,
201
+ after=after,
202
+ path=f"{self.container}/{self.tables[mm_schemas.V3IOTSDBTables.EVENTS]}",
203
+ rate="10/m",
204
+ time_col=mm_schemas.EventFieldType.TIMESTAMP,
205
+ container=self.container,
206
+ v3io_frames=self.v3io_framesd,
207
+ infer_columns_from_data=True,
208
+ index_cols=[
209
+ mm_schemas.EventFieldType.ENDPOINT_ID,
210
+ mm_schemas.EventFieldType.RECORD_TYPE,
211
+ mm_schemas.EventFieldType.ENDPOINT_TYPE,
212
+ ],
213
+ max_events=tsdb_batching_max_events,
214
+ flush_after_seconds=tsdb_batching_timeout_secs,
215
+ key=mm_schemas.EventFieldType.ENDPOINT_ID,
216
+ )
217
+
218
+ # unpacked base_metrics dictionary
219
+ apply_filter_and_unpacked_keys(
220
+ name="FilterAndUnpackKeys1",
221
+ keys=mm_schemas.EventKeyMetrics.BASE_METRICS,
222
+ )
223
+ apply_tsdb_target(name="tsdb1", after="FilterAndUnpackKeys1")
224
+
225
+ # unpacked endpoint_features dictionary
226
+ apply_filter_and_unpacked_keys(
227
+ name="FilterAndUnpackKeys2",
228
+ keys=mm_schemas.EventKeyMetrics.ENDPOINT_FEATURES,
229
+ )
230
+ apply_tsdb_target(name="tsdb2", after="FilterAndUnpackKeys2")
231
+
232
+ # unpacked custom_metrics dictionary. In addition, use storey.Filter remove none values
233
+ apply_filter_and_unpacked_keys(
234
+ name="FilterAndUnpackKeys3",
235
+ keys=mm_schemas.EventKeyMetrics.CUSTOM_METRICS,
236
+ )
237
+
238
+ def apply_storey_filter():
239
+ graph.add_step(
240
+ "storey.Filter",
241
+ "FilterNotNone",
242
+ after="FilterAndUnpackKeys3",
243
+ _fn="(event is not None)",
244
+ )
245
+
246
+ apply_storey_filter()
247
+ apply_tsdb_target(name="tsdb3", after="FilterNotNone")
248
+
249
+ def write_application_event(
250
+ self,
251
+ event: dict,
252
+ kind: mm_schemas.WriterEventKind = mm_schemas.WriterEventKind.RESULT,
253
+ ) -> None:
254
+ """Write a single result or metric to TSDB"""
255
+
256
+ event[mm_schemas.WriterEvent.END_INFER_TIME] = datetime.fromisoformat(
257
+ event[mm_schemas.WriterEvent.END_INFER_TIME]
258
+ )
259
+ index_cols_base = [
260
+ mm_schemas.WriterEvent.END_INFER_TIME,
261
+ mm_schemas.WriterEvent.ENDPOINT_ID,
262
+ mm_schemas.WriterEvent.APPLICATION_NAME,
263
+ ]
264
+
265
+ if kind == mm_schemas.WriterEventKind.METRIC:
266
+ table = self.tables[mm_schemas.V3IOTSDBTables.METRICS]
267
+ index_cols = index_cols_base + [mm_schemas.MetricData.METRIC_NAME]
268
+ elif kind == mm_schemas.WriterEventKind.RESULT:
269
+ table = self.tables[mm_schemas.V3IOTSDBTables.APP_RESULTS]
270
+ index_cols = index_cols_base + [mm_schemas.ResultData.RESULT_NAME]
271
+ del event[mm_schemas.ResultData.RESULT_EXTRA_DATA]
272
+ else:
273
+ raise ValueError(f"Invalid {kind = }")
274
+
275
+ try:
276
+ self._frames_client.write(
277
+ backend=_TSDB_BE,
278
+ table=table,
279
+ dfs=pd.DataFrame.from_records([event]),
280
+ index_cols=index_cols,
281
+ )
282
+ logger.info("Updated V3IO TSDB successfully", table=table)
283
+ except v3io_frames.errors.Error as err:
284
+ logger.exception(
285
+ "Could not write drift measures to TSDB",
286
+ err=err,
287
+ table=table,
288
+ event=event,
289
+ )
290
+ raise mlrun.errors.MLRunRuntimeError(
291
+ f"Failed to write application result to TSDB: {err}"
292
+ )
293
+
294
+ def delete_tsdb_resources(self, table: typing.Optional[str] = None):
295
+ if table:
296
+ # Delete a specific table
297
+ tables = [table]
298
+ else:
299
+ # Delete all tables
300
+ tables = mm_schemas.V3IOTSDBTables.list()
301
+ for table_to_delete in tables:
302
+ try:
303
+ self._frames_client.delete(backend=_TSDB_BE, table=table_to_delete)
304
+ except v3io_frames.errors.DeleteError as e:
305
+ logger.warning(
306
+ f"Failed to delete TSDB table '{table}'",
307
+ err=mlrun.errors.err_to_str(e),
308
+ )
309
+
310
+ # Final cleanup of tsdb path
311
+ tsdb_path = self._get_v3io_source_directory()
312
+ tsdb_path.replace("://u", ":///u")
313
+ store, _, _ = mlrun.store_manager.get_or_create_store(tsdb_path)
314
+ store.rm(tsdb_path, recursive=True)
315
+
316
+ def get_model_endpoint_real_time_metrics(
317
+ self, endpoint_id: str, metrics: list[str], start: str, end: str
318
+ ) -> dict[str, list[tuple[str, float]]]:
319
+ """
320
+ Getting real time metrics from the TSDB. There are pre-defined metrics for model endpoints such as
321
+ `predictions_per_second` and `latency_avg_5m` but also custom metrics defined by the user. Note that these
322
+ metrics are being calculated by the model monitoring stream pod.
323
+ :param endpoint_id: The unique id of the model endpoint.
324
+ :param metrics: A list of real-time metrics to return for the model endpoint.
325
+ :param start: The start time of the metrics. Can be represented by a string containing an RFC 3339
326
+ time, a Unix timestamp in milliseconds, a relative time (`'now'` or
327
+ `'now-[0-9]+[mhd]'`, where `m` = minutes, `h` = hours, `'d'` = days, and
328
+ `'s'` = seconds), or 0 for the earliest time.
329
+ :param end: The end time of the metrics. Can be represented by a string containing an RFC 3339
330
+ time, a Unix timestamp in milliseconds, a relative time (`'now'` or
331
+ `'now-[0-9]+[mhd]'`, where `m` = minutes, `h` = hours, and `'d'` = days, and
332
+ `'s'` = seconds), or 0 for the earliest time.
333
+ :return: A dictionary of metrics in which the key is a metric name and the value is a list of tuples that
334
+ includes timestamps and the values.
335
+ """
336
+
337
+ if not metrics:
338
+ raise mlrun.errors.MLRunInvalidArgumentError(
339
+ "Metric names must be provided"
340
+ )
341
+
342
+ metrics_mapping = {}
343
+
344
+ try:
345
+ data = self._get_records(
346
+ table=mm_schemas.V3IOTSDBTables.EVENTS,
347
+ columns=["endpoint_id", *metrics],
348
+ filter_query=f"endpoint_id=='{endpoint_id}'",
349
+ start=start,
350
+ end=end,
351
+ )
352
+
353
+ # Fill the metrics mapping dictionary with the metric name and values
354
+ data_dict = data.to_dict()
355
+ for metric in metrics:
356
+ metric_data = data_dict.get(metric)
357
+ if metric_data is None:
358
+ continue
359
+
360
+ values = [
361
+ (str(timestamp), value) for timestamp, value in metric_data.items()
362
+ ]
363
+ metrics_mapping[metric] = values
364
+
365
+ except v3io_frames.errors.Error as err:
366
+ logger.warn("Failed to read tsdb", err=err, endpoint=endpoint_id)
367
+
368
+ return metrics_mapping
369
+
370
+ def _get_records(
371
+ self,
372
+ table: str,
373
+ start: Union[datetime, str],
374
+ end: Union[datetime, str],
375
+ columns: typing.Optional[list[str]] = None,
376
+ filter_query: str = "",
377
+ interval: typing.Optional[str] = None,
378
+ agg_funcs: typing.Optional[list] = None,
379
+ limit: typing.Optional[int] = None,
380
+ sliding_window_step: typing.Optional[str] = None,
381
+ **kwargs,
382
+ ) -> pd.DataFrame:
383
+ """
384
+ Getting records from V3IO TSDB data collection.
385
+ :param table: Path to the collection to query.
386
+ :param start: The start time of the metrics. Can be represented by a string containing an RFC
387
+ 3339 time, a Unix timestamp in milliseconds, a relative time (`'now'` or
388
+ `'now-[0-9]+[mhd]'`, where `m` = minutes, `h` = hours, `'d'` = days, and
389
+ `'s'` = seconds), or 0 for the earliest time.
390
+ :param end: The end time of the metrics. Can be represented by a string containing an RFC
391
+ 3339 time, a Unix timestamp in milliseconds, a relative time (`'now'` or
392
+ `'now-[0-9]+[mhd]'`, where `m` = minutes, `h` = hours, `'d'` = days, and
393
+ `'s'` = seconds), or 0 for the earliest time.
394
+ :param columns: Columns to include in the result.
395
+ :param filter_query: V3IO filter expression. The expected filter expression includes different
396
+ conditions, divided by ' AND '.
397
+ :param interval: The interval to aggregate the data by. Note that if interval is provided,
398
+ agg_funcs must bg provided as well. Provided as a string in the format of '1m',
399
+ '1h', etc.
400
+ :param agg_funcs: The aggregation functions to apply on the columns. Note that if `agg_funcs` is
401
+ provided, `interval` must bg provided as well. Provided as a list of strings in
402
+ the format of ['sum', 'avg', 'count', ...].
403
+ :param limit: The maximum number of records to return.
404
+ :param sliding_window_step: The time step for which the time window moves forward. Note that if
405
+ `sliding_window_step` is provided, interval must be provided as well. Provided
406
+ as a string in the format of '1m', '1h', etc.
407
+ :param kwargs: Additional keyword arguments passed to the read method of frames client.
408
+ :return: DataFrame with the provided attributes from the data collection.
409
+ :raise: MLRunNotFoundError if the provided table wasn't found.
410
+ """
411
+ if table not in self.tables:
412
+ raise mlrun.errors.MLRunNotFoundError(
413
+ f"Table '{table}' does not exist in the tables list of the TSDB connector. "
414
+ f"Available tables: {list(self.tables.keys())}"
415
+ )
416
+
417
+ if agg_funcs:
418
+ # Frames client expects the aggregators to be a comma-separated string
419
+ agg_funcs = ",".join(agg_funcs)
420
+ table_path = self.tables[table]
421
+ try:
422
+ df = self._frames_client.read(
423
+ backend=_TSDB_BE,
424
+ table=table_path,
425
+ start=start,
426
+ end=end,
427
+ columns=columns,
428
+ filter=filter_query,
429
+ aggregation_window=interval,
430
+ aggregators=agg_funcs,
431
+ step=sliding_window_step,
432
+ **kwargs,
433
+ )
434
+ except v3io_frames.ReadError as err:
435
+ if "No TSDB schema file found" in str(err):
436
+ return pd.DataFrame()
437
+ else:
438
+ raise err
439
+
440
+ if limit:
441
+ df = df.head(limit)
442
+ return df
443
+
444
+ def _get_v3io_source_directory(self) -> str:
445
+ """
446
+ Get the V3IO source directory for the current project. Usually the source directory will
447
+ be under 'v3io:///users/pipelines/<project>'
448
+
449
+ :return: The V3IO source directory for the current project.
450
+ """
451
+ events_table_full_path = mlrun.mlconf.get_model_monitoring_file_target_path(
452
+ project=self.project,
453
+ kind=mm_schemas.FileTargetKind.EVENTS,
454
+ )
455
+
456
+ # Generate the main directory with the V3IO resources
457
+ source_directory = (
458
+ mlrun.common.model_monitoring.helpers.parse_model_endpoint_project_prefix(
459
+ events_table_full_path, self.project
460
+ )
461
+ )
462
+
463
+ return source_directory
464
+
465
+ @staticmethod
466
+ def _get_v3io_frames_client(v3io_container: str) -> v3io_frames.client.ClientBase:
467
+ return mlrun.utils.v3io_clients.get_frames_client(
468
+ address=mlrun.mlconf.v3io_framesd,
469
+ container=v3io_container,
470
+ )
471
+
472
+ def read_metrics_data(
473
+ self,
474
+ *,
475
+ endpoint_id: str,
476
+ start: datetime,
477
+ end: datetime,
478
+ metrics: list[mm_schemas.ModelEndpointMonitoringMetric],
479
+ type: Literal["metrics", "results"] = "results",
480
+ ) -> Union[
481
+ list[
482
+ Union[
483
+ mm_schemas.ModelEndpointMonitoringResultValues,
484
+ mm_schemas.ModelEndpointMonitoringMetricNoData,
485
+ ],
486
+ ],
487
+ list[
488
+ Union[
489
+ mm_schemas.ModelEndpointMonitoringMetricValues,
490
+ mm_schemas.ModelEndpointMonitoringMetricNoData,
491
+ ],
492
+ ],
493
+ ]:
494
+ """
495
+ Read metrics OR results from the TSDB and return as a list.
496
+ Note: the type must match the actual metrics in the `metrics` parameter.
497
+ If the type is "results", pass only results in the `metrics` parameter.
498
+ """
499
+
500
+ if type == "metrics":
501
+ table_path = self.tables[mm_schemas.V3IOTSDBTables.METRICS]
502
+ name = mm_schemas.MetricData.METRIC_NAME
503
+ df_handler = self.df_to_metrics_values
504
+ elif type == "results":
505
+ table_path = self.tables[mm_schemas.V3IOTSDBTables.APP_RESULTS]
506
+ name = mm_schemas.ResultData.RESULT_NAME
507
+ df_handler = self.df_to_results_values
508
+ else:
509
+ raise ValueError(f"Invalid {type = }")
510
+
511
+ query = self._get_sql_query(
512
+ endpoint_id,
513
+ [(metric.app, metric.name) for metric in metrics],
514
+ table_path=table_path,
515
+ name=name,
516
+ )
517
+
518
+ logger.debug("Querying V3IO TSDB", query=query)
519
+
520
+ df: pd.DataFrame = self._frames_client.read(
521
+ backend=_TSDB_BE,
522
+ start=start,
523
+ end=end,
524
+ query=query, # the filter argument does not work for this complex condition
525
+ )
526
+
527
+ logger.debug(
528
+ "Converting a DataFrame to a list of metrics or results values",
529
+ table=table_path,
530
+ project=self.project,
531
+ endpoint_id=endpoint_id,
532
+ is_empty=df.empty,
533
+ )
534
+
535
+ return df_handler(df=df, metrics=metrics, project=self.project)
536
+
537
+ @staticmethod
538
+ def _get_sql_query(
539
+ endpoint_id: str,
540
+ names: list[tuple[str, str]],
541
+ table_path: str,
542
+ name: str = mm_schemas.ResultData.RESULT_NAME,
543
+ ) -> str:
544
+ """Get the SQL query for the results/metrics table"""
545
+ with StringIO() as query:
546
+ query.write(
547
+ f"SELECT * FROM '{table_path}' "
548
+ f"WHERE {mm_schemas.WriterEvent.ENDPOINT_ID}='{endpoint_id}'"
549
+ )
550
+ if names:
551
+ query.write(" AND (")
552
+
553
+ for i, (app_name, result_name) in enumerate(names):
554
+ sub_cond = (
555
+ f"({mm_schemas.WriterEvent.APPLICATION_NAME}='{app_name}' "
556
+ f"AND {name}='{result_name}')"
557
+ )
558
+ if i != 0: # not first sub condition
559
+ query.write(" OR ")
560
+ query.write(sub_cond)
561
+
562
+ query.write(")")
563
+
564
+ query.write(";")
565
+ return query.getvalue()
566
+
567
+ def read_predictions(
568
+ self,
569
+ *,
570
+ endpoint_id: str,
571
+ start: Union[datetime, str],
572
+ end: Union[datetime, str],
573
+ aggregation_window: Optional[str] = None,
574
+ agg_funcs: Optional[list[str]] = None,
575
+ limit: Optional[int] = None,
576
+ ) -> Union[
577
+ mm_schemas.ModelEndpointMonitoringMetricNoData,
578
+ mm_schemas.ModelEndpointMonitoringMetricValues,
579
+ ]:
580
+ if (agg_funcs and not aggregation_window) or (
581
+ aggregation_window and not agg_funcs
582
+ ):
583
+ raise mlrun.errors.MLRunInvalidArgumentError(
584
+ "both or neither of `aggregation_window` and `agg_funcs` must be provided"
585
+ )
586
+ df = self._get_records(
587
+ table=mm_schemas.FileTargetKind.PREDICTIONS,
588
+ start=start,
589
+ end=end,
590
+ columns=[mm_schemas.EventFieldType.LATENCY],
591
+ filter_query=f"endpoint_id=='{endpoint_id}'",
592
+ interval=aggregation_window,
593
+ agg_funcs=agg_funcs,
594
+ limit=limit,
595
+ sliding_window_step=aggregation_window,
596
+ )
597
+
598
+ full_name = get_invocations_fqn(self.project)
599
+
600
+ if df.empty:
601
+ return mm_schemas.ModelEndpointMonitoringMetricNoData(
602
+ full_name=full_name,
603
+ type=mm_schemas.ModelEndpointMonitoringMetricType.METRIC,
604
+ )
605
+
606
+ latency_column = (
607
+ f"{agg_funcs[0]}({mm_schemas.EventFieldType.LATENCY})"
608
+ if agg_funcs
609
+ else mm_schemas.EventFieldType.LATENCY
610
+ )
611
+
612
+ return mm_schemas.ModelEndpointMonitoringMetricValues(
613
+ full_name=full_name,
614
+ values=list(
615
+ zip(
616
+ df.index,
617
+ df[latency_column],
618
+ )
619
+ ), # pyright: ignore[reportArgumentType]
620
+ )
621
+
622
+ def read_prediction_metric_for_endpoint_if_exists(
623
+ self, endpoint_id: str
624
+ ) -> Optional[mm_schemas.ModelEndpointMonitoringMetric]:
625
+ # Read just one record, because we just want to check if there is any data for this endpoint_id
626
+ predictions = self.read_predictions(
627
+ endpoint_id=endpoint_id, start="0", end="now", limit=1
628
+ )
629
+ if predictions.data:
630
+ return mm_schemas.ModelEndpointMonitoringMetric(
631
+ project=self.project,
632
+ app=mm_schemas.SpecialApps.MLRUN_INFRA,
633
+ type=mm_schemas.ModelEndpointMonitoringMetricType.METRIC,
634
+ name=mm_schemas.PredictionsQueryConstants.INVOCATIONS,
635
+ full_name=get_invocations_fqn(self.project),
636
+ )