dt-extensions-sdk 1.3.1__py3-none-any.whl → 1.4.1__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dt-extensions-sdk
3
- Version: 1.3.1
3
+ Version: 1.4.1
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
@@ -16,6 +16,7 @@ Requires-Python: <3.11,>=3.10
16
16
  Provides-Extra: cli
17
17
  Requires-Dist: dt-cli>=1.6.13; extra == 'cli'
18
18
  Requires-Dist: pyyaml; extra == 'cli'
19
+ Requires-Dist: ruff; extra == 'cli'
19
20
  Requires-Dist: typer[all]; extra == 'cli'
20
21
  Description-Content-Type: text/markdown
21
22
 
@@ -1,25 +1,26 @@
1
- dynatrace_extension/__about__.py,sha256=-jEpNU1eRpe8GBW_KoSO-6Flc820P-KhyssXl0K9kO4,110
1
+ dynatrace_extension/__about__.py,sha256=sfmpNB9deUupD5FEf51kP87yHBs_W3e0oAyN3Zd3_vI,110
2
2
  dynatrace_extension/__init__.py,sha256=BvQuknmA7ti3WJi3zEXZfY7aAxJrie37VNitWICsUvI,752
3
3
  dynatrace_extension/cli/__init__.py,sha256=HCboY_eJPoqjFmoPDsBL8Jk6aNvank8K7JpkVrgwzUM,123
4
- dynatrace_extension/cli/main.py,sha256=Z8gFcp0vIMBkzV6jUd5mwvP1U0JcLqAHoMLJw_6puz4,18260
4
+ dynatrace_extension/cli/main.py,sha256=afgSPayrpxwQjcCZzv9dIEbYBWs84708BbwoQr8XL4w,20157
5
5
  dynatrace_extension/cli/schema.py,sha256=d8wKUodRiaU3hfSZDWVNpD15lBfhmif2oQ-k07IxcaA,3230
6
6
  dynatrace_extension/cli/create/__init__.py,sha256=NfyOJCVlxs8dYtfDAMHS1Q5SJTuZcFzOg5rtaI-ZPRE,72
7
7
  dynatrace_extension/cli/create/create.py,sha256=apXden2M93MDDDm7aa-Os-AEtUtyKbk_PsS56j32NK4,2708
8
8
  dynatrace_extension/cli/create/extension_template/.gitignore.template,sha256=FPye23W8dqmked4HQBCDCAKFf1UbBGkwhKjQpgmXhdg,3101
9
9
  dynatrace_extension/cli/create/extension_template/README.md.template,sha256=QcV0fYqJ1PHaouKdGQgzveJY5zAFBSICp7xSfgnoQj0,637
10
10
  dynatrace_extension/cli/create/extension_template/activation.json.template,sha256=qX-Fgq_JhUWNYMe1-RMcjwQPzF4scJYGfGlBr043UiY,266
11
+ dynatrace_extension/cli/create/extension_template/ruff.toml.template,sha256=knGDPYHrdrlSDXpe3XNofcgAP-6XyzyHNqLd6Tr8bbs,1853
11
12
  dynatrace_extension/cli/create/extension_template/secrets.json.template,sha256=fr-ya8lR0TiUmr6lf-PFko6RZw6sr54FxtMbqIWBVkA,36
12
- dynatrace_extension/cli/create/extension_template/setup.py.template,sha256=M4HPg3UFkvmWFHAz6vHbDG5Ocb-ZiR0TQuLt9eedW1M,846
13
+ dynatrace_extension/cli/create/extension_template/setup.py.template,sha256=CPSx_mRKP6R273wFN-tJFQEsMNF2ktSO-I58mAWLM9M,829
13
14
  dynatrace_extension/cli/create/extension_template/extension/activationSchema.json.template,sha256=me3DL_Q449q4VaOStTqaBL-gUkKnlafC8L2093O0LOY,2905
14
15
  dynatrace_extension/cli/create/extension_template/extension/extension.yaml.template,sha256=qDyAURGXKsZBIPjW2s1BPhD-xOC8_FoBgA33HbsOkAU,299
15
16
  dynatrace_extension/cli/create/extension_template/extension_name/__init__.py.template,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- dynatrace_extension/cli/create/extension_template/extension_name/__main__.py.template,sha256=NYuZ6BWZ6HOs4TCuboh6S0nuYp3F8MXW2lmjSpdREEA,1205
