dt-extensions-sdk 1.3.0__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.
@@ -1,11 +1,12 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: dt-extensions-sdk
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Project-URL: Documentation, https://github.com/dynatrace-extensions/dt-extensions-python-sdk#readme
5
5
  Project-URL: Issues, https://github.com/dynatrace-extensions/dt-extensions-python-sdk/issues
6
6
  Project-URL: Source, https://github.com/dynatrace-extensions/dt-extensions-python-sdk
7
7
  Author-email: dlopes7 <davidribeirolopes@gmail.com>
8
- License: MIT
8
+ License-Expression: MIT
9
+ License-File: LICENSE.txt
9
10
  Classifier: Development Status :: 4 - Beta
10
11
  Classifier: Programming Language :: Python
11
12
  Classifier: Programming Language :: Python :: 3.10
@@ -1,4 +1,4 @@
1
- dynatrace_extension/__about__.py,sha256=UOSMeuvYrIEfkKMe5OPGxqTQkGJsMCvBMaw2t3yMuJ4,110
1
+ dynatrace_extension/__about__.py,sha256=0eF7oFPAs4l6aF4bObZHJnLcIaLQ07bZ0ozH7nzz4HY,110
2
2
  dynatrace_extension/__init__.py,sha256=BvQuknmA7ti3WJi3zEXZfY7aAxJrie37VNitWICsUvI,752
3
3
  dynatrace_extension/cli/__init__.py,sha256=HCboY_eJPoqjFmoPDsBL8Jk6aNvank8K7JpkVrgwzUM,123
4
4
  dynatrace_extension/cli/main.py,sha256=Z8gFcp0vIMBkzV6jUd5mwvP1U0JcLqAHoMLJw_6puz4,18260
@@ -16,10 +16,10 @@ dynatrace_extension/cli/create/extension_template/extension_name/__init__.py.tem
16
16
  dynatrace_extension/cli/create/extension_template/extension_name/__main__.py.template,sha256=NYuZ6BWZ6HOs4TCuboh6S0nuYp3F8MXW2lmjSpdREEA,1205
17
17
  dynatrace_extension/sdk/__init__.py,sha256=RsqQ1heGyCmSK3fhuEKAcxQIRCg4gEK0-eSkIehL5Nc,86
18
18
  dynatrace_extension/sdk/activation.py,sha256=goTbT1tD2kn8xfyXFdTy_cTZNcFPJpgbvQM8HOzKECA,1480
19
- dynatrace_extension/sdk/callback.py,sha256=PNRvOVzNpKvpsa0wCf7HRDkfo3wMsF2otVWI9a1AAkU,6307
20
- dynatrace_extension/sdk/communication.py,sha256=GVxIdLvhJW7mr1HHoS3qILGx7MNZm1ujocBrNYbfnUc,18494
19
+ dynatrace_extension/sdk/callback.py,sha256=eMpC0F3fCI82mWHIFgmy9QmKl7Kq_9dSaanHdV6o7hA,6511
20
+ dynatrace_extension/sdk/communication.py,sha256=QkJgEBblOen-jmvsb3ZfYZYglYUc7jHbkEgPtRj9l6w,19123
21
21
  dynatrace_extension/sdk/event.py,sha256=J261imbFKpxfuAQ6Nfu3RRcsIQKKivy6fme1nww2g-8,388
22
- dynatrace_extension/sdk/extension.py,sha256=gwESrAhlTtlJO-VfRFtfevmzDbd81Dj9r00u-kqs0iQ,42940
22
+ dynatrace_extension/sdk/extension.py,sha256=59hg3aUXbrkhvEB7za8jujv1LZQ4sSx4ot9scijadQU,44926
23
23
  dynatrace_extension/sdk/helper.py,sha256=ZNrO9ao2hE3KQ934vAYD74k0fCr6QTG-_bAvbk9-hi8,6562
24
24
  dynatrace_extension/sdk/metric.py,sha256=7VClzJCFJNDCxA-d69uTu1pdPtDZBTwq7fbafs_L6nQ,3690
25
25
  dynatrace_extension/sdk/runtime.py,sha256=jyYsM1x-gMnW68eWq8IoZZZBarHgIcr_nVeGDDgpRDk,2802
