diff --git a/scenarios/python_flask_3.11/Dockerfile b/scenarios/python_flask_3.11/Dockerfile new file mode 100644 index 0000000..a1be74e --- /dev/null +++ b/scenarios/python_flask_3.11/Dockerfile @@ -0,0 +1,22 @@ +ARG BASE_IMAGE="prof-python-3.11" +FROM $BASE_IMAGE + +# Copy the Python target into the container +COPY ./scenarios/python_flask_3.11/requirements.txt /app/ +RUN chmod 644 /app/* + +# Set the working directory to the location of the program +WORKDIR /app + +RUN pip install -r requirements.txt + +COPY ./scenarios/python_flask_3.11/app.py /app/ + +ENV DD_PROFILING_ENABLED=true +ENV DD_TRACE_ENABLED=false +ENV DD_TRACE_DEBUG=false +ENV DD_PROFILING_OUTPUT_PPROF="/app/data/profiles" +ENV EXECUTION_TIME_SEC="5" + +# Run the program when the container starts +CMD ["ddtrace-run", "flask", "run", "--host=0.0.0.0", "--port=8000"] diff --git a/scenarios/python_flask_3.11/README.md b/scenarios/python_flask_3.11/README.md new file mode 100644 index 0000000..9693892 --- /dev/null +++ b/scenarios/python_flask_3.11/README.md @@ -0,0 +1,13 @@ +# Python Flask correctness check + +This test validates that Flask apps are properly profiled. + +## Test application + +The Flask app runs a web server with two endpoints: + +- `/`: Returns "Hello, World!" after performing CPU-intensive computation (0.5s of busy work) +- `/stop`: Terminates the process via SIGINT + +A background thread makes HTTP requests to the main endpoint for 5 seconds, then calls the stop endpoint to gracefully +terminate the application. This ensures the profiler captures Flask request handling and CPU activity. diff --git a/scenarios/python_flask_3.11/app.py b/scenarios/python_flask_3.11/app.py new file mode 100644 index 0000000..e62f192 --- /dev/null +++ b/scenarios/python_flask_3.11/app.py @@ -0,0 +1,46 @@ +import os +import signal +import time +from threading import Thread + +import requests +from flask import Flask + +app = Flask(__name__) + + +def _make_requests() -> None: + start = time.monotonic() + while time.monotonic() - start < 5.0: + try: + requests.get("http://localhost:8000", timeout=1) + except Exception as e: # noqa: PERF203,BLE001 + print(f"Error making request: {e}") + + requests.get("http://localhost:8000/stop", timeout=1) + + +def compute_big_number() -> int: + start = time.monotonic() + while time.monotonic() - start < 0.5: + x = 0 + for i in range(1000000): + x += i + return x + + +@app.route("/") +def hello_world() -> str: + x = compute_big_number() + return f"

Hello, World! {x}

" + + +@app.route("/stop") +def stop() -> str: + os.kill(os.getpid(), signal.SIGINT) + return "Stopping..." + + +t = Thread(target=_make_requests, name="Requester") +t.daemon = True +t.start() diff --git a/scenarios/python_flask_3.11/expected_profile.json b/scenarios/python_flask_3.11/expected_profile.json new file mode 100644 index 0000000..85d829e --- /dev/null +++ b/scenarios/python_flask_3.11/expected_profile.json @@ -0,0 +1,37 @@ +{ + "test_name": "python_flask_3.11", + "scale_by_duration": false, + "pprof-regex": "", + "stacks": [ + { + "profile-type": "wall-time", + "pprof-regex": "", + "stack-content": [ + { + "regular_expression": ".*_make_requests.*", + "percent": 33, + "error_margin": 5, + "labels": [ + { + "key": "thread name", + "values": [ + "Requester" + ] + } + ] + } + ] + }, + { + "profile-type": "wall-time", + "pprof-regex": "", + "stack-content": [ + { + "regular_expression": ".*hello_world;compute_big_number.*", + "percent": 33, + "error_margin": 5 + } + ] + } + ] +} \ No newline at end of file diff --git a/scenarios/python_flask_3.11/requirements.txt b/scenarios/python_flask_3.11/requirements.txt new file mode 100644 index 0000000..cc57c13 --- /dev/null +++ b/scenarios/python_flask_3.11/requirements.txt @@ -0,0 +1,3 @@ +ddtrace +requests +Flask