17
+ dynatrace_extension/cli/create/extension_template/extension_name/__main__.py.template,sha256=cS79GVxJB-V-gocu4ZOjmZ54HXJNg89eXdLf89zDHJQ,1249
17
18
  dynatrace_extension/sdk/__init__.py,sha256=RsqQ1heGyCmSK3fhuEKAcxQIRCg4gEK0-eSkIehL5Nc,86
18
19
  dynatrace_extension/sdk/activation.py,sha256=goTbT1tD2kn8xfyXFdTy_cTZNcFPJpgbvQM8HOzKECA,1480
19
20
  dynatrace_extension/sdk/callback.py,sha256=eMpC0F3fCI82mWHIFgmy9QmKl7Kq_9dSaanHdV6o7hA,6511
20
21
  dynatrace_extension/sdk/communication.py,sha256=QkJgEBblOen-jmvsb3ZfYZYglYUc7jHbkEgPtRj9l6w,19123
21
22
  dynatrace_extension/sdk/event.py,sha256=J261imbFKpxfuAQ6Nfu3RRcsIQKKivy6fme1nww2g-8,388
22
- dynatrace_extension/sdk/extension.py,sha256=RnF7Czlmg6QrQLLlP90dM0tAIxdX3LrqaUi8M1p1Guo,43251
23
+ dynatrace_extension/sdk/extension.py,sha256=59hg3aUXbrkhvEB7za8jujv1LZQ4sSx4ot9scijadQU,44926
23
24
  dynatrace_extension/sdk/helper.py,sha256=ZNrO9ao2hE3KQ934vAYD74k0fCr6QTG-_bAvbk9-hi8,6562
24
25
  dynatrace_extension/sdk/metric.py,sha256=7VClzJCFJNDCxA-d69uTu1pdPtDZBTwq7fbafs_L6nQ,3690
25
26
  dynatrace_extension/sdk/runtime.py,sha256=jyYsM1x-gMnW68eWq8IoZZZBarHgIcr_nVeGDDgpRDk,2802
@@ -28,8 +29,8 @@ dynatrace_extension/sdk/vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
28
29
  dynatrace_extension/sdk/vendor/mureq/LICENSE,sha256=8AVcgZgiT_mvK1fOofXtRRr2f1dRXS_K21NuxQgP4VM,671
29
30
  dynatrace_extension/sdk/vendor/mureq/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
31
  dynatrace_extension/sdk/vendor/mureq/mureq.py,sha256=znF4mvzk5L03CLNozRz8UpK-fMijmSkObDFwlbhwLUg,14656
31
- dt_extensions_sdk-1.3.1.dist-info/METADATA,sha256=LwKOdwizaSteiT3s-yPkAXYRyqkiqQC11TaPXiGlJ-Q,2685
32
- dt_extensions_sdk-1.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
- dt_extensions_sdk-1.3.1.dist-info/entry_points.txt,sha256=pweyOCgENGHjOlT6_kXYaBPOrE3p18K0UettqnNlnoE,55
34
- dt_extensions_sdk-1.3.1.dist-info/licenses/LICENSE.txt,sha256=3Zihv0lOVYHNfDkJC-tUAU6euP9r2NexsDW4w-zqgVk,1078
35
- dt_extensions_sdk-1.3.1.dist-info/RECORD,,
32
+ dt_extensions_sdk-1.4.1.dist-info/METADATA,sha256=MxsxFe30Bn3TW-Gpdul4OrkySRE6JHb_bDxWKZAWkCo,2721
33
+ dt_extensions_sdk-1.4.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
34
+ dt_extensions_sdk-1.4.1.dist-info/entry_points.txt,sha256=pweyOCgENGHjOlT6_kXYaBPOrE3p18K0UettqnNlnoE,55
35
+ dt_extensions_sdk-1.4.1.dist-info/licenses/LICENSE.txt,sha256=3Zihv0lOVYHNfDkJC-tUAU6euP9r2NexsDW4w-zqgVk,1078
36
+ dt_extensions_sdk-1.4.1.dist-info/RECORD,,
@@ -3,4 +3,4 @@
3
3
  # SPDX-License-Identifier: MIT
4
4
 
5
5
 
6
- __version__ = "1.3.1"
6
+ __version__ = "1.4.1"
@@ -2,7 +2,6 @@ from dynatrace_extension import Extension, Status, StatusValue
2
2
 
3
3
 
4
4
  class ExtensionImpl(Extension):
5
-
6
5
  def query(self):
