-
Notifications
You must be signed in to change notification settings - Fork 560
chore(flask): emit MicroVM request-starting event #19816
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| from werkzeug.exceptions import NotFound | ||
|
|
||
| from ddtrace.contrib import trace_utils | ||
| from ddtrace.contrib._events.web_framework import WebFrameworkEvents | ||
| from ddtrace.ext import SpanTypes | ||
| from ddtrace.internal import core | ||
| from ddtrace.internal.constants import COMPONENT | ||
|
|
@@ -15,6 +16,7 @@ | |
| from ddtrace.internal.schema import schematize_service_name | ||
| from ddtrace.internal.schema import schematize_url_operation | ||
| from ddtrace.internal.schema.span_attribute_schema import SpanDirection | ||
| from ddtrace.internal.serverless import in_aws_lambda_microvm | ||
| from ddtrace.internal.settings.appsec_telemetry import config as appsec_telemetry_config | ||
| from ddtrace.internal.span_bus import span_from_context | ||
| from ddtrace.internal.utils import get_blocked | ||
|
|
@@ -391,8 +393,14 @@ def unpatch(): | |
|
|
||
| def patched_wsgi_app(wrapped, instance, args, kwargs): | ||
| environ, start_response = args | ||
| script_name = (environ.get("SCRIPT_NAME") or "").rstrip("/") | ||
| if in_aws_lambda_microvm(): | ||
| path_info = environ.get("PATH_INFO") or "" | ||
| core.dispatch( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we do this from the base wsgi middleware, will it happen early enough for you, and also hit a larger number of web integrations in 1 go? same with the asgi base setup? it shouldn't stop us from adding test cases per-integration that this is being handled properly, but would save us from needing to mimic the same event firing from all integrations. wdyt?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or, does the existing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the suggestion! Not aware that dd-trace-py already has wsgi/asgi handling. I think patching them would cover many cases. For other cases such as python stdlib http.server, it would require integration work #19817. |
||
| WebFrameworkEvents.WEB_REQUEST_STARTING.value, (environ.get("REQUEST_METHOD"), script_name + path_info) | ||
| ) | ||
| # Registration is gated on asm_config, not tracing — keep this above the tracing short-circuit. | ||
| _collect_routes_once(instance, environ.get("SCRIPT_NAME") or "") | ||
| _collect_routes_once(instance, script_name) | ||
| if not is_tracing_enabled(): | ||
| return wrapped(*args, **kwargs) | ||
| middleware = _FlaskWSGIMiddleware(wrapped, None, config.flask) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import mock | ||
|
|
||
| from ddtrace.contrib._events.web_framework import WebFrameworkEvents | ||
| from ddtrace.contrib.internal.flask.patch import patched_wsgi_app | ||
| from ddtrace.internal import core | ||
|
|
||
| from . import BaseFlaskTestCase | ||
|
|
||
|
|
||
| REQUEST_STARTING_PATH = "/web-request-starting" | ||
| MICROVM_RUNTIME_PREFIX = "/aws/lambda-microvms/runtime/v1" | ||
| MICROVM_RUN_HOOK_PATH = "/run" | ||
|
|
||
|
|
||
| class FlaskMicrovmIdentityRefreshTestCase(BaseFlaskTestCase): | ||
| """patched_wsgi_app() must dispatch every request's method/path before tracing starts. | ||
|
|
||
| The matching logic itself is tested in tests/tracer/runtime/test_runtime_id.py. | ||
| """ | ||
|
|
||
| def test_microvm_run_hook_request(self): | ||
| """No route is registered at the hook path: wsgi_app() runs before routing, so it | ||
| must fire even on a 404 -- the stronger, more general form of this check (whether the | ||
| route matches doesn't change what gets dispatched). | ||
| """ | ||
| with ( | ||
| mock.patch("ddtrace.contrib.internal.flask.patch.in_aws_lambda_microvm", return_value=True), | ||
| mock.patch("ddtrace.contrib.internal.flask.patch.core.dispatch", wraps=core.dispatch) as m, | ||
| ): | ||
| res = self.client.post(REQUEST_STARTING_PATH) | ||
|
|
||
| self.assertEqual(res.status_code, 404) | ||
| m.assert_any_call(WebFrameworkEvents.WEB_REQUEST_STARTING.value, ("POST", REQUEST_STARTING_PATH)) | ||
|
|
||
| def test_other_request_does_not_dispatch_outside_microvm(self): | ||
| @self.app.route("/") | ||
| def index(): | ||
| return "ok", 200 | ||
|
|
||
| with ( | ||
| mock.patch("ddtrace.contrib.internal.flask.patch.in_aws_lambda_microvm", return_value=False), | ||
| mock.patch("ddtrace.contrib.internal.flask.patch.core.dispatch", wraps=core.dispatch) as m, | ||
| ): | ||
| res = self.client.get("/") | ||
|
|
||
| self.assertEqual(res.status_code, 200) | ||
| assert all(call.args[0] != WebFrameworkEvents.WEB_REQUEST_STARTING.value for call in m.call_args_list) | ||
|
|
||
| def test_dispatches_script_name_prefixed_request_path(self): | ||
| with ( | ||
| mock.patch("ddtrace.contrib.internal.flask.patch.in_aws_lambda_microvm", return_value=True), | ||
| mock.patch("ddtrace.contrib.internal.flask.patch.core.dispatch", wraps=core.dispatch) as m, | ||
| ): | ||
| res = self.client.post(MICROVM_RUN_HOOK_PATH, environ_overrides={"SCRIPT_NAME": MICROVM_RUNTIME_PREFIX}) | ||
|
|
||
| self.assertEqual(res.status_code, 404) | ||
| m.assert_any_call( | ||
| WebFrameworkEvents.WEB_REQUEST_STARTING.value, | ||
| ("POST", MICROVM_RUNTIME_PREFIX + MICROVM_RUN_HOOK_PATH), | ||
| ) | ||
|
|
||
| def test_pre_request_event_dispatches_before_wsgi_middleware(self): | ||
| events = [] | ||
| environ = {"REQUEST_METHOD": "POST", "PATH_INFO": REQUEST_STARTING_PATH, "SCRIPT_NAME": ""} | ||
|
|
||
| def start_response(status, headers, exc_info=None): | ||
| pass | ||
|
|
||
| def wrapped(environ, start_response): | ||
| return [] | ||
|
|
||
| def dispatch(name, args): | ||
| if name == WebFrameworkEvents.WEB_REQUEST_STARTING.value: | ||
| events.append("starting") | ||
|
|
||
|
Comment on lines
+63
to
+75
|
||
| class WSGIMiddleware: | ||
| def __init__(self, app, tracer, integration_config): | ||
| pass | ||
|
|
||
| def __call__(self, environ, start_response): | ||
| events.append("middleware") | ||
| return [] | ||
|
|
||
| with ( | ||
| mock.patch("ddtrace.contrib.internal.flask.patch.in_aws_lambda_microvm", return_value=True), | ||
| mock.patch("ddtrace.contrib.internal.flask.patch.core.dispatch", side_effect=dispatch), | ||
| mock.patch("ddtrace.contrib.internal.flask.patch._collect_routes_once"), | ||
| mock.patch("ddtrace.contrib.internal.flask.patch.is_tracing_enabled", return_value=True), | ||
| mock.patch("ddtrace.contrib.internal.flask.patch._FlaskWSGIMiddleware", WSGIMiddleware), | ||
| ): | ||
| patched_wsgi_app(wrapped, self.app, (environ, start_response), {}) | ||
|
|
||
| assert events == ["starting", "middleware"] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand entirely what's happening here, so apologies if the question is silly, but shouldn't we be checking for the expected
/runrequest at some point? It looks like we always emit the starting event as soon as we get a request within a MicroVM instanceThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes we can add pre-check here to minimize the event firing. To answer your question, the path check occurs on the listener side in the followup PR
https://github.com/DataDog/dd-trace-py/pull/19825/changes#diff-433e6ca1c168b17797cede9c3332a99224d530e0edbfdd2cceafeafd8bf79c51R105