Skip to content

Commit

Permalink
Advanced attribute manager (#23)
Browse files Browse the repository at this point in the history
**Why**

Our users can add advanced attributes as attributes and we should treat
them from the beginning as annotations. This approach requires us to
think about annotations and attributes in the attribute manager system
we already have.

This pull request checks each attribute provided by the user and
converts it to the attributes/annotations in the flight

---------

Co-authored-by: kdysput <[email protected]>
  • Loading branch information
konraddysput and kdysput authored Sep 27, 2024
1 parent 326c1ea commit e19dfac
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 46 deletions.
66 changes: 28 additions & 38 deletions backtracepython/attributes/attribute_manager.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,41 @@
import platform

from backtracepython.attributes.backtrace_attribute_provider import (
BacktraceAttributeProvider,
)
from backtracepython.attributes.linux_memory_attribute_provider import (
LinuxMemoryAttributeProvider,
)
from backtracepython.attributes.machine_attribute_provider import (
MachineAttributeProvider,
)
from backtracepython.attributes.machineId_attribute_provider import (
MachineIdAttributeProvider,
)
from backtracepython.attributes.process_attribute_provider import (
ProcessAttributeProvider,
)
from backtracepython.attributes.session_attribute_provider import (
SessionAttributeProvider,
)
from backtracepython.attributes.system_attribute_provider import SystemAttributeProvider
from .backtrace_attribute_provider import BacktraceAttributeProvider
from .linux_memory_attribute_provider import LinuxMemoryAttributeProvider
from .machine_attribute_provider import MachineAttributeProvider
from .machineId_attribute_provider import MachineIdAttributeProvider
from .process_attribute_provider import ProcessAttributeProvider
from .report_data_builder import get_report_attributes
from .session_attribute_provider import SessionAttributeProvider
from .system_attribute_provider import SystemAttributeProvider
from .user_attribute_provider import UserAttributeProvider


class AttributeManager:
def __init__(self):
self.dynamic_attributes = self.get_predefined_dynamic_attribute_providers()
self.scoped_attributes = {}
for (
scoped_attribute_provider
) in self.get_predefined_scoped_attribute_providers():
self.try_add(self.scoped_attributes, scoped_attribute_provider)
self.attribute_providers = (
self.get_predefined_dynamic_attribute_providers()
+ self.get_predefined_scoped_attribute_providers()
)

def get(self):
result = {}
for dynamic_attribute_provider in self.dynamic_attributes:
self.try_add(result, dynamic_attribute_provider)
result.update(self.scoped_attributes)

return result
attributes = {}
annotations = {}
for attribute_provider in self.attribute_providers:
try:
provider_attributes = attribute_provider.get()
generated_attributes, generated_annotations = get_report_attributes(
provider_attributes
)
attributes.update(generated_attributes)
annotations.update(generated_annotations)
except:
continue

return attributes, annotations

def add(self, attributes):
self.scoped_attributes.update(attributes)

def try_add(self, dictionary, provider):
try:
dictionary.update(provider.get())
except:
return
self.attribute_providers.append(UserAttributeProvider(attributes))

def get_predefined_scoped_attribute_providers(self):
return [
Expand Down
24 changes: 24 additions & 0 deletions backtracepython/attributes/report_data_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import sys

# unicode is not available in Python3. However due to the Python2 support
# We need to use it to verify primitive values.
primitive_types = (
(int, float, bool, type(None), str)
if sys.version_info.major >= 3
else (int, float, bool, type(None), str, unicode)
)


def get_report_attributes(provider_attributes):
attributes = {}
annotations = {}

# Iterate through input_dict and split based on value types
for key, value in provider_attributes.items():
if isinstance(value, primitive_types):
attributes[key] = value
else:
annotations[key] = value

# Return both dictionaries
return attributes, annotations
10 changes: 10 additions & 0 deletions backtracepython/attributes/user_attribute_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from backtracepython.attributes.attribute_provider import AttributeProvider
from backtracepython.version import version_string


class UserAttributeProvider(AttributeProvider):
def __init__(self, attributes):
self.attributes = attributes

def get(self):
return self.attributes
8 changes: 7 additions & 1 deletion backtracepython/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ class globs:


def get_attributes():
return attribute_manager.get()
attributes, _ = attribute_manager.get()
return attributes


def get_annotations():
_, annotations = attribute_manager.get()
return annotations


def set_attribute(key, value):
Expand Down
13 changes: 7 additions & 6 deletions backtracepython/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ def __init__(self):
self.source_path_dict = {}
self.attachments = []

init_attrs = {"error.type": "Exception"}
init_attrs.update(attribute_manager.get())
attributes, annotations = attribute_manager.get()
attributes.update({"error.type": "Exception"})

self.log_lines = []

Expand All @@ -30,10 +30,8 @@ def __init__(self):
"agent": "backtrace-python",
"agentVersion": version_string,
"mainThread": str(self.fault_thread.ident),
"attributes": init_attrs,
"annotations": {
"Environment Variables": dict(os.environ),
},
"attributes": attributes,
"annotations": annotations,
"threads": self.generate_stack_trace(),
}

Expand Down Expand Up @@ -124,6 +122,9 @@ def set_dict_attributes(self, target_dict):
def set_annotation(self, key, value):
self.report["annotations"][key] = value

def get_annotations(self):
return self.report["annotations"]

def get_attributes(self):
return self.report["attributes"]

Expand Down
62 changes: 62 additions & 0 deletions tests/test_client_attributes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from backtracepython.client import get_attributes, set_attribute, set_attributes
from backtracepython.report import BacktraceReport


def test_setting_client_attribute():
key = "foo"
value = "bar"
set_attribute(key, value)

client_attributes = get_attributes()
assert client_attributes[key] == value


def test_overriding_client_attribute():
current_attributes = get_attributes()
key = list(current_attributes.keys())[0]
previous_value = list(current_attributes.values())[0]

new_value = "bar"
set_attribute(key, new_value)

client_attributes = get_attributes()
assert client_attributes[key] == new_value
assert new_value != previous_value


def test_primitive_values_in_attributes():
primitive_attributes = {
"string": "test",
"int": 123,
"float": 123123.123,
"boolean": False,
"None": None,
}

set_attributes(primitive_attributes)
new_report = BacktraceReport()
report_attributes = new_report.get_attributes()

for primitive_value_key in primitive_attributes:
assert primitive_value_key in report_attributes
assert (
report_attributes[primitive_value_key]
== primitive_attributes[primitive_value_key]
)


def test_complex_objects_in_annotations():
objects_to_test = (
{"foo": 1, "bar": 2},
("foo", "bar", "baz"),
lambda: None,
BacktraceReport(),
)

for index, value in enumerate(objects_to_test):
set_attribute(index, value)

new_report = BacktraceReport()
report_annotations = new_report.get_annotations()

assert len(report_annotations) == len(objects_to_test)
26 changes: 25 additions & 1 deletion tests/test_report_attributes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from backtracepython.client import set_attribute
from backtracepython.client import set_attribute, set_attributes
from backtracepython.report import BacktraceReport

report = BacktraceReport()
Expand Down Expand Up @@ -61,3 +61,27 @@ def test_override_default_client_attribute_by_report():
new_report.set_attribute(test_attribute, test_attribute_value)
attributes = new_report.get_attributes()
assert attributes["guid"] == test_attribute_value


def test_annotation_in_annotations_data():
annotation_name = "annotation_name"
annotation = {"name": "foo", "surname": "bar"}

set_attribute(annotation_name, annotation)

new_report = BacktraceReport()
report_annotation = new_report.get_annotations()
assert report_annotation[annotation_name] == annotation


def test_override_client_annotation():
annotation_name = "annotation_name"
annotation = {"name": "foo", "surname": "bar"}
override_report_annotation = {"name": "foo", "surname": "bar", "age": "unknown"}

set_attribute(annotation_name, annotation)

new_report = BacktraceReport()
new_report.set_annotation(annotation_name, override_report_annotation)
report_annotation = new_report.get_annotations()
assert report_annotation[annotation_name] == override_report_annotation

0 comments on commit e19dfac

Please sign in to comment.