Skip to content
Draft
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
6 changes: 6 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,12 @@ class SPANDATA:
Example: "wzHcyEWfmOGDIE5QOhTAqFDoDWP3y8IUvpNINCwL9N4TEHbUw0/gZJ+VZTmCNCWR7fezEN3eCiQ="
"""

AWS_KINESIS_STREAM_NAME = "aws.kinesis.stream_name"
"""
The name of the AWS Kinesis stream the request refers to.
Example: "orders"
"""

AWS_LAMBDA_INVOKED_ARN = "aws.lambda.invoked_arn"
"""
The full ARN of the Lambda function that was invoked.
Expand Down
5 changes: 4 additions & 1 deletion sentry_sdk/integrations/boto3/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import TYPE_CHECKING

import sentry_sdk
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations import DidNotEnable
from sentry_sdk.integrations.boto3._context import AwsCallContext
from sentry_sdk.integrations.boto3._instrumentation import (
Expand All @@ -17,7 +18,7 @@
_resolve_service,
)
from sentry_sdk.integrations.boto3.consts import IDENTIFIER
from sentry_sdk.traces import NoOpSpan, Span
from sentry_sdk.traces import NoOpSpan, Span, SpanStatus
from sentry_sdk.utils import capture_internal_exceptions

if TYPE_CHECKING:
Expand Down Expand Up @@ -125,6 +126,8 @@ def sentry_patched_make_api_call(
attributes.update(_get_response_attributes(parsed))
with capture_internal_exceptions():
span.set_attributes(attributes)
if SPANDATA.ERROR_TYPE in attributes:
span.status = SpanStatus.ERROR
except BaseException as error:
_finish_span(span, error)
raise
Expand Down
33 changes: 33 additions & 0 deletions sentry_sdk/integrations/boto3/_services/kinesis.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
from typing import TYPE_CHECKING

from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.boto3._services.base import _ServiceExtension
from sentry_sdk.integrations.boto3._utils import _get_aws_arn_attributes
from sentry_sdk.utils import capture_internal_exceptions

if TYPE_CHECKING:
from typing import Any

from sentry_sdk._types import Attributes
from sentry_sdk.integrations.boto3._context import AwsCallContext


class _KinesisExtension(_ServiceExtension):
def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes":
attributes: "Attributes" = {}

# a stream name, or an arn that contains it: `…:stream/<name>` for a stream,
# `…:stream/<name>/consumer/<consumer>:<timestamp>` for a consumer.
for param in ("StreamName", "StreamARN", "ConsumerARN", "ResourceARN"):
if param in ctx.params:
attributes[SPANDATA.AWS_KINESIS_STREAM_NAME] = (
ctx.params[param].split(":stream/", 1)[-1].split("/", 1)[0]
)
break

with capture_internal_exceptions():
attributes.update(
_get_aws_arn_attributes(
Expand All @@ -24,4 +37,24 @@ def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes":
("kinesis",),
)
)

return attributes

def get_response_attributes(
self, ctx: "AwsCallContext", response: "Any"
) -> "Attributes":
# `PutRecords` returns `200 OK` even when records fail.
# https://docs.aws.amazon.com/kinesis/latest/APIReference/API_PutRecords.html
if ctx.operation_name != "PutRecords" or not response.get("FailedRecordCount"):
return {}

error_codes = {
record["ErrorCode"]
for record in response["Records"]
if "ErrorCode" in record
}
return {
SPANDATA.ERROR_TYPE: error_codes.pop()
if len(error_codes) == 1
else "aws_batch_partial_failure"
}
122 changes: 109 additions & 13 deletions tests/integrations/boto3/test_kinesis.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,120 @@
import pytest
from botocore.stub import Stubber

from sentry_sdk.consts import OP, SPANDATA
from tests.integrations.boto3.helpers import capture_spans_by_op
from tests.integrations.boto3.helpers import (
capture_spans_by_op,
require_botocore_model_fields,
)
from tests.integrations.boto3.helpers import client_factory as client_factory

STREAM_ARN = "arn:aws:kinesis:eu-north-1:123456789012:stream/orders"
CONSUMER_ARN = STREAM_ARN + "/consumer/billing:1760000000"

def test_request_attributes(client_factory, capture_items):
client = client_factory("kinesis")
stream_arn = "arn:aws:kinesis:eu-north-1:123456789012:stream/orders"

def _stubbed_span(client_factory, capture_items, method, params, response):
client = client_factory("kinesis")
require_botocore_model_fields(
client, method, input_fields=tuple(params), output_fields=tuple(response)
)
with Stubber(client) as stubber:
stubber.add_response(
"list_stream_consumers", {"Consumers": []}, {"StreamARN": stream_arn}
)
stubber.add_response(method, response, params)
spans = capture_spans_by_op(
lambda: client.list_stream_consumers(**{"StreamARN": stream_arn}),
capture_items,
lambda: getattr(client, method)(**params), capture_items
)

(span,) = spans[OP.RPC]
attributes = span["attributes"]
assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012"
assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == stream_arn
return span


@pytest.mark.parametrize(
"method,params,response,expected",
[
pytest.param(
"put_record",
{"StreamName": "orders", "Data": b"{}", "PartitionKey": "customer-1"},
{"ShardId": "shardId-000000000000", "SequenceNumber": "1"},
{SPANDATA.AWS_KINESIS_STREAM_NAME: "orders"},
id="stream-name",
),
pytest.param(
"list_stream_consumers",
{"StreamARN": STREAM_ARN},
{"Consumers": []},
{
SPANDATA.AWS_KINESIS_STREAM_NAME: "orders",
SPANDATA.CLOUD_RESOURCE_ID: STREAM_ARN,
},
id="stream-arn",
),
pytest.param(
"deregister_stream_consumer",
{"ConsumerARN": CONSUMER_ARN},
{},
{
SPANDATA.AWS_KINESIS_STREAM_NAME: "orders",
SPANDATA.CLOUD_RESOURCE_ID: CONSUMER_ARN,
},
id="consumer-arn",
),
pytest.param(
"list_tags_for_resource",
{"ResourceARN": CONSUMER_ARN},
{"Tags": []},
{
SPANDATA.AWS_KINESIS_STREAM_NAME: "orders",
SPANDATA.CLOUD_RESOURCE_ID: CONSUMER_ARN,
},
id="resource-arn",
),
],
)
def test_stream_attributes(
client_factory, capture_items, method, params, response, expected
):
span = _stubbed_span(client_factory, capture_items, method, params, response)

assert {
key: value
for key, value in span["attributes"].items()
if key in (SPANDATA.AWS_KINESIS_STREAM_NAME, SPANDATA.CLOUD_RESOURCE_ID)
} == expected


@pytest.mark.parametrize(
"error_codes,expected_error_type",
[
pytest.param((), None, id="ok"),
pytest.param(
("ProvisionedThroughputExceededException",) * 2,
"ProvisionedThroughputExceededException",
id="one-code",
),
pytest.param(
("ProvisionedThroughputExceededException", "InternalFailure"),
"aws_batch_partial_failure",
id="different-codes",
),
],
)
def test_put_records_outcome(
client_factory, capture_items, error_codes, expected_error_type
):
params = {
"StreamName": "orders",
"Records": [
{"Data": b"card 4242", "PartitionKey": "customer-1"},
{"Data": b"card 4242", "PartitionKey": "customer-2"},
{"Data": b"card 4242", "PartitionKey": "customer-3"},
],
}
response = {
"Records": [{"ShardId": "shardId-000000000000", "SequenceNumber": "1"}]
+ [{"ErrorCode": code, "ErrorMessage": "failed"} for code in error_codes],
}
# the model's minimum is 1, so the stubber only accepts it for failed records.
if error_codes:
response["FailedRecordCount"] = len(error_codes) # type: ignore
span = _stubbed_span(client_factory, capture_items, "put_records", params, response)

assert span["status"] == ("ok" if expected_error_type is None else "error")
assert span["attributes"].get(SPANDATA.ERROR_TYPE) == expected_error_type
Loading