-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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 logging to credentials #12319
Add logging to credentials #12319
Conversation
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.
Should we use span?
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.
Another question is does user always want to log the successful info?
Or we want to make it as opt-in mode?
What do you mean?
It's already opt in. Users control what's logged and how by configuring |
I meant e.g. https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/core/azure-core-tracing-opencensus
I meant should we only log these information (including successful calls) only in debug mode? |
When we first added tracing, we chose not to decorate credentials for it. Doing so would be work for a separate PR. This one's focused on the cross-language effort to improve what users get from their language's standard logging tools.
I think this is useful basic information not detailed enough to debug a complex scenario. Logging it at |
@@ -61,6 +66,11 @@ def __init__(self, client_id, **kwargs): | |||
self._prompt_callback = kwargs.pop("prompt_callback", None) | |||
super(DeviceCodeCredential, self).__init__(client_id=client_id, **kwargs) | |||
|
|||
@log_get_token(_LOGGER, "DeviceCodeCredential") |
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.
Does self.class.name work here? Then we don't need to have this in the sub-classes.
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 wish it did. However, the decorator is applied during parsing of the class definition. There's no self
and the class isn't yet defined (so this can't be just DeviceCodeCredential
).
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.
Then I doubt if decorator is the right way to go.
It will require us dup a lot of code for some complex hierarchy.
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 interested in better ways to do this but I don't see duplication as one of the problems here. The decorator adds 2 lines of boilerplate (import the decorator, apply it to get_token
) to every credential module. How could we get the same result with less duplication? What do you mean by "complex hierarchy"?
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 meant if the hierarchy is very deep and wide like a tree, we would not be able to fully benefit from polymorphism.
And my another concern is like
class A()
class B(A)
class C(B)
My expected behavior: when I call A->get_token(), it can recognize the real class is C hence it calls C->get_token() and only logs C->get_token() is called.
In our case, C->get_token() ("C is called" is logged) will call into B->get_token() and call into A->get_token(), will we have "B is called" & "A is called" in our log?
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.
First, let's agree not to build a deep and wide inheritance hierarchy 🤝
We're talking about an instance method. Calling C().get_token()
only invokes A().get_token()
if it calls super
or C
doesn't override get_token
. But yes, if one decorated method calls another, each adds log entries. This isn't particular to the decorator though. Imagine the obvious alternative implementation: every get_token
does its own logging. Then if C.get_token()
calls A().get_token()
, you still get logs from each. Nor is this necessarily a problem. One of our goals is to provide insight into what composite credentials like DefaultAzureCredential
are doing. We want to see logs from both DefaultAzureCredential
and the inner credentials it invokes.
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 already have it?
UsernamePasswordCredential, InteractiveBrowserCredential, DeviceCodeCredential all inherit from InteractiveCredential which is a subclass of PublicClientCredential which is a sub class of MsalCredential.
Though now we are not quite hit by the problem because get_token in PublicClientCredential & MsalCredential are abstractmethod. We need to think about the cost of one day it happens.
Another question is let's say we want the logs of UsernamePasswordCredential, InteractiveBrowserCredential & DeviceCodeCredential to be a little bit different. How can we achieve it?
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.
Another question is let's say we want the logs of UsernamePasswordCredential, InteractiveBrowserCredential & DeviceCodeCredential to be a little bit different. How can we achieve it?
The differences in their behavior are in their _request_token
methods. We can log anything specific to an implementation in that method.
@@ -76,6 +81,21 @@ def __init__(self, **kwargs): | |||
**kwargs | |||
) | |||
|
|||
if self._credential: | |||
_LOGGER.info("Environment is configured for %s", self._credential.__class__.__name__) |
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.
Just a question since I'm not familiar enough with how the credentials are set in python, do you log environment configurations for other credentials that are affected when there are environment variables present? For instance, in managed identity, if client ID is set, that affects the credential.
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.
No, in this PR only EnvironmentCredential
logs environment variables. Do we intend to log that a particular variable is set everywhere it's used?
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 did settle on the fact that this is an important logging scenario, since env vars do impact the request for credentials when they are present. I would say that if possible we should only log them once per total log output. In Go, I have not quite reached that, I log them once per credential that is instantiated.
except Exception as ex: # pylint: disable=broad-except | ||
# credential failed to authenticate, or something unexpectedly raised -> break | ||
history.append((credential, str(ex))) | ||
_LOGGER.warning( | ||
'%s.get_token failed: %s raised unexpected error "%s"', |
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.
Given this is chained credential, should this be a warning? Or just info?
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.
The design guidelines stipulate exceptions be logged as warnings.
@@ -124,6 +123,10 @@ def get_token(self, *scopes, **kwargs): | |||
`message` attribute listing each authentication attempt and its error message. | |||
""" | |||
if self._successful_credential: | |||
return self._successful_credential.get_token(*scopes, **kwargs) | |||
token = self._successful_credential.get_token(*scopes, **kwargs) |
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 call must succeed, right?
Then maybe we can just do
_LOGGER.info(
"%s acquired a token from %s", self.__class__.__name__, self._successful_credential.__class__.__name__
)
return self._successful_credential.get_token(*scopes, **kwargs)
?
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 call could fail. Maybe the credential requests a new token and something goes wrong.
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.
Do we want to add timestamps into the logs?
No, |
@chlowell can you please get this merged ahead of the Wednesday out-of-band Identity preview? |
…into ta_opinion_mining_sample * 'master' of https://github.com/Azure/azure-sdk-for-python: (124 commits) [formrecognizer] Add type to FormField (Azure#12561) Add example summary for azure-identity readme.md (Azure#12509) Add logging to credentials (Azure#12319) Sdk automation/track2 azure mgmt keyvault (Azure#12638) Remove unnecessary coroutine declaration (Azure#12602) [Cosmos] Fix type comment (Azure#12598) replace aka link (Azure#12597) [ServiceBus] Message/ReceivedMessage Properties alignment with other languages (Azure#12451) Find list of installed packages using pkg_resources (Azure#12591) token refresh offset (Azure#12136) updates (Azure#12595) User authentication samples (Azure#11343) Remove unnecessary base class (Azure#12374) Sequence -> Iterable for scopes (Azure#12579) Disable apistubgen step until issue is fixed (Azure#12594) fix pylint issue (Azure#12578) fix name in example (Azure#12572) Update tests.md (Azure#12574) Add stress tests for max batch size/prefetch, and for unsettled message receipt. Add capability to not auto-complete and adjust max_batch_size into the base stress tester. (Azure#12344) [formrecognizer] Capitalize enum values (Azure#12540) ...
expected_variables = set(EnvironmentVariables.CERT_VARS + EnvironmentVariables.CLIENT_SECRET_VARS) | ||
set_variables = [v for v in expected_variables if v in os.environ] | ||
if set_variables: | ||
_LOGGER.warning("Incomplete environment configuration. Set variables: %s", ", ".join(set_variables)) |
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 sentence feels semantically ambiguous to me, it can mean either:
- [imperative mood] Please set variables
- [past participle] Variables that are already set
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.
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 adds logging to all credentials in alignment with the broad cross-language design implemented in Go and Java SDKs (closes #5548). I'm less than fully satisfied with the decorator approach I took here but it's the most expedient way I found to meet these requirements:
get_token
callget_token
exceptions at WARNING level, add tracebacks when the logger is enabled for DEBUG levelBasically I wanted context scoped logging. Getting a weak version of it here felt like hammering a square peg into a round hole but I'm not so familiar with
logging
and wouldn't be surprised to learn I missed a better way--alternative suggestions welcome 😸example output
typical success and failure
DefaultAzureCredential.get_token()
EnvironmentCredential()
nothing configured
partially configured
fully configured
ManagedIdentityCredential