7
6
  """
8
7
  The query method is automatically scheduled to run every minute
@@ -11,22 +10,24 @@ class ExtensionImpl(Extension):
11
10
 
12
11
  for endpoint in self.activation_config["endpoints"]:
13
12
  url = endpoint["url"]
14
- user = endpoint["user"]
15
- password = endpoint["password"]
13
+ # user = endpoint["user"]
14
+ # password = endpoint["password"]
16
15
  self.logger.debug(f"Running endpoint with url '{url}'")
17
16
 
18
17
  # Your extension code goes here, e.g.
19
18
  # response = requests.get(url, auth=(user, password))
20
19
 
21
20
  # Report metrics with
22
- self.report_metric("my_metric", 1, dimensions={"my_dimension": "dimension1"})
21
+ self.report_metric("metric_key", 1, dimensions={"key": "value"})
23
22
 
24
23
  self.logger.info("query method ended for %extension_name%.")
25
24
 
26
25
  def fastcheck(self) -> Status:
27
26
  """
28
- This is called when the extension runs for the first time.
29
- If this AG cannot run this extension, raise an Exception or return StatusValue.ERROR!
27
+ Use to check if the extension can run.
28
+ If this Activegate cannot run this extension, you can
29
+ raise an Exception or return StatusValue.ERROR.
30
+ This does not run for OneAgent extensions.
30
31
  """
31
32
  return Status(StatusValue.OK)
32
33
 
@@ -35,6 +36,5 @@ def main():
35
36
  ExtensionImpl(name="%extension_name%").run()
36
37
 
37
38
 
38
-
39
- if __name__ == '__main__':
39
+ if __name__ == "__main__":
40
40
  main()
@@ -0,0 +1,77 @@
1
+ exclude = [
2
+ ".bzr",
3
+ ".direnv",
4
+ ".eggs",
5
+ ".git",
6
+ ".git-rewrite",
7
+ ".hg",
8
+ ".ipynb_checkpoints",
9
+ ".mypy_cache",
10
+ ".nox",
11
+ ".pants.d",
12
+ ".pyenv",
13
+ ".pytest_cache",
14
+ ".pytype",
15
+ ".ruff_cache",
16
+ ".svn",
17
+ ".tox",
18
+ ".venv",
19
+ ".vscode",
20
+ "__pypackages__",
21
+ "_build",
22
+ "buck-out",
23
+ "build",
24
+ "dist",
25
+ "node_modules",
26
+ "site-packages",
27
+ "venv",
28
+ "windows.py", # windows run as
29
+ "windows_runas.py", # windows run as
30
+ "*grpc.py", # automatically generated grpc files
31
+ "mureq.py", # a replacement for requests
32
+ "vendor", # vendored dependencies
33
+ "alembic", # alembic migrations
34
+ "oci", # special case for an extension
35
+ "pymqi", # special case for an extension
36
+ "lib" # some extensions commit a lib folder
37
+ ]
38
+
39
+ line-length = 110
40
+ indent-width = 4
41
+
42
+ # Assume Python 3.10
43
+ target-version = "py310"
44
+
45
+ [lint]
46
+ select = ["E", "F", "W", "C90", "I", "N", "UP", "B", "A", "FA", "T20", "Q", "RET", "SIM", "ARG", "PTH", "C"]
47
+ ignore = [
48
+ "T201", # we allow print because these are logged into the extension logs
49
+ "PTH123" # open is used too frequently to open files
50
+ ]
51
+
52
+ # Allow fix for all enabled rules (when `--fix`) is provided.
53
+ fixable = ["ALL"]
54
+ unfixable = []
55
+
56
+ # Allow unused variables when underscore-prefixed.
57
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
58
+
59
+ [lint.mccabe]
60
+ # Flag errors (`C901`) whenever the complexity level exceeds 20.
61
+ max-complexity = 20
62
+
63
+ [format]
64
+ # Like Black, use double quotes for strings.
65
+ quote-style = "double"
66
+
67
+ # Like Black, indent with spaces, rather than tabs.
68
+ indent-style = "space"
69
+
70
+ # Like Black, respect magic trailing commas.
71
+ skip-magic-trailing-comma = false
72
+
73
+ # Like Black, automatically detect the appropriate line ending.
74
+ line-ending = "auto"
75
+
76
+ docstring-code-format = false
77
+ docstring-code-line-length = "dynamic"
@@ -1,5 +1,6 @@
1
1
  from pathlib import Path
2
- from setuptools import setup, find_packages
2
+
3
+ from setuptools import find_packages, setup
3
4
 
4
5
 
5
6
  def find_version() -> str:
@@ -9,20 +10,21 @@ def find_version() -> str:
9
10
  with open(extension_yaml_path, encoding="utf-8") as f:
10
11
  for line in f:
11
12
  if line.startswith("version"):
12
- version = line.split(" ")[-1].strip("\"")
13
+ version = line.split(" ")[-1].strip('"')
13
14
  break
14
15
  except Exception:
15
16
  pass
16
17
  return version
17
18
 
18
19
 
19
- setup(name="%extension_name%",
20
- version=find_version(),
21
- description="%Extension_Name% python EF2 extension",
22
- author="Dynatrace",
23
- packages=find_packages(),
24
- python_requires=">=3.10",
25
- include_package_data=True,
26
- install_requires=["dt-extensions-sdk"],
27
- extras_require={"dev": ["dt-extensions-sdk[cli]"]},
28
- )
20
+ setup(
21
+ name="%extension_name%",
22
+ version=find_version(),
23
+ description="%Extension_Name% python EF2 extension",
24
+ author="Dynatrace",
25
+ packages=find_packages(),
26
+ python_requires=">=3.10",
27
+ include_package_data=True,
28
+ install_requires=["dt-extensions-sdk"],
29
+ extras_require={"dev": ["dt-extensions-sdk[cli]"]},
30
+ )
@@ -425,6 +425,56 @@ def create(extension_name: str, output: Path = typer.Option(None, "--output", "-
425
425
  console.print(f"Extension created at {extension_path}", style="bold green")
426
426
 
427
427
 
428
+ @app.command(
429
+ "format",
430
+ help="Runs ruff format on the extension code",
431
+ )
432
+ def fmt(extension_dir: Path = typer.Argument(".", help="Path to the python extension")):
433
+ """
434
+ Runs ruff format on the extension code
435
+
436
+ :param extension_dir: The directory of the extension, by default this is the current directory
437
+ """
438
+ run_process(["ruff", "format", str(extension_dir.resolve())])
439
+
440
+
441
+ @app.command(help="Runs ruff check on the extension code")
442
+ def lint(
443
+ extension_dir: Path = typer.Argument(".", help="Path to the python extension"),
444
+ fix: bool = typer.Option(False, "--fix", "-f", help="Fix linting issues"),
445
+ ):
446
+ """
447
+ Runs ruff lint on the extension code
448
+
449
+ :param extension_dir: The directory of the extension, by default this is the current directory
450
+ :param fix: If true, ask ruff to also fix the linting issues
451
+ """
452
+ command = ["ruff", "check", "--exit-zero"]
453
+ if fix:
454
+ command.append("--fix")
455
+ command.append(str(extension_dir.resolve()))
456
+ run_process(command)
457
+
458
+
459
+ @app.command(help="Adds ruff rules if they don't exist already")
460
+ def ruff_init(extension_dir: Path = typer.Argument(".", help="Path to the python extension")):
461
+ """
462
+ Adds ruff rules if they don't exist already
463
+
464
+ :param extension_dir: The directory of the extension, by default this is the current directory
465
+ """
466
+
467
+ if (extension_dir / "ruff.toml").exists():
468
+ console.print(f"ruff.toml already exists in {extension_dir.resolve()}, skipping", style="bold yellow")
469
+ return
470
+ else:
471
+ # create\extension_template\ruff.toml.template -> extension\ruff.toml
472
+ ruff_template = Path(__file__).parent / "create" / "extension_template" / "ruff.toml.template"
473
+ ruff_file = extension_dir / "ruff.toml"
474
+ shutil.copy(ruff_template, ruff_file)
475
+ console.print(f"Added ruff.toml to {extension_dir.resolve()}", style="bold green")
476
+
477
+
428
478
  def run_process(
429
479
  command: List[str], cwd: Optional[Path] = None, env: Optional[dict] = None, print_message: Optional[str] = None
430
480
  ):
@@ -215,6 +215,7 @@ class Extension:
215
215
  "timediff": datetime.now() + TIME_DIFF_INTERVAL,
216
216
  "heartbeat": datetime.now() + HEARTBEAT_INTERVAL,
217
217
  "metrics": datetime.now() + METRIC_SENDING_INTERVAL,
218
+ "events": datetime.now() + METRIC_SENDING_INTERVAL,
218
219
  "sfm_metrics": datetime.now() + SFM_METRIC_SENDING_INTERVAL,
219
220
  }
220
221
 
@@ -227,6 +228,10 @@ class Extension:
227
228
  self._metrics_lock = RLock()
228
229
  self._metrics: List[str] = []
229
230
 
231
+ # Extension logs
232
+ self._logs_lock = RLock()
233
+ self._logs: List[dict] = []
234
+
230
235
  # Self monitoring metrics
231
236
  self._sfm_metrics_lock = Lock()
232
237
  self._callbackSfmReport: Dict[str, WrappedCallback] = {}
@@ -505,6 +510,7 @@ class Extension:
505
510
  properties: Optional[dict] = None,
506
511
  timestamp: Optional[datetime] = None,
507
512
  severity: Union[Severity, str] = Severity.INFO,
513
+ send_immediately: bool = False,
508
514
  ) -> None:
509
515
  """Report an event using log ingest.
510
516
 
@@ -514,6 +520,7 @@ class Extension:
514
520
  properties: A dictionary of extra event properties
515
521
  timestamp: The timestamp of the event, defaults to the current time
516
522
  severity: The severity of the event, defaults to Severity.INFO
523
+ send_immediately: Option to directly schedule log to be sent without batching
517
524
  """
518
525
  if timestamp is None:
519
526
  timestamp = datetime.now(tz=timezone.utc)
@@ -530,7 +537,7 @@ class Extension:
530
537
  **self._metadata,
531
538
  **properties,
532
539
  }
533
- self._send_events(event)
540
+ self._send_events(event, send_immediately=send_immediately)
534
541
 
535
542
  def report_dt_event(
536
543
  self,
@@ -635,7 +642,7 @@ class Extension:
635
642
  raise ValueError(msg)
636
643
  self._send_dt_event(event)
637
644
 
638
- def report_log_event(self, log_event: dict):
645
+ def report_log_event(self, log_event: dict, send_immediately: bool = False):
639
646
  """Report a custom log event using log ingest.
640
647
 
641
648
  Note:
@@ -643,25 +650,28 @@ class Extension:
643
650
 
644
651
  Args:
645
652
  log_event: The log event dictionary.
653
+ send_immediately: Option to directly schedule log to be sent without batching
646
654
  """
647
- self._send_events(log_event)
655
+ self._send_events(log_event, send_immediately=send_immediately)
648
656
 
649
- def report_log_events(self, log_events: List[dict]):
657
+ def report_log_events(self, log_events: List[dict], send_immediately: bool = False):
650
658
  """Report a list of custom log events using log ingest.
