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?