dt-extensions-sdk 1.1.22__py3-none-any.whl → 1.1.24__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.
Files changed (31) hide show
  1. {dt_extensions_sdk-1.1.22.dist-info → dt_extensions_sdk-1.1.24.dist-info}/METADATA +2 -2
  2. dt_extensions_sdk-1.1.24.dist-info/RECORD +33 -0
  3. {dt_extensions_sdk-1.1.22.dist-info → dt_extensions_sdk-1.1.24.dist-info}/WHEEL +1 -1
  4. {dt_extensions_sdk-1.1.22.dist-info → dt_extensions_sdk-1.1.24.dist-info}/licenses/LICENSE.txt +9 -9
  5. dynatrace_extension/__about__.py +5 -5
  6. dynatrace_extension/__init__.py +27 -27
  7. dynatrace_extension/cli/__init__.py +5 -5
  8. dynatrace_extension/cli/create/__init__.py +1 -1
  9. dynatrace_extension/cli/create/create.py +76 -76
  10. dynatrace_extension/cli/create/extension_template/.gitignore.template +160 -160
  11. dynatrace_extension/cli/create/extension_template/README.md.template +33 -33
  12. dynatrace_extension/cli/create/extension_template/activation.json.template +15 -15
  13. dynatrace_extension/cli/create/extension_template/extension/activationSchema.json.template +118 -118
  14. dynatrace_extension/cli/create/extension_template/extension/extension.yaml.template +17 -17
  15. dynatrace_extension/cli/create/extension_template/extension_name/__main__.py.template +43 -43
  16. dynatrace_extension/cli/create/extension_template/setup.py.template +28 -28
  17. dynatrace_extension/cli/main.py +428 -428
  18. dynatrace_extension/cli/schema.py +129 -129
  19. dynatrace_extension/sdk/__init__.py +3 -3
  20. dynatrace_extension/sdk/activation.py +43 -43
  21. dynatrace_extension/sdk/callback.py +134 -134
  22. dynatrace_extension/sdk/communication.py +482 -482
  23. dynatrace_extension/sdk/event.py +19 -19
  24. dynatrace_extension/sdk/extension.py +1045 -1045
  25. dynatrace_extension/sdk/helper.py +191 -191
  26. dynatrace_extension/sdk/metric.py +118 -118
  27. dynatrace_extension/sdk/runtime.py +67 -67
  28. dynatrace_extension/sdk/vendor/mureq/LICENSE +13 -13
  29. dynatrace_extension/sdk/vendor/mureq/mureq.py +447 -447
  30. dt_extensions_sdk-1.1.22.dist-info/RECORD +0 -33
  31. {dt_extensions_sdk-1.1.22.dist-info → dt_extensions_sdk-1.1.24.dist-info}/entry_points.txt +0 -0
