conductor-python 1.3.11__py3-none-any.whl → 1.4.0__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.
- conductor/client/__init__.py +2 -1
- conductor/client/automator/async_task_runner.py +21 -2
- conductor/client/automator/task_handler.py +21 -10
- conductor/client/automator/task_runner.py +21 -2
- conductor/client/configuration/settings/metrics_settings.py +94 -2
- conductor/client/http/api/workflow_resource_api.py +6 -2
- conductor/client/http/api_client.py +67 -24
- conductor/client/http/async_api_client.py +41 -16
- conductor/client/http/async_rest.py +4 -1
- conductor/client/http/rest.py +36 -25
- conductor/client/orkes/orkes_base_client.py +9 -2
- conductor/client/orkes/orkes_workflow_client.py +48 -5
- conductor/client/orkes_clients.py +14 -3
- conductor/client/telemetry/canonical_metrics_collector.py +242 -0
- conductor/client/telemetry/legacy_metrics_collector.py +324 -0
- conductor/client/telemetry/metrics_collector.py +29 -918
- conductor/client/telemetry/metrics_collector_base.py +504 -0
- conductor/client/telemetry/metrics_factory.py +62 -0
- conductor/client/telemetry/model/metric_documentation.py +12 -1
- conductor/client/telemetry/model/metric_label.py +1 -0
- conductor/client/telemetry/model/metric_name.py +9 -0
- conductor/client/workflow/executor/workflow_executor.py +22 -6
- {conductor_python-1.3.11.dist-info → conductor_python-1.4.0.dist-info}/METADATA +42 -9
- {conductor_python-1.3.11.dist-info → conductor_python-1.4.0.dist-info}/RECORD +25 -21
- {conductor_python-1.3.11.dist-info → conductor_python-1.4.0.dist-info}/WHEEL +1 -1
conductor/client/__init__.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from conductor.client.configuration.configuration import Configuration
|
|
4
4
|
from conductor.client.automator.task_handler import TaskHandler
|
|
5
5
|
from conductor.client.automator.task_runner import TaskRunner
|
|
6
|
-
from conductor.client.orkes_clients import OrkesClients
|
|
6
|
+
from conductor.client.orkes_clients import OrkesClients, ConductorClients
|
|
7
7
|
from conductor.client.workflow.conductor_workflow import ConductorWorkflow
|
|
8
8
|
from conductor.client.workflow.executor.workflow_executor import WorkflowExecutor
|
|
9
9
|
from conductor.client.worker.worker_task import worker_task
|
|
@@ -18,6 +18,7 @@ __all__ = [
|
|
|
18
18
|
"TaskHandler",
|
|
19
19
|
"TaskRunner",
|
|
20
20
|
"OrkesClients",
|
|
21
|
+
"ConductorClients",
|
|
21
22
|
"ConductorWorkflow",
|
|
22
23
|
"WorkflowExecutor",
|
|
23
24
|
"worker_task",
|
|
@@ -27,7 +27,7 @@ from conductor.client.http.models.schema_def import SchemaDef, SchemaType
|
|
|
27
27
|
from conductor.client.http.rest import AuthorizationException, ApiException
|
|
28
28
|
from conductor.client.orkes.orkes_metadata_client import OrkesMetadataClient
|
|
29
29
|
from conductor.client.orkes.orkes_schema_client import OrkesSchemaClient
|
|
30
|
-
from conductor.client.telemetry.
|
|
30
|
+
from conductor.client.telemetry.metrics_factory import create_metrics_collector
|
|
31
31
|
from conductor.client.worker.worker_interface import WorkerInterface
|
|
32
32
|
from conductor.client.worker.worker_config import resolve_worker_config, get_worker_config_oneline
|
|
33
33
|
from conductor.client.worker.exception import NonRetryableException
|
|
@@ -88,7 +88,7 @@ class AsyncTaskRunner:
|
|
|
88
88
|
|
|
89
89
|
self.metrics_collector = None
|
|
90
90
|
if metrics_settings is not None:
|
|
91
|
-
self.metrics_collector =
|
|
91
|
+
self.metrics_collector = create_metrics_collector(
|
|
92
92
|
metrics_settings
|
|
93
93
|
)
|
|
94
94
|
# Register metrics collector as event listener
|
|
@@ -863,6 +863,7 @@ class AsyncTaskRunner:
|
|
|
863
863
|
if attempt > 0:
|
|
864
864
|
# Exponential backoff: [10s, 20s, 30s] before retry
|
|
865
865
|
await asyncio.sleep(attempt * 10)
|
|
866
|
+
update_start = time.time()
|
|
866
867
|
try:
|
|
867
868
|
if self._use_update_v2:
|
|
868
869
|
next_task = await self.async_task_client.update_task_v2(body=task_result)
|
|
@@ -873,6 +874,10 @@ class AsyncTaskRunner:
|
|
|
873
874
|
task_definition_name,
|
|
874
875
|
next_task.task_id if next_task else None
|
|
875
876
|
)
|
|
877
|
+
if self.metrics_collector is not None:
|
|
878
|
+
self.metrics_collector.record_task_update_time(
|
|
879
|
+
task_definition_name, time.time() - update_start, status="SUCCESS"
|
|
880
|
+
)
|
|
876
881
|
return next_task
|
|
877
882
|
else:
|
|
878
883
|
await self.async_task_client.update_task(body=task_result)
|
|
@@ -882,6 +887,10 @@ class AsyncTaskRunner:
|
|
|
882
887
|
task_result.workflow_instance_id,
|
|
883
888
|
task_definition_name,
|
|
884
889
|
)
|
|
890
|
+
if self.metrics_collector is not None:
|
|
891
|
+
self.metrics_collector.record_task_update_time(
|
|
892
|
+
task_definition_name, time.time() - update_start, status="SUCCESS"
|
|
893
|
+
)
|
|
885
894
|
return None
|
|
886
895
|
except ApiException as e:
|
|
887
896
|
if e.status in (404, 405) and self._use_update_v2:
|
|
@@ -895,12 +904,19 @@ class AsyncTaskRunner:
|
|
|
895
904
|
# Retry immediately with v1
|
|
896
905
|
try:
|
|
897
906
|
await self.async_task_client.update_task(body=task_result)
|
|
907
|
+
if self.metrics_collector is not None:
|
|
908
|
+
self.metrics_collector.record_task_update_time(
|
|
909
|
+
task_definition_name, time.time() - update_start, status="SUCCESS"
|
|
910
|
+
)
|
|
898
911
|
return None
|
|
899
912
|
except Exception as fallback_e:
|
|
900
913
|
last_exception = fallback_e
|
|
901
914
|
continue
|
|
902
915
|
last_exception = e
|
|
903
916
|
if self.metrics_collector is not None:
|
|
917
|
+
self.metrics_collector.record_task_update_time(
|
|
918
|
+
task_definition_name, time.time() - update_start, status="FAILURE"
|
|
919
|
+
)
|
|
904
920
|
self.metrics_collector.increment_task_update_error(
|
|
905
921
|
task_definition_name, type(e)
|
|
906
922
|
)
|
|
@@ -917,6 +933,9 @@ class AsyncTaskRunner:
|
|
|
917
933
|
except Exception as e:
|
|
918
934
|
last_exception = e
|
|
919
935
|
if self.metrics_collector is not None:
|
|
936
|
+
self.metrics_collector.record_task_update_time(
|
|
937
|
+
task_definition_name, time.time() - update_start, status="FAILURE"
|
|
938
|
+
)
|
|
920
939
|
self.metrics_collector.increment_task_update_error(
|
|
921
940
|
task_definition_name, type(e)
|
|
922
941
|
)
|
|
@@ -18,7 +18,7 @@ from conductor.client.configuration.settings.metrics_settings import MetricsSett
|
|
|
18
18
|
from conductor.client.event.task_runner_events import TaskRunnerEvent
|
|
19
19
|
from conductor.client.event.sync_event_dispatcher import SyncEventDispatcher
|
|
20
20
|
from conductor.client.event.sync_listener_register import register_task_runner_listener
|
|
21
|
-
from conductor.client.telemetry.
|
|
21
|
+
from conductor.client.telemetry.metrics_collector_base import MetricsCollectorBase
|
|
22
22
|
from conductor.client.telemetry.model.metric_documentation import MetricDocumentation
|
|
23
23
|
from conductor.client.telemetry.model.metric_label import MetricLabel
|
|
24
24
|
from conductor.client.telemetry.model.metric_name import MetricName
|
|
@@ -33,13 +33,24 @@ logger = logging.getLogger(
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
_decorated_functions = {}
|
|
36
|
+
_VALID_MP_START_METHODS = {"spawn", "fork", "forkserver"}
|
|
36
37
|
_mp_fork_set = False
|
|
37
38
|
if not _mp_fork_set:
|
|
38
39
|
try:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
# The prometheus_client library holds a module-level threading lock;
|
|
41
|
+
# forking while that lock is held causes a deadlock in child processes.
|
|
42
|
+
# Set CONDUCTOR_MP_START_METHOD=spawn to avoid this if you hit the
|
|
43
|
+
# deadlock. Default is fork for backward compatibility (spawn requires
|
|
44
|
+
# all Process arguments to be picklable).
|
|
45
|
+
_default_method = "spawn" if platform == "win32" else "fork"
|
|
46
|
+
_method = os.environ.get("CONDUCTOR_MP_START_METHOD", "").strip().lower() or _default_method
|
|
47
|
+
if _method not in _VALID_MP_START_METHODS:
|
|
48
|
+
logger.warning(
|
|
49
|
+
"Ignoring invalid CONDUCTOR_MP_START_METHOD=%r; falling back to %r",
|
|
50
|
+
_method, _default_method,
|
|
51
|
+
)
|
|
52
|
+
_method = _default_method
|
|
53
|
+
set_start_method(_method)
|
|
43
54
|
_mp_fork_set = True
|
|
44
55
|
except Exception as e:
|
|
45
56
|
logger.info("error when setting multiprocessing.set_start_method - maybe the context is set %s", e.args)
|
|
@@ -221,11 +232,11 @@ class TaskHandler:
|
|
|
221
232
|
self._configuration = configuration
|
|
222
233
|
self._metrics_settings = metrics_settings
|
|
223
234
|
|
|
224
|
-
# Set
|
|
225
|
-
#
|
|
235
|
+
# Set PROMETHEUS_MULTIPROC_DIR BEFORE any worker processes start.
|
|
236
|
+
# MetricsSettings resolves the subdirectory eagerly at construction.
|
|
226
237
|
if metrics_settings is not None:
|
|
227
|
-
os.environ["PROMETHEUS_MULTIPROC_DIR"] = metrics_settings.
|
|
228
|
-
logger.info(f"Set PROMETHEUS_MULTIPROC_DIR={metrics_settings.
|
|
238
|
+
os.environ["PROMETHEUS_MULTIPROC_DIR"] = metrics_settings.metrics_directory
|
|
239
|
+
logger.info(f"Set PROMETHEUS_MULTIPROC_DIR={metrics_settings.metrics_directory}")
|
|
229
240
|
|
|
230
241
|
# Store event listeners to pass to each worker process
|
|
231
242
|
self.event_listeners = event_listeners or []
|
|
@@ -345,7 +356,7 @@ class TaskHandler:
|
|
|
345
356
|
self.metrics_provider_process = None
|
|
346
357
|
return
|
|
347
358
|
self.metrics_provider_process = Process(
|
|
348
|
-
target=
|
|
359
|
+
target=MetricsCollectorBase.provide_metrics,
|
|
349
360
|
args=(metrics_settings,)
|
|
350
361
|
)
|
|
351
362
|
logger.info("Created MetricsProvider process")
|
|
@@ -30,7 +30,7 @@ from conductor.client.http.models.schema_def import SchemaDef, SchemaType
|
|
|
30
30
|
from conductor.client.http.rest import AuthorizationException, ApiException
|
|
31
31
|
from conductor.client.orkes.orkes_metadata_client import OrkesMetadataClient
|
|
32
32
|
from conductor.client.orkes.orkes_schema_client import OrkesSchemaClient
|
|
33
|
-
from conductor.client.telemetry.
|
|
33
|
+
from conductor.client.telemetry.metrics_factory import create_metrics_collector
|
|
34
34
|
from conductor.client.worker.worker import ASYNC_TASK_RUNNING
|
|
35
35
|
from conductor.client.worker.worker_interface import WorkerInterface
|
|
36
36
|
from conductor.client.worker.worker_config import resolve_worker_config, get_worker_config_oneline
|
|
@@ -69,7 +69,7 @@ class TaskRunner:
|
|
|
69
69
|
|
|
70
70
|
self.metrics_collector = None
|
|
71
71
|
if metrics_settings is not None:
|
|
72
|
-
self.metrics_collector =
|
|
72
|
+
self.metrics_collector = create_metrics_collector(
|
|
73
73
|
metrics_settings
|
|
74
74
|
)
|
|
75
75
|
# Register metrics collector as event listener
|
|
@@ -972,6 +972,7 @@ class TaskRunner:
|
|
|
972
972
|
if attempt > 0:
|
|
973
973
|
# Exponential backoff: [10s, 20s, 30s] before retry
|
|
974
974
|
time.sleep(attempt * 10)
|
|
975
|
+
update_start = time.time()
|
|
975
976
|
try:
|
|
976
977
|
if self._use_update_v2:
|
|
977
978
|
next_task = self.task_client.update_task_v2(body=task_result)
|
|
@@ -982,6 +983,10 @@ class TaskRunner:
|
|
|
982
983
|
task_definition_name,
|
|
983
984
|
next_task.task_id if next_task else None
|
|
984
985
|
)
|
|
986
|
+
if self.metrics_collector is not None:
|
|
987
|
+
self.metrics_collector.record_task_update_time(
|
|
988
|
+
task_definition_name, time.time() - update_start, status="SUCCESS"
|
|
989
|
+
)
|
|
985
990
|
return next_task
|
|
986
991
|
else:
|
|
987
992
|
self.task_client.update_task(body=task_result)
|
|
@@ -991,6 +996,10 @@ class TaskRunner:
|
|
|
991
996
|
task_result.workflow_instance_id,
|
|
992
997
|
task_definition_name,
|
|
993
998
|
)
|
|
999
|
+
if self.metrics_collector is not None:
|
|
1000
|
+
self.metrics_collector.record_task_update_time(
|
|
1001
|
+
task_definition_name, time.time() - update_start, status="SUCCESS"
|
|
1002
|
+
)
|
|
994
1003
|
return None
|
|
995
1004
|
except ApiException as e:
|
|
996
1005
|
if e.status in (404, 405) and self._use_update_v2:
|
|
@@ -1004,12 +1013,19 @@ class TaskRunner:
|
|
|
1004
1013
|
# Retry immediately with v1
|
|
1005
1014
|
try:
|
|
1006
1015
|
self.task_client.update_task(body=task_result)
|
|
1016
|
+
if self.metrics_collector is not None:
|
|
1017
|
+
self.metrics_collector.record_task_update_time(
|
|
1018
|
+
task_definition_name, time.time() - update_start, status="SUCCESS"
|
|
1019
|
+
)
|
|
1007
1020
|
return None
|
|
1008
1021
|
except Exception as fallback_e:
|
|
1009
1022
|
last_exception = fallback_e
|
|
1010
1023
|
continue
|
|
1011
1024
|
last_exception = e
|
|
1012
1025
|
if self.metrics_collector is not None:
|
|
1026
|
+
self.metrics_collector.record_task_update_time(
|
|
1027
|
+
task_definition_name, time.time() - update_start, status="FAILURE"
|
|
1028
|
+
)
|
|
1013
1029
|
self.metrics_collector.increment_task_update_error(
|
|
1014
1030
|
task_definition_name, type(e)
|
|
1015
1031
|
)
|
|
@@ -1044,6 +1060,9 @@ class TaskRunner:
|
|
|
1044
1060
|
except Exception as e:
|
|
1045
1061
|
last_exception = e
|
|
1046
1062
|
if self.metrics_collector is not None:
|
|
1063
|
+
self.metrics_collector.record_task_update_time(
|
|
1064
|
+
task_definition_name, time.time() - update_start, status="FAILURE"
|
|
1065
|
+
)
|
|
1047
1066
|
self.metrics_collector.increment_task_update_error(
|
|
1048
1067
|
task_definition_name, type(e)
|
|
1049
1068
|
)
|
|
@@ -14,6 +14,16 @@ logger = logging.getLogger(
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
CANONICAL_SUBDIR = "canonical"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _env_bool(name: str, default: bool) -> bool:
|
|
21
|
+
value = os.environ.get(name, "")
|
|
22
|
+
if not value:
|
|
23
|
+
return default
|
|
24
|
+
return value.strip().lower() in ("true", "1", "yes")
|
|
25
|
+
|
|
26
|
+
|
|
17
27
|
def get_default_temporary_folder() -> str:
|
|
18
28
|
return f"{Path.home()!s}/tmp/"
|
|
19
29
|
|
|
@@ -24,12 +34,23 @@ class MetricsSettings:
|
|
|
24
34
|
directory: Optional[str] = None,
|
|
25
35
|
file_name: str = "metrics.log",
|
|
26
36
|
update_interval: float = 0.1,
|
|
27
|
-
http_port: Optional[int] = None
|
|
37
|
+
http_port: Optional[int] = None,
|
|
38
|
+
clean_directory: bool = False,
|
|
39
|
+
clean_dead_pids: bool = False):
|
|
28
40
|
"""
|
|
29
41
|
Configure metrics collection settings.
|
|
30
42
|
|
|
43
|
+
The ``WORKER_CANONICAL_METRICS`` env var is read at construction time
|
|
44
|
+
to decide whether ``.db`` files go in *directory* (legacy) or in a
|
|
45
|
+
``canonical/`` subdirectory (canonical mode). Set the env var before
|
|
46
|
+
creating this object.
|
|
47
|
+
|
|
31
48
|
Args:
|
|
32
|
-
directory:
|
|
49
|
+
directory: Base directory for storing multiprocess metrics .db files.
|
|
50
|
+
Legacy metrics use this directory directly (unchanged from
|
|
51
|
+
prior releases). Canonical metrics use a ``canonical/``
|
|
52
|
+
subdirectory so that switching implementations never
|
|
53
|
+
produces stale metric names.
|
|
33
54
|
file_name: Name of the metrics output file (only used when http_port is None)
|
|
34
55
|
update_interval: How often to update metrics (in seconds)
|
|
35
56
|
http_port: Optional HTTP port to expose metrics endpoint for Prometheus scraping.
|
|
@@ -40,6 +61,13 @@ class MetricsSettings:
|
|
|
40
61
|
If None:
|
|
41
62
|
- Metrics will be written to file at {directory}/{file_name}
|
|
42
63
|
- No HTTP server will be started
|
|
64
|
+
clean_directory: If True, remove all prometheus_client .db files from
|
|
65
|
+
the metrics directory when the collector is created. Only
|
|
66
|
+
safe when no other live process shares the same directory.
|
|
67
|
+
Defaults to False.
|
|
68
|
+
clean_dead_pids: If True, remove .db files whose owning PID no
|
|
69
|
+
longer exists. Safer than ``clean_directory`` in shared
|
|
70
|
+
environments. Defaults to False.
|
|
43
71
|
"""
|
|
44
72
|
if directory is None:
|
|
45
73
|
directory = get_default_temporary_folder()
|
|
@@ -47,6 +75,30 @@ class MetricsSettings:
|
|
|
47
75
|
self.file_name = file_name
|
|
48
76
|
self.update_interval = update_interval
|
|
49
77
|
self.http_port = http_port
|
|
78
|
+
self.clean_directory = clean_directory
|
|
79
|
+
self.clean_dead_pids = clean_dead_pids
|
|
80
|
+
self._subdir: str = (
|
|
81
|
+
CANONICAL_SUBDIR
|
|
82
|
+
if _env_bool("WORKER_CANONICAL_METRICS", False)
|
|
83
|
+
else ""
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def is_canonical(self) -> bool:
|
|
88
|
+
return self._subdir == CANONICAL_SUBDIR
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def metrics_directory(self) -> str:
|
|
92
|
+
"""Full path where .db files live (base directory + optional subdir).
|
|
93
|
+
|
|
94
|
+
Legacy leaves the subdirectory empty (base directory unchanged from
|
|
95
|
+
prior releases); canonical sets it to ``"canonical"`` to avoid stale
|
|
96
|
+
metric-name collisions. Resolved eagerly at construction time from
|
|
97
|
+
the ``WORKER_CANONICAL_METRICS`` env var.
|
|
98
|
+
"""
|
|
99
|
+
if self._subdir:
|
|
100
|
+
return os.path.join(self.directory, self._subdir)
|
|
101
|
+
return self.directory
|
|
50
102
|
|
|
51
103
|
def __set_dir(self, dir: str) -> None:
|
|
52
104
|
if not os.path.isdir(dir):
|
|
@@ -57,3 +109,43 @@ class MetricsSettings:
|
|
|
57
109
|
"Failed to create metrics temporary folder, reason: %s", e)
|
|
58
110
|
|
|
59
111
|
self.directory = dir
|
|
112
|
+
|
|
113
|
+
def _clean_stale_db_files(self) -> None:
|
|
114
|
+
"""Remove all prometheus_client multiprocess .db files."""
|
|
115
|
+
import glob
|
|
116
|
+
pattern = os.path.join(self.metrics_directory, "*.db")
|
|
117
|
+
for path in glob.glob(pattern):
|
|
118
|
+
try:
|
|
119
|
+
os.remove(path)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.debug("Could not remove stale metrics db file %s: %s", path, e)
|
|
122
|
+
|
|
123
|
+
def _clean_dead_pid_files(self) -> None:
|
|
124
|
+
"""Remove .db files whose owning PID no longer exists."""
|
|
125
|
+
import glob
|
|
126
|
+
import re
|
|
127
|
+
pattern = os.path.join(self.metrics_directory, "*.db")
|
|
128
|
+
for path in glob.glob(pattern):
|
|
129
|
+
match = re.search(r'_(\d+)\.db$', os.path.basename(path))
|
|
130
|
+
if not match:
|
|
131
|
+
continue
|
|
132
|
+
pid = int(match.group(1))
|
|
133
|
+
try:
|
|
134
|
+
os.kill(pid, 0)
|
|
135
|
+
except ProcessLookupError:
|
|
136
|
+
# ESRCH: no process owns this PID -> safe to remove its db file.
|
|
137
|
+
try:
|
|
138
|
+
os.remove(path)
|
|
139
|
+
logger.debug("Removed dead-pid metrics file %s (pid %d)", path, pid)
|
|
140
|
+
except OSError as e:
|
|
141
|
+
logger.debug("Could not remove dead-pid metrics db file %s: %s", path, e)
|
|
142
|
+
except OSError as e:
|
|
143
|
+
# Any non-ESRCH probe failure (commonly EPERM: the process is
|
|
144
|
+
# alive but owned by another user) -> keep the file; deleting it
|
|
145
|
+
# could corrupt a live worker's metrics in a shared directory.
|
|
146
|
+
logger.debug(
|
|
147
|
+
"Keeping metrics db file %s; pid %d probe returned a "
|
|
148
|
+
"non-ProcessLookupError OSError (process likely alive): %s",
|
|
149
|
+
path, pid, e,
|
|
150
|
+
)
|
|
151
|
+
continue
|
|
@@ -2329,6 +2329,7 @@ class WorkflowResourceApi(object):
|
|
|
2329
2329
|
all_params.append('_return_http_data_only')
|
|
2330
2330
|
all_params.append('_preload_content')
|
|
2331
2331
|
all_params.append('_request_timeout')
|
|
2332
|
+
all_params.append('metric_context')
|
|
2332
2333
|
|
|
2333
2334
|
params = locals()
|
|
2334
2335
|
for key, val in six.iteritems(params['kwargs']):
|
|
@@ -2383,7 +2384,8 @@ class WorkflowResourceApi(object):
|
|
|
2383
2384
|
_return_http_data_only=params.get('_return_http_data_only'),
|
|
2384
2385
|
_preload_content=params.get('_preload_content', True),
|
|
2385
2386
|
_request_timeout=params.get('_request_timeout'),
|
|
2386
|
-
collection_formats=collection_formats
|
|
2387
|
+
collection_formats=collection_formats,
|
|
2388
|
+
metric_context=params.get('metric_context'))
|
|
2387
2389
|
|
|
2388
2390
|
def start_workflow1(self, body, name, **kwargs): # noqa: E501
|
|
2389
2391
|
"""Start a new workflow. Returns the ID of the workflow instance that can be later used for tracking # noqa: E501
|
|
@@ -2434,6 +2436,7 @@ class WorkflowResourceApi(object):
|
|
|
2434
2436
|
all_params.append('_return_http_data_only')
|
|
2435
2437
|
all_params.append('_preload_content')
|
|
2436
2438
|
all_params.append('_request_timeout')
|
|
2439
|
+
all_params.append('metric_context')
|
|
2437
2440
|
|
|
2438
2441
|
params = locals()
|
|
2439
2442
|
for key, val in six.iteritems(params['kwargs']):
|
|
@@ -2500,7 +2503,8 @@ class WorkflowResourceApi(object):
|
|
|
2500
2503
|
_return_http_data_only=params.get('_return_http_data_only'),
|
|
2501
2504
|
_preload_content=params.get('_preload_content', True),
|
|
2502
2505
|
_request_timeout=params.get('_request_timeout'),
|
|
2503
|
-
collection_formats=collection_formats
|
|
2506
|
+
collection_formats=collection_formats,
|
|
2507
|
+
metric_context=params.get('metric_context'))
|
|
2504
2508
|
|
|
2505
2509
|
def terminate1(self, workflow_id, **kwargs): # noqa: E501
|
|
2506
2510
|
"""
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import base64
|
|
2
4
|
import datetime
|
|
3
5
|
import logging
|
|
@@ -6,7 +8,7 @@ import os
|
|
|
6
8
|
import re
|
|
7
9
|
import tempfile
|
|
8
10
|
import time
|
|
9
|
-
from typing import Dict
|
|
11
|
+
from typing import Dict, Optional, TYPE_CHECKING
|
|
10
12
|
import uuid
|
|
11
13
|
|
|
12
14
|
import six
|
|
@@ -20,13 +22,15 @@ from conductor.client.http import rest
|
|
|
20
22
|
from conductor.client.http.rest import AuthorizationException
|
|
21
23
|
from conductor.client.http.thread import AwaitableThread
|
|
22
24
|
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from conductor.client.telemetry.metrics_collector_base import MetricsCollectorBase
|
|
27
|
+
|
|
23
28
|
logger = logging.getLogger(
|
|
24
29
|
Configuration.get_logging_formatted_name(
|
|
25
30
|
__name__
|
|
26
31
|
)
|
|
27
32
|
)
|
|
28
33
|
|
|
29
|
-
|
|
30
34
|
class ApiClient(object):
|
|
31
35
|
PRIMITIVE_TYPES = (float, bool, bytes, six.text_type) + six.integer_types
|
|
32
36
|
NATIVE_TYPES_MAPPING = {
|
|
@@ -46,7 +50,7 @@ class ApiClient(object):
|
|
|
46
50
|
header_name=None,
|
|
47
51
|
header_value=None,
|
|
48
52
|
cookie=None,
|
|
49
|
-
metrics_collector=None
|
|
53
|
+
metrics_collector: Optional[MetricsCollectorBase] = None
|
|
50
54
|
):
|
|
51
55
|
if configuration is None:
|
|
52
56
|
configuration = Configuration()
|
|
@@ -75,14 +79,15 @@ class ApiClient(object):
|
|
|
75
79
|
query_params=None, header_params=None, body=None, post_params=None,
|
|
76
80
|
files=None, response_type=None, auth_settings=None,
|
|
77
81
|
_return_http_data_only=None, collection_formats=None,
|
|
78
|
-
_preload_content=True, _request_timeout=None):
|
|
82
|
+
_preload_content=True, _request_timeout=None, metric_context=None):
|
|
79
83
|
try:
|
|
80
84
|
return self.__call_api_no_retry(
|
|
81
85
|
resource_path=resource_path, method=method, path_params=path_params,
|
|
82
86
|
query_params=query_params, header_params=header_params, body=body, post_params=post_params,
|
|
83
87
|
files=files, response_type=response_type, auth_settings=auth_settings,
|
|
84
88
|
_return_http_data_only=_return_http_data_only, collection_formats=collection_formats,
|
|
85
|
-
_preload_content=_preload_content, _request_timeout=_request_timeout
|
|
89
|
+
_preload_content=_preload_content, _request_timeout=_request_timeout,
|
|
90
|
+
metric_context=metric_context
|
|
86
91
|
)
|
|
87
92
|
except AuthorizationException as ae:
|
|
88
93
|
if ae.token_expired or ae.invalid_token:
|
|
@@ -99,7 +104,8 @@ class ApiClient(object):
|
|
|
99
104
|
query_params=query_params, header_params=header_params, body=body, post_params=post_params,
|
|
100
105
|
files=files, response_type=response_type, auth_settings=auth_settings,
|
|
101
106
|
_return_http_data_only=_return_http_data_only, collection_formats=collection_formats,
|
|
102
|
-
_preload_content=_preload_content, _request_timeout=_request_timeout
|
|
107
|
+
_preload_content=_preload_content, _request_timeout=_request_timeout,
|
|
108
|
+
metric_context=metric_context
|
|
103
109
|
)
|
|
104
110
|
else:
|
|
105
111
|
logger.error('Failed to renew authentication token. Please check your credentials.')
|
|
@@ -110,7 +116,7 @@ class ApiClient(object):
|
|
|
110
116
|
query_params=None, header_params=None, body=None, post_params=None,
|
|
111
117
|
files=None, response_type=None, auth_settings=None,
|
|
112
118
|
_return_http_data_only=None, collection_formats=None,
|
|
113
|
-
_preload_content=True, _request_timeout=None):
|
|
119
|
+
_preload_content=True, _request_timeout=None, metric_context=None):
|
|
114
120
|
|
|
115
121
|
config = self.configuration
|
|
116
122
|
|
|
@@ -124,6 +130,9 @@ class ApiClient(object):
|
|
|
124
130
|
header_params = dict(self.parameters_to_tuples(header_params,
|
|
125
131
|
collection_formats))
|
|
126
132
|
|
|
133
|
+
# Save the URI template before path param substitution for metrics
|
|
134
|
+
metric_uri = resource_path
|
|
135
|
+
|
|
127
136
|
# path parameters
|
|
128
137
|
if path_params:
|
|
129
138
|
path_params = self.sanitize_for_serialization(path_params)
|
|
@@ -171,7 +180,9 @@ class ApiClient(object):
|
|
|
171
180
|
method, url, query_params=query_params, headers=header_params,
|
|
172
181
|
post_params=post_params, body=body,
|
|
173
182
|
_preload_content=_preload_content,
|
|
174
|
-
_request_timeout=_request_timeout
|
|
183
|
+
_request_timeout=_request_timeout,
|
|
184
|
+
metric_uri=metric_uri,
|
|
185
|
+
metric_context=metric_context)
|
|
175
186
|
|
|
176
187
|
return_data = response_data
|
|
177
188
|
if _preload_content:
|
|
@@ -326,7 +337,7 @@ class ApiClient(object):
|
|
|
326
337
|
body=None, post_params=None, files=None,
|
|
327
338
|
response_type=None, auth_settings=None, async_req=None,
|
|
328
339
|
_return_http_data_only=None, collection_formats=None,
|
|
329
|
-
_preload_content=True, _request_timeout=None):
|
|
340
|
+
_preload_content=True, _request_timeout=None, metric_context=None):
|
|
330
341
|
"""Makes the HTTP request (synchronous) and returns deserialized data.
|
|
331
342
|
|
|
332
343
|
To make an async request, set the async_req parameter.
|
|
@@ -369,7 +380,8 @@ class ApiClient(object):
|
|
|
369
380
|
body, post_params, files,
|
|
370
381
|
response_type, auth_settings,
|
|
371
382
|
_return_http_data_only, collection_formats,
|
|
372
|
-
_preload_content, _request_timeout
|
|
383
|
+
_preload_content, _request_timeout,
|
|
384
|
+
metric_context)
|
|
373
385
|
thread = AwaitableThread(
|
|
374
386
|
target=self.__call_api,
|
|
375
387
|
args=(
|
|
@@ -378,15 +390,36 @@ class ApiClient(object):
|
|
|
378
390
|
body, post_params, files,
|
|
379
391
|
response_type, auth_settings,
|
|
380
392
|
_return_http_data_only, collection_formats,
|
|
381
|
-
_preload_content, _request_timeout
|
|
393
|
+
_preload_content, _request_timeout,
|
|
394
|
+
metric_context
|
|
382
395
|
)
|
|
383
396
|
)
|
|
384
397
|
thread.start()
|
|
385
398
|
return thread
|
|
386
399
|
|
|
400
|
+
def _safe_record_api_request_time(self, method, uri, status, time_spent,
|
|
401
|
+
metric_uri):
|
|
402
|
+
"""Record the api-request metric without ever raising.
|
|
403
|
+
|
|
404
|
+
Metrics are best-effort instrumentation; a failure here must not
|
|
405
|
+
propagate and take down the actual API call.
|
|
406
|
+
"""
|
|
407
|
+
if self.metrics_collector is None:
|
|
408
|
+
return
|
|
409
|
+
try:
|
|
410
|
+
self.metrics_collector.record_api_request_time(
|
|
411
|
+
method=method,
|
|
412
|
+
uri=uri,
|
|
413
|
+
status=status,
|
|
414
|
+
time_spent=time_spent,
|
|
415
|
+
metric_uri=metric_uri,
|
|
416
|
+
)
|
|
417
|
+
except Exception as e:
|
|
418
|
+
logger.debug(f'Failed to record api request metric (ignored): {e}')
|
|
419
|
+
|
|
387
420
|
def request(self, method, url, query_params=None, headers=None,
|
|
388
421
|
post_params=None, body=None, _preload_content=True,
|
|
389
|
-
_request_timeout=None):
|
|
422
|
+
_request_timeout=None, metric_uri=None, metric_context=None):
|
|
390
423
|
"""Makes the HTTP request using RESTClient."""
|
|
391
424
|
# Extract URI path from URL (remove query params and domain)
|
|
392
425
|
try:
|
|
@@ -461,16 +494,28 @@ class ApiClient(object):
|
|
|
461
494
|
# Extract status code from response
|
|
462
495
|
status_code = str(response.status) if hasattr(response, 'status') else "200"
|
|
463
496
|
|
|
464
|
-
# Record metrics
|
|
497
|
+
# Record metrics. A metrics failure must never turn a successful
|
|
498
|
+
# request into a failed one, so the emit is isolated.
|
|
465
499
|
if self.metrics_collector is not None:
|
|
466
500
|
elapsed_time = time.time() - start_time
|
|
467
|
-
self.
|
|
468
|
-
method
|
|
469
|
-
uri=uri,
|
|
470
|
-
status=status_code,
|
|
471
|
-
time_spent=elapsed_time
|
|
501
|
+
self._safe_record_api_request_time(
|
|
502
|
+
method, uri, status_code, elapsed_time, metric_uri,
|
|
472
503
|
)
|
|
473
504
|
|
|
505
|
+
# Record workflow input payload size when the caller supplies
|
|
506
|
+
# workflow metadata (only the start-workflow paths do). The size
|
|
507
|
+
# was computed once in rest.py and lives on the response.
|
|
508
|
+
if metric_context is not None:
|
|
509
|
+
try:
|
|
510
|
+
size = getattr(response, 'request_body_size', 0)
|
|
511
|
+
self.metrics_collector.record_workflow_input_payload_size(
|
|
512
|
+
metric_context["workflow_name"],
|
|
513
|
+
metric_context["version"],
|
|
514
|
+
size,
|
|
515
|
+
)
|
|
516
|
+
except Exception:
|
|
517
|
+
pass
|
|
518
|
+
|
|
474
519
|
return response
|
|
475
520
|
|
|
476
521
|
except Exception as e:
|
|
@@ -482,14 +527,12 @@ class ApiClient(object):
|
|
|
482
527
|
else:
|
|
483
528
|
status_code = "error"
|
|
484
529
|
|
|
485
|
-
# Record metrics for failed requests
|
|
530
|
+
# Record metrics for failed requests. Guard the emit so a metrics
|
|
531
|
+
# error can't mask the original request exception below.
|
|
486
532
|
if self.metrics_collector is not None:
|
|
487
533
|
elapsed_time = time.time() - start_time
|
|
488
|
-
self.
|
|
489
|
-
method
|
|
490
|
-
uri=uri,
|
|
491
|
-
status=status_code,
|
|
492
|
-
time_spent=elapsed_time
|
|
534
|
+
self._safe_record_api_request_time(
|
|
535
|
+
method, uri, status_code, elapsed_time, metric_uri,
|
|
493
536
|
)
|
|
494
537
|
|
|
495
538
|
# Re-raise the exception
|