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 a context manager object and a method to call said object #357

Open
wants to merge 32 commits into
base: master
Choose a base branch
from

Conversation

ajtran
Copy link

@ajtran ajtran commented Nov 6, 2020

Description of the change

Introduce watch method to wrap the calling of the context manager. The context manager _BigBrother handles adding scope objects to the stack when the code flow enters the context manager and removing the most recent scope object when exiting the context manager without an exception. If there is an exception, the most recent scope object will be added to the root level of the raw item payload and be sent under the key scope.

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Related issues

Checklists

Development

  • Lint rules pass locally
  • The code changed/added as part of this pull request has been covered with tests
  • All tests related to the changed code pass in development

Code review

  • This pull request has a descriptive title and information useful to a reviewer. There may be a screenshot or screencast attached
  • "Ready for review" label attached to the PR and reviewers mentioned in a comment
  • Changes have been reviewed by at least one other engineer
  • Issue from task tracker has a link to this pull request

Copy link
Contributor

@waltjones waltjones left a comment

Choose a reason for hiding this comment

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

Curious about a couple things. Otherwise looks good 👍

rollbar/__init__.py Outdated Show resolved Hide resolved
rollbar/__init__.py Outdated Show resolved Hide resolved
Copy link

@nieblara nieblara left a comment

Choose a reason for hiding this comment

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

Looks great man!

Copy link
Contributor

@jondeandres jondeandres left a comment

Choose a reason for hiding this comment

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

I am seeing few things that we might need to discuss and define.

First of all, the watch() name, I think it should be renamed to scoped() or something related to "scope" since it's quite clear that it's setting a scope.

Scopes can be nested and the best way to represent them is using dictionaries that are merged, as we do in other libraries (at least in the ruby gem).

That means we have let customer do:

with watch({'user_id': 1}):
    with watch({'project_id': 2}):
        raise Exception('not found')

And the final scope in the raise would be {'user_id': 1, 'project_id': 2}.

There is another aspect to treat here. Since users can do report_exc_info() several times inside a watch() block we shouldn't clear the scopes when we send data to Rollbar, but always in __exit__()

Other thing to consider is that since scopes out of watch() are empty, an exception caught and reported out of watch() will not be able to send the scopes inside the watch() block. An approach to resolve this would be setting a __rollbar_scopes property in the exception in __exit__() method and we take those scopes in the code that sends the exception to Rollbar. I think we do something similar in the ruby gem, but now I am not sure of this.

And to finalize, we need to discuss how that scope: {extra} schema works. extra is never a good name for something that seems to be part of the first iteration of a feature. I mean, it's not "extra", it's the core part of the functionality.

IMHO we should have a scope dictionary with simple keys/values so we can index that information also in clickhouse easily and we can build a good "scopes" functionality for the customers.

Take into account we already use custom in the API, if we want to have also scope we need to define properly how it looks like, since a bad decision can make complicated to build the system around the data we want, like be able to look for that data in clickhouse.

rollbar/__init__.py Outdated Show resolved Hide resolved
rollbar/__init__.py Outdated Show resolved Hide resolved
rollbar/__init__.py Outdated Show resolved Hide resolved
rollbar/__init__.py Outdated Show resolved Hide resolved
rollbar/__init__.py Outdated Show resolved Hide resolved
@ajtran
Copy link
Author

ajtran commented Nov 11, 2020

@jondeandres I believe I answered all your questions. Let me know what you think.

Other thing to consider is that since scopes out of watch() are empty, an exception caught and reported out of watch() will not be able to send the scopes inside the watch() block. An approach to resolve this would be setting a __rollbar_scopes property in the exception in __exit__() method and we take those scopes in the code that sends the exception to Rollbar. I think we do something similar in the ruby gem, but now I am not sure of this.

How does one do this? I tried to get the exception information in the __exit__() method via exc_type, exc_value, traceback, and sys.exc_info(), but none seemed like a viable option for me to add a property __rollbar_scopes.

