Skip to content

Commit

Permalink
feat(tracer): [SVLS-5264] S3 CopyObject span pointers (#10758)
Browse files Browse the repository at this point in the history
Adding span pointers for the S3 CopyObject operation.

## Checklist
- [x] PR author has checked that all the criteria below are met
- The PR description includes an overview of the change
- The PR description articulates the motivation for the change
- The change includes tests OR the PR description describes a testing
strategy
- The PR description notes risks associated with the change, if any
- Newly-added code is easy to change
- The change follows the [library release note
guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html)
- The change includes or references documentation updates if necessary
- Backport labels are set (if
[applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting))

## Reviewer Checklist
- [x] Reviewer has checked that all the criteria below are met 
- Title is accurate
- All changes are related to the pull request's stated goal
- Avoids breaking
[API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces)
changes
- Testing strategy adequately addresses listed risks
- Newly-added code is easy to change
- Release note makes sense to a user of the library
- If necessary, author has acknowledged and discussed the performance
implications of this PR as reported in the benchmarks PR comment
- Backport labels are set in a manner that is consistent with the
[release branch maintenance
policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)
  • Loading branch information
apiarian-datadog authored Sep 23, 2024
1 parent 5dbd7ef commit 3bffd02
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 10 deletions.
58 changes: 51 additions & 7 deletions ddtrace/_trace/utils_botocore/span_pointers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import NamedTuple

from ddtrace._trace._span_pointer import _SpanPointerDescription
from ddtrace._trace._span_pointer import _SpanPointerDirection
Expand Down Expand Up @@ -29,30 +31,71 @@ def _extract_span_pointers_for_s3_response(
response: Dict[str, Any],
) -> List[_SpanPointerDescription]:
if operation_name == "PutObject":
return _extract_span_pointers_for_s3_put_object_response(request_parameters, response)
return _extract_span_pointers_for_s3_response_with_helper(
operation_name,
_AWSS3ObjectHashingProperties.for_put_object,
request_parameters,
response,
)

if operation_name == "CopyObject":
return _extract_span_pointers_for_s3_response_with_helper(
operation_name,
_AWSS3ObjectHashingProperties.for_copy_object,
request_parameters,
response,
)

return []


def _extract_span_pointers_for_s3_put_object_response(
class _AWSS3ObjectHashingProperties(NamedTuple):
bucket: str
key: str
etag: str

@staticmethod
def for_put_object(request_parameters: Dict[str, Any], response: Dict[str, Any]) -> "_AWSS3ObjectHashingProperties":
return _AWSS3ObjectHashingProperties(
bucket=request_parameters["Bucket"],
key=request_parameters["Key"],
etag=response["ETag"],
)

@staticmethod
def for_copy_object(
request_parameters: Dict[str, Any], response: Dict[str, Any]
) -> "_AWSS3ObjectHashingProperties":
return _AWSS3ObjectHashingProperties(
bucket=request_parameters["Bucket"],
key=request_parameters["Key"],
etag=response["CopyObjectResult"]["ETag"],
)


def _extract_span_pointers_for_s3_response_with_helper(
operation_name: str,
extractor: Callable[[Dict[str, Any], Dict[str, Any]], _AWSS3ObjectHashingProperties],
request_parameters: Dict[str, Any],
response: Dict[str, Any],
) -> List[_SpanPointerDescription]:
# Endpoint Reference:
# https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html

try:
bucket = request_parameters["Bucket"]
key = request_parameters["Key"]
etag = response["ETag"]
hashing_properties = extractor(request_parameters, response)
bucket = hashing_properties.bucket
key = hashing_properties.key
etag = hashing_properties.etag

# The ETag is surrounded by double quotes for some reason.
if etag.startswith('"') and etag.endswith('"'):
etag = etag[1:-1]

except KeyError as e:
log.warning(
"missing a parameter or response field required to make span pointer for S3.PutObject: %s",
"missing a parameter or response field required to make span pointer for S3.%s: %s",
operation_name,
str(e),
)
return []
Expand All @@ -68,7 +111,8 @@ def _extract_span_pointers_for_s3_put_object_response(
]
except Exception as e:
log.warning(
"failed to generate S3.PutObject span pointer: %s",
"failed to generate S3.%s span pointer: %s",
operation_name,
str(e),
)
return []
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
features:
- |
botocore: Adds span pointers for successful S3 CopyObject spans.
52 changes: 49 additions & 3 deletions tests/tracer/utils_botocore/test_span_pointers.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,52 @@ class PointersCase(NamedTuple):
],
expected_warning_regex=None,
),
PointersCase(
name="s3.CopyObject",
endpoint_name="s3",
operation_name="CopyObject",
request_parameters={
"Bucket": "some-bucket",
"Key": "some-key.data",
},
response={
"CopyObjectResult": {
"ETag": "ab12ef34",
},
},
expected_pointers=[
_SpanPointerDescription(
pointer_kind="aws.s3.object",
pointer_direction=_SpanPointerDirection.DOWNSTREAM,
pointer_hash="e721375466d4116ab551213fdea08413",
extra_attributes={},
),
],
expected_warning_regex=None,
),
PointersCase(
name="s3.CopyObject with double quoted ETag",
endpoint_name="s3",
operation_name="CopyObject",
request_parameters={
"Bucket": "some-bucket",
"Key": "some-key.data",
},
response={
"CopyObjectResult": {
"ETag": '"ab12ef34"',
},
},
expected_pointers=[
_SpanPointerDescription(
pointer_kind="aws.s3.object",
pointer_direction=_SpanPointerDirection.DOWNSTREAM,
pointer_hash="e721375466d4116ab551213fdea08413",
extra_attributes={},
),
],
expected_warning_regex=None,
),
],
ids=lambda case: case.name,
)
Expand All @@ -207,12 +253,12 @@ def test_pointers(self, pointers_case: PointersCase) -> None:
mock_logger.assert_not_called()

else:
mock_logger.asser_called_once()
mock_logger.assert_called_once()

(args, kwargs) = mock_logger.call_args
assert not kwargs
fmt, other_args = args
fmt, *other_args = args
assert re.match(
pointers_case.expected_warning_regex,
fmt % other_args,
fmt % tuple(other_args),
)

0 comments on commit 3bffd02

Please sign in to comment.