@@ -28,8 +28,8 @@ dynatrace_extension/sdk/vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
28
28
  dynatrace_extension/sdk/vendor/mureq/LICENSE,sha256=8AVcgZgiT_mvK1fOofXtRRr2f1dRXS_K21NuxQgP4VM,671
29
29
  dynatrace_extension/sdk/vendor/mureq/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  dynatrace_extension/sdk/vendor/mureq/mureq.py,sha256=znF4mvzk5L03CLNozRz8UpK-fMijmSkObDFwlbhwLUg,14656
31
- dt_extensions_sdk-1.3.0.dist-info/METADATA,sha256=qyJXkr0QueRXicxxBHw_wO4X9gNQsHGZf0DelNJUabM,2648
32
- dt_extensions_sdk-1.3.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
33
- dt_extensions_sdk-1.3.0.dist-info/entry_points.txt,sha256=pweyOCgENGHjOlT6_kXYaBPOrE3p18K0UettqnNlnoE,55
34
- dt_extensions_sdk-1.3.0.dist-info/licenses/LICENSE.txt,sha256=3Zihv0lOVYHNfDkJC-tUAU6euP9r2NexsDW4w-zqgVk,1078
35
- dt_extensions_sdk-1.3.0.dist-info/RECORD,,
31
+ dt_extensions_sdk-1.4.0.dist-info/METADATA,sha256=LYTXYtBm33nrBL_jz22oNjpB0mRIhIdr27pFuHgNzao,2685
32
+ dt_extensions_sdk-1.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
+ dt_extensions_sdk-1.4.0.dist-info/entry_points.txt,sha256=pweyOCgENGHjOlT6_kXYaBPOrE3p18K0UettqnNlnoE,55
34
+ dt_extensions_sdk-1.4.0.dist-info/licenses/LICENSE.txt,sha256=3Zihv0lOVYHNfDkJC-tUAU6euP9r2NexsDW4w-zqgVk,1078
35
+ dt_extensions_sdk-1.4.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.26.3
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -3,4 +3,4 @@
3
3
  # SPDX-License-Identifier: MIT
4
4
 
5
5
 
6
- __version__ = "1.3.0"
6
+ __version__ = "1.4.0"
@@ -9,7 +9,7 @@ from timeit import default_timer as timer
9
9
  from typing import Callable, Dict, Optional, Tuple
10
10
 
11
11
  from .activation import ActivationType
12
- from .communication import Status, StatusValue
12
+ from .communication import MultiStatus, Status, StatusValue
13
13
 
14
14
 
15
15
  class WrappedCallback:
@@ -60,8 +60,13 @@ class WrappedCallback:
60
60
  start_time = timer()
61
61
  failed = False
62
62
  try:
63
- self.callback(*self.callback_args, **self.callback_kwargs)
64
- self.status = Status(StatusValue.OK)
63
+ ret = self.callback(*self.callback_args, **self.callback_kwargs)
64
+ if isinstance(ret, Status):
65
+ self.status = ret
66
+ elif isinstance(ret, MultiStatus):
67
+ self.status = ret.build()
68
+ else:
69
+ self.status = Status(StatusValue.OK)
65
70
  except Exception as e:
66
71
  failed = True
67
72
  self.logger.exception(f"Error running callback {self}: {e!r}")
@@ -59,6 +59,29 @@ class Status:
59
59
  return self.status not in (StatusValue.OK, StatusValue.EMPTY)
60
60
 
61
61
 
62
+ class MultiStatus:
63
+
64
+ def __init__(self):
65
+ self.statuses = []
66
+
67
+ def add_status(self, status: StatusValue, message):
68
+ self.statuses.append(Status(status, message))
69
+
70
+ def build(self) -> Status:
71
+ ret = Status(StatusValue.OK)
72
+ if len(self.statuses) == 0:
73
+ return ret
74
+
75
+ messages = []
76
+ for stored_status in self.statuses:
77
+ print(stored_status) # noqa: T201
78
+ if stored_status.is_error():
79
+ ret.status = stored_status.status
80
+ messages.append(stored_status.message)
81
+ ret.message = "\n".join(messages)
82
+ return ret
83
+
84
+
62
85
  class CommunicationClient(ABC):