And to finalize, we need to discuss how that scope: {extra} schema works. extra is never a good name for something that seems to be part of the first iteration of a feature. I mean, it's not "extra", it's the core part of the functionality.

IMHO we should have a scope dictionary with simple keys/values so we can index that information also in clickhouse easily and we can build a good "scopes" functionality for the customers.

Ah. I see. I can change the name of the "extra" key. I kind of threw that in there last minute trying to think of a good key for any value that the user might want to pass in that's not a dictionary. I agree with you that a scope dictionary with simple key/values would help us build good functionality for the customers and thus allowing them to add extra_data along with the key was what I thought a working solution. What do you think?

@jondeandres
Copy link
Contributor

How does one do this? I tried to get the exception information in the __exit__() method via exc_type, exc_value, traceback, and sys.exc_info(), but none seemed like a viable option for me to add a property __rollbar_scopes.

@ajtran I think we need to define first what we want to achieve with this watch() method and which problem we try to resolve

Ah. I see. I can change the name of the "extra" key. I kind of threw that in there last minute trying to think of a good key for any value that the user might want to pass in that's not a dictionary. I agree with you that a scope dictionary with simple key/values would help us build good functionality for the customers and thus allowing them to add extra_data along with the key was what I thought a working solution. What do you think?

What I see here is that we are adding a functionality not thinking on what we already have on other libraries and/or how the definition of the API would look like. Have into account that with this we are redefining this https://explorer.docs.rollbar.com/#operation/create-item, so we need to have very clear what kind of schema we want and why.

Some questions:

  • What is a scope?
  • Why it's a string but we allow "extra" data?
  • Why we clear the scopes when calling rollbar API? What if a user calls Rollbar API more than once inside the same watch() block?
  • Which other use cases we expect of scopes?
  • Will "scope" be a first class entity of the occurrence and the product?

I am not sure this is resolved somewhere, point me to the documentation if we have it defined please. The code changes we need to do are small as you can see, but the structures and API definition we define it's critical.

@ajtran
Copy link
Author

ajtran commented Nov 11, 2020

@jondeandres Thanks for the response! I'll try to answer your questions from easiest to more complicated. Ha.

Why we clear the scopes when calling rollbar API? What if a user calls Rollbar API more than once inside the same watch() block?

This was a decision on my part to try and handle the issue Walt mentioned above by making the intended uses of watch() stricter, but nevertheless, I've removed the clearing of the scopes and this should be resolved.

Why it's a string but we allow "extra" data?

By this you mean, why is scope a string, but we allow the users to send more data? Well, scope isn't a string, but an object, right? And by expecting a string param, we are setting first the key of the scope to ensure that it is in the scope object. The "extra" data, then comes in when we want to send more data about the scope. For example, in our LaunchDarkly use case, we can send the feature flag key as the key of the scope, but then we can send more information such as the user, the default, the variation and this is all extra data related to the scope.

What is a scope?

Maybe, scope isn't the right word here. What we are trying to do with this is allow the user to define a portion of the code they want us to watch and if they report within this context/watch/scope, we will tag the occurrence with this information that they had specified.

Which other use cases we expect of scopes?

The primary use case is for the LaunchDarkly integration and to allow the users to setup the code in a way, so that if an item is reported with in the watch(), it will contain the feature flag data and we can do some action on it. Other use cases I expect from this are similar, tag the error with more contextual data if it occurs in a certain part of the code.

Will "scope" be a first class entity of the occurrence and the product?

Yes, we would like for "scope" to be a first class entity of the occurrence and the product. I can see it being helpful to filter by scope and see the occurrences with in a certain scope. If the scope contains feature flag data, it would be nice to see all the occurrences/item with this feature flag tag.

@waltjones
Copy link
Contributor

I've still been concerned about situations where scopes grows unchecked. (Or if it's always popped during exit then information is lost.)

Thoughts on the suggestions here? https://gist.github.com/waltjones/3ffac7e966528c2963739814bb596d01

@ajtran
Copy link
Author

