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.
- oteltest-0.0.1/.gitignore +68 -0
- oteltest-0.0.1/LICENSE.txt +13 -0
- oteltest-0.0.1/PKG-INFO +160 -0
- oteltest-0.0.1/README.md +135 -0
- oteltest-0.0.1/example_scripts/simple_loop.json +222 -0
- oteltest-0.0.1/example_scripts/simple_loop.py +43 -0
- oteltest-0.0.1/pyproject.toml +166 -0
- oteltest-0.0.1/src/oteltest/__about__.py +15 -0
- oteltest-0.0.1/src/oteltest/__init__.py +13 -0
- oteltest-0.0.1/src/oteltest/common.py +132 -0
- oteltest-0.0.1/src/oteltest/main.py +184 -0
- oteltest-0.0.1/src/oteltest/sink.py +126 -0
- oteltest-0.0.1/tests/__init__.py +13 -0
|
@@ -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.
|
oteltest-0.0.1/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/oteltest)
|
|
29
|
+
[](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.
|
oteltest-0.0.1/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# oteltest
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/oteltest)
|
|
4
|
+
[](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.
|