63
86
  """
64
87
  Abstract class for extension communication
@@ -32,6 +32,7 @@ TIME_DIFF_INTERVAL = timedelta(seconds=60)
32
32
 
33
33
  CALLBACKS_THREAD_POOL_SIZE = 100
34
34
  INTERNAL_THREAD_POOL_SIZE = 20
35
+ HEARTBEAT_THREAD_POOL_SIZE = 10
35
36
 
36
37
  RFC_3339_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
37
38
  DATASOURCE_TYPE = "python"
@@ -214,17 +215,23 @@ class Extension:
214
215
  "timediff": datetime.now() + TIME_DIFF_INTERVAL,
215
216
  "heartbeat": datetime.now() + HEARTBEAT_INTERVAL,
216
217
  "metrics": datetime.now() + METRIC_SENDING_INTERVAL,
218
+ "events": datetime.now() + METRIC_SENDING_INTERVAL,
217
219
  "sfm_metrics": datetime.now() + SFM_METRIC_SENDING_INTERVAL,
218
220
  }
219
221
 
220
222
  # Executors for the callbacks and internal methods
221
223
  self._callbacks_executor = ThreadPoolExecutor(max_workers=CALLBACKS_THREAD_POOL_SIZE)
222
224
  self._internal_executor = ThreadPoolExecutor(max_workers=INTERNAL_THREAD_POOL_SIZE)
225
+ self._heartbeat_executor = ThreadPoolExecutor(max_workers=HEARTBEAT_THREAD_POOL_SIZE)
223
226
 
224
227
  # Extension metrics
225
228
  self._metrics_lock = RLock()
226
229
  self._metrics: List[str] = []
227
230
 
231
+ # Extension logs
232
+ self._logs_lock = RLock()
233
+ self._logs: List[dict] = []
234
+
228
235
  # Self monitoring metrics
229
236
  self._sfm_metrics_lock = Lock()
230
237
  self._callbackSfmReport: Dict[str, WrappedCallback] = {}
@@ -503,6 +510,7 @@ class Extension:
503
510
  properties: Optional[dict] = None,
504
511
  timestamp: Optional[datetime] = None,
505
512
  severity: Union[Severity, str] = Severity.INFO,
513
+ send_immediately: bool = False,
506
514
  ) -> None:
507
515
  """Report an event using log ingest.
508
516
 
@@ -512,6 +520,7 @@ class Extension:
512
520
  properties: A dictionary of extra event properties
513
521
  timestamp: The timestamp of the event, defaults to the current time
514
522
  severity: The severity of the event, defaults to Severity.INFO
523
+ send_immediately: Option to directly schedule log to be sent without batching
515
524
  """
516
525
  if timestamp is None:
517
526
  timestamp = datetime.now(tz=timezone.utc)
@@ -528,7 +537,7 @@ class Extension:
528
537
  **self._metadata,
529
538
  **properties,
530
539
  }
531
- self._send_events(event)
540
+ self._send_events(event, send_immediately=send_immediately)
532
541
 
533
542
  def report_dt_event(
534
543
  self,
@@ -633,7 +642,7 @@ class Extension:
633
642
  raise ValueError(msg)
634
643
  self._send_dt_event(event)
635
644
 
636
- def report_log_event(self, log_event: dict):
645
+ def report_log_event(self, log_event: dict, send_immediately: bool = False):
637
646
  """Report a custom log event using log ingest.
638
647
 
639
648
  Note:
@@ -641,25 +650,28 @@ class Extension:
641
650
 
642
651
  Args:
643
652
  log_event: The log event dictionary.
653
+ send_immediately: Option to directly schedule log to be sent without batching
644
654
  """
645
- self._send_events(log_event)
655
+ self._send_events(log_event, send_immediately=send_immediately)
646
656
 
647
- def report_log_events(self, log_events: List[dict]):
657
+ def report_log_events(self, log_events: List[dict], send_immediately: bool = False):
648
658
  """Report a list of custom log events using log ingest.
649
659
 
650
660
  Args:
651
661
  log_events: The list of log events
662
+ send_immediately: Option to directly schedule log to be sent without batching
652
663
  """
653
- self._send_events(log_events)
664
+ self._send_events(log_events, send_immediately=send_immediately)
654
665
 
655
- def report_log_lines(self, log_lines: List[Union[str, bytes]]):
666
+ def report_log_lines(self, log_lines: List[Union[str, bytes]], send_immediately: bool = False):
656
667
  """Report a list of log lines using log ingest