ajtran commented Nov 12, 2020

I've still been concerned about situations where scopes grows unchecked. (Or if it's always popped during exit then information is lost.)

Thoughts on the suggestions here? https://gist.github.com/waltjones/3ffac7e966528c2963739814bb596d01

Woahh! Thanks for the suggestion, @waltjones ! I think with some minor adjustments, this would work nicely! I went with the approach to save the tag data on the exception and got some help from Brian. What do you think of the most recent commit to attach the tags to the exception on exit and while reporting the exception info, either send the attached tags if there are any or the tags if there are any and there are no attached tags?

@waltjones
Copy link
Contributor

@ajtran I had thought exc_value was a copy, but if exc_value._rollbar_tags = [] works then it must not be.

Great news though, as all the other stuff was just working around not having the original exception object. Doesn't this mean the global tags array isn't needed any longer?

@ajtran
Copy link
Author

ajtran commented Nov 12, 2020

@waltjones Ah. I believe it's still needed.. for reporting messages inside the context manger and also for caught exceptions that haven't had a chance to exit yet.

@@ -702,6 +719,12 @@ def _report_exc_info(exc_info, request, extra_data, payload_data, level=None):
if extra_trace_data and not extra_data:
data['custom'] = extra_trace_data

# if there are feature flags attached to the exception, append that to the feature flags on
# singleton to create the full feature flags stack.
feature_flags = _feature_flags + getattr(exc_info[1], '_rollbar_feature_flags', [])[::-1]
Copy link
Contributor

Choose a reason for hiding this comment

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

Could be that we have repeated feature flags here? I am thinking if we should use set() for this, but in that case we need to add a FeatureVariation class or similar in order to generate a unique __hash__ since dict is not hashable.

Copy link
Author

@ajtran ajtran Nov 17, 2020

Choose a reason for hiding this comment

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

Hm. I think it's alright to have repeated feature flags here, right? Are you thinking of a case like:

with rollbar.watch_feature_flag('feature-a'):
  # do some stuff
  with rollbar.watch_feature_flag('feature-b'):
    # do some other stuff
    with rollbar.watch_feature_flag('feature-a'):
      # do some more stuff

where the exception happens in the deepest level of code

rollbar/__init__.py Outdated Show resolved Hide resolved
Comment on lines 1651 to 1654
if hasattr(self._registry, 'tags'):
return self._registry.tags

return []
Copy link
Author

Choose a reason for hiding this comment

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

why or when would be the case where _LocalTags is initialized, but does not have a tags attribute in self._registry?

removing this if condition, fails this test https://github.com/rollbar/pyrollbar/blob/master/rollbar/test/test_rollbar.py#L289

with this:

test_report_exception_with_same_exception_as_cause (rollbar.test.test_rollbar.RollbarTest) ... ERROR:rollbar:Exception while reporting exc_info to Rollbar. AttributeError("'thread._local' object has no attribute 'tags'",)
Traceback (most recent call last):
  File "/Users/ajtran/Development/tmp/test-launchdarkly/pyrollbar/rollbar/__init__.py", line 439, in report_exc_info
    return _report_exc_info(exc_info, request, extra_data, payload_data, level=level)
  File "/Users/ajtran/Development/tmp/test-launchdarkly/pyrollbar/rollbar/__init__.py", line 722, in _report_exc_info
    tags = _tags.value + getattr(exc_info[1], '_rollbar_tags', [])[::-1]
  File "/Users/ajtran/Development/tmp/test-launchdarkly/pyrollbar/rollbar/__init__.py", line 1652, in value
    return self._registry.tags
AttributeError: 'thread._local' object has no attribute 'tags'

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you should try to get the root exception that is triggering that report and see at which moment that's happening

Copy link
Author

Choose a reason for hiding this comment

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

Ah. Ok ok. I think I see what is happening right now. For this test test_report_exception_with_same_exception_as_cause, we are running the report_exc_info inside a threading.Thread and when we do that, tags is not set in that thread._local, thus this runtime error when trying to call report_exc_info. I think I might know a way around this.

