dt-extensions-sdk 1.2.7__py3-none-any.whl → 1.2.9__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 (32) hide show
  1. {dt_extensions_sdk-1.2.7.dist-info → dt_extensions_sdk-1.2.9.dist-info}/METADATA +3 -3
  2. dt_extensions_sdk-1.2.9.dist-info/RECORD +34 -0
  3. {dt_extensions_sdk-1.2.7.dist-info → dt_extensions_sdk-1.2.9.dist-info}/licenses/LICENSE.txt +9 -9
  4. dynatrace_extension/__about__.py +5 -5
  5. dynatrace_extension/__init__.py +27 -27
  6. dynatrace_extension/cli/__init__.py +5 -5
  7. dynatrace_extension/cli/create/__init__.py +1 -1
  8. dynatrace_extension/cli/create/create.py +76 -76
  9. dynatrace_extension/cli/create/extension_template/.gitignore.template +160 -160
  10. dynatrace_extension/cli/create/extension_template/README.md.template +33 -33
  11. dynatrace_extension/cli/create/extension_template/activation.json.template +15 -15
  12. dynatrace_extension/cli/create/extension_template/extension/activationSchema.json.template +118 -118
  13. dynatrace_extension/cli/create/extension_template/extension/extension.yaml.template +17 -17
  14. dynatrace_extension/cli/create/extension_template/extension_name/__main__.py.template +40 -40
  15. dynatrace_extension/cli/create/extension_template/setup.py.template +28 -28
  16. dynatrace_extension/cli/main.py +437 -437
  17. dynatrace_extension/cli/schema.py +129 -129
  18. dynatrace_extension/sdk/__init__.py +3 -3
  19. dynatrace_extension/sdk/activation.py +43 -43
  20. dynatrace_extension/sdk/callback.py +145 -145
  21. dynatrace_extension/sdk/communication.py +483 -483
  22. dynatrace_extension/sdk/event.py +19 -19
  23. dynatrace_extension/sdk/extension.py +1093 -1076
  24. dynatrace_extension/sdk/helper.py +191 -191
  25. dynatrace_extension/sdk/metric.py +118 -118
  26. dynatrace_extension/sdk/runtime.py +67 -67
  27. dynatrace_extension/sdk/snapshot.py +198 -198
  28. dynatrace_extension/sdk/vendor/mureq/LICENSE +13 -13
  29. dynatrace_extension/sdk/vendor/mureq/mureq.py +448 -448
  30. dt_extensions_sdk-1.2.7.dist-info/RECORD +0 -34
  31. {dt_extensions_sdk-1.2.7.dist-info → dt_extensions_sdk-1.2.9.dist-info}/WHEEL +0 -0
  32. {dt_extensions_sdk-1.2.7.dist-info → dt_extensions_sdk-1.2.9.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,145 +1,145 @@
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
- self.iterations = 0 # how many times we ran the callback iterator for this callback
49
-
50
- def get_current_time_with_cluster_diff(self):
51
- return datetime.now() + timedelta(milliseconds=self.cluster_time_diff)
52
-
53
- def __call__(self):
54
- self.logger.debug(f"Running scheduled callback {self}")
55
- if self.executions_total == 0:
56
- self.start_timestamp = self.get_current_time_with_cluster_diff()
57
- self.running = True
58
- self.executions_total += 1
59
- self.executions_per_interval += 1
60
- start_time = timer()
61
- failed = False
62
- try:
63
- self.callback(*self.callback_args, **self.callback_kwargs)
64
- self.status = Status(StatusValue.OK)
65
- except Exception as e:
66
- failed = True
67
- self.logger.exception(f"Error running callback {self}: {e!r}")
68
- self.status = Status(StatusValue.GENERIC_ERROR, repr(e))
69
- self.exception_count += 1
70
-
71
- self.running = False
72
- self.duration = timer() - start_time
73
- self.duration_interval_total += self.duration
74
- self.logger.debug(f"Ran scheduled callback {self} in {self.duration:.2f} seconds")
75
- if self.duration > self.interval.total_seconds():
76
- message = f"Callback {self} took {self.duration:.4f} seconds to execute, which is longer than the interval of {self.interval.total_seconds()}s"
77
- self.logger.warning(message)
78
- self.status = Status(StatusValue.GENERIC_ERROR, message)
79
- self.timeouts_count += 1
80
- elif not failed:
81
- self.ok_count += 1
82
-
83
- def __repr__(self):
84
- return f"Method={self.callback.__name__}"
85
-
86
- def name(self):
87
- return self.callback.__name__
88
-
89
- def initial_wait_time(self) -> float:
90
- if not self.running_in_sim:
91
- """
92
- Here we chose a random second between 1 and 59 to start the callback
93
- This is to distribute load for extension running on this host
94
- When running from the simulator, this is not done
95
- """
96
-
97
- now = self.get_current_time_with_cluster_diff()
98
- random_second = random.randint(1, 59) # noqa: S311
99
- next_execution = datetime.now().replace(second=random_second, microsecond=0)
100
- if next_execution <= now:
101
- # The random chosen second already passed this minute
102
- next_execution += timedelta(minutes=1)
103
- wait_time = (next_execution - now).total_seconds()
104
- self.logger.debug(f"Randomly choosing next execution time for callback {self} to be {next_execution}")
105
- return wait_time
106
- return 0
107
-
108
- def get_adjusted_metric_timestamp(self) -> datetime:
109
- """
110
- Callbacks can't run all together, so they must start at random times
111
- This means that when reporting metrics for a callback, we need to consider
112
- the time the callback was started, instead of the current timestamp
113
- this is done to avoid situations like:
114
- - 14:00:55 - callback A runs
115
- - 14:01:03 - 8 seconds later, a metric is reported
116
- - 14:01:55 - callback A runs again (60 seconds after the first run)
117
- - 14:01:58 - 3 seconds later a metric is reported
118
- In this scenario a metric is reported twice in the same minute
119
- This can also cause minutes where a metric is not reported at all, creating gaps
120
-
121
- Here we calculate the metric timestamp based on the start timestamp of the callback
122
- If the callback started in the last minute, we use the callback start timestamp
123
- between 60 seconds and 120 seconds, we use the callback timestamp + 1 minute
124
- between 120 seconds and 180 seconds, we use the callback timestamp + 2 minutes, and so forth
125
- """
126
- now = self.get_current_time_with_cluster_diff()
127
- minutes_since_start = int((now - self.start_timestamp).total_seconds() / 60)
128
- return self.start_timestamp + timedelta(minutes=minutes_since_start)
129
-
130
- def clear_sfm_metrics(self):
131
- self.ok_count = 0
132
- self.timeouts_count = 0
133
- self.duration_interval_total = 0
134
- self.exception_count = 0
135
- self.executions_per_interval = 0
136
-
137
- def get_next_execution_timestamp(self) -> float:
138
- """
139
- Get the timestamp for the next execution of the callback
140
- This is done using execution total, the interval and the start timestamp
141
- :return: datetime
142
- """
143
- return (
144
- self.start_timestamp + timedelta(seconds=self.interval.total_seconds() * (self.iterations or 1))
145
- ).timestamp()
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
+ self.iterations = 0 # how many times we ran the callback iterator for this callback
49
+
50
+ def get_current_time_with_cluster_diff(self):
51
+ return datetime.now() + timedelta(milliseconds=self.cluster_time_diff)
52
+
53
+ def __call__(self):
54
+ self.logger.debug(f"Running scheduled callback {self}")
55
+ if self.executions_total == 0:
56
+ self.start_timestamp = self.get_current_time_with_cluster_diff()
57
+ self.running = True
58
+ self.executions_total += 1
59
+ self.executions_per_interval += 1
60
+ start_time = timer()
61
+ failed = False
62
+ try:
63
+ self.callback(*self.callback_args, **self.callback_kwargs)
64
+ self.status = Status(StatusValue.OK)
65
+ except Exception as e:
66
+ failed = True
67
+ self.logger.exception(f"Error running callback {self}: {e!r}")
68
+ self.status = Status(StatusValue.GENERIC_ERROR, repr(e))
69
+ self.exception_count += 1
70
+
71
+ self.running = False
72
+ self.duration = timer() - start_time
73
+ self.duration_interval_total += self.duration
74
+ self.logger.debug(f"Ran scheduled callback {self} in {self.duration:.2f} seconds")
75
+ if self.duration > self.interval.total_seconds():
76
+ message = f"Callback {self} took {self.duration:.4f} seconds to execute, which is longer than the interval of {self.interval.total_seconds()}s"
77
+ self.logger.warning(message)
78
+ self.status = Status(StatusValue.GENERIC_ERROR, message)
79
+ self.timeouts_count += 1
80
+ elif not failed:
81
+ self.ok_count += 1
82
+
83
+ def __repr__(self):
84
+ return f"Method={self.callback.__name__}"
85
+
86
+ def name(self):
87
+ return self.callback.__name__
88
+
89
+ def initial_wait_time(self) -> float:
90
+ if not self.running_in_sim:
91
+ """
92
+ Here we chose a random second between 1 and 59 to start the callback
93
+ This is to distribute load for extension running on this host
94
+ When running from the simulator, this is not done
95
+ """
96
+
97
+ now = self.get_current_time_with_cluster_diff()
98
+ random_second = random.randint(1, 59) # noqa: S311
99
+ next_execution = datetime.now().replace(second=random_second, microsecond=0)
100
+ if next_execution <= now:
101
+ # The random chosen second already passed this minute
102
+ next_execution += timedelta(minutes=1)
103
+ wait_time = (next_execution - now).total_seconds()
104
+ self.logger.debug(f"Randomly choosing next execution time for callback {self} to be {next_execution}")
105
+ return wait_time
106
+ return 0
107
+
108
+ def get_adjusted_metric_timestamp(self) -> datetime:
109
+ """
110
+ Callbacks can't run all together, so they must start at random times
111
+ This means that when reporting metrics for a callback, we need to consider
112
+ the time the callback was started, instead of the current timestamp
113
+ this is done to avoid situations like:
114
+ - 14:00:55 - callback A runs
115
+ - 14:01:03 - 8 seconds later, a metric is reported
116
+ - 14:01:55 - callback A runs again (60 seconds after the first run)
117
+ - 14:01:58 - 3 seconds later a metric is reported
118
+ In this scenario a metric is reported twice in the same minute
119
+ This can also cause minutes where a metric is not reported at all, creating gaps
120
+
121
+ Here we calculate the metric timestamp based on the start timestamp of the callback
122
+ If the callback started in the last minute, we use the callback start timestamp
123
+ between 60 seconds and 120 seconds, we use the callback timestamp + 1 minute
124
+ between 120 seconds and 180 seconds, we use the callback timestamp + 2 minutes, and so forth
125
+ """
126
+ now = self.get_current_time_with_cluster_diff()
127
+ minutes_since_start = int((now - self.start_timestamp).total_seconds() / 60)
128
+ return self.start_timestamp + timedelta(minutes=minutes_since_start)
129
+
130
+ def clear_sfm_metrics(self):
131
+ self.ok_count = 0
132
+ self.timeouts_count = 0
133
+ self.duration_interval_total = 0
134
+ self.exception_count = 0
135
+ self.executions_per_interval = 0
136
+
137
+ def get_next_execution_timestamp(self) -> float:
138
+ """
139
+ Get the timestamp for the next execution of the callback
140
+ This is done using execution total, the interval and the start timestamp
141
+ :return: datetime
142
+ """
143
+ return (
144
+ self.start_timestamp + timedelta(seconds=self.interval.total_seconds() * (self.iterations or 1))
145
+ ).timestamp()