651
659
 
652
660
  Args:
653
661
  log_events: The list of log events
662
+ send_immediately: Option to directly schedule log to be sent without batching
654
663
  """
655
- self._send_events(log_events)
664
+ self._send_events(log_events, send_immediately=send_immediately)
656
665
 
657
- 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):
658
667
  """Report a list of log lines using log ingest
659
668
 
660
669
  Args:
661
670
  log_lines: The list of log lines
671
+ send_immediately: Option to directly schedule log to be sent without batching
662
672
  """
663
673
  events = [{"content": line} for line in log_lines]
664
- self._send_events(events)
674
+ self._send_events(events, send_immediately=send_immediately)
665
675
 
666
676
  @property
667
677
  def enabled_feature_sets(self) -> dict[str, list[str]]:
@@ -819,6 +829,7 @@ class Extension:
819
829
  for callback in self._scheduled_callbacks_before_run:
820
830
  self._schedule_callback(callback)
821
831
  self._metrics_iteration()
832
+ self._events_iteration()
822
833
  self._sfm_metrics_iteration()
823
834
  self._timediff_iteration()
824
835
  self._scheduler.run()
@@ -838,6 +849,11 @@ class Extension:
838
849
  next_timestamp = self._get_and_set_next_internal_callback_timestamp("metrics", METRIC_SENDING_INTERVAL)
839
850
  self._scheduler.enterabs(next_timestamp, 1, self._metrics_iteration)
840
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
+
841
857
  def _sfm_metrics_iteration(self):
842
858
  self._internal_executor.submit(self._send_sfm_metrics)
843
859
  next_timestamp = self._get_and_set_next_internal_callback_timestamp("sfm_metrics", SFM_METRIC_SENDING_INTERVAL)
@@ -1044,8 +1060,23 @@ class Extension:
1044
1060
  with self._internal_callbacks_results_lock:
1045
1061
  self._internal_callbacks_results[self._send_events.__name__] = Status(StatusValue.GENERIC_ERROR, str(e))
1046
1062
 
1047
- def _send_events(self, events: Union[dict, List[dict]]):
1048
- 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 = []
1049
1080
 
1050
1081
  def _send_dt_event(self, event: dict[str, str | int | dict[str, str]]):
1051
1082
  self._client.send_dt_event(event)