dt-extensions-sdk 1.1.9__py3-none-any.whl → 1.1.10__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.9.dist-info → dt_extensions_sdk-1.1.10.dist-info}/METADATA +2 -2
  2. dt_extensions_sdk-1.1.10.dist-info/RECORD +33 -0
  3. {dt_extensions_sdk-1.1.9.dist-info → dt_extensions_sdk-1.1.10.dist-info}/WHEEL +1 -1
  4. {dt_extensions_sdk-1.1.9.dist-info → dt_extensions_sdk-1.1.10.dist-info}/licenses/LICENSE.txt +9 -9
  5. dynatrace_extension/__about__.py +4 -4
  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 +16 -16
  15. dynatrace_extension/cli/create/extension_template/extension_name/__main__.py.template +43 -43
  16. dynatrace_extension/cli/create/extension_template/setup.py.template +12 -12
  17. dynatrace_extension/cli/main.py +414 -422
  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 +141 -141
  22. dynatrace_extension/sdk/communication.py +454 -446
  23. dynatrace_extension/sdk/event.py +19 -19
  24. dynatrace_extension/sdk/extension.py +1034 -1033
  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.9.dist-info/RECORD +0 -33
  31. {dt_extensions_sdk-1.1.9.dist-info → dt_extensions_sdk-1.1.10.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,141 +1,141 @@
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 inspect import signature
9
- from timeit import default_timer as timer
10
- from typing import Callable, Optional
11
-
12
- from .activation import ActivationType
13
- from .communication import Status, StatusValue
14
-
15
-
16
- class WrappedCallback:
17
- def __init__(
18
- self,
19
- interval: timedelta,
20
- callback: Callable,
21
- logger: logging.Logger,
22
- args: Optional[tuple] = None,
23
- kwargs: Optional[dict] = None,
24
- running_in_sim=False,
25
- activation_type: Optional[ActivationType] = None,
26
- ):
27
- self.callback: Callable = callback
28
- if args is None:
29
- args = ()
30
- self.callback_args = args
31
- if kwargs is None:
32
- kwargs = {}
33
- self.callback_kwargs = kwargs
34
- self.interval: timedelta = interval
35
- self.logger = logger
36
- self.running: bool = False
37
- self.status = Status(StatusValue.OK)
38
- self.executions_total = 0 # global counter
39
- self.executions_per_interval = 0 # counter per interval = 1 min by default
40
- self.duration = 0 # global counter
41
- self.duration_interval_total = 0 # counter per interval = 1 min by default
42
- self.cluster_time_diff = 0
43
- self.start_timestamp = self.get_current_time_with_cluster_diff()
44
- self.running_in_sim = running_in_sim
45
- self.activation_type = activation_type
46
- self.ok_count = 0 # counter per interval = 1 min by default
47
- self.timeouts_count = 0 # counter per interval = 1 min by default
48
- self.exception_count = 0 # counter per interval = 1 min by default
49
-
50
- self.callback_parameters = signature(callback).parameters
51
-
52
- def get_current_time_with_cluster_diff(self):
53
- return datetime.now() + timedelta(milliseconds=self.cluster_time_diff)
54
-
55
- def __call__(self, activation_config, extension_config):
56
- self.logger.debug(f"Running scheduled callback {self}")
57
- self.start_timestamp = self.get_current_time_with_cluster_diff()
58
- self.running = True
59
- self.executions_total += 1
60
- self.executions_per_interval += 1
61
- self.next_run = datetime.now() + self.interval
62
- start_time = timer()
63
- failed = False
64
- try:
65
- if "kwargs" in self.callback_parameters:
66
- kwargs = {"activation_config": activation_config, "extension_config": extension_config}
67
- self.callback(*self.callback_args, **kwargs)
68
- else:
69
- self.callback(*self.callback_args)
70
- self.status = Status(StatusValue.OK)
71
- except Exception as e:
72
- failed = True
73
- self.logger.exception(f"Error running callback {self}: {e!r}")
74
- self.status = Status(StatusValue.GENERIC_ERROR, repr(e))
75
- self.exception_count += 1
76
-
77
- self.running = False
78
- self.duration = timer() - start_time
79
- self.duration_interval_total += self.duration
80
- self.logger.debug(f"Ran scheduled callback {self} in {self.duration:.2f} seconds")
81
- if self.duration > self.interval.total_seconds():
82
- message = f"Callback {self} took {self.duration:.4f} seconds to execute, which is longer than the interval of {self.interval.total_seconds()}s"
83
- self.logger.warning(message)
84
- self.status = Status(StatusValue.GENERIC_ERROR, message)
85
- self.timeouts_count += 1
86
- elif not failed:
87
- self.ok_count += 1
88
-
89
- def __repr__(self):
90
- return f"Method={self.callback.__name__}"
91
-
92
- def name(self):
93
- return self.callback.__name__
94
-
95
- def initial_wait_time(self) -> float:
96
- if not self.running_in_sim:
97
- """
98
- Here we chose a random second between 1 and 59 to start the callback
99
- This is to distribute load for extension running on this host
100
- When running from the simulator, this is not done
101
- """
102
-
103
- now = self.get_current_time_with_cluster_diff()
104
- random_second = random.randint(1, 59) # noqa: S311
105
- next_execution = datetime.now().replace(second=random_second, microsecond=0)
106
- if next_execution <= now:
107
- # The random chosen second already passed this minute
108
- next_execution += timedelta(minutes=1)
109
- wait_time = (next_execution - now).total_seconds()
110
- self.logger.debug(f"Randomly choosing next execution time for callback {self} to be {next_execution}")
111
- return wait_time
112
- return 0
113
-
114
- def get_adjusted_metric_timestamp(self) -> datetime:
115
- """
116
- Callbacks can't run all together, so they must start at random times
117
- This means that when reporting metrics for a callback, we need to consider
118
- the time the callback was started, instead of the current timestamp
119
- this is done to avoid situations like:
120
- - 14:00:55 - callback A runs
121
- - 14:01:03 - 8 seconds later, a metric is reported
122
- - 14:01:55 - callback A runs again (60 seconds after the first run)
123
- - 14:01:58 - 3 seconds later a metric is reported
124
- In this scenario a metric is reported twice in the same minute
125
- This can also cause minutes where a metric is not reported at all, creating gaps
126
-
127
- Here we calculate the metric timestamp based on the start timestamp of the callback
128
- If the callback started in the last minute, we use the callback start timestamp
129
- between 60 seconds and 120 seconds, we use the callback timestamp + 1 minute
130
- between 120 seconds and 180 seconds, we use the callback timestamp + 2 minutes, and so forth
131
- """
132
- now = self.get_current_time_with_cluster_diff()
133
- minutes_since_start = int((now - self.start_timestamp).total_seconds() / 60)
134
- return self.start_timestamp + timedelta(minutes=minutes_since_start)
135
-
136
- def clear_sfm_metrics(self):
137
- self.ok_count = 0
138
- self.timeouts_count = 0
139
- self.duration_interval_total = 0
140
- self.exception_count = 0
141
- 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 inspect import signature
9
+ from timeit import default_timer as timer
10
+ from typing import Callable, Optional
11
+
12
+ from .activation import ActivationType
13
+ from .communication import Status, StatusValue
14
+
15
+
16
+ class WrappedCallback:
17
+ def __init__(
18
+ self,
19
+ interval: timedelta,
20
+ callback: Callable,
21
+ logger: logging.Logger,
22
+ args: Optional[tuple] = None,
23
+ kwargs: Optional[dict] = None,
24
+ running_in_sim=False,
25
+ activation_type: Optional[ActivationType] = None,
26
+ ):
27
+ self.callback: Callable = callback
28
+ if args is None:
29
+ args = ()
30
+ self.callback_args = args
31
+ if kwargs is None:
32
+ kwargs = {}
33
+ self.callback_kwargs = kwargs
34
+ self.interval: timedelta = interval
35
+ self.logger = logger
36
+ self.running: bool = False
37
+ self.status = Status(StatusValue.OK)
38
+ self.executions_total = 0 # global counter
39
+ self.executions_per_interval = 0 # counter per interval = 1 min by default
40
+ self.duration = 0 # global counter
41
+ self.duration_interval_total = 0 # counter per interval = 1 min by default
42
+ self.cluster_time_diff = 0
43
+ self.start_timestamp = self.get_current_time_with_cluster_diff()
44
+ self.running_in_sim = running_in_sim
45
+ self.activation_type = activation_type
46
+ self.ok_count = 0 # counter per interval = 1 min by default
47
+ self.timeouts_count = 0 # counter per interval = 1 min by default
48
+ self.exception_count = 0 # counter per interval = 1 min by default
49
+
50
+ self.callback_parameters = signature(callback).parameters
51
+
52
+ def get_current_time_with_cluster_diff(self):
53
+ return datetime.now() + timedelta(milliseconds=self.cluster_time_diff)
54
+
55
+ def __call__(self, activation_config, extension_config):
56
+ self.logger.debug(f"Running scheduled callback {self}")
57
+ self.start_timestamp = self.get_current_time_with_cluster_diff()
58
+ self.running = True
59
+ self.executions_total += 1
60
+ self.executions_per_interval += 1
61
+ self.next_run = datetime.now() + self.interval
62
+ start_time = timer()
63
+ failed = False
64
+ try:
65
+ if "kwargs" in self.callback_parameters:
66
+ kwargs = {"activation_config": activation_config, "extension_config": extension_config}
67
+ self.callback(*self.callback_args, **kwargs)
68
+ else:
69
+ self.callback(*self.callback_args)
70
+ self.status = Status(StatusValue.OK)
71
+ except Exception as e:
72
+ failed = True
73
+ self.logger.exception(f"Error running callback {self}: {e!r}")
74
+ self.status = Status(StatusValue.GENERIC_ERROR, repr(e))
75
+ self.exception_count += 1
76
+
77
+ self.running = False
78
+ self.duration = timer() - start_time
79
+ self.duration_interval_total += self.duration
80
+ self.logger.debug(f"Ran scheduled callback {self} in {self.duration:.2f} seconds")
81
+ if self.duration > self.interval.total_seconds():
82
+ message = f"Callback {self} took {self.duration:.4f} seconds to execute, which is longer than the interval of {self.interval.total_seconds()}s"
83
+ self.logger.warning(message)
84
+ self.status = Status(StatusValue.GENERIC_ERROR, message)
85
+ self.timeouts_count += 1
86
+ elif not failed:
87
+ self.ok_count += 1
88
+
89
+ def __repr__(self):
90
+ return f"Method={self.callback.__name__}"
91
+
92
+ def name(self):
93
+ return self.callback.__name__
94
+
95
+ def initial_wait_time(self) -> float:
96
+ if not self.running_in_sim:
97
+ """
98
+ Here we chose a random second between 1 and 59 to start the callback
99
+ This is to distribute load for extension running on this host
100
+ When running from the simulator, this is not done
101
+ """
102
+
103
+ now = self.get_current_time_with_cluster_diff()
104
+ random_second = random.randint(1, 59) # noqa: S311
105
+ next_execution = datetime.now().replace(second=random_second, microsecond=0)
106
+ if next_execution <= now:
107
+ # The random chosen second already passed this minute
108
+ next_execution += timedelta(minutes=1)
109
+ wait_time = (next_execution - now).total_seconds()
110
+ self.logger.debug(f"Randomly choosing next execution time for callback {self} to be {next_execution}")
111
+ return wait_time
112
+ return 0
113
+
114
+ def get_adjusted_metric_timestamp(self) -> datetime:
115
+ """
116
+ Callbacks can't run all together, so they must start at random times
117
+ This means that when reporting metrics for a callback, we need to consider
118
+ the time the callback was started, instead of the current timestamp
119
+ this is done to avoid situations like:
120
+ - 14:00:55 - callback A runs
121
+ - 14:01:03 - 8 seconds later, a metric is reported
122
+ - 14:01:55 - callback A runs again (60 seconds after the first run)
123
+ - 14:01:58 - 3 seconds later a metric is reported
124
+ In this scenario a metric is reported twice in the same minute
125
+ This can also cause minutes where a metric is not reported at all, creating gaps
126
+
127
+ Here we calculate the metric timestamp based on the start timestamp of the callback
128
+ If the callback started in the last minute, we use the callback start timestamp
129
+ between 60 seconds and 120 seconds, we use the callback timestamp + 1 minute
130
+ between 120 seconds and 180 seconds, we use the callback timestamp + 2 minutes, and so forth
131
+ """
132
+ now = self.get_current_time_with_cluster_diff()
133
+ minutes_since_start = int((now - self.start_timestamp).total_seconds() / 60)
134
+ return self.start_timestamp + timedelta(minutes=minutes_since_start)
135
+
136
+ def clear_sfm_metrics(self):
137
+ self.ok_count = 0
138
+ self.timeouts_count = 0
139
+ self.duration_interval_total = 0
140
+ self.exception_count = 0
141
+ self.executions_per_interval = 0