Skip to content

Commit 0e36210

Browse files
mergify[bot]ZStriker19Kyle-Verhoog
authored
fixing Pyramid integration caller_package level (backport #2950) (#2954)
* Fix Pyramid integration caller_package level (#2950) This was causing Pyramid to fail to start. Co-authored-by: Zachary Groves <[email protected]> Co-authored-by: Kyle Verhoog <[email protected]>
1 parent f46f624 commit 0e36210

File tree

10 files changed

+183
-75
lines changed

10 files changed

+183
-75
lines changed

.circleci/config.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -722,10 +722,11 @@ jobs:
722722
pattern: '^pyodbc_contrib-'
723723

724724
pyramid:
725-
<<: *contrib_job
725+
<<: *machine_executor
726726
steps:
727727
- run_test:
728728
pattern: 'pyramid'
729+
snapshot: true
729730

730731
requests:
731732
<<: *contrib_job

ddtrace/contrib/pyramid/patch.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import os
22

33
import pyramid.config
4-
from pyramid.path import caller_package
54

65
from ddtrace import config
76
from ddtrace.vendor import wrapt
@@ -64,18 +63,6 @@ def traced_init(wrapped, instance, args, kwargs):
6463
# explicitly set our tween too since `add_tween` will be ignored.
6564
insert_tween_if_needed(trace_settings)
6665
kwargs["settings"] = trace_settings
67-
68-
# `caller_package` works by walking a fixed amount of frames up the stack
69-
# to find the calling package. So if we let the original `__init__`
70-
# function call it, our wrapper will mess things up.
71-
if not kwargs.get("package", None):
72-
# Get the package for the third frame up from this one.
73-
# - ddtrace.contrib.pyramid.path
74-
# - ddtrace.vendor.wrapt
75-
# - (this is the frame we want)
76-
# DEV: Default is `level=2` which will give us the package from `wrapt`
77-
kwargs["package"] = caller_package(level=3)
78-
7966
wrapped(*args, **kwargs)
8067
trace_pyramid(instance)
8168

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
fixes:
3+
- |
4+
Fix Pyramid caller_package level issue which resulted in crashes when starting Pyramid applications. Level now left at default (2).
5+

riotfile.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,7 @@ def select_pys(min_version=MIN_PYTHON_VERSION, max_version=MAX_PYTHON_VERSION):
942942
Venv(
943943
pys=select_pys(),
944944
pkgs={
945+
"requests": [latest],
945946
"webtest": [latest],
946947
"pyramid": [
947948
"~=1.7",

tests/contrib/django/django_app/settings.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
import os
22

33
from ddtrace import tracer
4-
from ddtrace.filters import TraceFilter
5-
6-
7-
class PingFilter(TraceFilter):
8-
def process_trace(self, trace):
9-
# Filter out all traces with trace_id = 1
10-
# This is done to prevent certain traces from being included in snapshots and
11-
# accomplished by propagating an http trace id of 1 with the request to Django.
12-
return None if trace and trace[0].trace_id == 1 else trace
4+
from tests.webclient import PingFilter
135

146

157
tracer.configure(

tests/contrib/django/test_django_snapshots.py

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,65 +3,14 @@
33

44
import django
55
import pytest
6-
import requests
7-
import six
8-
import tenacity
96

10-
from ddtrace.context import Context
11-
from ddtrace.propagation.http import HTTPPropagator
127
from tests.utils import snapshot
8+
from tests.webclient import Client
139

1410

1511
SERVER_PORT = 8000
1612

1713

18-
class Client(object):
19-
"""HTTP Client for making requests to a local http server."""
20-
21-
def __init__(self, base_url):
22-
# type: (str) -> None
23-
self._base_url = base_url
24-
self._session = requests.Session()
25-
# Propagate traces with trace_id = 1 for the ping trace so we can filter them out.
26-
c, d = Context(trace_id=1, span_id=1), {}
27-
HTTPPropagator.inject(c, d)
28-
self._ignore_headers = d
29-
30-
def _url(self, path):
31-
# type: (str) -> str
32-
return six.moves.urllib.parse.urljoin(self._base_url, path)
33-
34-
def get(self, path, **kwargs):
35-
return self._session.get(self._url(path), **kwargs)
36-
37-
def get_ignored(self, path, **kwargs):
38-
"""Do a normal get request but signal that the trace should be filtered out.
39-
40-
The signal is a distributed trace id header with the value 1.
41-
"""
42-
headers = kwargs.get("headers", {}).copy()
43-
headers.update(self._ignore_headers)
44-
kwargs["headers"] = headers
45-
return self._session.get(self._url(path), **kwargs)
46-
47-
def post(self, path, *args, **kwargs):
48-
return self._session.post(self._url(path), *args, **kwargs)
49-
50-
def request(self, method, path, *args, **kwargs):
51-
return self._session.request(method, self._url(path), *args, **kwargs)
52-
53-
def wait(self, path="/", max_tries=100, delay=0.1):
54-
# type: (str, int, float) -> None
55-
"""Wait for the server to start by repeatedly http `get`ting `path` until a 200 is received."""
56-
57-
@tenacity.retry(stop=tenacity.stop_after_attempt(max_tries), wait=tenacity.wait_fixed(delay))
58-
def ping():
59-
r = self.get_ignored(path)
60-
assert r.status_code == 200
61-
62-
ping()
63-
64-
6514
@pytest.fixture(scope="function")
6615
def daphne_client(snapshot):
6716
"""Runs a django app hosted with a daphne webserver in a subprocess and

tests/contrib/pyramid/app/app.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from wsgiref.simple_server import make_server
2+
3+
from pyramid.config import Configurator
4+
from pyramid.response import Response
5+
6+
from ddtrace import tracer
7+
from tests.webclient import PingFilter
8+
9+
10+
tracer.configure(
11+
settings={
12+
"FILTERS": [PingFilter()],
13+
}
14+
)
15+
16+
17+
def hello_world(request):
18+
return Response("Hello World!")
19+
20+
21+
def tracer_shutdown(request):
22+
tracer.shutdown()
23+
return Response("shutdown")
24+
25+
26+
if __name__ == "__main__":
27+
with Configurator() as config:
28+
config.add_route("hello", "/")
29+
config.add_view(hello_world, route_name="hello")
30+
31+
config.add_route("tracer-shutdown", "/shutdown-tracer")
32+
config.add_view(tracer_shutdown, route_name="tracer-shutdown")
33+
34+
app = config.make_wsgi_app()
35+
server = make_server("0.0.0.0", 8000, app)
36+
server.serve_forever()

tests/contrib/pyramid/test_pyramid.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1+
import os
2+
import subprocess
3+
4+
import pytest
5+
16
from ddtrace import config
27
from ddtrace.constants import ORIGIN_KEY
38
from ddtrace.constants import SAMPLING_PRIORITY_KEY
9+
from tests.webclient import Client
410

511
from .utils import PyramidBase
612
from .utils import PyramidTestCase
713

814

15+
SERVER_PORT = 8000
16+
17+
918
def includeme(config):
1019
pass
1120

@@ -102,3 +111,42 @@ def test_distributed_tracing_disabled(self):
102111
assert span.parent_id != 42
103112
assert span.get_metric(SAMPLING_PRIORITY_KEY) != 2
104113
assert span.get_tag(ORIGIN_KEY) != "synthetics"
114+
115+
116+
@pytest.fixture(scope="function")
117+
def pyramid_client(snapshot):
118+
"""Runs a Pyramid app in a subprocess and returns a client which can be used to query it.
119+
120+
Traces are flushed by invoking a tracer.shutdown() using a /shutdown-tracer route
121+
at the end of the testcase.
122+
"""
123+
124+
env = os.environ.copy()
125+
env["SERVER_PORT"] = str(SERVER_PORT)
126+
127+
cmd = ["ddtrace-run", "python", "tests/contrib/pyramid/app/app.py"]
128+
proc = subprocess.Popen(
129+
cmd,
130+
stdout=subprocess.PIPE,
131+
stderr=subprocess.PIPE,
132+
close_fds=True,
133+
env=env,
134+
)
135+
136+
client = Client("http://localhost:%d" % SERVER_PORT)
137+
138+
# Wait for the server to start up
139+
client.wait()
140+
141+
try:
142+
yield client
143+
finally:
144+
resp = client.get_ignored("/shutdown-tracer")
145+
assert resp.status_code == 200
146+
proc.terminate()
147+
148+
149+
@pytest.mark.snapshot()
150+
def test_simple_pyramid_app_endpoint(pyramid_client):
151+
r = pyramid_client.get("/")
152+
assert r.status_code == 200
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[[
2+
{
3+
"name": "pyramid.request",
4+
"service": "pyramid",
5+
"resource": "GET hello",
6+
"trace_id": 0,
7+
"span_id": 1,
8+
"error": 0,
9+
"parent_id": 0,
10+
"type": "web",
11+
"meta": {
12+
"http.method": "GET",
13+
"http.status_code": "200",
14+
"http.url": "http://localhost:8000/",
15+
"pyramid.route.name": "hello",
16+
"runtime-id": "af71bf0b951244fba40d444b10b2e462"
17+
},
18+
"metrics": {
19+
"_dd.agent_psr": 1.0,
20+
"_dd.measured": 1,
21+
"_dd.tracer_kr": 1.0,
22+
"_sampling_priority_v1": 1,
23+
"system.pid": 98244
24+
},
25+
"duration": 178000,
26+
"start": 1635361523989149000
27+
}]]

tests/webclient.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import requests
2+
import six
3+
import tenacity
4+
5+
from ddtrace.context import Context
6+
from ddtrace.filters import TraceFilter
7+
from ddtrace.propagation.http import HTTPPropagator
8+
9+
10+
class Client(object):
11+
"""HTTP Client for making requests to a local http server."""
12+
13+
def __init__(self, base_url):
14+
# type: (str) -> None
15+
self._base_url = base_url
16+
self._session = requests.Session()
17+
# Propagate traces with trace_id = 1 for the ping trace so we can filter them out.
18+
c, d = Context(trace_id=1, span_id=1), {}
19+
HTTPPropagator.inject(c, d)
20+
self._ignore_headers = d
21+
22+
def _url(self, path):
23+
# type: (str) -> str
24+
return six.moves.urllib.parse.urljoin(self._base_url, path)
25+
26+
def get(self, path, **kwargs):
27+
return self._session.get(self._url(path), **kwargs)
28+
29+
def get_ignored(self, path, **kwargs):
30+
"""Do a normal get request but signal that the trace should be filtered out.
31+
32+
The signal is a distributed trace id header with the value 1.
33+
"""
34+
headers = kwargs.get("headers", {}).copy()
35+
headers.update(self._ignore_headers)
36+
kwargs["headers"] = headers
37+
return self._session.get(self._url(path), **kwargs)
38+
39+
def post(self, path, *args, **kwargs):
40+
return self._session.post(self._url(path), *args, **kwargs)
41+
42+
def request(self, method, path, *args, **kwargs):
43+
return self._session.request(method, self._url(path), *args, **kwargs)
44+
45+
def wait(self, path="/", max_tries=100, delay=0.1):
46+
# type: (str, int, float) -> None
47+
"""Wait for the server to start by repeatedly http `get`ting `path` until a 200 is received."""
48+
49+
@tenacity.retry(stop=tenacity.stop_after_attempt(max_tries), wait=tenacity.wait_fixed(delay))
50+
def ping():
51+
r = self.get_ignored(path)
52+
assert r.status_code == 200
53+
54+
ping()
55+
56+
57+
class PingFilter(TraceFilter):
58+
def process_trace(self, trace):
59+
# Filter out all traces with trace_id = 1
60+
# This is done to prevent certain traces from being included in snapshots and
61+
# accomplished by propagating an http trace id of 1 with the request to the webserver.
62+
return None if trace and trace[0].trace_id == 1 else trace

0 commit comments

Comments
 (0)