oteltest 0.0.1__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.
@@ -0,0 +1,68 @@
1
+ *.py[cod]
2
+ *.sw[op]
3
+
4
+ # C extensions
5
+ *.so
6
+
7
+ # Packages
8
+ *.egg
9
+ *.egg-info
10
+ dist
11
+ build
12
+ eggs
13
+ parts
14
+ bin
15
+ include
16
+ var
17
+ sdist
18
+ develop-eggs
19
+ .installed.cfg
20
+ pyvenv.cfg
21
+ lib
22
+ share/
23
+ lib64
24
+ __pycache__
25
+ venv*/
26
+ .venv*/
27
+
28
+ # Installer logs
29
+ pip-log.txt
30
+
31
+ # Unit test / coverage reports
32
+ coverage.xml
33
+ .coverage
34
+ .nox
35
+ .tox
36
+ .cache
37
+ htmlcov
38
+
39
+ # Translations
40
+ *.mo
41
+
42
+ # Mac
43
+ .DS_Store
44
+
45
+ # Mr Developer
46
+ .mr.developer.cfg
47
+ .project
48
+ .pydevproject
49
+
50
+ # JetBrains
51
+ .idea
52
+
53
+ # VSCode
54
+ .vscode
55
+
56
+ # Sphinx
57
+ _build/
58
+
59
+ # mypy
60
+ .mypy_cache/
61
+ target
62
+
63
+ # Django example
64
+
65
+ docs/examples/django/db.sqlite3
66
+
67
+ # Semantic conventions
68
+ scripts/semconv/semantic-conventions
@@ -0,0 +1,13 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.3
2
+ Name: oteltest
3
+ Version: 0.0.1
4
+ Project-URL: Documentation, https://github.com/open-telemetry/opentelemetry-python#readme
5
+ Project-URL: Issues, https://github.com/open-telemetry/opentelemetry-python/issues
6
+ Project-URL: Source, https://github.com/open-telemetry/opentelemetry-python/
7
+ Author-email: OpenTelemetry Authors <cncf-opentelemetry-contributors@lists.cncf.io>
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE.txt
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3.7
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: Implementation :: CPython
18
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
19
+ Requires-Python: >=3.7
20
+ Requires-Dist: grpcio
21
+ Requires-Dist: opentelemetry-api
22
+ Requires-Dist: opentelemetry-proto
23
+ Requires-Dist: protobuf
24
+ Description-Content-Type: text/markdown
25
+
26
+ # oteltest
27
+
28
+ [![PyPI - Version](https://img.shields.io/pypi/v/oteltest.svg)](https://pypi.org/project/oteltest)
29
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/oteltest.svg)](https://pypi.org/project/oteltest)
30
+
31
+ -----
32
+
33
+ **Table of Contents**
34
+
35
+ - [Installation](#installation)
36
+ - [License](#license)
37
+
38
+ ## Installation
39
+
40
+ ```console
41
+ pip install oteltest
42
+ ```
43
+
44
+ ## Overview
45
+
46
+ The `oteltest` package contains utilities for testing OpenTelemetry Python.
47
+
48
+ ### oteltest
49
+
50
+ `oteltest` is a CLI command which you can use to run black box tests against OpenTelemetry Python scripts.
51
+
52
+ #### Execution
53
+
54
+ You can run `oteltest` as a shell command and provide as an argument either a file:
55
+
56
+ ```shell
57
+ oteltest my_script.py
58
+ ```
59
+
60
+ or a directory:
61
+
62
+ ```shell
63
+ oteltest my_script_dir
64
+ ```
65
+
66
+ Given a directory, the `oteltest` command will attempt to run all `oteltest`-runnable scripts in `my_script_dir`,
67
+ non-recursively.
68
+
69
+ #### Operation
70
+
71
+ Running `oteltest` against `my_script.py` 1) starts a Python OTLP listener, 2) creates a new Python virtual
72
+ environment, 3) installs any `requirements()`, 4) starts a subprocess with any requested `wrapper_script()`
73
+ and `environment_variables()`, 5) waits for `my_script.py` to complete, 6) stops the OTLP listener, and finally 7) sends
74
+ the listener's telemetry received back to `my_script.py` in the form of a call to `validate(telemetry)`. It also writes
75
+ a `.json` file with that telemetry next to the script (with the same name but `.json` extension).
76
+
77
+ #### Scripts
78
+
79
+ For a Python script to be runnable by `oteltest`, it must both be executable and define an implementation of
80
+ `OtelTest`. The script below has an implementation called `MyTest`:
81
+
82
+ ```python
83
+ import time
84
+ from opentelemetry import trace
85
+ from oteltest.common import OtelTest, Telemetry
86
+
87
+ SERVICE_NAME = "my-test"
88
+ NUM_ADDS = 12
89
+
90
+ if __name__ == "__main__":
91
+ tracer = trace.get_tracer("my-tracer")
92
+ for i in range(NUM_ADDS):
93
+ with tracer.start_as_current_span("my-span"):
94
+ print(f"{i + 1}/{NUM_ADDS}")
95
+ time.sleep(0.5)
96
+
97
+
98
+ class MyTest(OtelTest):
99
+ def requirements(self):
100
+ return "opentelemetry-distro", "opentelemetry-exporter-otlp-proto-grpc"
101
+
102
+ def wrapper_script(self):
103
+ return "opentelemetry-instrument"
104
+
105
+ def environment_variables(self):
106
+ return {"OTEL_SERVICE_NAME": SERVICE_NAME}
107
+
108
+ def validate(self, telemetry: Telemetry):
109
+ assert telemetry.num_spans() == NUM_ADDS
110
+ ```
111
+
112
+ #### Usage with OTel Examples
113
+
114
+ Runnable examples are a great way for new users to get up to speed, but it would be nice to give them an opportunity
115
+ to see the telemetry they just generated. Adding an `OtelTest` implementation (e.g. `MyTest` above) to existing
116
+ example scripts lets users run examples without having to set up Python environments with dependencies. It also lets
117
+ them potentially see the output of their instrumented script.
118
+
119
+ ### otelsink
120
+
121
+ `otelsink` is a gRPC server that listens for OTel metrics, traces, and logs.
122
+
123
+ #### Operation
124
+
125
+ You can run it either from the command line by using the `otelsink` command (installed when you
126
+ `pip install oteltest`), or programatically.
127
+
128
+ Either way, `otelsink` runs a gRPC server listening on 0.0.0.0:4317.
129
+
130
+ #### Command Line
131
+
132
+ ```
133
+ % otelsink
134
+ starting otelsink with print handler
135
+ ```
136
+
137
+ #### Programmatic
138
+
139
+ ```
140
+ from oteltest.sink import GrpcSink, PrintHandler
141
+
142
+ class MyHandler(RequestHandler):
143
+ def handle_logs(self, request, context):
144
+ print(f"received log request: {request}")
145
+
146
+ def handle_metrics(self, request, context):
147
+ print(f"received metrics request: {request}")
148
+
149
+ def handle_trace(self, request, context):
150
+ print(f"received trace request: {request}")
151
+
152
+
153
+ sink = GrpcSink(MyHandler())
154
+ sink.start()
155
+ sink.wait_for_termination()
156
+ ```
157
+
158
+ ## License
159
+
160
+ `oteltest` is distributed under the terms of the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) license.
@@ -0,0 +1,135 @@
1
+ # oteltest
2
+
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/oteltest.svg)](https://pypi.org/project/oteltest)
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/oteltest.svg)](https://pypi.org/project/oteltest)
5
+
6
+ -----
7
+
8
+ **Table of Contents**
9
+
10
+ - [Installation](#installation)
11
+ - [License](#license)
12
+
13
+ ## Installation
14
+
15
+ ```console
16
+ pip install oteltest
17
+ ```
18
+
19
+ ## Overview
20
+
21
+ The `oteltest` package contains utilities for testing OpenTelemetry Python.
22
+
23
+ ### oteltest
24
+
25
+ `oteltest` is a CLI command which you can use to run black box tests against OpenTelemetry Python scripts.
26
+
27
+ #### Execution
28
+
29
+ You can run `oteltest` as a shell command and provide as an argument either a file:
30
+
31
+ ```shell
32
+ oteltest my_script.py
33
+ ```
34
+
35
+ or a directory:
36
+
37
+ ```shell
38
+ oteltest my_script_dir
39
+ ```
40
+
41
+ Given a directory, the `oteltest` command will attempt to run all `oteltest`-runnable scripts in `my_script_dir`,
42
+ non-recursively.
43
+
44
+ #### Operation
45
+
46
+ Running `oteltest` against `my_script.py` 1) starts a Python OTLP listener, 2) creates a new Python virtual
47
+ environment, 3) installs any `requirements()`, 4) starts a subprocess with any requested `wrapper_script()`
48
+ and `environment_variables()`, 5) waits for `my_script.py` to complete, 6) stops the OTLP listener, and finally 7) sends
49
+ the listener's telemetry received back to `my_script.py` in the form of a call to `validate(telemetry)`. It also writes
50
+ a `.json` file with that telemetry next to the script (with the same name but `.json` extension).
51
+
52
+ #### Scripts
53
+
54
+ For a Python script to be runnable by `oteltest`, it must both be executable and define an implementation of
55
+ `OtelTest`. The script below has an implementation called `MyTest`:
56
+
57
+ ```python
58
+ import time
59
+ from opentelemetry import trace
60
+ from oteltest.common import OtelTest, Telemetry
61
+
62
+ SERVICE_NAME = "my-test"
63
+ NUM_ADDS = 12
64
+
65
+ if __name__ == "__main__":
66
+ tracer = trace.get_tracer("my-tracer")
67
+ for i in range(NUM_ADDS):
68
+ with tracer.start_as_current_span("my-span"):
69
+ print(f"{i + 1}/{NUM_ADDS}")
70
+ time.sleep(0.5)
71
+
72
+
73
+ class MyTest(OtelTest):
74
+ def requirements(self):
75
+ return "opentelemetry-distro", "opentelemetry-exporter-otlp-proto-grpc"
76
+
77
+ def wrapper_script(self):
78
+ return "opentelemetry-instrument"
79
+
80
+ def environment_variables(self):
81
+ return {"OTEL_SERVICE_NAME": SERVICE_NAME}
82
+
83
+ def validate(self, telemetry: Telemetry):
84
+ assert telemetry.num_spans() == NUM_ADDS
85
+ ```
86
+
87
+ #### Usage with OTel Examples
88
+
89
+ Runnable examples are a great way for new users to get up to speed, but it would be nice to give them an opportunity
90
+ to see the telemetry they just generated. Adding an `OtelTest` implementation (e.g. `MyTest` above) to existing
91
+ example scripts lets users run examples without having to set up Python environments with dependencies. It also lets
92
+ them potentially see the output of their instrumented script.
93
+
94
+ ### otelsink
95
+
96
+ `otelsink` is a gRPC server that listens for OTel metrics, traces, and logs.
97
+
98
+ #### Operation
99
+
100
+ You can run it either from the command line by using the `otelsink` command (installed when you
101
+ `pip install oteltest`), or programatically.
102
+
103
+ Either way, `otelsink` runs a gRPC server listening on 0.0.0.0:4317.
104
+
105
+ #### Command Line
106
+
107
+ ```
108
+ % otelsink
109
+ starting otelsink with print handler
110
+ ```
111
+
112
+ #### Programmatic
113
+
114
+ ```
115
+ from oteltest.sink import GrpcSink, PrintHandler
116
+
117
+ class MyHandler(RequestHandler):
118
+ def handle_logs(self, request, context):
119
+ print(f"received log request: {request}")
120
+
121
+ def handle_metrics(self, request, context):
122
+ print(f"received metrics request: {request}")
123
+
124
+ def handle_trace(self, request, context):
125
+ print(f"received trace request: {request}")
126
+
127
+
128
+ sink = GrpcSink(MyHandler())
129
+ sink.start()
130
+ sink.wait_for_termination()
131
+ ```
132
+
133
+ ## License
134
+
135
+ `oteltest` is distributed under the terms of the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) license.
@@ -0,0 +1,222 @@
1
+ {
2
+ "log_reqs": [],
3
+ "metric_reqs": [],
4
+ "trace_reqs": [
5
+ {
6
+ "request": {
7
+ "resourceSpans": [
8
+ {
9
+ "resource": {
10
+ "attributes": [
11
+ {
12
+ "key": "telemetry.sdk.language",
13
+ "value": {
14
+ "stringValue": "python"
15
+ }
16
+ },
17
+ {
18
+ "key": "telemetry.sdk.name",
19
+ "value": {
20
+ "stringValue": "opentelemetry"
21
+ }
22
+ },
23
+ {
24
+ "key": "telemetry.sdk.version",
25
+ "value": {
26
+ "stringValue": "1.23.0"
27
+ }
28
+ },
29
+ {
30
+ "key": "service.name",
31
+ "value": {
32
+ "stringValue": "integration-test"
33
+ }
34
+ },
35
+ {
36
+ "key": "telemetry.auto.version",
37
+ "value": {
38
+ "stringValue": "0.44b0"
39
+ }
40
+ }
41
+ ]
42
+ },
43
+ "scopeSpans": [
44
+ {
45
+ "scope": {
46
+ "name": "my-tracer"
47
+ },
48
+ "spans": [
49
+ {
50
+ "traceId": "et4vTTA8/bm5YyjFrF/T7w==",
51
+ "spanId": "zsOz4GIerwc=",
52
+ "name": "my-span",
53
+ "kind": "SPAN_KIND_INTERNAL",
54
+ "startTimeUnixNano": "1711635618595538000",
55
+ "endTimeUnixNano": "1711635619098602000",
56
+ "status": {}
57
+ },
58
+ {
59
+ "traceId": "oQrQAFMZiCEMprV4/LNrwQ==",
60
+ "spanId": "iMUV/jNqC5w=",
61
+ "name": "my-span",
62
+ "kind": "SPAN_KIND_INTERNAL",
63
+ "startTimeUnixNano": "1711635619098867000",
64
+ "endTimeUnixNano": "1711635619603935000",
65
+ "status": {}
66
+ },
67
+ {
68
+ "traceId": "GaHP1buKnyub7z0lFPtltg==",
69
+ "spanId": "UTXTLUVZhoU=",
70
+ "name": "my-span",
71
+ "kind": "SPAN_KIND_INTERNAL",
72
+ "startTimeUnixNano": "1711635619604104000",
73
+ "endTimeUnixNano": "1711635620105109000",
74
+ "status": {}
75
+ },
76
+ {
77
+ "traceId": "oqJOz/B6nzvpq8L3EfcSUQ==",
78
+ "spanId": "WYJ0Kpi5F+Q=",
79
+ "name": "my-span",
80
+ "kind": "SPAN_KIND_INTERNAL",
81
+ "startTimeUnixNano": "1711635620105286000",
82
+ "endTimeUnixNano": "1711635620607148000",
83
+ "status": {}
84
+ },
85
+ {
86
+ "traceId": "dh4hTfwNZhjGamgmvCTSyA==",
87
+ "spanId": "VvA2+TOIPPQ=",
88
+ "name": "my-span",
89
+ "kind": "SPAN_KIND_INTERNAL",
90
+ "startTimeUnixNano": "1711635620607325000",
91
+ "endTimeUnixNano": "1711635621112208000",
92
+ "status": {}
93
+ },
94
+ {
95
+ "traceId": "4THaXRxEQE//Q6Ah9ibzMA==",
96
+ "spanId": "9w1RAuVW8R8=",
97
+ "name": "my-span",
98
+ "kind": "SPAN_KIND_INTERNAL",
99
+ "startTimeUnixNano": "1711635621112384000",
100
+ "endTimeUnixNano": "1711635621617394000",
101
+ "status": {}
102
+ },
103
+ {
104
+ "traceId": "AobQXFEK52+9JtF7cRojrg==",
105
+ "spanId": "4bxxVSJdgss=",
106
+ "name": "my-span",
107
+ "kind": "SPAN_KIND_INTERNAL",
108
+ "startTimeUnixNano": "1711635621617543000",
109
+ "endTimeUnixNano": "1711635622122152000",
110
+ "status": {}
111
+ },
112
+ {
113
+ "traceId": "D1IloLl9gAQWEuHhbMEkjg==",
114
+ "spanId": "qTbjatfaKxs=",
115
+ "name": "my-span",
116
+ "kind": "SPAN_KIND_INTERNAL",
117
+ "startTimeUnixNano": "1711635622122414000",
118
+ "endTimeUnixNano": "1711635622623191000",
119
+ "status": {}
120
+ },
121
+ {
122
+ "traceId": "un5hDAa1mhAiI0cURE/nOw==",
123
+ "spanId": "TyT3ZlMSJx0=",
124
+ "name": "my-span",
125
+ "kind": "SPAN_KIND_INTERNAL",
126
+ "startTimeUnixNano": "1711635622623366000",
127
+ "endTimeUnixNano": "1711635623126592000",
128
+ "status": {}
129
+ }
130
+ ]
131
+ }
132
+ ]
133
+ }
134
+ ]
135
+ },
136
+ "headers": {
137
+ "user-agent": "grpc-python/1.62.1 grpc-c/39.0.0 (osx; chttp2)"
138
+ }
139
+ },
140
+ {
141
+ "request": {
142
+ "resourceSpans": [
143
+ {
144
+ "resource": {
145
+ "attributes": [
146
+ {
147
+ "key": "telemetry.sdk.language",
148
+ "value": {
149
+ "stringValue": "python"
150
+ }
151
+ },
152
+ {
153
+ "key": "telemetry.sdk.name",
154
+ "value": {
155
+ "stringValue": "opentelemetry"
156
+ }
157
+ },
158
+ {
159
+ "key": "telemetry.sdk.version",
160
+ "value": {
161
+ "stringValue": "1.23.0"
162
+ }
163
+ },
164
+ {
165
+ "key": "service.name",
166
+ "value": {
167
+ "stringValue": "integration-test"
168
+ }
169
+ },
170
+ {
171
+ "key": "telemetry.auto.version",
172
+ "value": {
173
+ "stringValue": "0.44b0"
174
+ }
175
+ }
176
+ ]
177
+ },
178
+ "scopeSpans": [
179
+ {
180
+ "scope": {
181
+ "name": "my-tracer"
182
+ },
183
+ "spans": [
184
+ {
185
+ "traceId": "nhjQZtiuQzR7c2gk+ibSCA==",
186
+ "spanId": "Qs6Wd6WSyWs=",
187
+ "name": "my-span",
188
+ "kind": "SPAN_KIND_INTERNAL",
189
+ "startTimeUnixNano": "1711635623126694000",
190
+ "endTimeUnixNano": "1711635623626949000",
191
+ "status": {}
192
+ },
193
+ {
194
+ "traceId": "cIYgFYTWwl+aRkNSHk8/Hw==",
195
+ "spanId": "hCN25UIOSao=",
196
+ "name": "my-span",
197
+ "kind": "SPAN_KIND_INTERNAL",
198
+ "startTimeUnixNano": "1711635623627119000",
199
+ "endTimeUnixNano": "1711635624131442000",
200
+ "status": {}
201
+ },
202
+ {
203
+ "traceId": "bZtpEfpiSVdAVmDhuKOYaw==",
204
+ "spanId": "A6gHTRBvC3Y=",
205
+ "name": "my-span",
206
+ "kind": "SPAN_KIND_INTERNAL",
207
+ "startTimeUnixNano": "1711635624131546000",
208
+ "endTimeUnixNano": "1711635624631675000",
209
+ "status": {}
210
+ }
211
+ ]
212
+ }
213
+ ]
214
+ }
215
+ ]
216
+ },
217
+ "headers": {
218
+ "user-agent": "grpc-python/1.62.1 grpc-c/39.0.0 (osx; chttp2)"
219
+ }
220
+ }
221
+ ]
222
+ }
@@ -0,0 +1,43 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import time
16
+ from typing import Mapping, Sequence
17
+
18
+ from opentelemetry import trace
19
+ from oteltest.common import OtelTest, Telemetry
20
+
21
+ SERVICE_NAME = "integration-test"
22
+ NUM_ADDS = 12
23
+
24
+ if __name__ == "__main__":
25
+ tracer = trace.get_tracer("my-tracer")
26
+ for i in range(NUM_ADDS):
27
+ with tracer.start_as_current_span("my-span"):
28
+ print(f"{i+1}/{NUM_ADDS}")
29
+ time.sleep(0.5)
30
+
31
+
32
+ class MyTest(OtelTest):
33
+ def requirements(self) -> Sequence[str]:
34
+ return "opentelemetry-distro", "opentelemetry-exporter-otlp-proto-grpc"
35
+
36
+ def environment_variables(self) -> Mapping[str, str]:
37
+ return {"OTEL_SERVICE_NAME": SERVICE_NAME}
38
+
39
+ def wrapper_script(self) -> str:
40
+ return "opentelemetry-instrument"
41
+
42
+ def validate(self, telemetry: Telemetry) -> None:
43
+ assert telemetry.num_spans() == NUM_ADDS
@@ -0,0 +1,166 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "oteltest"
7
+ dynamic = ["version"]
8
+ description = ''
9
+ readme = "README.md"
10
+ requires-python = ">=3.7"
11
+ license = "Apache-2.0"
12
+ keywords = []
13
+ authors = [
14
+ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" },
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Programming Language :: Python",
19
+ "Programming Language :: Python :: 3.7",
20
+ "Programming Language :: Python :: 3.8",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: Implementation :: CPython",
25
+ "Programming Language :: Python :: Implementation :: PyPy",
26
+ ]
27
+ dependencies = [
28
+ "opentelemetry-proto",
29
+ "opentelemetry-api",
30
+ "grpcio",
31
+ "protobuf",
32
+ ]
33
+
34
+ [project.urls]
35
+ Documentation = "https://github.com/open-telemetry/opentelemetry-python#readme"
36
+ Issues = "https://github.com/open-telemetry/opentelemetry-python/issues"
37
+ Source = "https://github.com/open-telemetry/opentelemetry-python/"
38
+
39
+ [project.scripts]
40
+ oteltest = "oteltest.main:main"
41
+ otelsink = "otelsink:run_with_print_handler"
42
+
43
+ [tool.hatch.version]
44
+ path = "src/oteltest/__about__.py"
45
+
46
+ [tool.hatch.envs.default]
47
+ dependencies = [
48
+ "coverage[toml]>=6.5",
49
+ "pytest",
50
+ ]
51
+ [tool.hatch.envs.default.scripts]
52
+ test = "pytest {args:tests}"
53
+ test-cov = "coverage run -m pytest {args:tests}"
54
+ cov-report = [
55
+ "- coverage combine",
56
+ "coverage report",
57
+ ]
58
+ cov = [
59
+ "test-cov",
60
+ "cov-report",
61
+ ]
62
+
63
+ [[tool.hatch.envs.all.matrix]]
64
+ python = ["3.7", "3.8", "3.9", "3.10", "3.11"]
65
+
66
+ [tool.hatch.envs.lint]
67
+ detached = true
68
+ dependencies = [
69
+ "black>=23.1.0",
70
+ "mypy>=1.0.0",
71
+ "ruff>=0.0.243",
72
+ ]
73
+ [tool.hatch.envs.lint.scripts]
74
+ typing = "mypy --install-types --non-interactive {args:src/oteltest tests}"
75
+ style = [
76
+ "ruff {args:.}",
77
+ "black --check --diff {args:.}",
78
+ ]
79
+ fmt = [
80
+ "black {args:.}",
81
+ "ruff --fix {args:.}",
82
+ "style",
83
+ ]
84
+ all = [
85
+ "style",
86
+ "typing",
87
+ ]
88
+
89
+ [tool.black]
90
+ target-version = ["py37"]
91
+ line-length = 120
92
+ skip-string-normalization = true
93
+
94
+ [tool.ruff]
95
+ target-version = "py37"
96
+ line-length = 120
97
+ select = [
98
+ "A",
99
+ "ARG",
100
+ "B",
101
+ "C",
102
+ "DTZ",
103
+ "E",
104
+ "EM",
105
+ "F",
106
+ "FBT",
107
+ "I",
108
+ "ICN",
109
+ "ISC",
110
+ "N",
111
+ "PLC",
112
+ "PLE",
113
+ "PLR",
114
+ "PLW",
115
+ "Q",
116
+ "RUF",
117
+ "S",
118
+ "T",
119
+ "TID",
120
+ "UP",
121
+ "W",
122
+ "YTT",
123
+ ]
124
+ ignore = [
125
+ # Allow non-abstract empty methods in abstract base classes
126
+ "B027",
127
+ # Allow boolean positional values in function calls, like `dict.get(... True)`
128
+ "FBT003",
129
+ # Ignore checks for possible passwords
130
+ "S105", "S106", "S107",
131
+ # Ignore complexity
132
+ "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915",
133
+ ]
134
+ unfixable = [
135
+ # Don't touch unused imports
136
+ "F401",
137
+ ]
138
+
139
+ [tool.ruff.isort]
140
+ known-first-party = ["oteltest"]
141
+
142
+ [tool.ruff.flake8-tidy-imports]
143
+ ban-relative-imports = "all"
144
+
145
+ [tool.ruff.per-file-ignores]
146
+ # Tests can use magic values, assertions, and relative imports
147
+ "tests/**/*" = ["PLR2004", "S101", "TID252"]
148
+
149
+ [tool.coverage.run]
150
+ source_pkgs = ["oteltest", "tests"]
151
+ branch = true
152
+ parallel = true
153
+ omit = [
154
+ "src/oteltest/__about__.py",
155
+ ]
156
+
157
+ [tool.coverage.paths]
158
+ oteltest = ["src/oteltest", "*/oteltest/src/oteltest"]
159
+ tests = ["tests", "*/oteltest/tests"]
160
+
161
+ [tool.coverage.report]
162
+ exclude_lines = [
163
+ "no cov",
164
+ "if __name__ == .__main__.:",
165
+ "if TYPE_CHECKING:",
166
+ ]
@@ -0,0 +1,15 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ __version__ = "0.0.1"
@@ -0,0 +1,13 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
@@ -0,0 +1,132 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import abc
16
+ import dataclasses
17
+ import json
18
+ from typing import List, Mapping, Optional
19
+
20
+
21
+ @dataclasses.dataclass
22
+ class Request:
23
+ request: dict
24
+ headers: dict
25
+
26
+ def get_header(self, name):
27
+ return self.headers.get(name)
28
+
29
+ def to_json(self):
30
+ return json.dumps(self.to_dict())
31
+
32
+ def to_dict(self):
33
+ return {
34
+ "request": self.request,
35
+ "headers": self.headers,
36
+ }
37
+
38
+
39
+ class Telemetry:
40
+ def __init__(
41
+ self,
42
+ log_reqs: Optional[List[Request]] = None,
43
+ metric_reqs: Optional[List[Request]] = None,
44
+ trace_reqs: Optional[List[Request]] = None,
45
+ ):
46
+ self.log_reqs: List[Request] = log_reqs or []
47
+ self.metric_reqs: List[Request] = metric_reqs or []
48
+ self.trace_reqs: List[Request] = trace_reqs or []
49
+
50
+ def add_log(self, log: dict, headers: dict):
51
+ self.log_reqs.append(Request(log, headers))
52
+
53
+ def add_metric(self, metric: dict, headers: dict):
54
+ self.metric_reqs.append(Request(metric, headers))
55
+
56
+ def add_trace(self, trace: dict, headers: dict):
57
+ self.trace_reqs.append(Request(trace, headers))
58
+
59
+ def get_trace_requests(self) -> List[Request]:
60
+ return self.trace_reqs
61
+
62
+ def num_metrics(self) -> int:
63
+ out = 0
64
+ for req in self.metric_reqs:
65
+ for rm in req.request["resourceMetrics"]:
66
+ for sm in rm["scopeMetrics"]:
67
+ out += len(sm["metrics"])
68
+ return out
69
+
70
+ def metric_names(self) -> set:
71
+ out = set()
72
+ for req in self.metric_reqs:
73
+ for rm in req.request["resourceMetrics"]:
74
+ for sm in rm["scopeMetrics"]:
75
+ for m in sm["metrics"]:
76
+ out.add(m["name"])
77
+ return out
78
+
79
+ def num_spans(self) -> int:
80
+ out = 0
81
+ for req in self.trace_reqs:
82
+ for rs in req.request["resourceSpans"]:
83
+ for ss in rs["scopeSpans"]:
84
+ out += len(ss["spans"])
85
+ return out
86
+
87
+ def has_trace_header(self, key, expected) -> bool:
88
+ for req in self.trace_reqs:
89
+ actual = req.get_header(key)
90
+ if expected == actual:
91
+ return True
92
+ return False
93
+
94
+ def to_json(self):
95
+ return json.dumps(self.to_dict(), indent=2)
96
+
97
+ def to_dict(self):
98
+ return {
99
+ "log_reqs": [req.to_dict() for req in self.log_reqs],
100
+ "metric_reqs": [req.to_dict() for req in self.metric_reqs],
101
+ "trace_reqs": [req.to_dict() for req in self.trace_reqs],
102
+ }
103
+
104
+ def __str__(self):
105
+ return self.to_json()
106
+
107
+
108
+ def trace_attribute_as_str_array(tr: dict, attr_name) -> [str]:
109
+ out = []
110
+ for rs in tr["resourceSpans"]:
111
+ for attr in rs["resource"]["attributes"]:
112
+ if attr["key"] == attr_name:
113
+ out.append(attr["value"]["stringValue"])
114
+ return out
115
+
116
+
117
+ class OtelTest(abc.ABC):
118
+ @abc.abstractmethod
119
+ def environment_variables(self) -> Mapping[str, str]:
120
+ pass
121
+
122
+ @abc.abstractmethod
123
+ def requirements(self) -> List[str]:
124
+ pass
125
+
126
+ @abc.abstractmethod
127
+ def wrapper_script(self) -> str:
128
+ pass
129
+
130
+ @abc.abstractmethod
131
+ def validate(self, telemetry: Telemetry) -> None:
132
+ pass
@@ -0,0 +1,184 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import glob
16
+ import importlib
17
+ import inspect
18
+ import os
19
+ import shutil
20
+ import subprocess
21
+ import sys
22
+ import tempfile
23
+ import venv
24
+ from pathlib import Path
25
+
26
+ from google.protobuf.json_format import MessageToDict
27
+
28
+ from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ExportLogsServiceRequest
29
+ from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ExportMetricsServiceRequest
30
+ from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ExportTraceServiceRequest
31
+ from oteltest.common import OtelTest, Telemetry
32
+ from oteltest.sink import GrpcSink, RequestHandler
33
+
34
+ import argparse
35
+
36
+
37
+ def main():
38
+ parser = argparse.ArgumentParser(description="OpenTelemetry Python Tester")
39
+
40
+ w_help = "Path to a wheel (.whl) file to be used instead of `pip install oteltest`"
41
+ parser.add_argument("-w", "--wheel-file", type=str, required=False, help=w_help)
42
+
43
+ d_help = "A directory to hold per-script venv directories"
44
+ parser.add_argument("-d", "--venv-parent-dir", type=str, required=False, help=d_help)
45
+
46
+ parser.add_argument("script_dir", type=str, help="The directory containing your oteltest scripts")
47
+
48
+ args = parser.parse_args()
49
+ run(args.script_dir, args.wheel_file, args.venv_parent_dir)
50
+
51
+
52
+ def run(script_dir: str, wheel_file: str, venv_parent_dir: str):
53
+ venv_dir = venv_parent_dir or tempfile.mkdtemp()
54
+ print(f"using temp dir: {venv_dir}")
55
+
56
+ sys.path.append(script_dir)
57
+
58
+ for script in ls_scripts(script_dir):
59
+ setup_script_environment(script, script_dir, venv_dir, wheel_file)
60
+
61
+
62
+ def ls_scripts(script_dir):
63
+ original_dir = os.getcwd()
64
+ os.chdir(script_dir)
65
+ scripts = [script_name for script_name in glob.glob("*.py")]
66
+ os.chdir(original_dir)
67
+ return scripts
68
+
69
+
70
+ def setup_script_environment(script, script_dir, tempdir, wheel_file):
71
+ handler = AccumulatingHandler()
72
+ sink = GrpcSink(handler)
73
+ sink.start()
74
+
75
+ module_name = script[:-3]
76
+ test_class = load_test_class_for_script(module_name)
77
+ test_instance: OtelTest = test_class()
78
+
79
+ v = Venv(str(Path(tempdir) / module_name))
80
+ v.create()
81
+
82
+ pip_path = v.path_to_executable("pip")
83
+
84
+ oteltest_dep = wheel_file or "oteltest"
85
+ run_subprocess([pip_path, "install", oteltest_dep])
86
+
87
+ for req in test_instance.requirements():
88
+ print(f"- Will install requirement: '{req}'")
89
+ run_subprocess([pip_path, "install", req])
90
+
91
+ run_python_script(script, script_dir, test_instance, v)
92
+
93
+ v.rm()
94
+
95
+ with open(str(Path(script_dir) / f"{module_name}.json"), "w") as file:
96
+ file.write(handler.telemetry_to_json())
97
+
98
+ test_instance.validate(handler.telemetry)
99
+ print(f"- {script} PASSED")
100
+
101
+
102
+ def run_python_script(script, script_dir, test_instance, v):
103
+ python_script_cmd = [v.path_to_executable("python"), str(Path(script_dir) / script)]
104
+ ws = test_instance.wrapper_script()
105
+ if ws is not None:
106
+ python_script_cmd.insert(0, v.path_to_executable(ws))
107
+ run_subprocess(python_script_cmd, test_instance.environment_variables())
108
+
109
+
110
+ def run_subprocess(args, env_vars=None):
111
+ print(f"- Subprocess: {args}")
112
+ print(f"- Environment: {env_vars}")
113
+ result = subprocess.run(
114
+ args,
115
+ capture_output=True,
116
+ env=env_vars,
117
+ )
118
+ print(f"- Return Code: {result.returncode}")
119
+ print("- Standard Output:")
120
+ if result.stdout:
121
+ print(result.stdout.decode('utf-8').strip())
122
+ print("- Standard Error:")
123
+ if result.stderr:
124
+ print(result.stderr.decode('utf-8').strip())
125
+ print("- End Subprocess -\n")
126
+
127
+
128
+ def load_test_class_for_script(module_name):
129
+ module = importlib.import_module(module_name)
130
+ for attr_name in dir(module):
131
+ value = getattr(module, attr_name)
132
+ if is_test_class(value):
133
+ return value
134
+ return None
135
+
136
+
137
+ def is_test_class(value):
138
+ return inspect.isclass(value) and issubclass(value, OtelTest) and value is not OtelTest
139
+
140
+
141
+ class Venv:
142
+ def __init__(self, venv_dir):
143
+ self.venv_dir = venv_dir
144
+
145
+ def create(self):
146
+ venv.create(self.venv_dir, with_pip=True)
147
+
148
+ def path_to_executable(self, executable_name: str):
149
+ return f"{self.venv_dir}/bin/{executable_name}"
150
+
151
+ def rm(self):
152
+ shutil.rmtree(self.venv_dir)
153
+
154
+
155
+ class AccumulatingHandler(RequestHandler):
156
+ def __init__(self):
157
+ self.telemetry = Telemetry()
158
+
159
+ def handle_logs(self, request: ExportLogsServiceRequest, context): # noqa: ARG002
160
+ self.telemetry.add_log(MessageToDict(request), get_context_headers(context))
161
+
162
+ def handle_metrics(self, request: ExportMetricsServiceRequest, context): # noqa: ARG002
163
+ self.telemetry.add_metric(MessageToDict(request), get_context_headers(context))
164
+
165
+ def handle_trace(self, request: ExportTraceServiceRequest, context): # noqa: ARG002
166
+ self.telemetry.add_trace(MessageToDict(request), get_context_headers(context))
167
+
168
+ def telemetry_to_json(self):
169
+ return self.telemetry.to_json()
170
+
171
+
172
+ def get_context_headers(context):
173
+ return pbmetadata_to_dict(context.invocation_metadata())
174
+
175
+
176
+ def pbmetadata_to_dict(pbmetadata):
177
+ out = {}
178
+ for k, v in pbmetadata:
179
+ out[k] = v
180
+ return out
181
+
182
+
183
+ if __name__ == '__main__':
184
+ main()
@@ -0,0 +1,126 @@
1
+ import abc
2
+ from concurrent import futures
3
+
4
+ import grpc # type: ignore
5
+
6
+ from opentelemetry.proto.collector.logs.v1 import logs_service_pb2, logs_service_pb2_grpc # type: ignore
7
+ from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ExportLogsServiceRequest # type: ignore
8
+ from opentelemetry.proto.collector.metrics.v1 import metrics_service_pb2, metrics_service_pb2_grpc # type: ignore
9
+ from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ExportMetricsServiceRequest # type: ignore
10
+ from opentelemetry.proto.collector.trace.v1 import trace_service_pb2, trace_service_pb2_grpc # type: ignore
11
+ from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ExportTraceServiceRequest # type: ignore
12
+
13
+
14
+ class _LogsServiceServicer(logs_service_pb2_grpc.LogsServiceServicer):
15
+ def __init__(self, handle_request):
16
+ self.handle_request = handle_request
17
+
18
+ def Export(self, request, context): # noqa: N802
19
+ self.handle_request(request, context)
20
+ return logs_service_pb2.ExportLogsServiceResponse()
21
+
22
+
23
+ class _TraceServiceServicer(trace_service_pb2_grpc.TraceServiceServicer):
24
+ def __init__(self, handle_request):
25
+ self.handle_request = handle_request
26
+
27
+ def Export(self, request, context): # noqa: N802
28
+ self.handle_request(request, context)
29
+ return trace_service_pb2.ExportTraceServiceResponse()
30
+
31
+
32
+ class _MetricsServiceServicer(metrics_service_pb2_grpc.MetricsServiceServicer):
33
+ def __init__(self, handle_request):
34
+ self.handle_request = handle_request
35
+
36
+ def Export(self, request, context): # noqa: N802
37
+ self.handle_request(request, context)
38
+ return metrics_service_pb2.ExportMetricsServiceResponse()
39
+
40
+
41
+ class RequestHandler(abc.ABC):
42
+ """
43
+ This interface is meant to be implemented by users of the otelsink API. If you use the API, you'll want to create
44
+ an implementation, instantiate it, and pass the instance to the GrpcSink constructor. As messages arrive, the
45
+ callbacks defined by this interface will be invoked.
46
+
47
+ grpc_sink = GrpcSink(MyRequestHandler())
48
+ """
49
+
50
+ @abc.abstractmethod
51
+ def handle_logs(self, request: ExportLogsServiceRequest, context):
52
+ pass
53
+
54
+ @abc.abstractmethod
55
+ def handle_metrics(self, request: ExportMetricsServiceRequest, context):
56
+ pass
57
+
58
+ @abc.abstractmethod
59
+ def handle_trace(self, request: ExportTraceServiceRequest, context):
60
+ pass
61
+
62
+
63
+ class GrpcSink:
64
+ """
65
+ This is an OTel GRPC server to which you can send metrics, traces, and
66
+ logs. It requires a RequestHandler implementation passed in.
67
+ """
68
+
69
+ def __init__(self, request_handler: RequestHandler):
70
+ self.svr = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
71
+ trace_service_pb2_grpc.add_TraceServiceServicer_to_server(
72
+ _TraceServiceServicer(request_handler.handle_trace), self.svr
73
+ )
74
+ metrics_service_pb2_grpc.add_MetricsServiceServicer_to_server(
75
+ _MetricsServiceServicer(request_handler.handle_metrics), self.svr
76
+ )
77
+ logs_service_pb2_grpc.add_LogsServiceServicer_to_server(
78
+ _LogsServiceServicer(request_handler.handle_logs), self.svr
79
+ )
80
+ self.svr.add_insecure_port("0.0.0.0:4317")
81
+
82
+ def start(self):
83
+ """Starts the server. Does not block."""
84
+ self.svr.start()
85
+
86
+ def wait_for_termination(self):
87
+ """Blocks until the server stops. Stops the server on KeyboardInterrupt."""
88
+ try:
89
+ self.svr.wait_for_termination()
90
+ except KeyboardInterrupt:
91
+ print("\nstopping...", end="")
92
+ self.stop()
93
+ print("done")
94
+
95
+ def stop(self):
96
+ """Stops the server immediately."""
97
+ self.svr.stop(grace=None)
98
+
99
+
100
+ class PrintHandler(RequestHandler):
101
+ """
102
+ A RequestHandler implementation that prints the received messages.
103
+ """
104
+
105
+ def handle_logs(self, request, context): # noqa: ARG002
106
+ print(f"log request: {request}") # noqa: T201
107
+
108
+ def handle_metrics(self, request, context): # noqa: ARG002
109
+ print(f"metrics request: {request}") # noqa: T201
110
+
111
+ def handle_trace(self, request: ExportTraceServiceRequest, context): # noqa: ARG002
112
+ print(f"trace request: {request}") # noqa: T201
113
+
114
+
115
+ def run_with_print_handler():
116
+ """
117
+ Runs otelsink with a PrintHandler.
118
+ """
119
+ print("starting otelsink with a print handler")
120
+ sink = GrpcSink(PrintHandler())
121
+ sink.start()
122
+ sink.wait_for_termination()
123
+
124
+
125
+ if __name__ == "__main__":
126
+ run_with_print_handler()
@@ -0,0 +1,13 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.