657
668
 
658
669
  Args:
659
670
  log_lines: The list of log lines
671
+ send_immediately: Option to directly schedule log to be sent without batching
660
672
  """
661
673
  events = [{"content": line} for line in log_lines]
662
- self._send_events(events)
674
+ self._send_events(events, send_immediately=send_immediately)
663
675
 
664
676
  @property
665
677
  def enabled_feature_sets(self) -> dict[str, list[str]]:
@@ -817,6 +829,7 @@ class Extension:
817
829
  for callback in self._scheduled_callbacks_before_run:
818
830
  self._schedule_callback(callback)
819
831
  self._metrics_iteration()
832
+ self._events_iteration()
820
833
  self._sfm_metrics_iteration()
821
834
  self._timediff_iteration()
822
835
  self._scheduler.run()
@@ -827,7 +840,7 @@ class Extension:
827
840
  self._scheduler.enterabs(next_timestamp, 1, self._timediff_iteration)
828
841
 
829
842
  def _heartbeat_iteration(self):
830
- self._internal_executor.submit(self._heartbeat)
843
+ self._heartbeat_executor.submit(self._heartbeat)
831
844
  next_timestamp = self._get_and_set_next_internal_callback_timestamp("heartbeat", HEARTBEAT_INTERVAL)
832
845
  self._scheduler.enterabs(next_timestamp, 2, self._heartbeat_iteration)
833
846
 
@@ -836,6 +849,11 @@ class Extension:
836
849
  next_timestamp = self._get_and_set_next_internal_callback_timestamp("metrics", METRIC_SENDING_INTERVAL)
837
850
  self._scheduler.enterabs(next_timestamp, 1, self._metrics_iteration)
838
851
 
852
+ def _events_iteration(self):
853
+ self._internal_executor.submit(self._send_buffered_events)
854
+ next_timestamp = self._get_and_set_next_internal_callback_timestamp("events", METRIC_SENDING_INTERVAL)
855
+ self._scheduler.enterabs(next_timestamp, 1, self._events_iteration)
856
+
839
857
  def _sfm_metrics_iteration(self):
840
858
  self._internal_executor.submit(self._send_sfm_metrics)
841
859
  next_timestamp = self._get_and_set_next_internal_callback_timestamp("sfm_metrics", SFM_METRIC_SENDING_INTERVAL)
@@ -976,7 +994,9 @@ class Extension:
976
994
  if callback.status.is_error():
977
995
  overall_status.status = callback.status.status
978
996
  messages.append(f"{callback}: {callback.status.message}")
979
-
997
+ continue
998
+ if callback.status.message is not None and callback.status.message != "":
999
+ messages.append(f"{callback}: {callback.status.message}")
980
1000
  overall_status.message = "\n".join(messages)
981
1001
  return overall_status
982
1002
 
@@ -1040,8 +1060,23 @@ class Extension:
1040
1060
  with self._internal_callbacks_results_lock:
1041
1061
  self._internal_callbacks_results[self._send_events.__name__] = Status(StatusValue.GENERIC_ERROR, str(e))
1042
1062
 
1043
- def _send_events(self, events: Union[dict, List[dict]]):
1044
- self._internal_executor.submit(self._send_events_internal, events)
1063
+ def _send_events(self, events: Union[dict, List[dict]], send_immediately: bool = False):
1064
+ if send_immediately:
1065
+ self._internal_executor.submit(self._send_events_internal, events)
1066
+ return
1067
+ with self._logs_lock:
1068
+ if isinstance(events, dict):
1069
+ self._logs.append(events)
1070
+ elif isinstance(events, list):
1071
+ self._logs.extend(events)
1072
+ else:
1073
+ self.logger.error(f"Invalid log format: {events}")
1074
+
1075
+ def _send_buffered_events(self):
1076
+ with self._logs_lock:
1077
+ if len(self._logs) > 0:
1078
+ self._send_events_internal(self._logs)
1079
+ self._logs = []
1045
1080
 
1046
1081
  def _send_dt_event(self, event: dict[str, str | int | dict[str, str]]):
1047
1082
  self._client.send_dt_event(event)