I started to use Flask APM package today. It worked fine. But when I tried to add contextual information, I got into trouble.
I looked for official doc (Flask support | APM Python Agent Reference [2.x] | Elastic) and got nothing. Then I started looking into site-packages/elasticapm/contrib/flask/__init__.py and found that there isn't any method in ElasticAPM class to add contextual information. (The elasticapm package does have a series of methods like set_user_context though)
Since there is already a plugin for Flask, I don't want to make a wheel again. So I started hacking the package. Here is how I think and what I've done:
The request_finished method in ElasticAPM class connected to Flask's request_finished signal. And when a request is finished, the signal would be triggered. So what I need to do is to add contextual information before request_finished method get executed, by using a monkey patch to this method.
Here the original request_finished method:
def request_finished(self, app, response):
if not self.app.debug or self.client.config.debug:
...[omitted]...
elasticapm.set_transaction_name(rule, override=False)
elasticapm.set_transaction_result(result, override=False)
# Instead of calling end_transaction here, we defer the call until the response is closed.
# This ensures that we capture things that happen until the WSGI server closes the response.
response.call_on_close(self.client.end_transaction)
Here is my app/__init__.py:
from .utils import monkey_patch
apm = ElasticAPM()
apm.request_finished = monkey_patch.elastic_apm_request_finished(apm.request_finished)
def create_app():
app = Flask(__name__)
app.config.from_object(config)
apm.init_app(app,
service_name=app.config['ELASTIC_APM']['SERVICE_NAME'],
secret_token=app.config['ELASTIC_APM']['SECRET_TOKEN'],
server_url=app.config['ELASTIC_APM']['SERVER_URL'])
...
return app
And my monkey_patch.py:
import functools
import elasticapm
from flask import request
def elastic_apm_request_finished(original_func):
@functools.wraps(original_func)
def _patched(self, app, response):
if not self.app.debug or self.client.config.debug:
# add user_id in context
elasticapm.set_user_context(user_id=request.cookies.get('UM_distinctid', None))
# execute the original `request_finished` function
original_func(self, app, response)
return _patched
It should work as expected. But unfortunately it didn't. I got
TypeError: _patched() missing 1 required positional argument: 'app'
After a quite-deep look into the code, I found the reason for the error.
The blinker (which is the package handles the signals) only pass two parameters when invoking the request_finished. app as positional parameter, and response as named parameter. As a method in a class, Python should add parameter self automatically. But it is not actually adding this parameter, after I decorated the request_finished method. So the actual app parameter is taking the position of self, and the second second parameter gets nothing!
I'm working with this problem for hours and getting quite frustrated. Does anyone knows where I'm wrong, or is there any better way to add contextual information?
