Skip to content

Commit 6babff1

Browse files
ocelotlAlex Botenc24t
authored
auto-instr: Fix the auto instrumentation command (#567)
Right now the auto instrumentation command works like this: opentelemetry-auto-instrumentation some_file.py The auto instrumentation command invokes an interpreter that executes the some_file.py. Nevertheless, this will not work for frameworks that have their own execution command (flask_run, for example). Fixes #566 Co-authored-by: Alex Boten <[email protected]> Co-authored-by: Chris Kleinknecht <[email protected]>
1 parent c772bb7 commit 6babff1

File tree

6 files changed

+235
-27
lines changed

6 files changed

+235
-27
lines changed

docs/examples/auto-instrumentation/README.md

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,9 @@ $ source auto_instrumentation/bin/activate
5555
# Installation
5656

5757
```sh
58-
$ pip install opentelemetry-api
5958
$ pip install opentelemetry-sdk
6059
$ pip install opentelemetry-auto-instrumentation
61-
$ pip install ext/opentelemetry-ext-flask
62-
$ pip install flask
60+
$ pip install opentelemetry-ext-flask
6361
$ pip install requests
6462
```
6563

@@ -71,42 +69,97 @@ This is done in 2 separate consoles, one to run each of the scripts that make up
7169

7270
```sh
7371
$ source auto_instrumentation/bin/activate
74-
$ python opentelemetry-python/opentelemetry-auto-instrumentation/example/server_instrumented.py
72+
$ python opentelemetry-python/docs/examples/auto-instrumentation/server_instrumented.py
7573
```
7674

7775
```sh
7876
$ source auto_instrumentation/bin/activate
79-
$ python opentelemetry-python/opentelemetry-auto-instrumentation/example/client.py testing
77+
$ python opentelemetry-python/docs/examples/auto-instrumentation/client.py testing
8078
```
8179

8280
The execution of `server_instrumented.py` should return an output similar to:
8381

8482
```sh
85-
Hello, testing!
86-
Span(name="serv_request", context=SpanContext(trace_id=0x9c0e0ce8f7b7dbb51d1d6e744a4dad49, span_id=0xd1ba3ec4c76a0d7f, trace_state={}), kind=SpanKind.INTERNAL, parent=None, start_time=2020-03-19T00:06:31.275719Z, end_time=2020-03-19T00:06:31.275920Z)
87-
127.0.0.1 - - [18/Mar/2020 18:06:31] "GET /serv_request?helloStr=Hello%2C+testing%21 HTTP/1.1" 200 -
83+
{
84+
"name": "server_request",
85+
"context": {
86+
"trace_id": "0xfa002aad260b5f7110db674a9ddfcd23",
87+
"span_id": "0x8b8bbaf3ca9c5131",
88+
"trace_state": "{}"
89+
},
90+
"kind": "SpanKind.SERVER",
91+
"parent_id": null,
92+
"start_time": "2020-04-30T17:28:57.886397Z",
93+
"end_time": "2020-04-30T17:28:57.886490Z",
94+
"status": {
95+
"canonical_code": "OK"
96+
},
97+
"attributes": {
98+
"component": "http",
99+
"http.method": "GET",
100+
"http.server_name": "127.0.0.1",
101+
"http.scheme": "http",
102+
"host.port": 8082,
103+
"http.host": "localhost:8082",
104+
"http.target": "/server_request?param=testing",
105+
"net.peer.ip": "127.0.0.1",
106+
"net.peer.port": 52872,
107+
"http.flavor": "1.1"
108+
},
109+
"events": [],
110+
"links": []
111+
}
88112
```
89113

90114
## Execution of an automatically instrumented server
91115

92116
Now, kill the execution of `server_instrumented.py` with `ctrl + c` and run this instead:
93117

94118
```sh
95-
$ opentelemetry-auto-instrumentation opentelemetry-python/opentelemetry-auto-instrumentation/example/server_uninstrumented.py
119+
$ opentelemetry-auto-instrumentation python docs/examples/auto-instrumentation/server_uninstrumented.py
96120
```
97121

98122
In the console where you previously executed `client.py`, run again this again:
99123

100124
```sh
101-
$ python opentelemetry-python/opentelemetry-auto-instrumentation/example/client.py testing
125+
$ python opentelemetry-python/docs/examples/auto-instrumentation/client.py testing
102126
```
103127

104128
The execution of `server_uninstrumented.py` should return an output similar to:
105129

106130
```sh
107-
Hello, testing!
108-
Span(name="serv_request", context=SpanContext(trace_id=0xf26b28b5243e48f5f96bfc753f95f3f0, span_id=0xbeb179a095d087ed, trace_state={}), kind=SpanKind.SERVER, parent=<opentelemetry.trace.DefaultSpan object at 0x7f1a20a54908>, start_time=2020-03-19T00:24:18.828561Z, end_time=2020-03-19T00:24:18.845127Z)
109-
127.0.0.1 - - [18/Mar/2020 18:24:18] "GET /serv_request?helloStr=Hello%2C+testing%21 HTTP/1.1" 200 -
131+
{
132+
"name": "server_request",
133+
"context": {
134+
"trace_id": "0x9f528e0b76189f539d9c21b1a7a2fc24",
135+
"span_id": "0xd79760685cd4c269",
136+
"trace_state": "{}"
137+
},
138+
"kind": "SpanKind.SERVER",
139+
"parent_id": "0xb4fb7eee22ef78e4",
140+
"start_time": "2020-04-30T17:10:02.400604Z",
141+
"end_time": "2020-04-30T17:10:02.401858Z",
142+
"status": {
143+
"canonical_code": "OK"
144+
},
145+
"attributes": {
146+
"component": "http",
147+
"http.method": "GET",
148+
"http.server_name": "127.0.0.1",
149+
"http.scheme": "http",
150+
"host.port": 8082,
151+
"http.host": "localhost:8082",
152+
"http.target": "/server_request?param=testing",
153+
"net.peer.ip": "127.0.0.1",
154+
"net.peer.port": 48240,
155+
"http.flavor": "1.1",
156+
"http.route": "/server_request",
157+
"http.status_text": "OK",
158+
"http.status_code": 200
159+
},
160+
"events": [],
161+
"links": []
162+
}
110163
```
111164

112-
As you can see, both outputs are equivalentsince the automatic instrumentation does what the manual instrumentation does too.
165+
Both outputs are equivalent since the automatic instrumentation does what the manual instrumentation does too.

docs/examples/auto-instrumentation/server_instrumented.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from flask import Flask, request
1616

1717
from opentelemetry import propagators, trace
18+
from opentelemetry.ext.wsgi import collect_request_attributes
1819
from opentelemetry.sdk.trace import TracerProvider
1920
from opentelemetry.sdk.trace.export import (
2021
ConsoleSpanExporter,
@@ -38,6 +39,8 @@ def server_request():
3839
parent=propagators.extract(
3940
lambda dict_, key: dict_.get(key, []), request.headers
4041
)["current-span"],
42+
kind=trace.SpanKind.SERVER,
43+
attributes=collect_request_attributes(request.environ),
4144
):
4245
print(request.args.get("param"))
4346
return "served"

opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
2121
::
2222
23-
opentelemetry-auto-instrumentation program.py
23+
opentelemetry-auto-instrumentation python program.py
2424
2525
The code in ``program.py`` needs to use one of the packages for which there is
26-
an OpenTelemetry extension. For a list of the available extensions please check
27-
`here <https://opentelemetry-python.readthedocs.io/>`_.
26+
an OpenTelemetry integration. For a list of the available integrations please
27+
check `here <https://opentelemetry-python.readthedocs.io/>`_.
2828
"""

opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,40 @@
1515
# limitations under the License.
1616

1717
from logging import getLogger
18-
from runpy import run_path
18+
from os import environ, execl, getcwd
19+
from os.path import abspath, dirname, pathsep
20+
from shutil import which
1921
from sys import argv
2022

21-
from pkg_resources import iter_entry_points
22-
2323
logger = getLogger(__file__)
2424

2525

2626
def run() -> None:
2727

28-
for entry_point in iter_entry_points("opentelemetry_instrumentor"):
29-
try:
30-
entry_point.load()().instrument() # type: ignore
31-
logger.debug("Instrumented %s", entry_point.name)
28+
python_path = environ.get("PYTHONPATH")
29+
30+
if not python_path:
31+
python_path = []
32+
33+
else:
34+
python_path = python_path.split(pathsep)
35+
36+
cwd_path = getcwd()
37+
38+
# This is being added to support applications that are being run from their
39+
# own executable, like Django.
40+
# FIXME investigate if there is another way to achieve this
41+
if cwd_path not in python_path:
42+
python_path.insert(0, cwd_path)
43+
44+
filedir_path = dirname(abspath(__file__))
45+
46+
python_path = [path for path in python_path if path != filedir_path]
47+
48+
python_path.insert(0, filedir_path)
49+
50+
environ["PYTHONPATH"] = pathsep.join(python_path)
3251

33-
except Exception: # pylint: disable=broad-except
34-
logger.exception("Instrumenting of %s failed", entry_point.name)
52+
executable = which(argv[1])
3553

36-
run_path(argv[1], run_name="__main__") # type: ignore
54+
execl(executable, executable, *argv[2:])
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
from logging import getLogger
16+
17+
from pkg_resources import iter_entry_points
18+
19+
logger = getLogger(__file__)
20+
21+
22+
for entry_point in iter_entry_points("opentelemetry_instrumentor"):
23+
try:
24+
entry_point.load()().instrument() # type: ignore
25+
logger.debug("Instrumented %s", entry_point.name)
26+
27+
except Exception: # pylint: disable=broad-except
28+
logger.exception("Instrumenting of %s failed", entry_point.name)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
# type: ignore
15+
16+
from os import environ, getcwd
17+
from os.path import abspath, dirname, pathsep
18+
from unittest import TestCase
19+
from unittest.mock import patch
20+
21+
from opentelemetry.auto_instrumentation import auto_instrumentation
22+
23+
24+
class TestRun(TestCase):
25+
auto_instrumentation_path = dirname(abspath(auto_instrumentation.__file__))
26+
27+
@classmethod
28+
def setUpClass(cls):
29+
cls.argv_patcher = patch(
30+
"opentelemetry.auto_instrumentation.auto_instrumentation.argv"
31+
)
32+
cls.execl_patcher = patch(
33+
"opentelemetry.auto_instrumentation.auto_instrumentation.execl"
34+
)
35+
cls.which_patcher = patch(
36+
"opentelemetry.auto_instrumentation.auto_instrumentation.which"
37+
)
38+
39+
cls.argv_patcher.start()
40+
cls.execl_patcher.start()
41+
cls.which_patcher.start()
42+
43+
@classmethod
44+
def tearDownClass(cls):
45+
cls.argv_patcher.stop()
46+
cls.execl_patcher.stop()
47+
cls.which_patcher.stop()
48+
49+
@patch.dict("os.environ", {"PYTHONPATH": ""})
50+
def test_empty(self):
51+
auto_instrumentation.run()
52+
self.assertEqual(
53+
environ["PYTHONPATH"],
54+
pathsep.join([self.auto_instrumentation_path, getcwd()]),
55+
)
56+
57+
@patch.dict("os.environ", {"PYTHONPATH": "abc"})
58+
def test_non_empty(self):
59+
auto_instrumentation.run()
60+
self.assertEqual(
61+
environ["PYTHONPATH"],
62+
pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]),
63+
)
64+
65+
@patch.dict(
66+
"os.environ",
67+
{"PYTHONPATH": pathsep.join(["abc", auto_instrumentation_path])},
68+
)
69+
def test_after_path(self):
70+
auto_instrumentation.run()
71+
self.assertEqual(
72+
environ["PYTHONPATH"],
73+
pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]),
74+
)
75+
76+
@patch.dict(
77+
"os.environ",
78+
{
79+
"PYTHONPATH": pathsep.join(
80+
[auto_instrumentation_path, "abc", auto_instrumentation_path]
81+
)
82+
},
83+
)
84+
def test_single_path(self):
85+
auto_instrumentation.run()
86+
self.assertEqual(
87+
environ["PYTHONPATH"],
88+
pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]),
89+
)
90+
91+
92+
class TestExecl(TestCase):
93+
@patch(
94+
"opentelemetry.auto_instrumentation.auto_instrumentation.argv",
95+
new=[1, 2, 3],
96+
)
97+
@patch("opentelemetry.auto_instrumentation.auto_instrumentation.which")
98+
@patch("opentelemetry.auto_instrumentation.auto_instrumentation.execl")
99+
def test_execl(
100+
self, mock_execl, mock_which
101+
): # pylint: disable=no-self-use
102+
mock_which.configure_mock(**{"return_value": "python"})
103+
104+
auto_instrumentation.run()
105+
106+
mock_execl.assert_called_with("python", "python", 3)

0 commit comments

Comments
 (0)