Copy link
Author

Choose a reason for hiding this comment

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

I've resolved this by initializing self._registry.tags in the append and value method of _LocalTags.

- remove watch method
- utilize feature_flag method that models feature flags as tags
Copy link
Contributor

@jondeandres jondeandres left a comment

Choose a reason for hiding this comment

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

Great job @ajtran! I think we need to write some tests though.

I am still a little sceptical yet about using tags to model feature flags but let's go for it. We should add the "tags" definition to the OAS in moxapi.

cc @mrunalk

rollbar/__init__.py Outdated Show resolved Hide resolved
@@ -788,6 +837,9 @@ def _report_message(message, level, request, extra_data, payload_data):
_add_lambda_context_data(data)
data['server'] = _build_server_data()

if _tags.value:
data['tags'] = _flatten_nested_lists(_tags.value)
Copy link
Contributor

Choose a reason for hiding this comment

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

What _flatten_nested_lists() is doing? isn't _tags already a flatten list and not nested?

Copy link
Author

Choose a reason for hiding this comment

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

no, _tags.value at this point would look something like

[
  [
    {'key': 'feature_flag.name', 'value': 'feature-1'},
    {'key': 'feature_flag.data.feature-1.order', 'value': 0}
  ],
  [
    {'key': 'feature_flag.name', 'value': 'feature-2'},
    {'key': 'feature_flag.data.feature-2.order', 'value': 1},
    {'key': 'feature_flag.data.feature-2.variation', 'value': True}
  ]
]

where each inner list keeps the tags used to represent a feature flag together. I thought this was necessary because when an exception occurs in the scope of a context manager and we pop and attach the tags to the exception.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok I see. I think we need to change the tag manager so it appends each tag to the list:

class _TagManager(object):
    """
    Context manager object that interfaces with the `_tags` stack:
        On enter, puts the tag object at top of the stack.
        On exit, pops off the top element of the stack.
          - If there is an exception, attach the tag object to the exception
            for rebuilding of the `_tags` stack before reporting.
    """
    def __init__(self, tag):
        self.tag = tag

    def __enter__(self):
        _tags.append(self.tag)

That self.tag should be self.tags and we shouldn't do append() but maybe extend() so _tags is always flatten

Copy link
Author

Choose a reason for hiding this comment

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

I'm not sure I agree with using extend or keeping _tags always flatten. In the __exit__ method:

    def __exit__(self, exc_type, exc_value, traceback):

        if exc_value:
            if not hasattr(exc_value, '_rollbar_tags'):
                exc_value._rollbar_tags = []

            exc_value._rollbar_tags.append(self.tag)

        _tags.pop()

we pop from _tags whenever we exit the context manager. If this was flattened, we'd have to keep track of the number of times we should pop.

Well, I guess this is something we can do. Would something like this be preferred?

    def __exit__(self, exc_type, exc_value, traceback):

        if exc_value:
            if not hasattr(exc_value, '_rollbar_tags'):
                exc_value._rollbar_tags = []

            exc_value._rollbar_tags.append(self.tag)

        for tag in self.tag:
            _tags.pop()

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh ok, I see what you mean. You are right although maybe we can:

  • rename _tags to something different that represents better that they are "context tags" or similar
  • Add a method in _LocalTags that gives you the flatten tags. That way _tags will hide the internal structure and I think the code that uses it can be cleaner since will not deal with flattening the list, etc...

What you think?

Copy link
Author

Choose a reason for hiding this comment

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

Oooo I like that! Let me see if I cam implement it :D

Copy link
Author

Choose a reason for hiding this comment

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

done

rollbar/__init__.py Outdated Show resolved Hide resolved
@ajtran
Copy link
Author

ajtran commented Nov 25, 2020

@jondeandres I've written the tests and updated the OAS in moxapi here. https://github.com/rollbar/moxapi/pull/546

Let me know what you think!

@bxsx bxsx self-requested a review February 16, 2021 16:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants