diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index abf1464b68..96447c2f0e 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1376,6 +1376,16 @@ class OP: HUGGINGFACE_HUB_CHAT_COMPLETIONS_CREATE = ( "ai.chat_completions.create.huggingface_hub" ) + OBJECT_COPY = "object.copy" + OBJECT_DELETE = "object.delete" + OBJECT_GET = "object.get" + OBJECT_HEAD = "object.head" + OBJECT_LIST = "object.list" + OBJECT_MULTIPART_UPLOAD_ABORT = "object.multipart_upload.abort" + OBJECT_MULTIPART_UPLOAD_COMPLETE = "object.multipart_upload.complete" + OBJECT_MULTIPART_UPLOAD_CREATE = "object.multipart_upload.create" + OBJECT_PUT = "object.put" + OBJECT_UPLOAD_PART = "object.upload_part" QUEUE_PROCESS = "queue.process" QUEUE_PUBLISH = "queue.publish" QUEUE_SUBMIT_ARQ = "queue.submit.arq" @@ -1389,6 +1399,7 @@ class OP: QUEUE_TASK_RAY = "queue.task.ray" QUEUE_TASK_DRAMATIQ = "queue.task.dramatiq" QUEUE_SUBMIT_DJANGO = "queue.submit.django" + RPC = "rpc" SUBPROCESS = "subprocess" SUBPROCESS_WAIT = "subprocess.wait" SUBPROCESS_COMMUNICATE = "subprocess.communicate" diff --git a/sentry_sdk/integrations/boto3/_instrumentation.py b/sentry_sdk/integrations/boto3/_instrumentation.py index e3ee4fd7d6..fab0122ccb 100644 --- a/sentry_sdk/integrations/boto3/_instrumentation.py +++ b/sentry_sdk/integrations/boto3/_instrumentation.py @@ -169,7 +169,7 @@ def _start_client_span( # `sentry.op` and `sentry.origin` are set to generic defaults; # a service extension can override them with `get_span_op()` and `get_span_origin()`. - span_op = OP.HTTP_CLIENT + span_op = OP.RPC span_origin = ORIGIN if service_ext is not None: diff --git a/sentry_sdk/integrations/boto3/_services/s3.py b/sentry_sdk/integrations/boto3/_services/s3.py index 17c47fa4c1..caea75ef82 100644 --- a/sentry_sdk/integrations/boto3/_services/s3.py +++ b/sentry_sdk/integrations/boto3/_services/s3.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import TYPE_CHECKING -from sentry_sdk.consts import SPANDATA +from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations.boto3._services.base import _ServiceExtension from sentry_sdk.integrations.boto3._utils import ( _extract_attributes, @@ -11,7 +11,7 @@ from sentry_sdk.utils import capture_internal_exceptions if TYPE_CHECKING: - from typing import Any, Sequence + from typing import Any, Dict, Optional, Sequence from sentry_sdk._types import Attributes from sentry_sdk.integrations.boto3._context import AwsCallContext @@ -19,6 +19,28 @@ _AttributeSpec, ) +# s3 object apis get `object.*` ops, every other s3 api, e.g. `ListBuckets`, keeps the default `rpc`. +# https://github.com/getsentry/sentry-conventions/blob/main/model/op/object.json +_OBJECT_OPS: "Dict[str, str]" = { + "GetObject": OP.OBJECT_GET, + "HeadObject": OP.OBJECT_HEAD, + # `GetObjectAttributes` combines `HeadObject` and `ListParts`. + # https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectAttributes.html + "GetObjectAttributes": OP.OBJECT_HEAD, + "PutObject": OP.OBJECT_PUT, + "CopyObject": OP.OBJECT_COPY, + "DeleteObject": OP.OBJECT_DELETE, + "DeleteObjects": OP.OBJECT_DELETE, + "ListObjects": OP.OBJECT_LIST, + "ListObjectsV2": OP.OBJECT_LIST, + "ListObjectVersions": OP.OBJECT_LIST, + "UploadPart": OP.OBJECT_UPLOAD_PART, + "UploadPartCopy": OP.OBJECT_UPLOAD_PART, + "CreateMultipartUpload": OP.OBJECT_MULTIPART_UPLOAD_CREATE, + "CompleteMultipartUpload": OP.OBJECT_MULTIPART_UPLOAD_COMPLETE, + "AbortMultipartUpload": OP.OBJECT_MULTIPART_UPLOAD_ABORT, +} + # maps s3 operations to the response field that contains the complete file size. _RESPONSE_FILE_SIZE_FIELDS = { "GetObjectAttributes": "ObjectSize", @@ -36,6 +58,9 @@ class _S3Extension(_ServiceExtension): __slots__ = () + def get_span_op(self, ctx: "AwsCallContext") -> "Optional[str]": + return _OBJECT_OPS.get(ctx.operation_name) + def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes": attributes: "Attributes" = _extract_attributes(ctx.params, _REQUEST_ATTRIBUTES) with capture_internal_exceptions(): diff --git a/tests/integrations/boto3/test_client.py b/tests/integrations/boto3/test_client.py index 087a6ec7df..ad73c62d86 100644 --- a/tests/integrations/boto3/test_client.py +++ b/tests/integrations/boto3/test_client.py @@ -16,6 +16,7 @@ from sentry_sdk.integrations.boto3 import Boto3Integration from sentry_sdk.integrations.boto3._services.base import _ServiceExtension from sentry_sdk.integrations.boto3._services.registry import _SERVICE_EXTENSIONS +from sentry_sdk.integrations.boto3._services.s3 import _S3Extension from sentry_sdk.integrations.boto3.consts import ( AWS_RPC_SYSTEM_NAME, CLOUD_PROVIDER, @@ -146,7 +147,7 @@ def record_client_span(request, **kwargs): if span["name"] == "S3.GetObject" and ( span.get("attributes", {}).get(SPANDATA.SENTRY_ORIGIN) == ORIGIN - and span.get("attributes", {}).get(SPANDATA.SENTRY_OP) == OP.HTTP_CLIENT + and span.get("attributes", {}).get(SPANDATA.SENTRY_OP) == OP.OBJECT_GET ) ] http_spans = [ @@ -231,7 +232,7 @@ def invoke(): assert sentry_sdk.get_current_span() is parent spans_by_op = _capture_boto3_spans_by_op(invoke, capture_items) - (client_span,) = spans_by_op[OP.HTTP_CLIENT] + (client_span,) = spans_by_op[OP.RPC] (stream_span,) = spans_by_op[OP.HTTP_CLIENT_STREAM] assert stream_span.get("parent_span_id") == client_span["span_id"] @@ -251,7 +252,7 @@ def test_omit_url_data_if_parsing_fails(capture_items, client_factory): ) parse_url.assert_called() - (span,) = spans_by_op[OP.HTTP_CLIENT] + (span,) = spans_by_op[OP.OBJECT_HEAD] attributes = span.get("attributes", {}) assert SPANDATA.URL_FULL not in attributes assert SPANDATA.URL_FRAGMENT not in attributes @@ -326,7 +327,7 @@ def test_url_query_data_collection( (span,) = [ item.payload for item in items - if item.payload.get("attributes", {}).get(SPANDATA.SENTRY_OP) == OP.HTTP_CLIENT + if item.payload.get("attributes", {}).get(SPANDATA.SENTRY_OP) == OP.OBJECT_LIST ] attributes = span.get("attributes", {}) @@ -428,9 +429,8 @@ def _capture_stubbed_client_span( ) stubber.assert_no_pending_responses() - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) - assert len(client_spans) == 1 - return client_spans[0] + ((client_span,),) = spans_by_op.values() + return client_span def test_service_extension_customizes_client_span( @@ -507,6 +507,7 @@ def get_response_attributes(self, ctx, response): "method_name", "api_params", "span_name", + "expected_op", "rpc_service", "rpc_method", "endpoint_url", @@ -519,6 +520,7 @@ def get_response_attributes(self, ctx, response): "head_object", {"Bucket": "bucket", "Key": "foo"}, "S3.HeadObject", + OP.OBJECT_HEAD, "S3", "HeadObject", "http://localhost:4566", @@ -530,6 +532,7 @@ def get_response_attributes(self, ctx, response): "list_event_buses", {}, "EventBridge.ListEventBuses", + OP.RPC, "EventBridge", "ListEventBuses", None, @@ -541,6 +544,7 @@ def get_response_attributes(self, ctx, response): "get_rest_apis", {}, "APIGateway.GetRestApis", + OP.RPC, "APIGateway", "GetRestApis", None, @@ -556,6 +560,7 @@ def test_client_call_has_common_attributes( method_name, api_params, span_name, + expected_op, rpc_service, rpc_method, endpoint_url, @@ -580,7 +585,7 @@ def test_client_call_has_common_attributes( assert span["name"] == span_name assert span.get("end_timestamp") is not None - assert attributes[SPANDATA.SENTRY_OP] == OP.HTTP_CLIENT + assert attributes[SPANDATA.SENTRY_OP] == expected_op assert attributes[SPANDATA.SENTRY_ORIGIN] == ORIGIN assert attributes[SPANDATA.SENTRY_KIND] == "client" assert attributes[SPANDATA.CLOUD_PROVIDER] == CLOUD_PROVIDER @@ -651,7 +656,7 @@ def test_client_call_has_response_header_attributes( capture_items, ) - spans = spans_by_op[OP.HTTP_CLIENT] + spans = spans_by_op[OP.OBJECT_HEAD] assert len(spans) == 1 attributes = spans[0].get("attributes", {}) assert attributes[SPANDATA.HTTP_STATUS_CODE] == 200 @@ -670,7 +675,7 @@ def test_retry_attempts_share_one_client_span( spans_by_op = _capture_boto3_spans_by_op( lambda: client.head_object(Bucket="bucket", Key="foo"), capture_items ) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.OBJECT_HEAD, []) assert len(request_span_ids) == attempt_count # all `AWSRequest` instances created during retries reference the same client span. @@ -693,7 +698,7 @@ def attempt_failed_head_object_call(): spans_by_op = _capture_boto3_spans_by_op( attempt_failed_head_object_call, capture_items ) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.OBJECT_HEAD, []) assert len(request_span_ids) == 2 assert len(set(request_span_ids)) == 1 @@ -710,7 +715,7 @@ def test_client_error_has_response_attributes_and_is_unchanged( monkeypatch, with_service_extension, ): - class TestServiceExtension(_ServiceExtension): + class TestServiceExtension(_S3Extension): def get_response_attributes(self, ctx, response): return { "aws.test.error": response["Error"]["Code"], @@ -749,14 +754,14 @@ def invoke_failing_client_method(): spans_by_op = _capture_boto3_spans_by_op( invoke_failing_client_method, capture_items ) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.OBJECT_HEAD, []) _assert_one_failed_span(client_spans) attributes = client_spans[0].get("attributes", {}) span = client_spans[0] assert span["name"] == "S3.HeadObject" assert span.get("end_timestamp") is not None - assert attributes[SPANDATA.SENTRY_OP] == OP.HTTP_CLIENT + assert attributes[SPANDATA.SENTRY_OP] == OP.OBJECT_HEAD assert attributes[SPANDATA.SENTRY_ORIGIN] == ORIGIN assert attributes[SPANDATA.SENTRY_KIND] == "client" assert attributes[SPANDATA.CLOUD_PROVIDER] == CLOUD_PROVIDER @@ -810,7 +815,7 @@ def invoke_failing_client_method(): spans_by_op = _capture_boto3_spans_by_op( invoke_failing_client_method, capture_items ) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.OBJECT_HEAD, []) _assert_one_failed_span(client_spans) expected_error_type = ( @@ -856,7 +861,7 @@ def invoke_client_method(): stubber.add_response("head_object", original_response, api_params) spans_by_op = _capture_boto3_spans_by_op(invoke_client_method, capture_items) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.OBJECT_HEAD, []) assert returned_responses == [original_response] assert returned_responses[0] is original_response if failing_instrumentation == "_get_response_attributes": @@ -893,7 +898,7 @@ def invoke_failing_client_method(): spans_by_op = _capture_boto3_spans_by_op( invoke_failing_client_method, capture_items ) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.OBJECT_HEAD, []) assert len(client_spans) == 1 assert client_spans[0]["status"] == "error" @@ -926,7 +931,7 @@ def invoke_client_method_and_read_body(): spans_by_op = _capture_boto3_spans_by_op( invoke_client_method_and_read_body, capture_items ) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.OBJECT_GET, []) stream_spans = spans_by_op.get(OP.HTTP_CLIENT_STREAM, []) assert len(client_spans) == 1 @@ -975,7 +980,7 @@ def invoke_client_method_and_read_body(): spans_by_op = _capture_boto3_spans_by_op( invoke_client_method_and_read_body, capture_items ) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.OBJECT_GET, []) stream_spans = spans_by_op.get(OP.HTTP_CLIENT_STREAM, []) assert len(client_spans) == 1 diff --git a/tests/integrations/boto3/test_dynamodb.py b/tests/integrations/boto3/test_dynamodb.py index 339628099c..5399e035ae 100644 --- a/tests/integrations/boto3/test_dynamodb.py +++ b/tests/integrations/boto3/test_dynamodb.py @@ -36,7 +36,7 @@ def test_request_attributes( lambda: getattr(client, method)(**params), capture_items ) - (span,) = spans[OP.HTTP_CLIENT] + (span,) = spans[OP.RPC] attributes = span["attributes"] if expected_resource_id is None: assert SPANDATA.CLOUD_ACCOUNT_ID not in attributes diff --git a/tests/integrations/boto3/test_kinesis.py b/tests/integrations/boto3/test_kinesis.py index c3911b078d..c643d62595 100644 --- a/tests/integrations/boto3/test_kinesis.py +++ b/tests/integrations/boto3/test_kinesis.py @@ -18,7 +18,7 @@ def test_request_attributes(client_factory, capture_items): capture_items, ) - (span,) = spans[OP.HTTP_CLIENT] + (span,) = spans[OP.RPC] attributes = span["attributes"] assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == stream_arn diff --git a/tests/integrations/boto3/test_lambda.py b/tests/integrations/boto3/test_lambda.py index d74818fd3b..00c5d58df1 100644 --- a/tests/integrations/boto3/test_lambda.py +++ b/tests/integrations/boto3/test_lambda.py @@ -16,7 +16,7 @@ def test_request_attributes(client_factory, capture_items): capture_items, ) - (span,) = spans[OP.HTTP_CLIENT] + (span,) = spans[OP.RPC] attributes = span["attributes"] assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == function_arn diff --git a/tests/integrations/boto3/test_s3.py b/tests/integrations/boto3/test_s3.py index c21dfbb713..de70f3a4af 100644 --- a/tests/integrations/boto3/test_s3.py +++ b/tests/integrations/boto3/test_s3.py @@ -5,7 +5,7 @@ import pytest from botocore.stub import Stubber -from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations.boto3.consts import AWS_RPC_SYSTEM_NAME, ORIGIN from tests.integrations.boto3.helpers import ( capture_spans_by_op, @@ -26,12 +26,11 @@ def _stubbed_span(client, capture_items, method, params, response): lambda: getattr(client, method)(**params), capture_items ) stubber.assert_no_pending_responses() - (span,) = spans[OP.HTTP_CLIENT] + ((span,),) = spans.values() operation = client.meta.method_to_api_mapping[method] assert span["name"] == "S3.%s" % operation assert span.get("end_timestamp") is not None attributes = span.get("attributes", {}) - assert attributes[SPANDATA.SENTRY_OP] == OP.HTTP_CLIENT assert attributes[SPANDATA.SENTRY_ORIGIN] == ORIGIN assert attributes[SPANDATA.SENTRY_KIND] == "client" assert attributes[SPANDATA.CLOUD_PROVIDER] == "aws" diff --git a/tests/integrations/boto3/test_secretsmanager.py b/tests/integrations/boto3/test_secretsmanager.py index f44f25515a..da0a844e13 100644 --- a/tests/integrations/boto3/test_secretsmanager.py +++ b/tests/integrations/boto3/test_secretsmanager.py @@ -22,7 +22,7 @@ def test_request_attributes(client_factory, capture_items): capture_items, ) - (span,) = spans[OP.HTTP_CLIENT] + (span,) = spans[OP.RPC] attributes = span["attributes"] assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == secret_arn diff --git a/tests/integrations/boto3/test_sns.py b/tests/integrations/boto3/test_sns.py index 7e613e919e..7f593d357e 100644 --- a/tests/integrations/boto3/test_sns.py +++ b/tests/integrations/boto3/test_sns.py @@ -15,7 +15,7 @@ def test_request_attributes(client_factory, capture_items): lambda: client.delete_topic(**{"TopicArn": topic_arn}), capture_items ) - (span,) = spans[OP.HTTP_CLIENT] + (span,) = spans[OP.RPC] attributes = span["attributes"] assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == topic_arn diff --git a/tests/integrations/boto3/test_sqs.py b/tests/integrations/boto3/test_sqs.py index b05d40de91..ecc81c69d9 100644 --- a/tests/integrations/boto3/test_sqs.py +++ b/tests/integrations/boto3/test_sqs.py @@ -33,7 +33,7 @@ def test_request_attributes(client_factory, capture_items, source): lambda: getattr(client, method)(**params), capture_items ) - (span,) = spans[OP.HTTP_CLIENT] + (span,) = spans[OP.RPC] attributes = span["attributes"] assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" if resource_id is None: diff --git a/tests/integrations/boto3/test_stepfunctions.py b/tests/integrations/boto3/test_stepfunctions.py index 52de542eb3..e080effc46 100644 --- a/tests/integrations/boto3/test_stepfunctions.py +++ b/tests/integrations/boto3/test_stepfunctions.py @@ -20,7 +20,7 @@ def test_request_attributes(client_factory, capture_items): capture_items, ) - (span,) = spans[OP.HTTP_CLIENT] + (span,) = spans[OP.RPC] attributes = span["attributes"] assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == state_machine_arn