-
Notifications
You must be signed in to change notification settings - Fork 656
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
Add support for programmatic instrumentation #579
Conversation
I think
That interface is important because it's called by the autoinstrumenation command here Line 30 in b62c233
I think we could implement the I also think we should just call them About programmatic instrumentation, I think it is a concept, having a function with that name is not clear at all. In the specific case of from opentelemetry.ext.flask import InstrumentedFlask
app = InstrumentedFlask(__name__)
# app is now instrumented To summarize my proposal:
I'd love to get more feedback on this. |
We should not get rid of BaseInstrumentor, because if we do, we'll end up implementing manually the very same checks, accurate error reporting and idempotence that this ABC does for us already.
Just to be clear, I am not suggesting that we have a programmatic instrumentation function for every instrumentation (that is why there is no
|
def _instrument(self): | ||
self._original_flask = flask.Flask | ||
def _instrument( | ||
self, flask_class=None |
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.
any reason not to use kwargs here? and in _uninstrument
?
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.
yes, only because kwargs
is not used in _instrument
nor in _uninstrument
. I mean, there is no reference to kwargs
in the code of _instrument
or _uninstrument
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.
but you could pull flask_class
from the kwargs
instead of overriding the method signature correct? any reason not to go that route?
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.
we could do that but it may not play nice with the documentation of the optional flask_class
argument (not that there is any here, though 😅)
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.
Only question regarding overriding the interface with different arguments. Otherwise this is a good improvement!
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.
Even if I don't completely agree with having an ABC and so on, I think the changes to BaseInstrumentor
in the last iteration are good. Making it a singleton and allowing arbitrary arguments helps.
However I think the changes to the Flask are not that good, the proposed way for users to enable instrumentation is rather complicated. I still think we should have instrument
and uninstrument
methods that are simple to use (just a single call, few parameters and not return value) that are used by both, the automatic instrumentation command and the users. I'm aware that in the users case this function could not work in all the cases, for instance if you call it too late in the code after importing some modules, but I think that with correct documentation and warnings they would be very useful.
For the programmatic case, we could use a much simpler approach, just expose and let the user interact with InstrumentedFlask
.
from opentelemetry.ext.flask import InstrumentedFlask as Flask
app = Flask(...) # app will be instrumented
or
from flask import Flask
from opentelemetry.ext.flask import InstrumentedFlask
app1 = Flask(...) # this won't be instrument
app2 = InstrumentedFlask(...) # this will be
The instrument
method should be used for automatic instrumentation and when the user want's to instrument everything in that framework, the later with some restrictions, like enforcing a proper order in the imports.
from opentelemetry.ext.flask import FlaskInstrumentor
FlaskInstrumentor().instrument()
from flask import Flask # needed to do after
app = Flask(...) # instrumented
The following case won't work, but we could print a warning to the user that the flask module is already imported and it might not work.
from flask import Flask
from opentelemetry.ext.flask import FlaskInstrumentor
FlaskInstrumentor().instrument()
app = Flask(...) # uninstrumented, reference to Flask was taken before instrumenting
from flask import Flask | ||
from opentelemetry.ext.flask import FlaskInstrumentor | ||
|
||
Flask = FlaskInstrumentor().instrument(flask_class=Flask) |
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.
This looks too complicated to me. The instrument
method does not do any instrumentation but just returns an instrumented version of the Flask class that the user has to assign to the local Flask reference. Btw, the current uninstrument
will do nothing after that call, somehow the user will have to update the Flask
reference to the original flask.Flask
class.
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.
This leads me to believe we might need to patch the flask object instead of the class itself (like a middleware)? This way we will have direct control over what is instrumented or not. What if the user wants only some flask instances to be instrumented and others not to be? And same goes for uninstrumenting as well.
def _instrument( | ||
self, flask_class=None | ||
): # pylint: disable=arguments-differ | ||
if flask_class is not None: |
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.
This if
makes this function to behave very different. Related to my long comment above, the case where you pass flask_class
is not doing any instrumentation but just returning _InstrumentedFlask
.
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 think we can find another way of using _instrument
here. Maybe pass the Flask app as was done before? That would be fine. I still would like to have instrumentation depend on it being done before something is imported only as a very last option because of how fragile and hard to debug this approach is (it also breaks PEP8).
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.
What is the reason to implement extra functionalities in _instrument()
?, I think the scope of this method is to be called by the autoinstrument command and by users that want to enable instrumentation in all that framework. If we start adding extra functionalities and special cases we'll end up with a lot of different _instrument()
what must be used in a specific way to work.
What do you think about exposing InstrumentedFlask to the user for the "programmatic instrumentation" case as I exposed before?
from flask import Flask | ||
from opentelemetry.ext.flask import FlaskInstrumentor | ||
|
||
Flask = FlaskInstrumentor().instrument(flask_class=Flask) |
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.
Any naming conflicts in this example due to Flask
imported library and instantiated variable?
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.
That's actually the purpose of it, to overwrite the imported Flask
to make it instrumented.
flask.Flask = _InstrumentedFlask | ||
|
||
def _uninstrument(self): | ||
flask.Flask = self._original_flask | ||
return None |
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.
Why is None
returned if the original class is not supplied? Shouldn't InstrumentedFlask
be returned?
I'm not able to find the |
Thanks for pointing that out, I have updated the comment. |
I'd propose to split this PR up. I think we could easily agree on the changes to the instrumentor (make it a singleton and add kwargs). About the Flask I think there is still some discussion to do, so better to move ahead with the part we agree now. |
Good idea, splitting... |
Ok, this has been split, let's move the conversation of Flask changes to #601. |
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.
A couple of non blocking comments.
opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py
Show resolved
Hide resolved
self._is_instrumented = True | ||
return result | ||
|
||
_LOG.warning("Attempting to instrument while already instrumented") | ||
_LOG.warning( | ||
"Attempting to automatically instrument while already instrumented" |
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.
What about removing "auomatically" and keeping the message as it was?
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.
+1, since this can be called both from auto and programmatic instrumentation, the original warning was clearer
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.
Couple of minor comments, otherwise this makes the interface more usable. thanks for separating this from the flask changes, it makes the review much simpler
ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py
Outdated
Show resolved
Hide resolved
self._is_instrumented = True | ||
return result | ||
|
||
_LOG.warning("Attempting to instrument while already instrumented") | ||
_LOG.warning( | ||
"Attempting to automatically instrument while already instrumented" |
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.
+1, since this can be called both from auto and programmatic instrumentation, the original warning was clearer
self._is_instrumented = False | ||
return result | ||
|
||
_LOG.warning("Attempting to uninstrument while already uninstrumented") | ||
_LOG.warning( | ||
"Attempting to automatically uninstrument while already" |
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.
same comment about the warning message
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.
LGTM
…try#579) Fixes open-telemetry#554 This makes it possible to call the instrument method with arguments that make programmatic instrumentation possible. This also makes the children of BaseInstrumentors to be singletons. In this way regardless of how many times the programmatic instrumentation or uninstrumentation methods are called they will only be executed once.
closes open-telemetry#579 Signed-off-by: Olivier Albertini <[email protected]>
Fixes #554
This makes it possible to call the
instrument
method with arguments that make programmatic instrumentation possible.This also makes the children of
BaseInstrumentors
to be singletons. In this way regardless of how many times the programmatic instrumentation or uninstrumentation methods are called they will only be executed once.