Skip to content

Commit c94b980

Browse files
[python_gunicorn] Initial commit
1 parent 35c644a commit c94b980

File tree

5 files changed

+198
-0
lines changed

5 files changed

+198
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
ARG BASE_IMAGE="prof-python-3.11"
2+
FROM $BASE_IMAGE
3+
4+
# Copy the Python target into the container
5+
COPY ./scenarios/python_gunicorn_3.11/requirements.txt /app/
6+
RUN chmod 644 /app/*
7+
8+
# Set the working directory to the location of the program
9+
WORKDIR /app
10+
11+
RUN pip install -r requirements.txt
12+
13+
COPY ./scenarios/python_gunicorn_3.11/main.py /app/
14+
15+
ENV DD_PROFILING_ENABLED=true
16+
ENV DD_TRACE_ENABLED=false
17+
ENV DD_TRACE_DEBUG=false
18+
ENV DD_PROFILING_OUTPUT_PPROF="/app/data/profiles"
19+
ENV EXECUTION_TIME_SEC="5"
20+
21+
# Run the program when the container starts
22+
CMD ["ddtrace-run", "gunicorn", "main:app"]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Description
2+
3+
Correctness check for `gunicorn`.
4+
5+
Because we want to run the test for a few seconds, then to exit automatically, the script is both the app definition and
6+
a worker thread that issues HTTP requests to the app. After a few seconds, a special request is sent which makes the app
7+
exit, terminating the process (at which point we look at the resulting Profiles from the testing framework).
8+
9+
In the expected Profile, we look both at how much CPU and time the app consumes, and how much CPU and time the Requester
10+
thread uses.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
{
2+
"test_name": "python_gunicorn_3.11",
3+
"scale_by_duration": false,
4+
"pprof-regex": "profiles.*\\.pprof$",
5+
"stacks": [
6+
{
7+
"profile-type": "cpu-time",
8+
"pprof-regex": "profiles\\.([2-9]|[1-9][0-9]+)\\..*\\.pprof$",
9+
"stack-content": [
10+
{
11+
"regular_expression": ".*make_request.*",
12+
"percent": 5,
13+
"error_margin": 10,
14+
"labels": [
15+
{
16+
"key": "thread name",
17+
"values": [
18+
"Requester"
19+
]
20+
}
21+
]
22+
}
23+
]
24+
},
25+
{
26+
"profile-type": "wall-time",
27+
"pprof-regex": "profiles\\.([2-9]|[1-9][0-9]+)\\..*\\.pprof$",
28+
"stack-content": [
29+
{
30+
"regular_expression": ".*make_request.*",
31+
"percent": 50,
32+
"error_margin": 10,
33+
"labels": [
34+
{
35+
"key": "thread name",
36+
"values": [
37+
"Requester"
38+
]
39+
}
40+
]
41+
}
42+
]
43+
},
44+
{
45+
"profile-type": "cpu-time",
46+
"pprof-regex": "profiles\\.([2-9]|[1-9][0-9]+)\\..*\\.pprof$",
47+
"stack-content": [
48+
{
49+
"regular_expression": ".*process.*",
50+
"percent": 85,
51+
"error_margin": 15,
52+
"labels": [
53+
{
54+
"key": "thread name",
55+
"values": [
56+
"MainThread"
57+
]
58+
}
59+
]
60+
}
61+
]
62+
},
63+
{
64+
"profile-type": "cpu-time",
65+
"pprof-regex": "profiles\\.([2-9]|[1-9][0-9]+)\\..*\\.pprof$",
66+
"stack-content": [
67+
{
68+
"regular_expression": ".*sub_process.*",
69+
"percent": 15,
70+
"error_margin": 10,
71+
"labels": [
72+
{
73+
"key": "thread name",
74+
"values": [
75+
"MainThread"
76+
]
77+
}
78+
]
79+
}
80+
]
81+
},
82+
{
83+
"profile-type": "wall-time",
84+
"pprof-regex": "profiles\\.([2-9]|[1-9][0-9]+)\\..*\\.pprof$",
85+
"stack-content": [
86+
{
87+
"regular_expression": ".*process.*",
88+
"percent": 50,
89+
"error_margin": 10,
90+
"labels": [
91+
{
92+
"key": "thread name",
93+
"values": [
94+
"MainThread"
95+
]
96+
}
97+
]
98+
}
99+
]
100+
}
101+
]
102+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
This is a correctness check for ddtrace with a very basic (sync) Gunicorn application.
3+
Gunicorn will run in the process we start and create one worker process.
4+
5+
What we do is start a thread that will issue HTTP requests to the Gunicorn application.
6+
Processing this HTTP request will consume some CPU time (that we should profile).
7+
After a while, this threads sends a special HTTP request, asking to stop the application.
8+
"""
9+
10+
import os
11+
import signal
12+
import time
13+
from collections.abc import Callable, Iterator
14+
from threading import Thread
15+
16+
import requests
17+
18+
19+
def make_request() -> None:
20+
expected_duration = int(os.environ.get("EXECUTION_TIME_SEC", "5"))
21+
22+
start = time.monotonic()
23+
while time.monotonic() - start < expected_duration:
24+
try:
25+
requests.get("http://localhost:8000", timeout=1)
26+
except Exception: # noqa: PERF203,BLE001
27+
print("Error making request")
28+
29+
requests.get("http://localhost:8000/stop", timeout=1)
30+
31+
32+
def sub_process(x: float) -> float:
33+
return x * 1.0001
34+
35+
36+
def process(target: int) -> tuple[float, int]:
37+
x = 1.0
38+
i = 0
39+
while x < target:
40+
x = sub_process(x)
41+
i += 1
42+
43+
return x, i
44+
45+
46+
def app(environ: dict[str, str], start_response: Callable[[str, list[tuple[str, str]]], None]) -> Iterator[bytes]:
47+
x, iterations = process(target=123)
48+
data = f"Result: {(x, iterations)}".encode()
49+
status = "200 OK"
50+
response_headers = [("Content-type", "text/plain"), ("Content-Length", str(len(data)))]
51+
52+
if environ["RAW_URI"] == "/stop":
53+
os.kill(os.getppid(), signal.SIGQUIT)
54+
55+
start_response(status, response_headers)
56+
return iter([data])
57+
58+
59+
t = Thread(target=make_request, name="Requester")
60+
t.daemon = True
61+
t.start()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ddtrace
2+
gunicorn
3+
requests

0 commit comments

Comments
 (0)