dt-extensions-sdk 1.1.10__tar.gz → 1.1.20__tar.gz

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 (82) hide show
  1. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/PKG-INFO +1 -1
  2. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/conf.py +1 -1
  3. dt_extensions_sdk-1.1.20/docs/guides/building.rst +129 -0
  4. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/guides/extension_structure.rst +2 -2
  5. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/index.rst +1 -0
  6. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/__about__.py +2 -1
  7. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/extension_template/extension/extension.yaml.template +3 -3
  8. dt_extensions_sdk-1.1.20/dynatrace_extension/cli/create/extension_template/setup.py.template +28 -0
  9. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/main.py +15 -1
  10. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/callback.py +5 -12
  11. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/communication.py +82 -58
  12. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/extension.py +46 -35
  13. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/pyproject.toml +2 -0
  14. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/cli/test_dt_sdk.py +5 -9
  15. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/cli/test_types.py +3 -3
  16. dt_extensions_sdk-1.1.20/tests/sdk/test_communication.py +75 -0
  17. dt_extensions_sdk-1.1.10/dynatrace_extension/cli/create/extension_template/setup.py.template +0 -12
  18. dt_extensions_sdk-1.1.10/tests/sdk/test_communication.py +0 -35
  19. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/.github/workflows/gh-pages-docs.yml +0 -0
  20. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/.github/workflows/publish.yml +0 -0
  21. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/.gitignore +0 -0
  22. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/LICENSE.txt +0 -0
  23. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/README.md +0 -0
  24. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/dt-sdk-header.png +0 -0
  25. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/dt-sdk-logo.png +0 -0
  26. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/favicon.ico +0 -0
  27. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-01-new-extension.png +0 -0
  28. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-02-type.png +0 -0
  29. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-03-import.png +0 -0
  30. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-04-import-remote.png +0 -0
  31. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-05-activation.png +0 -0
  32. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-06-activation-config.png +0 -0
  33. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/api/events/event_severity.rst +0 -0
  34. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/api/events/event_type.rst +0 -0
  35. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/api/events/index.rst +0 -0
  36. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/api/extension.rst +0 -0
  37. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/api/metrics/index.rst +0 -0
  38. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/api/metrics/metric.rst +0 -0
  39. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/api/metrics/metric_type.rst +0 -0
  40. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/assemble.rst +0 -0
  41. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/build.rst +0 -0
  42. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/create.rst +0 -0
  43. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/gencerts.rst +0 -0
  44. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/help.rst +0 -0
  45. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/run.rst +0 -0
  46. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/sign.rst +0 -0
  47. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/upload.rst +0 -0
  48. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/wheel.rst +0 -0
  49. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/guides/installation.rst +0 -0
  50. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/guides/migration.rst +0 -0
  51. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/requirements.txt +0 -0
  52. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/__init__.py +0 -0
  53. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/__init__.py +0 -0
  54. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/__init__.py +0 -0
  55. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/create.py +0 -0
  56. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/extension_template/.gitignore.template +0 -0
  57. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/extension_template/README.md.template +0 -0
  58. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/extension_template/activation.json.template +0 -0
  59. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/extension_template/extension/activationSchema.json.template +0 -0
  60. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/extension_template/extension_name/__init__.py.template +0 -0
  61. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/extension_template/extension_name/__main__.py.template +0 -0
  62. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/schema.py +0 -0
  63. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/__init__.py +0 -0
  64. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/activation.py +0 -0
  65. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/event.py +0 -0
  66. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/helper.py +0 -0
  67. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/metric.py +0 -0
  68. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/runtime.py +0 -0
  69. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/vendor/__init__.py +0 -0
  70. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/vendor/mureq/LICENSE +0 -0
  71. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/vendor/mureq/__init__.py +0 -0
  72. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/vendor/mureq/mureq.py +0 -0
  73. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/__init__.py +0 -0
  74. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/cli/__init__.py +0 -0
  75. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/cli/test_templates.py +0 -0
  76. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/sdk/__init__.py +0 -0
  77. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/sdk/test_activation.py +0 -0
  78. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/sdk/test_callback.py +0 -0
  79. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/sdk/test_extension.py +0 -0
  80. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/sdk/test_metric.py +0 -0
  81. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/sdk/test_runtime_properties.py +0 -0
  82. {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/sdk/test_status.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dt-extensions-sdk
3
- Version: 1.1.10
3
+ Version: 1.1.20
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
@@ -88,7 +88,7 @@ html_theme_options = {
88
88
  "logo": "dt-sdk-logo.png",
89
89
  "logo_alt": "dt-sdk",
90
90
  "logo_url": "/",
91
- "github_url": "https://github.com/dynatrace-extensions/dt-extensions-python-sdk",
91
+ "github_url": "https://github.com/dynatrace-extensions/dt-extensions-python-sdk/tree/main/docs/",
92
92
  "footer_links": ",".join(
93
93
  [
94
94
  "Dynatrace|https://dynatrace.com/",
@@ -0,0 +1,129 @@
1
+ Building Extensions
2
+ ###################
3
+
4
+ | This guide provides some best practices on:
5
+ |
6
+
7
+ * Building extensions
8
+ * Python dependencies
9
+ * CI systems, offline installs
10
+
11
+ Native dependencies
12
+ ===================
13
+
14
+ | Some python libraries require "native" dependencies, they are not written in pure python and usually contain C, C++, Rust or other compiled languages code.
15
+ | This means that they might be compiled for a very specific version of python, or for a specific operating system.
16
+ |
17
+
18
+ | Examples:
19
+ |
20
+
21
+ * **requests** requires **charset_normalizer**, a **native dependency**
22
+
23
+
24
+ | If you navigate to the `charset-normalizer pypi page`_ you will see dozens of different wheel files.
25
+ | Each one of these files is compiled for a different version of python, and for a different operating system.
26
+ |
27
+ | Your extension will run on a Dynatrace **Activegate** or **OneAgent**, which is a Linux or Windows machine, and it has a specific version of python.
28
+ | This means that your extension must be built on a machine that has the same version of python as the Activegate.
29
+ |
30
+ | At this time, Dynatrace extensions run on **python 3.10**.
31
+ |
32
+ | When you build the extension with **dt-sdk build**, it downloads the dependencies **whl** files and places them in the lib folder of the extension.
33
+ | To obtain whl files for different a operating system than what the build machine is, the SDK provides the **--extra-platform** flag.
34
+ |
35
+ | In summary, when building from Windows, you should use:
36
+ |
37
+
38
+ .. code-block:: bash
39
+
40
+ dt-sdk build --extra-platform manylinux2014_x86_64
41
+
42
+
43
+ | To get the correct extra wheel files for linux. Note, **manylinux2014_x86_64** works for several packages, but not all of them.
44
+ | You need to investigate the dependencies of your extension to find the correct extra platform if that is the case.
45
+ |
46
+ | When building from Linux, you should use:
47
+ |
48
+
49
+
50
+ .. code-block:: bash
51
+
52
+ dt-sdk build --extra-platform win_amd64
53
+
54
+
55
+ | To get the correct extra wheel files for Windows.
56
+ |
57
+
58
+ PyPI Access
59
+ ===========
60
+
61
+ | When building extensions, the SDK downloads the dependencies from PyPI.
62
+ |
63
+ | In some organizations, you are not allowed to access the internet from the build machine.
64
+ | In most cases you will have either:
65
+ |
66
+
67
+ * A local PyPI mirror
68
+ * A directory with all the wheel files present
69
+
70
+ |
71
+ | Both of these solutions can be used with the SDK.
72
+ |
73
+
74
+ PyPI Mirror
75
+ """""""""""
76
+
77
+ | Suppose you have a local PyPi server running on http://my-pypi-server:8080.
78
+ |
79
+ | To use it with the SDK, run the build command as:
80
+ |
81
+
82
+ .. code-block:: bash
83
+
84
+ PIP_INDEX_URL=http://my-pypi-server:8080/simple PIP_TRUSTED_HOST=my-pypi-server dt-sdk build
85
+
86
+
87
+ | This will tell the SDK to use the local PyPI server to download the dependencies.
88
+ | The SDK uses **pip** under the covers, so all the environment variables that **pip** supports can be used with the SDK.
89
+ |
90
+ | Note, that assumes the build machine is a linux machine. If you are building from Windows on Powershell, you can use:
91
+ |
92
+
93
+ .. code-block:: bash
94
+
95
+ $ENV:PIP_INDEX_URL="http://my-pypi-server:8080/simple"; $ENV:PIP_TRUSTED_HOST="my-pypi-server"; dt-sdk build
96
+
97
+
98
+
99
+
100
+
101
+ Local Directory
102
+ """""""""""""""
103
+
104
+
105
+ | Another option is to manually download the different whl files you need, and place them in a directory on the build machine.
106
+ | In that case, that directory can be used as the source for the dependencies.
107
+ |
108
+
109
+ .. code-block:: bash
110
+
111
+ dt-sdk build --find-links /path/to/whl/files
112
+
113
+
114
+ | This will tell the SDK to use the directory as the source for the dependencies.
115
+ |
116
+
117
+ Musl vs libc
118
+ ============
119
+
120
+ | Extensions run on `libc`_ based systems, like Ubuntu, CentOS, Windows, etc.
121
+ | You should not use a `musl`_ based system, like Alpine, to build extensions.
122
+ |
123
+ | This means that if you are using a docker container to build the extension, you should use the **python:3.10** image, or any other image that is based on a `libc`_ system.
124
+ |
125
+ | The reason for this is that a **musl** based system will download native whl files that are not compatible with **libc** based systems.
126
+
127
+ .. _charset-normalizer pypi page: https://pypi.org/project/charset-normalizer/#files
128
+ .. _musl: https://musl.libc.org/
129
+ .. _libc: https://en.wikipedia.org/wiki/C_standard_library
@@ -33,7 +33,7 @@ Here is what a sample extension definition looks like:
33
33
 
34
34
  name: custom:my-extension
35
35
  version: 0.0.1
36
- minDynatraceVersion: "1.253"
36
+ minDynatraceVersion: "1.285"
37
37
  author:
38
38
  name: "Dynatrace"
39
39
 
@@ -41,7 +41,7 @@ Here is what a sample extension definition looks like:
41
41
  runtime:
42
42
  module: my_extension
43
43
  version:
44
- min: "3.9"
44
+ min: "3.10"
45
45
 
46
46
  activation:
47
47
  remote:
@@ -273,6 +273,7 @@ Documentation
273
273
 
274
274
  guides/installation
275
275
  guides/extension_structure
276
+ guides/building
276
277
  guides/migration
277
278
 
278
279
  .. toctree::
@@ -1,4 +1,5 @@
1
1
  # SPDX-FileCopyrightText: 2023-present Dynatrace LLC
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "1.1.10"
4
+
5
+ __version__ = "1.1.20"
@@ -1,6 +1,6 @@
1
1
  name: %extension-prefix%%extension-name%
2
2
  version: 0.0.1
3
- minDynatraceVersion: "1.253"
3
+ minDynatraceVersion: "1.285"
4
4
  author:
5
5
  name: "Dynatrace"
6
6
 
@@ -8,10 +8,10 @@ python:
8
8
  runtime:
9
9
  module: %extension_name%
10
10
  version:
11
- min: "3.9"
11
+ min: "3.10"
12
12
 
13
13
  activation:
14
14
  remote:
15
15
  path: activationSchema.json
16
16
  local:
17
- path: activationSchema.json
17
+ path: activationSchema.json
@@ -0,0 +1,28 @@
1
+ from pathlib import Path
2
+ from setuptools import setup, find_packages
3
+
4
+
5
+ def find_version() -> str:
6
+ version = "0.0.1"
7
+ extension_yaml_path = Path(__file__).parent / "extension" / "extension.yaml"
8
+ try:
9
+ with open(extension_yaml_path, encoding="utf-8") as f:
10
+ for line in f:
11
+ if line.startswith("version"):
12
+ version = line.split(" ")[-1].strip("\"")
13
+ break
14
+ except Exception:
15
+ pass
16
+ return version
17
+
18
+
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
+ )
@@ -38,6 +38,7 @@ def run(
38
38
  fast_check: bool = typer.Option(False, "--fastcheck"),
39
39
  local_ingest: bool = typer.Option(False, "--local-ingest"),
40
40
  local_ingest_port: int = typer.Option(14499, "--local-ingest-port"),
41
+ print_metrics: bool = typer.Option(True),
41
42
  ):
42
43
  """
43
44
  Runs an extension, this is used during development to locally run and test an extension
@@ -47,6 +48,7 @@ def run(
47
48
  :param fast_check: If true, run a fastcheck and exits
48
49
  :param local_ingest: If true, send metrics to localhost:14499 on top of printing them
49
50
  :param local_ingest_port: The port to send metrics to, by default this is 14499
51
+ :param print_metrics: If true, print metrics to the console
50
52
  """
51
53
 
52
54
  # This parses the yaml, which validates it before running
@@ -58,6 +60,8 @@ def run(
58
60
  if local_ingest:
59
61
  command.append("--local-ingest")
60
62
  command.append(f"--local-ingest-port={local_ingest_port}")
63
+ if not print_metrics:
64
+ command.append("--no-print-metrics")
61
65
  run_process(command, cwd=extension_dir)
62
66
  except KeyboardInterrupt:
63
67
  console.print("\nRun interrupted with a KeyboardInterrupt, stopping", style="bold yellow")
@@ -79,6 +83,7 @@ def build(
79
83
  extra_index_url: Optional[str] = typer.Option(
80
84
  None, "--extra-index-url", "-i", help="Extra index url to use when downloading dependencies"
81
85
  ),
86
+ find_links: Optional[str] = typer.Option( None, "--find-links", "-f", help="Extra index url to use when downloading dependencies" ),
82
87
  ):
83
88
  """
84
89
  Builds and signs an extension using the developer fused key-certificate
@@ -90,6 +95,7 @@ def build(
90
95
  folder
91
96
  :param extra_platforms: Attempt to also download wheels for an extra platform (e.g. manylinux1_x86_64 or win_amd64)
92
97
  :param extra_index_url: Extra index url to use when downloading dependencies
98
+ :param find_links: Extra index url to use when downloading dependencies
93
99
  """
94
100
  console.print(f"Building and signing extension from {extension_dir} to {target_directory}", style="cyan")
95
101
  if target_directory is None:
@@ -98,7 +104,7 @@ def build(
98
104
  target_directory.mkdir()
99
105
 
100
106
  console.print("Stage 1 - Download and build dependencies", style="bold blue")
101
- wheel(extension_dir, extra_platforms, extra_index_url)
107
+ wheel(extension_dir, extra_platforms, extra_index_url, find_links)
102
108
 
103
109
  console.print("Stage 2 - Create the extension zip file", style="bold blue")
104
110
  built_zip = assemble(extension_dir, target_directory)
@@ -163,6 +169,7 @@ def wheel(
163
169
  extra_index_url: Optional[str] = typer.Option(
164
170
  None, "--extra-index-url", "-i", help="Extra index url to use when downloading dependencies"
165
171
  ),
172
+ find_links: Optional[str] = typer.Option( None, "--find-links", "-f", help="Extra index url to use when downloading dependencies" ),
166
173
  ):
167
174
  """
168
175
  Builds the extension and it's dependencies into wheel files
@@ -171,6 +178,7 @@ def wheel(
171
178
  :param extension_dir: The directory of the extension, by default this is the current directory
172
179
  :param extra_platforms: Attempt to also download wheels for an extra platform (e.g. manylinux1_x86_64 or win_amd64)
173
180
  :param extra_index_url: Extra index url to use when downloading dependencies
181
+ :param find_links: Extra index url to use when downloading dependencies
174
182
  """
175
183
  relative_lib_folder_dir = "extension/lib"
176
184
  lib_folder: Path = extension_dir / relative_lib_folder_dir
@@ -182,6 +190,8 @@ def wheel(
182
190
  command = [sys.executable, "-m", "pip", "wheel", "-w", relative_lib_folder_dir]
183
191
  if extra_index_url is not None:
184
192
  command.extend(["--extra-index-url", extra_index_url])
193
+ if find_links is not None:
194
+ command.extend(["--find-links", find_links])
185
195
  command.append(".")
186
196
  run_process(command, cwd=extension_dir)
187
197
 
@@ -201,6 +211,8 @@ def wheel(
201
211
  ]
202
212
  if extra_index_url:
203
213
  command.extend(["--extra-index-url", extra_index_url])
214
+ if find_links:
215
+ command.extend(["--find-links", find_links])
204
216
  command.append(".")
205
217
 
206
218
  run_process(command, cwd=extension_dir)
@@ -292,6 +304,8 @@ def upload(
292
304
  zip_file_path = Path(extension_path, "dist", zip_file_name)
293
305
 
294
306
  api_url = tenant_url or os.environ.get("DT_API_URL", "")
307
+ api_url = api_url.rstrip("/")
308
+
295
309
  if not api_url:
296
310
  console.print("Set the --tenant-url parameter or the DT_API_URL environment variable", style="bold red")
297
311
  sys.exit(1)
@@ -5,9 +5,8 @@
5
5
  import logging
6
6
  import random
7
7
  from datetime import datetime, timedelta
8
- from inspect import signature
9
8
  from timeit import default_timer as timer
10
- from typing import Callable, Optional
9
+ from typing import Callable, Dict, Optional, Tuple
11
10
 
12
11
  from .activation import ActivationType
13
12
  from .communication import Status, StatusValue
@@ -19,8 +18,8 @@ class WrappedCallback:
19
18
  interval: timedelta,
20
19
  callback: Callable,
21
20
  logger: logging.Logger,
22
- args: Optional[tuple] = None,
23
- kwargs: Optional[dict] = None,
21
+ args: Optional[Tuple] = None,
22
+ kwargs: Optional[Dict] = None,
24
23
  running_in_sim=False,
25
24
  activation_type: Optional[ActivationType] = None,
26
25
  ):
@@ -47,12 +46,10 @@ class WrappedCallback:
47
46
  self.timeouts_count = 0 # counter per interval = 1 min by default
48
47
  self.exception_count = 0 # counter per interval = 1 min by default
49
48
 
50
- self.callback_parameters = signature(callback).parameters
51
-
52
49
  def get_current_time_with_cluster_diff(self):
53
50
  return datetime.now() + timedelta(milliseconds=self.cluster_time_diff)
54
51
 
55
- def __call__(self, activation_config, extension_config):
52
+ def __call__(self):
56
53
  self.logger.debug(f"Running scheduled callback {self}")
57
54
  self.start_timestamp = self.get_current_time_with_cluster_diff()
58
55
  self.running = True
@@ -62,11 +59,7 @@ class WrappedCallback:
62
59
  start_time = timer()
63
60
  failed = False
64
61
  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)
62
+ self.callback(*self.callback_args, **self.callback_kwargs)
70
63
  self.status = Status(StatusValue.OK)
71
64
  except Exception as e:
72
65
  failed = True
@@ -10,16 +10,21 @@ import sys
10
10
  from abc import ABC, abstractmethod
11
11
  from dataclasses import dataclass
12
12
  from enum import Enum
13
- from itertools import islice
14
13
  from pathlib import Path
15
- from typing import Any, Iterable, List, TypeVar
14
+ from typing import Any, Generator, List, Sequence, TypeVar, Union
16
15
 
17
16
  from .vendor.mureq.mureq import HTTPException, Response, request
18
17
 
19
18
  CONTENT_TYPE_JSON = "application/json;charset=utf-8"
20
19
  CONTENT_TYPE_PLAIN = "text/plain;charset=utf-8"
21
20
  COUNT_METRIC_ITEMS_DICT = TypeVar("COUNT_METRIC_ITEMS_DICT", str, List[str])
21
+
22
+ # TODO - I believe these can be adjusted via RuntimeConfig, they can't be constants
22
23
  MAX_MINT_LINES_PER_REQUEST = 1000
24
+ MAX_LOG_EVENTS_PER_REQUEST = 50_000
25
+ MAX_LOG_REQUEST_SIZE = 5_000_000 # actually 5_242_880
26
+ MAX_METRIC_REQUEST_SIZE = 1_000_000 # actually 1_048_576
27
+
23
28
  HTTP_BAD_REQUEST = 400
24
29
 
25
30
 
@@ -92,7 +97,7 @@ class CommunicationClient(ABC):
92
97
  pass
93
98
 
94
99
  @abstractmethod
95
- def send_events(self, event: dict | list[dict], eec_enrichment: bool) -> dict | None:
100
+ def send_events(self, event: dict | list[dict], eec_enrichment: bool) -> list[Union[dict | None]]:
96
101
  pass
97
102
 
98
103
  @abstractmethod
@@ -114,8 +119,6 @@ class HttpClient(CommunicationClient):
114
119
  """
115
120
 
116
121
  def __init__(self, base_url: str, datasource_id: str, id_token_file_path: str, logger: logging.Logger):
117
- # TODO - Do we need to replace 127.0.0.1 with localhost?
118
-
119
122
  self._activation_config_url = f"{base_url}/userconfig/{datasource_id}"
120
123
  self._extension_config_url = f"{base_url}/extconfig/{datasource_id}"
121
124
  self._metric_url = f"{base_url}/mint/{datasource_id}"
@@ -261,41 +264,40 @@ class HttpClient(CommunicationClient):
261
264
  return self.send_status(Status())
262
265
 
263
266
  def send_metrics(self, mint_lines: list[str]) -> list[MintResponse]:
264
- total_lines = len(mint_lines)
265
- lines_sent = 0
266
-
267
- self.logger.debug(f"Start sending {total_lines} metrics to the EEC")
268
267
  responses = []
269
268
 
270
- # We divide into chunks of MAX_MINT_LINES_PER_REQUEST lines to avoid hitting the body size limit
271
- chunks = divide_into_chunks(mint_lines, MAX_MINT_LINES_PER_REQUEST)
272
-
273
- for chunk in chunks:
274
- lines_in_chunk = len(chunk)
275
- lines_sent += lines_in_chunk
276
- self.logger.debug(f"Sending chunk with {lines_in_chunk} metric lines. ({lines_sent}/{total_lines})")
277
- mint_data = "\n".join(chunk).encode("utf-8")
269
+ # We divide into batches of MAX_METRIC_REQUEST_SIZE bytes to avoid hitting the body size limit
270
+ batches = divide_into_batches(mint_lines, MAX_METRIC_REQUEST_SIZE, "\n")
271
+ for batch in batches:
278
272
  response = self._make_request(
279
- self._metric_url, "POST", mint_data, extra_headers={"Content-Type": CONTENT_TYPE_PLAIN}
273
+ self._metric_url, "POST", batch, extra_headers={"Content-Type": CONTENT_TYPE_PLAIN}
280
274
  ).json()
281
275
  self.logger.debug(f"{self._metric_url}: {response}")
282
276
  mint_response = MintResponse.from_json(response)
283
277
  responses.append(mint_response)
284
278
  return responses
285
279
 
286
- def send_events(self, events: dict | list[dict], eec_enrichment: bool = True) -> dict | None:
280
+ def send_events(self, events: dict | list[dict], eec_enrichment: bool = True) -> list[dict | None]:
287
281
  self.logger.debug(f"Sending log events: {events}")
288
- event_data = json.dumps(events).encode("utf-8")
289
- try:
290
- # EEC returns empty body on success
291
- return self._make_request(
292
- self._events_url,
293
- "POST",
294
- event_data,
295
- extra_headers={"Content-Type": CONTENT_TYPE_JSON, "eec-enrichment": str(eec_enrichment).lower()},
296
- ).json()
297
- except json.JSONDecodeError:
298
- return None
282
+
283
+ responses = []
284
+ if isinstance(events, dict):
285
+ events = [events]
286
+ batches = divide_into_batches(events, MAX_LOG_REQUEST_SIZE)
287
+
288
+ for batch in batches:
289
+ try:
290
+ eec_response = self._make_request(
291
+ self._events_url,
292
+ "POST",
293
+ batch,
294
+ extra_headers={"Content-Type": CONTENT_TYPE_JSON, "eec-enrichment": str(eec_enrichment).lower()},
295
+ ).json()
296
+ responses.append(eec_response)
297
+ except json.JSONDecodeError:
298
+ responses.append(None)
299
+
300
+ return responses
299
301
 
300
302
  def send_sfm_metrics(self, mint_lines: list[str]) -> MintResponse:
301
303
  mint_data = "\n".join(mint_lines).encode("utf-8")
@@ -324,6 +326,7 @@ class DebugClient(CommunicationClient):
324
326
  logger: logging.Logger,
325
327
  local_ingest: bool = False,
326
328
  local_ingest_port: int = 14499,
329
+ print_metrics: bool = True
327
330
  ):
328
331
  self.activation_config = {}
329
332
  if activation_config_path and Path(activation_config_path).exists():
@@ -339,6 +342,7 @@ class DebugClient(CommunicationClient):
339
342
  self.logger = logger
340
343
  self.local_ingest = local_ingest
341
344
  self.local_ingest_port = local_ingest_port
345
+ self.print_metrics = print_metrics
342
346
 
343
347
  def get_activation_config(self) -> dict:
344
348
  return self.activation_config
@@ -386,28 +390,35 @@ class DebugClient(CommunicationClient):
386
390
  return self.send_status(Status())
387
391
 
388
392
  def send_metrics(self, mint_lines: list[str]) -> list[MintResponse]:
393
+ total_lines = len(mint_lines)
394
+ self.logger.info(f"Start sending {total_lines} metrics to the EEC")
395
+
389
396
  responses = []
390
- for line in mint_lines:
391
- self.logger.info(f"send_metric: {line}")
392
397
 
393
- if self.local_ingest:
394
- mint_data = "\n".join(mint_lines).encode("utf-8")
395
- response = request(
396
- "POST",
397
- f"http://localhost:{self.local_ingest_port}/metrics/ingest",
398
- body=mint_data,
399
- headers={"Content-Type": CONTENT_TYPE_PLAIN},
400
- ).json()
401
- mint_response = MintResponse.from_json(response)
402
- responses.append(mint_response)
398
+ batches = divide_into_batches(mint_lines, MAX_METRIC_REQUEST_SIZE)
399
+ for batch in batches:
400
+ if self.local_ingest:
401
+ response = request(
402
+ "POST",
403
+ f"http://localhost:{self.local_ingest_port}/metrics/ingest",
404
+ body=batch,
405
+ headers={"Content-Type": CONTENT_TYPE_PLAIN},
406
+ ).json()
407
+ mint_response = MintResponse.from_json(response)
408
+ responses.append(mint_response)
409
+ else:
410
+ if self.print_metrics:
411
+ for line in mint_lines:
412
+ self.logger.info(f"send_metric: {line}")
403
413
 
404
- if not responses:
405
- responses = [MintResponse(lines_invalid=0, lines_ok=len(mint_lines), error=None, warnings=None)]
406
414
  return responses
407
415
 
408
- def send_events(self, events: dict | list[dict], eec_enrichment: bool = True) -> dict | None:
409
- self.logger.info(f"send_events (enrichment = {eec_enrichment}): {events}")
410
- return None
416
+ def send_events(self, events: dict | list[dict], eec_enrichment: bool = True) -> list[dict | None]:
417
+ self.logger.info(f"send_events (enrichment = {eec_enrichment}): {len(events)} events")
418
+ if self.print_metrics:
419
+ for event in events:
420
+ self.logger.info(f"send_event: {event}")
421
+ return []
411
422
 
412
423
  def send_sfm_metrics(self, mint_lines: list[str]) -> MintResponse:
413
424
  for line in mint_lines:
@@ -418,20 +429,33 @@ class DebugClient(CommunicationClient):
418
429
  return 0
419
430
 
420
431
 
421
- def divide_into_chunks(iterable: Iterable, chunk_size: int) -> Iterable:
432
+ def divide_into_batches(items: Sequence[dict | str], max_size_bytes: int, join_with: str | None = None) -> Generator[bytes, None, None]:
422
433
  """
423
- Yield successive n-sized chunks from iterable.
424
- Example: _chunk([1, 2, 3, 4, 5, 6, 7, 8, 9], 3) -> [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
434
+ Yield successive batches from a list, according to sizing limitations
425
435
 
426
- :param iterable: The iterable to chunk
427
- :param chunk_size: The size of the chunks
436
+ :param items: The list items to divide, they myst be encodable to bytes
437
+ :param max_size_bytes: The maximum size of the payload in bytes
438
+ :param join_with: A string to join the items with before encoding
439
+ :return: A generator of batches of log events already encoded
428
440
  """
429
- iterator = iter(iterable)
430
- while True:
431
- subset = list(islice(iterator, chunk_size))
432
- if not subset:
433
- return
434
- yield subset
441
+
442
+ if not items:
443
+ return
444
+
445
+ if join_with is not None:
446
+ items = join_with.join(items)
447
+ encoded = f"{items}".encode(errors="replace")
448
+ size = len(encoded)
449
+ if size <= max_size_bytes:
450
+ yield encoded
451
+ return
452
+
453
+ # if we get here, the payload is too large, split it in half until we have chunks that are small enough
454
+ half = len(items) // 2
455
+ first_half = items[:half]
456
+ second_half = items[half:]
457
+ yield from divide_into_batches(first_half, max_size_bytes)
458
+ yield from divide_into_batches(second_half, max_size_bytes)
435
459
 
436
460
 
437
461
  @dataclass
@@ -9,6 +9,7 @@ import sys
9
9
  import threading
10
10
  import time
11
11
  from argparse import ArgumentParser
12
+ from collections import deque
12
13
  from concurrent.futures import ThreadPoolExecutor
13
14
  from datetime import datetime, timedelta, timezone
14
15
  from enum import Enum
@@ -79,14 +80,15 @@ class DtEventType(str, Enum):
79
80
  https://docs.dynatrace.com/docs/dynatrace-api/environment-api/events-v2/post-event
80
81
  """
81
82
 
83
+ AVAILABILITY_EVENT = "AVAILABILITY_EVENT"
82
84
  CUSTOM_INFO = "CUSTOM_INFO"
83
85
  CUSTOM_ALERT = "CUSTOM_ALERT"
84
86
  CUSTOM_ANNOTATION = "CUSTOM_ANNOTATION"
85
87
  CUSTOM_CONFIGURATION = "CUSTOM_CONFIGURATION"
86
88
  CUSTOM_DEPLOYMENT = "CUSTOM_DEPLOYMENT"
87
- MARKED_FOR_TERMINATION = "MARKED_FOR_TERMINATION"
88
89
  ERROR_EVENT = "ERROR_EVENT"
89
- AVAILABILITY_EVENT = "AVAILABILITY_EVENT"
90
+ MARKED_FOR_TERMINATION = "MARKED_FOR_TERMINATION"
91
+ PERFORMANCE_EVENT = "PERFORMANCE_EVENT"
90
92
  RESOURCE_CONTENTION_EVENT = "RESOURCE_CONTENTION_EVENT"
91
93
 
92
94
 
@@ -159,8 +161,6 @@ class Extension:
159
161
  if hasattr(self, "logger"):
160
162
  return
161
163
 
162
- # TODO - Move the logging implementation to its own file
163
- # TODO - Add sfm logging
164
164
  self.logger = extension_logger
165
165
 
166
166
  self.extension_config: str = ""
@@ -313,7 +313,6 @@ class Extension:
313
313
  api_logger.debug(f"Scheduling callback {callback}")
314
314
 
315
315
  # These properties are updated after the extension starts
316
- # TODO - These should be part of an ext singleton object instead
317
316
  callback.cluster_time_diff = self._cluster_time_diff
318
317
  callback.running_in_sim = self._running_in_sim
319
318
  self._scheduled_callbacks.append(callback)
@@ -698,14 +697,21 @@ class Extension:
698
697
  # Debug parameters, these are used when running the extension locally
699
698
  parser.add_argument("--extensionconfig", required=False, default=None)
700
699
  parser.add_argument("--activationconfig", required=False, default="activation.json")
700
+ parser.add_argument("--no-print-metrics", required=False, action="store_true")
701
701
 
702
702
  args, unknown = parser.parse_known_args()
703
703
  self._is_fastcheck = args.fastcheck
704
704
  if args.dsid is None:
705
705
  # DEV mode
706
706
  self._running_in_sim = True
707
+ print_metrics = not args.no_print_metrics
707
708
  self._client = DebugClient(
708
- args.activationconfig, args.extensionconfig, api_logger, args.local_ingest, args.local_ingest_port
709
+ activation_config_path=args.activationconfig,
710
+ extension_config_path=args.extensionconfig,
711
+ logger=api_logger,
712
+ local_ingest=args.local_ingest,
713
+ local_ingest_port=args.local_ingest_port,
714
+ print_metrics=print_metrics
709
715
  )
710
716
  RuntimeProperties.set_default_log_level(args.loglevel)
711
717
  else:
@@ -769,7 +775,7 @@ class Extension:
769
775
  current_thread_id = threading.get_ident()
770
776
  self._running_callbacks[current_thread_id] = callback
771
777
 
772
- callback(self.activation_config, self.extension_config)
778
+ callback()
773
779
 
774
780
  with self._sfm_metrics_lock:
775
781
  self._callbackSfmReport[callback.name()] = callback
@@ -810,23 +816,22 @@ class Extension:
810
816
  self._scheduler.enter(SFM_METRIC_SENDING_INTERVAL.total_seconds(), 1, self._sfm_metrics_iteration)
811
817
 
812
818
  def _send_metrics(self):
813
- # TODO - we might need to check size and number of lines before sending
814
- # Maybe break it down into multiple packets
815
- with self._metrics_lock and self._internal_callbacks_results_lock:
816
- if self._metrics:
817
- number_of_metrics = len(self._metrics)
818
- responses = self._client.send_metrics(self._metrics)
819
-
820
- self._internal_callbacks_results[self._send_metrics.__name__] = Status(StatusValue.OK)
821
- lines_invalid = sum(response.lines_invalid for response in responses)
822
- if lines_invalid > 0:
823
- message = f"{lines_invalid} invalid metric lines found"
824
- self._internal_callbacks_results[self._send_metrics.__name__] = Status(
825
- StatusValue.GENERIC_ERROR, message
826
- )
827
-
828
- api_logger.info(f"Sent {number_of_metrics} metric lines to EEC: {responses}")
829
- self._metrics = []
819
+ with self._metrics_lock:
820
+ with self._internal_callbacks_results_lock:
821
+ if self._metrics:
822
+ number_of_metrics = len(self._metrics)
823
+ responses = self._client.send_metrics(self._metrics)
824
+
825
+ self._internal_callbacks_results[self._send_metrics.__name__] = Status(StatusValue.OK)
826
+ lines_invalid = sum(response.lines_invalid for response in responses)
827
+ if lines_invalid > 0:
828
+ message = f"{lines_invalid} invalid metric lines found"
829
+ self._internal_callbacks_results[self._send_metrics.__name__] = Status(
830
+ StatusValue.GENERIC_ERROR, message
831
+ )
832
+
833
+ api_logger.info(f"Sent {number_of_metrics} metric lines to EEC: {responses}")
834
+ self._metrics = []
830
835
 
831
836
  def _prepare_sfm_metrics(self) -> List[str]:
832
837
  """Prepare self monitoring metrics.
@@ -992,14 +997,21 @@ class Extension:
992
997
  self._metrics.extend(lines)
993
998
 
994
999
  def _send_events_internal(self, events: Union[dict, List[dict]]):
995
- response = self._client.send_events(events, self.log_event_enrichment)
996
- with self._internal_callbacks_results_lock:
997
- self._internal_callbacks_results[self._send_events.__name__] = Status(StatusValue.OK)
998
- if not response or "error" not in response or "message" not in response["error"]:
999
- return
1000
- self._internal_callbacks_results[self._send_events.__name__] = Status(
1001
- StatusValue.GENERIC_ERROR, response["error"]["message"]
1002
- )
1000
+ try:
1001
+ responses = self._client.send_events(events, self.log_event_enrichment)
1002
+
1003
+ for response in responses:
1004
+ with self._internal_callbacks_results_lock:
1005
+ self._internal_callbacks_results[self._send_events.__name__] = Status(StatusValue.OK)
1006
+ if not response or "error" not in response or "message" not in response["error"]:
1007
+ return
1008
+ self._internal_callbacks_results[self._send_events.__name__] = Status(
1009
+ StatusValue.GENERIC_ERROR, response["error"]["message"]
1010
+ )
1011
+ except Exception as e:
1012
+ api_logger.error(f"Error sending events: {e!r}", exc_info=True)
1013
+ with self._internal_callbacks_results_lock:
1014
+ self._internal_callbacks_results[self._send_events.__name__] = Status(StatusValue.GENERIC_ERROR, str(e))
1003
1015
 
1004
1016
  def _send_events(self, events: Union[dict, List[dict]]):
1005
1017
  self._internal_executor.submit(self._send_events_internal, events)
@@ -1008,9 +1020,8 @@ class Extension:
1008
1020
  self._client.send_dt_event(event)
1009
1021
 
1010
1022
  def get_version(self) -> str:
1011
- """Return the version of extensions sdk library."""
1012
-
1013
- return __version__
1023
+ """Return the extension version."""
1024
+ return self.activation_config.version
1014
1025
 
1015
1026
  @property
1016
1027
  def techrule(self) -> str:
@@ -49,6 +49,7 @@ dependencies = [
49
49
  "pytest",
50
50
  "typer[all]",
51
51
  "pyyaml",
52
+ "dt-cli>=1.6.13"
52
53
  ]
53
54
 
54
55
  [tool.hatch.envs.default.scripts]
@@ -101,6 +102,7 @@ dependencies = [
101
102
  "dt-cli",
102
103
  "typer[all]",
103
104
  "pyyaml",
105
+ "tomli"
104
106
  ]
105
107
 
106
108
  [tool.hatch.envs.docs.env-vars]
@@ -19,20 +19,15 @@ class TestDtSdk(TestCase):
19
19
  shutil.rmtree(self.temp_dir, ignore_errors=True)
20
20
  pass
21
21
 
22
- @patch("dynatrace_extension.cli.main.subprocess.run")
22
+ @patch("dynatrace_extension.cli.main.dt_cli_upload")
23
+ @patch("dynatrace_extension.cli.main.dt_cli_validate")
23
24
  @patch("builtins.open", mock_open(read_data=SAMPLE_EXTENSION_DATA))
24
- def test_dt_sdk_upload(self, mock_subprocess_run):
25
+ def test_dt_sdk_upload(self, mock_upload, mock_validate):
25
26
  extension_path = Path("test_extension_dir")
26
27
  tenant_url = "test_tenant_url"
27
28
  api_token = "test_api_token"
28
29
 
29
- dt_sdk.upload(extension_path, tenant_url, api_token)
30
- mock_subprocess_run.assert_called_once_with(
31
- ["dt", "ext", "upload", "--tenant-url", tenant_url, "--api-token", api_token, f"{extension_path}"],
32
- cwd=None,
33
- env=None,
34
- check=True,
35
- )
30
+ dt_sdk.upload(extension_path, tenant_url, api_token, validate=False)
36
31
 
37
32
  @patch("dynatrace_extension.cli.main.subprocess.run")
38
33
  def test_dt_sdk_gen_certs(self, mock_subprocess_run: NonCallableMock):
@@ -102,6 +97,7 @@ class TestDtSdk(TestCase):
102
97
  target_directory=None,
103
98
  extra_platforms=None,
104
99
  extra_index_url=None,
100
+ find_links=None,
105
101
  )
106
102
 
107
103
  # Check that the built extension file exists
@@ -8,7 +8,7 @@ from dynatrace_extension.cli.schema import ExtensionYaml
8
8
  VALID_YAML = """
9
9
  name: custom:mulesoft-cloudhub
10
10
  version: 0.0.1
11
- minDynatraceVersion: "1.902"
11
+ minDynatraceVersion: "1.285"
12
12
  author:
13
13
  name: "Dynatrace"
14
14
 
@@ -16,7 +16,7 @@ python:
16
16
  runtime:
17
17
  module: mulesoft_cloudhub
18
18
  version:
19
- min: "3.9"
19
+ min: "3.10"
20
20
 
21
21
  activation:
22
22
  remote:
@@ -38,7 +38,7 @@ class TestTypes(TestCase):
38
38
  assert extension.min_dynatrace_version == "1.902"
39
39
  assert extension.author.name == "Dynatrace"
40
40
  assert extension.python.runtime.module == "mulesoft_cloudhub"
41
- assert extension.python.runtime.version.min_version == "3.9"
41
+ assert extension.python.runtime.version.min_version == "3.10"
42
42
  assert extension.python.activation.remote.path == "activationSchema.json"
43
43
  assert extension.python.activation.local is None
44
44
 
@@ -0,0 +1,75 @@
1
+ import unittest
2
+ from unittest.mock import MagicMock, mock_open, patch
3
+
4
+ from dynatrace_extension.sdk.communication import MAX_LOG_REQUEST_SIZE, MAX_METRIC_REQUEST_SIZE, HttpClient, divide_into_batches
5
+
6
+
7
+ class TestCommunication(unittest.TestCase):
8
+ @patch("builtins.open", mock_open(read_data="test_token"))
9
+ @patch.object(HttpClient, "_make_request", return_value=MagicMock())
10
+ def test_http_client_metric_report(self, mock_make_request):
11
+ http_client = HttpClient("https://localhost:9999", "1", "token", MagicMock())
12
+ few_metrics = ["metric 1", "metric 2"]
13
+ responses = http_client.send_metrics(few_metrics)
14
+ self.assertEqual(len(responses), 1)
15
+
16
+ many_metrics = ['my.metric,dim="dim" 10'] * 500 * 100
17
+ responses = http_client.send_metrics(many_metrics)
18
+ self.assertEqual(len(responses), 2)
19
+
20
+ no_metrics = []
21
+ responses = http_client.send_metrics(no_metrics)
22
+ self.assertEqual(len(responses), 0)
23
+
24
+ def test_large_log_chunk(self):
25
+
26
+ # This is 14_660_000 bytes
27
+ events = []
28
+ for i in range(5000):
29
+ attributes = {}
30
+ for j in range(150):
31
+ attributes[f"attribute{j}"] = j
32
+ events.append(attributes)
33
+
34
+ # it needs to be divided into 4 lists, each with 3_665_000 bytes
35
+ chunks = list(divide_into_batches(events, MAX_LOG_REQUEST_SIZE))
36
+ self.assertEqual(len(chunks), 4)
37
+ self.assertEqual(len(chunks[0]), 3665000)
38
+ self.assertEqual(len(chunks[1]), 3665000)
39
+ self.assertEqual(len(chunks[2]), 3665000)
40
+ self.assertEqual(len(chunks[3]), 3665000)
41
+
42
+ def test_small_log_chunk(self):
43
+ events = []
44
+ for i in range(10):
45
+ attributes = {}
46
+ for j in range(10):
47
+ attributes[f"attribute{j}"] = j
48
+ events.append(attributes)
49
+
50
+ chunks = list(divide_into_batches(events, MAX_LOG_REQUEST_SIZE))
51
+ self.assertEqual(len(chunks), 1)
52
+ self.assertEqual(len(chunks[0]), 1720)
53
+
54
+ def test_large_metric_chunk(self):
55
+
56
+ metrics = ['my.metric,dim="dim" 10'] * 500 * 100 # 1_300_000 bytes, but becomes 1_149_999 with the newlines
57
+
58
+ # it needs to be divided into 2 lists, each with 650_000 bytes
59
+ chunks = list(divide_into_batches(metrics, MAX_METRIC_REQUEST_SIZE, "\n"))
60
+ self.assertEqual(len(chunks), 2)
61
+ self.assertEqual(len(chunks[0]), 574999)
62
+ self.assertEqual(len(chunks[1]), 575000)
63
+
64
+ def test_small_metric_chunk(self):
65
+ metrics = ['my.metric,dim="dim" 10'] * 100
66
+
67
+ chunks = list(divide_into_batches(metrics, MAX_METRIC_REQUEST_SIZE, "\n"))
68
+ self.assertEqual(len(chunks), 1)
69
+ self.assertEqual(len(chunks[0]), 2299)
70
+
71
+ def test_no_metrics(self):
72
+ metrics = []
73
+
74
+ chunks = list(divide_into_batches(metrics, MAX_METRIC_REQUEST_SIZE, "\n"))
75
+ self.assertEqual(len(chunks), 0)
@@ -1,12 +0,0 @@
1
- from setuptools import setup, find_packages
2
-
3
- setup(name="%extension_name%",
4
- version="0.0.1",
5
- description="%Extension_Name% python EF2 extension",
6
- author="Dynatrace",
7
- packages=find_packages(),
8
- python_requires=">=3.10",
9
- include_package_data=True,
10
- install_requires=["dt-extensions-sdk"],
11
- extras_require={"dev": ["dt-extensions-sdk[cli]"]},
12
- )
@@ -1,35 +0,0 @@
1
- import unittest
2
- from unittest.mock import MagicMock, mock_open, patch
3
-
4
- from dynatrace_extension.sdk.communication import HttpClient, divide_into_chunks
5
-
6
-
7
- class TestCommunication(unittest.TestCase):
8
- def test_large_chunk(self):
9
- large_list = ["metric 1"] * 1400
10
- chunks = list(divide_into_chunks(large_list, 1000))
11
- self.assertEqual(len(chunks), 2)
12
- self.assertEqual(len(chunks[0]), 1000)
13
- self.assertEqual(len(chunks[1]), 400)
14
-
15
- def test_small_chunk(self):
16
- small_list = ["metric 1"] * 10
17
- chunks = list(divide_into_chunks(small_list, 1000))
18
- self.assertEqual(len(chunks), 1)
19
- self.assertEqual(len(chunks[0]), 10)
20
-
21
- @patch("builtins.open", mock_open(read_data="test_token"))
22
- @patch.object(HttpClient, "_make_request", return_value=MagicMock())
23
- def test_http_client_metric_report(self, mock_make_request):
24
- http_client = HttpClient("https://localhost:9999", "1", "token", MagicMock())
25
- few_metrics = ["metric 1", "metric 2"]
26
- responses = http_client.send_metrics(few_metrics)
27
- self.assertEqual(len(responses), 1)
28
-
29
- many_metrics = ["metric 1"] * 1400
30
- responses = http_client.send_metrics(many_metrics)
31
- self.assertEqual(len(responses), 2)
32
-
33
- no_metrics = []
34
- responses = http_client.send_metrics(no_metrics)
35
- self.assertEqual(len(responses), 0)