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.
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/PKG-INFO +1 -1
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/conf.py +1 -1
- dt_extensions_sdk-1.1.20/docs/guides/building.rst +129 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/guides/extension_structure.rst +2 -2
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/index.rst +1 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/__about__.py +2 -1
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/extension_template/extension/extension.yaml.template +3 -3
- dt_extensions_sdk-1.1.20/dynatrace_extension/cli/create/extension_template/setup.py.template +28 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/main.py +15 -1
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/callback.py +5 -12
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/communication.py +82 -58
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/extension.py +46 -35
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/pyproject.toml +2 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/cli/test_dt_sdk.py +5 -9
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/cli/test_types.py +3 -3
- dt_extensions_sdk-1.1.20/tests/sdk/test_communication.py +75 -0
- dt_extensions_sdk-1.1.10/dynatrace_extension/cli/create/extension_template/setup.py.template +0 -12
- dt_extensions_sdk-1.1.10/tests/sdk/test_communication.py +0 -35
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/.github/workflows/gh-pages-docs.yml +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/.github/workflows/publish.yml +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/.gitignore +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/LICENSE.txt +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/README.md +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/dt-sdk-header.png +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/dt-sdk-logo.png +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/favicon.ico +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-01-new-extension.png +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-02-type.png +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-03-import.png +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-04-import-remote.png +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-05-activation.png +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-06-activation-config.png +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/api/events/event_severity.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/api/events/event_type.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/api/events/index.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/api/extension.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/api/metrics/index.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/api/metrics/metric.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/api/metrics/metric_type.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/assemble.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/build.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/create.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/gencerts.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/help.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/run.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/sign.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/upload.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/cli/wheel.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/guides/installation.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/guides/migration.rst +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/requirements.txt +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/__init__.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/__init__.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/__init__.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/create.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/extension_template/.gitignore.template +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/extension_template/README.md.template +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/extension_template/activation.json.template +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/extension_template/extension/activationSchema.json.template +0 -0
- {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
- {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
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/schema.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/__init__.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/activation.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/event.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/helper.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/metric.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/runtime.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/vendor/__init__.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/vendor/mureq/LICENSE +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/vendor/mureq/__init__.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/vendor/mureq/mureq.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/__init__.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/cli/__init__.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/cli/test_templates.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/sdk/__init__.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/sdk/test_activation.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/sdk/test_callback.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/sdk/test_extension.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/sdk/test_metric.py +0 -0
- {dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/tests/sdk/test_runtime_properties.py +0 -0
- {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.
|
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.
|
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.
|
44
|
+
min: "3.10"
|
45
45
|
|
46
46
|
activation:
|
47
47
|
remote:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
name: %extension-prefix%%extension-name%
|
2
2
|
version: 0.0.1
|
3
|
-
minDynatraceVersion: "1.
|
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.
|
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[
|
23
|
-
kwargs: Optional[
|
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
|
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
|
-
|
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
|
{dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/communication.py
RENAMED
@@ -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,
|
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
|
271
|
-
|
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",
|
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
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
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
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
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
|
-
|
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
|
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
|
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
|
427
|
-
:param
|
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
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
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
|
-
|
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,
|
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(
|
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
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
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
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
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
|
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.
|
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,
|
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.
|
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.
|
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.
|
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)
|
dt_extensions_sdk-1.1.10/dynatrace_extension/cli/create/extension_template/setup.py.template
DELETED
@@ -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)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-01-new-extension.png
RENAMED
File without changes
|
File without changes
|
{dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-03-import.png
RENAMED
File without changes
|
{dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-04-import-remote.png
RENAMED
File without changes
|
{dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/docs/_static/img/migrate-05-activation.png
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/__init__.py
RENAMED
File without changes
|
{dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/cli/create/create.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/vendor/__init__.py
RENAMED
File without changes
|
{dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/vendor/mureq/LICENSE
RENAMED
File without changes
|
File without changes
|
{dt_extensions_sdk-1.1.10 → dt_extensions_sdk-1.1.20}/dynatrace_extension/sdk/vendor/mureq/mureq.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|