@@ -1,129 +1,129 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- from pathlib import Path
5
-
6
- import yaml
7
-
8
-
9
- class ExtensionYaml:
10
- def __init__(self, yaml_file: Path):
11
- self._file = yaml_file
12
- self._data = yaml.safe_load(yaml_file.read_text())
13
-
14
- @property
15
- def name(self) -> str:
16
- return self._data.get("name", "")
17
-
18
- @property
19
- def version(self) -> str:
20
- return self._data.get("version", "")
21
-
22
- @property
23
- def min_dynatrace_version(self) -> str:
24
- return self._data.get("minDynatraceVersion", "")
25
-
26
- @property
27
- def author(self) -> Author:
28
- return Author(self._data.get("author", {}))
29
-
30
- @property
31
- def python(self) -> Python:
32
- return Python(self._data.get("python", {}))
33
-
34
- def validate(self):
35
- """
36
- Checks that the files under 'python.activation' exist and are valid json files
37
- """
38
- if self.python.activation.remote and self.python.activation.remote.path:
39
- self._validate_json_file(self.python.activation.remote.path)
40
-
41
- if self.python.activation.local and self.python.activation.local.path:
42
- self._validate_json_file(self.python.activation.local.path)
43
-
44
- def _validate_json_file(self, raw_path: str):
45
- path = Path(Path(self._file).parent / raw_path)
46
- if not path.exists():
47
- msg = f"Extension yaml validation failed, file {path} does not exist"
48
- raise ValueError(msg)
49
-
50
- # Parse the file to make sure it is valid json
51
- with path.open() as f:
52
- json.load(f)
53
-
54
- def zip_file_name(self) -> str:
55
- return f"{self.name.replace(':', '_')}-{self.version}.zip"
56
-
57
-
58
- class Python:
59
- def __init__(self, data: dict):
60
- self._data = data
61
-
62
- @property
63
- def runtime(self) -> Runtime:
64
- return Runtime(self._data.get("runtime", {}))
65
-
66
- @property
67
- def activation(self):
68
- return Activation(self._data.get("activation", {}))
69
-
70
-
71
- class Runtime:
72
- def __init__(self, data: dict):
73
- self._data = data
74
-
75
- @property
76
- def module(self) -> str:
77
- return self._data.get("module", "datasourcepy")
78
-
79
- @property
80
- def version(self) -> Version:
81
- return Version(self._data.get("version", {}))
82
-
83
-
84
- class Version:
85
- def __init__(self, data: dict):
86
- self._data = data
87
-
88
- @property
89
- def min_version(self) -> str:
90
- return self._data.get("min", "")
91
-
92
- @property
93
- def max_version(self) -> str:
94
- return self._data.get("max", "")
95
-
96
-
97
- class Activation:
98
- def __init__(self, data: dict):
99
- self._data = data
100
-
101
- @property
102
- def remote(self) -> ActivationInstance | None:
103
- if data := self._data.get("remote"):
104
- return ActivationInstance(data)
105
- return None
106
-
107
- @property
108
- def local(self) -> ActivationInstance | None:
109
- if data := self._data.get("local"):
110
- return ActivationInstance(data)
111
- return None
112
-
113
-
114
- class ActivationInstance:
115
- def __init__(self, data: dict):
116
- self._data = data
117
-
118
- @property
119
- def path(self) -> str:
120
- return self._data.get("path", "")
121
-
122
-
123
- class Author:
124
- def __init__(self, _data: dict):
125
- self._data = _data
126
-
127
- @property
128
- def name(self) -> str:
129
- return self._data.get("name", "")
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import yaml
7
+
8
+
9
+ class ExtensionYaml:
10
+ def __init__(self, yaml_file: Path):
11
+ self._file = yaml_file
12
+ self._data = yaml.safe_load(yaml_file.read_text())
13
+
14
+ @property
15
+ def name(self) -> str:
16
+ return self._data.get("name", "")
17
+
18
+ @property
19
+ def version(self) -> str:
20
+ return self._data.get("version", "")
21
+
22
+ @property
23
+ def min_dynatrace_version(self) -> str:
24
+ return self._data.get("minDynatraceVersion", "")
25
+
26
+ @property
27
+ def author(self) -> Author:
28
+ return Author(self._data.get("author", {}))
29
+
30
+ @property
31
+ def python(self) -> Python:
32
+ return Python(self._data.get("python", {}))
33
+
34
+ def validate(self):
35
+ """
36
+ Checks that the files under 'python.activation' exist and are valid json files
37
+ """
38
+ if self.python.activation.remote and self.python.activation.remote.path:
39
+ self._validate_json_file(self.python.activation.remote.path)
40
+
41
+ if self.python.activation.local and self.python.activation.local.path:
42
+ self._validate_json_file(self.python.activation.local.path)
43
+
44
+ def _validate_json_file(self, raw_path: str):
45
+ path = Path(Path(self._file).parent / raw_path)
46
+ if not path.exists():
47
+ msg = f"Extension yaml validation failed, file {path} does not exist"
48
+ raise ValueError(msg)
49
+
50
+ # Parse the file to make sure it is valid json
51
+ with path.open() as f:
52
+ json.load(f)
53
+
54
+ def zip_file_name(self) -> str:
55
+ return f"{self.name.replace(':', '_')}-{self.version}.zip"
56
+
57
+
58
+ class Python:
59
+ def __init__(self, data: dict):
60
+ self._data = data
61
+
62
+ @property
63
+ def runtime(self) -> Runtime:
64
+ return Runtime(self._data.get("runtime", {}))
65
+
66
+ @property
67
+ def activation(self):
68
+ return Activation(self._data.get("activation", {}))
69
+
70
+
71
+ class Runtime:
72
+ def __init__(self, data: dict):
73
+ self._data = data
74
+
75
+ @property
76
+ def module(self) -> str:
77
+ return self._data.get("module", "datasourcepy")
78
+
79
+ @property
80
+ def version(self) -> Version:
81
+ return Version(self._data.get("version", {}))
82
+
83
+
84
+ class Version:
85
+ def __init__(self, data: dict):
86
+ self._data = data
87
+
88
+ @property
89
+ def min_version(self) -> str:
90
+ return self._data.get("min", "")
91
+
92
+ @property
93
+ def max_version(self) -> str:
94
+ return self._data.get("max", "")
95
+
96
+
97
+ class Activation:
98
+ def __init__(self, data: dict):
99
+ self._data = data
100
+
101
+ @property
102
+ def remote(self) -> ActivationInstance | None:
103
+ if data := self._data.get("remote"):
104
+ return ActivationInstance(data)
105
+ return None
106
+
107
+ @property
108
+ def local(self) -> ActivationInstance | None:
109
+ if data := self._data.get("local"):
110
+ return ActivationInstance(data)
111
+ return None
112
+
113
+
114
+ class ActivationInstance:
115
+ def __init__(self, data: dict):
116
+ self._data = data
117
+
118
+ @property
119
+ def path(self) -> str:
120
+ return self._data.get("path", "")
121
+
122
+
123
+ class Author:
124
+ def __init__(self, _data: dict):
125
+ self._data = _data
126
+
127
+ @property
128
+ def name(self) -> str:
129
+ return self._data.get("name", "")
@@ -1,3 +1,3 @@
1
- # SPDX-FileCopyrightText: 2023-present Dynatrace LLC
2
- #
3
- # SPDX-License-Identifier: MIT
1
+ # SPDX-FileCopyrightText: 2023-present Dynatrace LLC
2
+ #
3
+ # SPDX-License-Identifier: MIT
@@ -1,43 +1,43 @@
1
- # SPDX-FileCopyrightText: 2023-present Dynatrace LLC
2
- #
3
- # SPDX-License-Identifier: MIT
4
-
5
- from enum import Enum
6
- from typing import List
7
-
8
-
9
- class ActivationType(Enum):
10
- REMOTE = "remote"
11
- LOCAL = "local"
12
-
13
-
14
- class ActivationConfig(dict):
15
- def __init__(self, activation_context_json: dict):
16
- self._activation_context_json = activation_context_json
17
- self.version: str = self._activation_context_json.get("version", "")
18
- self.enabled: bool = self._activation_context_json.get("enabled", True)
19
- self.description: str = self._activation_context_json.get("description", "")
20
- self.feature_sets: List[str] = self._activation_context_json.get("featureSets", [])
21
- self.type: ActivationType = ActivationType.REMOTE if self.remote else ActivationType.LOCAL
22
- super().__init__()
23
-
24
- @property
25
- def config(self) -> dict:
26
- return self.remote if self.remote else self.local
27
-
28
- @property
29
- def remote(self) -> dict:
30
- return self._activation_context_json.get("pythonRemote", {})
31
-
32
- @property
33
- def local(self) -> dict:
34
- return self._activation_context_json.get("pythonLocal", {})
35
-
36
- def __getitem__(self, item):
37
- return self.config[item]
38
-
39
- def get(self, key, default=None):
40
- return self.config.get(key, default)
41
-
42
- def __repr__(self):
43
- return f"ActivationConfig(version='{self.version}', enabled={self.enabled}, description='{self.description}', type={self.type}, config={self.config})"
1
+ # SPDX-FileCopyrightText: 2023-present Dynatrace LLC
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ from enum import Enum
6
+ from typing import List
7
+
8
+
9
+ class ActivationType(Enum):
10
+ REMOTE = "remote"
11
+ LOCAL = "local"
12
+
13
+
14
+ class ActivationConfig(dict):
15
+ def __init__(self, activation_context_json: dict):
16
+ self._activation_context_json = activation_context_json
17
+ self.version: str = self._activation_context_json.get("version", "")
18
+ self.enabled: bool = self._activation_context_json.get("enabled", True)
19
+ self.description: str = self._activation_context_json.get("description", "")
20
+ self.feature_sets: List[str] = self._activation_context_json.get("featureSets", [])
21
+ self.type: ActivationType = ActivationType.REMOTE if self.remote else ActivationType.LOCAL
22
+ super().__init__()
23
+
24
+ @property
25
+ def config(self) -> dict:
26
+ return self.remote if self.remote else self.local
27
+
28
+ @property
29
+ def remote(self) -> dict:
30
+ return self._activation_context_json.get("pythonRemote", {})
31
+
32
+ @property
33
+ def local(self) -> dict:
34
+ return self._activation_context_json.get("pythonLocal", {})
35
+
36
+ def __getitem__(self, item):
37
+ return self.config[item]
38
+
39
+ def get(self, key, default=None):
40
+ return self.config.get(key, default)
41
+
42
+ def __repr__(self):
43
+ return f"ActivationConfig(version='{self.version}', enabled={self.enabled}, description='{self.description}', type={self.type}, config={self.config})"
@@ -1,134 +1,134 @@
1
- # SPDX-FileCopyrightText: 2023-present Dynatrace LLC
2
- #
3
- # SPDX-License-Identifier: MIT
4
-
5
- import logging
6
- import random
7
- from datetime import datetime, timedelta
8
- from timeit import default_timer as timer
9
- from typing import Callable, Dict, Optional, Tuple
10
-
11
- from .activation import ActivationType
12
- from .communication import Status, StatusValue
13
-
14
-
15
- class WrappedCallback:
16
- def __init__(
17
- self,
18
- interval: timedelta,
19
- callback: Callable,
20
- logger: logging.Logger,
21
- args: Optional[Tuple] = None,
22
- kwargs: Optional[Dict] = None,
23
- running_in_sim=False,
24
- activation_type: Optional[ActivationType] = None,
25
- ):
26
- self.callback: Callable = callback
27
- if args is None:
28
- args = ()
29
- self.callback_args = args
30
- if kwargs is None:
31
- kwargs = {}
32
- self.callback_kwargs = kwargs
33
- self.interval: timedelta = interval
34
- self.logger = logger
35
- self.running: bool = False
36
- self.status = Status(StatusValue.OK)
37
- self.executions_total = 0 # global counter
38
- self.executions_per_interval = 0 # counter per interval = 1 min by default
39
- self.duration = 0 # global counter
40
- self.duration_interval_total = 0 # counter per interval = 1 min by default
41
- self.cluster_time_diff = 0
42
- self.start_timestamp = self.get_current_time_with_cluster_diff()
43
- self.running_in_sim = running_in_sim
44
- self.activation_type = activation_type
45
- self.ok_count = 0 # counter per interval = 1 min by default
46
- self.timeouts_count = 0 # counter per interval = 1 min by default
47
- self.exception_count = 0 # counter per interval = 1 min by default
48
-
49
- def get_current_time_with_cluster_diff(self):
50
- return datetime.now() + timedelta(milliseconds=self.cluster_time_diff)
51
-
52
- def __call__(self):
53
- self.logger.debug(f"Running scheduled callback {self}")
54
- self.start_timestamp = self.get_current_time_with_cluster_diff()
55
- self.running = True
56
- self.executions_total += 1
57
- self.executions_per_interval += 1
58
- self.next_run = datetime.now() + self.interval
59
- start_time = timer()
60
- failed = False
61
- try:
62
- self.callback(*self.callback_args, **self.callback_kwargs)
63
- self.status = Status(StatusValue.OK)
64
- except Exception as e:
65
- failed = True
66
- self.logger.exception(f"Error running callback {self}: {e!r}")
67
- self.status = Status(StatusValue.GENERIC_ERROR, repr(e))
68
- self.exception_count += 1
69
-
70
- self.running = False
71
- self.duration = timer() - start_time
72
- self.duration_interval_total += self.duration
73
- self.logger.debug(f"Ran scheduled callback {self} in {self.duration:.2f} seconds")
74
- if self.duration > self.interval.total_seconds():
75
- message = f"Callback {self} took {self.duration:.4f} seconds to execute, which is longer than the interval of {self.interval.total_seconds()}s"
76
- self.logger.warning(message)
77
- self.status = Status(StatusValue.GENERIC_ERROR, message)
78
- self.timeouts_count += 1
79
- elif not failed:
80
- self.ok_count += 1
81
-
82
- def __repr__(self):
83
- return f"Method={self.callback.__name__}"
84
-
85
- def name(self):
86
- return self.callback.__name__
87
-
88
- def initial_wait_time(self) -> float:
89
- if not self.running_in_sim:
90
- """
91
- Here we chose a random second between 1 and 59 to start the callback
92
- This is to distribute load for extension running on this host
93
- When running from the simulator, this is not done
94
- """
95
-
96
- now = self.get_current_time_with_cluster_diff()
97
- random_second = random.randint(1, 59) # noqa: S311
98
- next_execution = datetime.now().replace(second=random_second, microsecond=0)
99
- if next_execution <= now:
100
- # The random chosen second already passed this minute
101
- next_execution += timedelta(minutes=1)
102
- wait_time = (next_execution - now).total_seconds()
103
- self.logger.debug(f"Randomly choosing next execution time for callback {self} to be {next_execution}")
104
- return wait_time
105
- return 0
106
-
107
- def get_adjusted_metric_timestamp(self) -> datetime:
108
- """
109
- Callbacks can't run all together, so they must start at random times
110
- This means that when reporting metrics for a callback, we need to consider
111
- the time the callback was started, instead of the current timestamp
112
- this is done to avoid situations like:
113
- - 14:00:55 - callback A runs
114
- - 14:01:03 - 8 seconds later, a metric is reported
115
- - 14:01:55 - callback A runs again (60 seconds after the first run)
116
- - 14:01:58 - 3 seconds later a metric is reported
117
- In this scenario a metric is reported twice in the same minute
118
- This can also cause minutes where a metric is not reported at all, creating gaps
119
-
120
- Here we calculate the metric timestamp based on the start timestamp of the callback
121
- If the callback started in the last minute, we use the callback start timestamp
122
- between 60 seconds and 120 seconds, we use the callback timestamp + 1 minute
123
- between 120 seconds and 180 seconds, we use the callback timestamp + 2 minutes, and so forth
124
- """
125
- now = self.get_current_time_with_cluster_diff()
126
- minutes_since_start = int((now - self.start_timestamp).total_seconds() / 60)
127
- return self.start_timestamp + timedelta(minutes=minutes_since_start)
128
-
129
- def clear_sfm_metrics(self):
130
- self.ok_count = 0
131
- self.timeouts_count = 0
132
- self.duration_interval_total = 0
133
- self.exception_count = 0
134
- self.executions_per_interval = 0
1
+ # SPDX-FileCopyrightText: 2023-present Dynatrace LLC
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ import logging
6
+ import random
7
+ from datetime import datetime, timedelta
8
+ from timeit import default_timer as timer
9
+ from typing import Callable, Dict, Optional, Tuple
10
+
11
+ from .activation import ActivationType
12
+ from .communication import Status, StatusValue
13
+
14
+
15
+ class WrappedCallback:
16
+ def __init__(
17
+ self,
18
+ interval: timedelta,
19
+ callback: Callable,
20
+ logger: logging.Logger,
21
+ args: Optional[Tuple] = None,
22
+ kwargs: Optional[Dict] = None,
23
+ running_in_sim=False,
24
+ activation_type: Optional[ActivationType] = None,
25
+ ):
26
+ self.callback: Callable = callback
27
+ if args is None:
28
+ args = ()
29
+ self.callback_args = args
30
+ if kwargs is None:
31
+ kwargs = {}
32
+ self.callback_kwargs = kwargs
33
+ self.interval: timedelta = interval
34
+ self.logger = logger
35
+ self.running: bool = False
36
+ self.status = Status(StatusValue.OK)
37
+ self.executions_total = 0 # global counter
38
+ self.executions_per_interval = 0 # counter per interval = 1 min by default
39
+ self.duration = 0 # global counter
40
+ self.duration_interval_total = 0 # counter per interval = 1 min by default
41
+ self.cluster_time_diff = 0
42
+ self.start_timestamp = self.get_current_time_with_cluster_diff()
43
+ self.running_in_sim = running_in_sim
44
+ self.activation_type = activation_type
45
+ self.ok_count = 0 # counter per interval = 1 min by default
46
+ self.timeouts_count = 0 # counter per interval = 1 min by default
47
+ self.exception_count = 0 # counter per interval = 1 min by default
48
+
49
+ def get_current_time_with_cluster_diff(self):
50
+ return datetime.now() + timedelta(milliseconds=self.cluster_time_diff)
51
+
52
+ def __call__(self):
53
+ self.logger.debug(f"Running scheduled callback {self}")
54
+ self.start_timestamp = self.get_current_time_with_cluster_diff()
55
+ self.running = True
56
+ self.executions_total += 1
57
+ self.executions_per_interval += 1
58
+ self.next_run = datetime.now() + self.interval
59
+ start_time = timer()
60
+ failed = False
61
+ try:
62
+ self.callback(*self.callback_args, **self.callback_kwargs)
63
+ self.status = Status(StatusValue.OK)
64
+ except Exception as e:
65
+ failed = True
66
+ self.logger.exception(f"Error running callback {self}: {e!r}")
67
+ self.status = Status(StatusValue.GENERIC_ERROR, repr(e))
68
+ self.exception_count += 1
69
+
70
+ self.running = False
71
+ self.duration = timer() - start_time
72
+ self.duration_interval_total += self.duration
73
+ self.logger.debug(f"Ran scheduled callback {self} in {self.duration:.2f} seconds")
74
+ if self.duration > self.interval.total_seconds():
75
+ message = f"Callback {self} took {self.duration:.4f} seconds to execute, which is longer than the interval of {self.interval.total_seconds()}s"
76
+ self.logger.warning(message)
77
+ self.status = Status(StatusValue.GENERIC_ERROR, message)
78
+ self.timeouts_count += 1
79
+ elif not failed:
80
+ self.ok_count += 1
81
+
82
+ def __repr__(self):
83
+ return f"Method={self.callback.__name__}"
84
+
85
+ def name(self):
86
+ return self.callback.__name__
87
+
88
+ def initial_wait_time(self) -> float:
89
+ if not self.running_in_sim:
90
+ """
91
+ Here we chose a random second between 1 and 59 to start the callback
92
+ This is to distribute load for extension running on this host
93
+ When running from the simulator, this is not done
94
+ """
95
+
96
+ now = self.get_current_time_with_cluster_diff()
97
+ random_second = random.randint(1, 59) # noqa: S311
98
+ next_execution = datetime.now().replace(second=random_second, microsecond=0)
99
+ if next_execution <= now:
100
+ # The random chosen second already passed this minute
101
+ next_execution += timedelta(minutes=1)
102
+ wait_time = (next_execution - now).total_seconds()
103
+ self.logger.debug(f"Randomly choosing next execution time for callback {self} to be {next_execution}")
104
+ return wait_time
105
+ return 0
106
+
107
+ def get_adjusted_metric_timestamp(self) -> datetime:
108
+ """
109
+ Callbacks can't run all together, so they must start at random times
110
+ This means that when reporting metrics for a callback, we need to consider
111
+ the time the callback was started, instead of the current timestamp
112
+ this is done to avoid situations like:
113
+ - 14:00:55 - callback A runs
114
+ - 14:01:03 - 8 seconds later, a metric is reported
115
+ - 14:01:55 - callback A runs again (60 seconds after the first run)
116
+ - 14:01:58 - 3 seconds later a metric is reported
117
+ In this scenario a metric is reported twice in the same minute
118
+ This can also cause minutes where a metric is not reported at all, creating gaps
119
+
120
+ Here we calculate the metric timestamp based on the start timestamp of the callback
121
+ If the callback started in the last minute, we use the callback start timestamp
122
+ between 60 seconds and 120 seconds, we use the callback timestamp + 1 minute
123
+ between 120 seconds and 180 seconds, we use the callback timestamp + 2 minutes, and so forth
124
+ """
125
+ now = self.get_current_time_with_cluster_diff()
126
+ minutes_since_start = int((now - self.start_timestamp).total_seconds() / 60)
127
+ return self.start_timestamp + timedelta(minutes=minutes_since_start)
128
+
129
+ def clear_sfm_metrics(self):
130
+ self.ok_count = 0
131
+ self.timeouts_count = 0
132
+ self.duration_interval_total = 0
133
+ self.exception_count = 0
134
+ self.executions_per_interval = 0