Skip to content
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

Merged
merged 9 commits into from
Jul 21, 2020
Merged

Add logging to credentials #12319

merged 9 commits into from
Jul 21, 2020

Conversation

chlowell
Copy link
Member

@chlowell chlowell commented Jul 2, 2020

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:

  • log success or failure of every get_token call
  • log all unhandled get_token exceptions at WARNING level, add tracebacks when the logger is enabled for DEBUG level
  • correlate log records to credentials
    • i.e. include credential names, use loggers from credential modules

Basically 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

[INFO] ClientSecretCredential.get_token succeeded

[WARNING] ClientSecretCredential.get_token failed: Azure Active Directory error '(invalid_client) AADSTS7000215: Invalid client secret is provided.
Trace ID: 95a0fb68-9a2e-4bcf-ad0f-001672c33400
Correlation ID: bc737463-0ee7-4d61-9f80-fd2f5cbaf7cc
Timestamp: 2020-07-01 22:50:44Z'

DefaultAzureCredential.get_token()

[WARNING] EnvironmentCredential.get_token failed: EnvironmentCredential authentication unavailable. Environment variables are not fully configured.
[INFO] DefaultAzureCredential - EnvironmentCredential is unavailable
[INFO] No response from the IMDS endpoint.
[WARNING] ManagedIdentityCredential.get_token failed: ManagedIdentityCredential authentication unavailable, no managed identity endpoint found.
[INFO] DefaultAzureCredential - ManagedIdentityCredential is unavailable
[INFO] SharedTokenCacheCredential.get_token succeeded
[INFO] DefaultAzureCredential acquired a token from SharedTokenCacheCredential

EnvironmentCredential()

nothing configured

[INFO] No environment configuration found.

partially configured

[WARNING] Incomplete environment configuration. Set variables: AZURE_CLIENT_ID, AZURE_CLIENT_SECRET

fully configured

[INFO] Environment is configured for ClientSecretCredential

ManagedIdentityCredential

[INFO] ManagedIdentityCredential will use IMDS
[INFO] No response from the IMDS endpoint.
[WARNING] ManagedIdentityCredential.get_token failed: ManagedIdentityCredential authentication unavailable, no managed identity endpoint found.

@chlowell chlowell added Client This issue points to a problem in the data-plane of the library. Azure.Identity labels Jul 2, 2020
@chlowell chlowell requested a review from schaabs as a code owner July 2, 2020 00:10
Copy link
Member

@xiangyan99 xiangyan99 left a 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?

Copy link
Member

@xiangyan99 xiangyan99 left a 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?

@chlowell
Copy link
Member Author

chlowell commented Jul 2, 2020

Should we use span?

What do you mean?

Another question is does user always want to log the successful info?
Or we want to make it as opt-in mode?

It's already opt in. Users control what's logged and how by configuring logging.

@xiangyan99
Copy link
Member

Should we use span?

What do you mean?

I meant e.g. https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/core/azure-core-tracing-opencensus

Another question is does user always want to log the successful info?
Or we want to make it as opt-in mode?

It's already opt in. Users control what's logged and how by configuring logging.

I meant should we only log these information (including successful calls) only in debug mode?

@chlowell
Copy link
Member Author

chlowell commented Jul 2, 2020

I meant e.g. https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/core/azure-core-tracing-opencensus

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 meant should we only log these information (including successful calls) only in debug mode?

I think this is useful basic information not detailed enough to debug a complex scenario. Logging it at DEBUG level would hide it from users. It also wouldn't align with our Python guidelines, which want exceptions logged at WARNING level and normal operation at INFO level.

@@ -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")
Copy link
Member

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.

Copy link
Member Author

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).

Copy link
Member

@xiangyan99 xiangyan99 Jul 2, 2020

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.

Copy link
Member Author

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"?

Copy link
Member

@xiangyan99 xiangyan99 Jul 2, 2020

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?

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member Author

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.

@chlowell chlowell requested a review from xiangyan99 July 2, 2020 22:20
@chlowell chlowell marked this pull request as draft July 3, 2020 02:40
@chlowell chlowell marked this pull request as ready for review July 10, 2020 17:38
@@ -76,6 +81,21 @@ def __init__(self, **kwargs):
**kwargs
)

if self._credential:
_LOGGER.info("Environment is configured for %s", self._credential.__class__.__name__)
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

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"',
Copy link
Member

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?

Copy link
Member Author

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)
Copy link
Member

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)

?

Copy link
Member Author

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.

Copy link
Member

@xiangyan99 xiangyan99 left a 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?

@chlowell
Copy link
Member Author

Do we want to add timestamps into the logs?

No, logging timestamps logs and applications decide how to display them.

xiangyan99
xiangyan99 previously approved these changes Jul 18, 2020
@joshfree joshfree added the blocking-release Blocks release label Jul 20, 2020
@joshfree joshfree added this to the [2020] August milestone Jul 20, 2020
@joshfree
Copy link
Member

@chlowell can you please get this merged ahead of the Wednesday out-of-band Identity preview?

xiangyan99
xiangyan99 previously approved these changes Jul 20, 2020
@chlowell chlowell merged commit 86f4124 into Azure:master Jul 21, 2020
@chlowell chlowell deleted the logging branch July 21, 2020 15:16
iscai-msft added a commit to iscai-msft/azure-sdk-for-python that referenced this pull request Jul 21, 2020
…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))
Copy link
Member

@jiasli jiasli Dec 23, 2020

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ghost pushed a commit that referenced this pull request Jan 4, 2021
rakshith91 pushed a commit to rakshith91/azure-sdk-for-python that referenced this pull request Jan 8, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Azure.Identity blocking-release Blocks release Client This issue points to a problem in the data-plane of the library.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add logging to credentials
5 participants