Skip to content

Commit 7b2de88

Browse files
authored
[WIP] Wrapper script (#1709)
* Add wrapper script and sitecustomize.py * Add inline config * Add automated test for wrapper script * Simplify wrapper test * Add entry_points to setup.cfg * Disable wrapper test on Windows * Fix missing import * Remove testwrapper.py and call the wrapper script directly * Update copyright year * Set activation_method for wrapper script * Add activation_method to the wrapper test * Add Flask instrumentation * Add flask instrumentation to register * Target the instance, rather than the return value * Add a test for Flask auto-instrumentation It's causing some other failures. Tests pass by themselves so it's something related to cleanup on that new test, I think * Move the uninstrument behavior to the instrumentation * Fix newly-flakey tests + pytest random order * Fix test that was leaving things instrumented * Add pytest-random-order to test reqs * Modify new flask test to ensure fixture order * Even better cleanup on test_framework_name * Fix always_uninstrument() fixture failures * Late-import flask ElasticAPM object * Fix some more random_order test failures * Add docs * Fix a typo * Use __main__ to make `python -m` cleaner * Create separate instrumentation register for wrapper script * Rename register_wrapper to register_wrapper_instrumentations
1 parent 186453f commit 7b2de88

File tree

19 files changed

+554
-6
lines changed

19 files changed

+554
-6
lines changed

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ endif::[]
3737
===== Features
3838
3939
* GRPC support {pull}1703[#1703]
40+
* Wrapper script Flask support (experimental) {pull}1709[#1709]
4041
4142
[float]
4243
===== Bug fixes

docs/set-up.asciidoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ To get you off the ground, we’ve prepared guides for setting up the Agent with
1010
* <<starlette-support,Starlette/FastAPI>>
1111
* <<sanic-support,Sanic>>
1212
* <<lambda-support,AWS Lambda>>
13+
* <<wrapper-support,Wrapper (Experimental)>>
1314

1415
For custom instrumentation, see <<instrumenting-custom-code, Instrumenting Custom Code>>.
1516

@@ -26,3 +27,5 @@ include::./starlette.asciidoc[]
2627
include::./sanic.asciidoc[]
2728

2829
include::./serverless.asciidoc[]
30+
31+
include::./wrapper.asciidoc[]

docs/wrapper.asciidoc

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
[[wrapper-support]]
2+
=== Wrapper Support
3+
4+
experimental::[]
5+
6+
The following frameworks are supported using our new wrapper script for no-code-
7+
changes instrumentation:
8+
9+
* Flask
10+
11+
Please keep in mind that these instrumentations are a work in progress! We'd
12+
love to have feedback on our
13+
https://github.com/elastic/apm-agent-python/issues/new/choose[issue tracker].
14+
15+
[[wrapper-usage]]
16+
==== Usage
17+
18+
When installing the agent, an entrypoint script, `elasticapm-run` is installed
19+
as well. You can use this script to instrument your app (assuming it's using a
20+
supported framework) without changing your code!
21+
22+
[source,bash]
23+
----
24+
$ elasticapm-run --version
25+
elasticapm-run 6.14.0
26+
----
27+
28+
Alternatively, you can run the entrypoint directly:
29+
30+
[source,bash]
31+
----
32+
$ python -m elasticapm.instrumentation.wrapper --version
33+
elasticapm-run 6.14.0
34+
----
35+
36+
The `elasticapm-run` script can be used to run any Python script or module:
37+
38+
[source,bash]
39+
----
40+
$ elasticapm-run flask run
41+
$ elasticapm-run python myapp.py
42+
----
43+
44+
Generally, config should be passed in via environment variables. For example,
45+
46+
[source,bash]
47+
----
48+
$ ELASTIC_APM_SERVICE_NAME=my_flask_app elasticapm-run flask run
49+
----
50+
51+
You can also pass config options as arguments to the script:
52+
53+
[source,bash]
54+
----
55+
$ elasticapm-run --config "service_name=my_flask_app" --config "debug=true" flask run
56+
----

elasticapm/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ def __init__(self, config=None, **inline):
218218
if config.enabled:
219219
self.start_threads()
220220

221+
self.activation_method = None
222+
221223
# Save this Client object as the global CLIENT_SINGLETON
222224
set_client(self)
223225

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2022, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
31+
import logging
32+
33+
from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule
34+
35+
36+
class FlaskInstrumentation(AbstractInstrumentedModule):
37+
name = "flask"
38+
39+
instrument_list = [("flask", "Flask.__init__")]
40+
41+
creates_transactions = True
42+
43+
def call(self, module, method, wrapped, instance, args, kwargs):
44+
from elasticapm.contrib.flask import ElasticAPM
45+
46+
wrapped(*args, **kwargs)
47+
client = ElasticAPM(instance)
48+
instance.elasticapm_client = client
49+
self.instance = instance
50+
51+
def uninstrument(self):
52+
"""
53+
This is mostly here for testing. If we want to support live
54+
instrumenting and uninstrumenting, we'll need to also extend the
55+
`instrument()` method to add the signals removed here.
56+
"""
57+
super().uninstrument()
58+
59+
# Only remove signals during uninstrument if we auto-instrumented
60+
flask_app = getattr(self, "instance", None)
61+
if flask_app:
62+
client = flask_app.elasticapm_client
63+
from flask import signals
64+
65+
signals.request_started.disconnect(client.request_started)
66+
signals.request_finished.disconnect(client.request_finished)
67+
# remove logging handler if it was added
68+
logger = logging.getLogger()
69+
for handler in list(logger.handlers):
70+
if getattr(handler, "client", None) is client.client:
71+
logger.removeHandler(handler)
72+
self.instance = None

elasticapm/instrumentation/register.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,21 @@
9494
]
9595
)
9696

97+
# These instrumentations should only be enabled if we're instrumenting via the
98+
# wrapper script, which calls register_wrapper_instrumentations() below.
99+
_wrapper_register = {
100+
"elasticapm.instrumentation.packages.flask.FlaskInstrumentation",
101+
}
102+
97103

98104
def register(cls):
99105
_cls_register.add(cls)
100106

101107

108+
def register_wrapper_instrumentations():
109+
_cls_register.update(_wrapper_register)
110+
111+
102112
_instrumentation_singletons = {}
103113

104114

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2022, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
31+
import argparse
32+
import os
33+
import shutil
34+
35+
import elasticapm
36+
37+
38+
def setup():
39+
parser = argparse.ArgumentParser(
40+
prog="elasticapm-run",
41+
description="""
42+
%(prog)s is a wrapper script for running python applications
43+
while automatically instrumenting with the Elastic APM python agent.
44+
""",
45+
)
46+
47+
parser.add_argument(
48+
"--version", help="Print ElasticAPM version", action="version", version="%(prog)s " + elasticapm.VERSION
49+
)
50+
51+
parser.add_argument(
52+
"--config",
53+
action="append",
54+
help="Config values to pass to ElasticAPM. Can be used multiple times. Ex: --config 'service_name=foo'",
55+
)
56+
parser.add_argument("app", help="Your python application")
57+
parser.add_argument("app_args", nargs=argparse.REMAINDER, help="Arguments for your python application", default=[])
58+
59+
args = parser.parse_args()
60+
61+
our_path = os.path.dirname(os.path.abspath(__file__))
62+
pythonpath = os.environ.get("PYTHONPATH", "").split(";")
63+
pythonpath = [path for path in pythonpath if path != our_path]
64+
pythonpath.insert(0, our_path)
65+
os.environ["PYTHONPATH"] = os.path.pathsep.join(pythonpath)
66+
67+
for config in args.config or []:
68+
key, value = config.split("=", 1)
69+
os.environ["ELASTIC_APM_" + key.upper()] = value
70+
71+
if args.app:
72+
app = shutil.which(args.app)
73+
os.execl(app, app, *args.app_args)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2023, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
31+
from . import setup
32+
33+
if __name__ == "__main__":
34+
setup()
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2022, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
31+
import elasticapm
32+
from elasticapm import Client
33+
from elasticapm.instrumentation.register import register_wrapper_instrumentations
34+
35+
36+
def setup():
37+
client = Client()
38+
client.activation_method = "wrapper"
39+
register_wrapper_instrumentations()
40+
if client.config.instrument and client.config.enabled:
41+
elasticapm.instrument()
42+
43+
44+
setup()

setup.cfg

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ install_requires =
4141
wrapt>=1.14.1
4242
test_suite=tests
4343
tests_require =
44-
pytest==4.6.9
44+
pytest==6.2.5
45+
pytest-random-order==1.1.0
4546
py==1.8.1
4647
more-itertools==4.1.0
4748
pluggy==0.13.1
@@ -99,6 +100,10 @@ tests_require =
99100
httpx ; python_version >= '3.6'
100101
sanic ; python_version >= '3.7'
101102

103+
[options.entry_points]
104+
console_scripts =
105+
elasticapm-run = elasticapm.instrumentation.wrapper:setup
106+
102107
[options.extras_require]
103108
flask =
104109
blinker
@@ -163,6 +168,7 @@ markers =
163168
aiobotocore
164169
kafka
165170
grpc
171+
addopts=--random-order
166172

167173
[isort]
168174
line_length=120

0 commit comments

Comments
 (0)