Skip to content

Replace jsonpickle with json to serialize entity #275

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

Merged
merged 7 commits into from
Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/Release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Release X-Ray Python SDK

on:
workflow_dispatch:
inputs:
version:
description: The version to tag the release with, e.g., 1.2.0, 1.3.0
required: true

jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout master branch
uses: actions/checkout@v2

- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: '${{ github.event.inputs.version }}'
release_name: '${{ github.event.inputs.version }} Release'
body: 'See details in [CHANGELOG](https://github.com/aws/aws-xray-sdk-python/blob/master/CHANGELOG.rst)'
draft: true
prerelease: false
57 changes: 32 additions & 25 deletions aws_xray_sdk/core/models/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import time
import string

import jsonpickle
import json

from ..utils.compat import annotation_value_types, string_types
from ..utils.conversion import metadata_to_dict
from .throwable import Throwable
from . import http
from ..exceptions.exceptions import AlreadyEndedException
Expand Down Expand Up @@ -256,36 +257,42 @@ def get_origin_trace_header(self):
def serialize(self):
"""
Serialize to JSON document that can be accepted by the
X-Ray backend service. It uses jsonpickle to perform
serialization.
X-Ray backend service. It uses json to perform serialization.
"""
try:
return jsonpickle.encode(self, unpicklable=False)
return json.dumps(self.to_dict(), default=str)
except Exception:
log.exception("got an exception during serialization")
log.exception("Failed to serialize %s", self.name)

def _delete_empty_properties(self, properties):
def to_dict(self):
"""
Delete empty properties before serialization to avoid
extra keys with empty values in the output json.
Convert Entity(Segment/Subsegment) object to dict
with required properties that have non-empty values.
"""
if not self.parent_id:
del properties['parent_id']
if not self.subsegments:
del properties['subsegments']
if not self.aws:
del properties['aws']
if not self.http:
del properties['http']
if not self.cause:
del properties['cause']
if not self.annotations:
del properties['annotations']
if not self.metadata:
del properties['metadata']
properties.pop(ORIGIN_TRACE_HEADER_ATTR_KEY, None)

del properties['sampled']
entity_dict = {}

for key, value in vars(self).items():
if isinstance(value, bool) or value:
if key == 'subsegments':
Copy link
Contributor

Choose a reason for hiding this comment

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

How do we handle the case if key is annotations, aws or http ? I see that you're handling metadata case separately. Shouldn't we do the same with annotations too? since in that case also we don't know how many key-value pairs would be there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

annotations, aws or http are all dict with serializable values like string, int or boolean, so there is no need to handle these cases.

# child subsegments are stored as List
subsegments = []
for subsegment in value:
subsegments.append(subsegment.to_dict())
entity_dict[key] = subsegments
elif key == 'cause':
entity_dict[key] = {}
entity_dict[key]['working_directory'] = self.cause['working_directory']

Choose a reason for hiding this comment

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

@lupengamzn

I couldn't dive into this implementation yet to provide a fix, but this is causing a regression in the AWS Lambda Powertools Tracer feature leading to a TypeError: string must be a list of integers

aws-powertools/powertools-lambda-python#383

# exceptions are stored as List
throwables = []
for throwable in value['exceptions']:
throwables.append(throwable.to_dict())
entity_dict[key]['exceptions'] = throwables
elif key == 'metadata':
entity_dict[key] = metadata_to_dict(value)
elif key != 'sampled' and key != ORIGIN_TRACE_HEADER_ATTR_KEY:
entity_dict[key] = value

return entity_dict

def _check_ended(self):
if not self.in_progress:
Expand Down
22 changes: 11 additions & 11 deletions aws_xray_sdk/core/models/segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,14 @@ def set_rule_name(self, rule_name):
self.aws['xray'] = {}
self.aws['xray']['sampling_rule_name'] = rule_name

def __getstate__(self):
"""
Used by jsonpikle to remove unwanted fields.
"""
properties = copy.copy(self.__dict__)
super(Segment, self)._delete_empty_properties(properties)
if not self.user:
del properties['user']
del properties['ref_counter']
del properties['_subsegments_counter']
return properties
def to_dict(self):
"""
Convert Segment object to dict with required properties
that have non-empty values.
"""
segment_dict = super(Segment, self).to_dict()
del segment_dict['ref_counter']
del segment_dict['_subsegments_counter']

return segment_dict
19 changes: 10 additions & 9 deletions aws_xray_sdk/core/models/subsegment.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,13 @@ def set_sql(self, sql):
"""
self.sql = sql

def __getstate__(self):

properties = copy.copy(self.__dict__)
super(Subsegment, self)._delete_empty_properties(properties)

del properties['parent_segment']
if not self.sql:
del properties['sql']
return properties
def to_dict(self):
"""
Convert Subsegment object to dict with required properties
that have non-empty values.
"""
subsegment_dict = super(Subsegment, self).to_dict()

del subsegment_dict['parent_segment']

return subsegment_dict
21 changes: 13 additions & 8 deletions aws_xray_sdk/core/models/throwable.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ def __init__(self, exception, stack, remote=False):
if exception:
setattr(exception, '_recorded', True)
setattr(exception, '_cause_id', self.id)

def to_dict(self):
"""
Convert Throwable object to dict with required properties that
have non-empty values.
"""
throwable_dict = {}

for key, value in vars(self).items():
if isinstance(value, bool) or value:
throwable_dict[key] = value

return throwable_dict

def _normalize_stack_trace(self, stack):
if stack is None:
Expand All @@ -66,11 +79,3 @@ def _normalize_stack_trace(self, stack):
normalized['label'] = label.strip()

self.stack.append(normalized)

def __getstate__(self):
properties = copy.copy(self.__dict__)

if not self.stack:
del properties['stack']

return properties
35 changes: 35 additions & 0 deletions aws_xray_sdk/core/utils/conversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import logging

log = logging.getLogger(__name__)

def metadata_to_dict(obj):
"""
Convert object to dict with all serializable properties like:
dict, list, set, tuple, str, bool, int, float, type, object, etc.
"""
try:
if isinstance(obj, dict):
metadata = {}
for key, value in obj.items():
metadata[key] = metadata_to_dict(value)
return metadata
elif isinstance(obj, type):
return str(obj)
elif hasattr(obj, "_ast"):
return metadata_to_dict(obj._ast())
elif hasattr(obj, "__iter__") and not isinstance(obj, str):
metadata = []
for item in obj:
metadata.append(metadata_to_dict(item))
return metadata
elif hasattr(obj, "__dict__"):
metadata = {}
for key, value in vars(obj).items():
if not callable(value) and not key.startswith('_'):
metadata[key] = metadata_to_dict(value)
return metadata
else:
return obj
except Exception:
log.exception("Failed to convert {} to dict".format(str(obj)))
return {}
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
],

install_requires=[
'jsonpickle',
'enum34;python_version<"3.4"',
'wrapt',
'future',
Expand Down
Loading