GeezerVison is showing github.com — feat(boto3): Add SNS extension (#8142) · getsentry/sentry-python@36870b8
File treeExpand file treeCollapse file tree Open diff view settings integrations/boto3/_services
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: MIGRATION_GUIDE.md +2-2Lines changed: 2 additions & 2 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -47,7 +47,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh | 47 | 47 | - The `RustTracingIntegration` no longer includes the values of tracing fields if `send_default_pii` is enabled. Pass `include_tracing_fields=True` to the integration to include them. | 48 | 48 | - Whether the Dramatiq integration attaches message bodies to events is now controlled by `data_collection`'s `queues` setting instead of `http_bodies`. | 49 | 49 | - The Celery integration no longer sets the status of task spans to error when a task raises a control flow exception (`Retry`, `Ignore`, or `Reject`). | 50 | | -- The boto3 integration now names spans `<ServiceID>.<Operation>` (for example, `S3.GetObject` or `DynamoDB.GetItem`), following OpenTelemetry semantic conventions. Previously, spans were named `aws.<service-id>.<Operation>` (for example, `aws.s3.GetObject`). Its spans, including the span for reading a streamed response body, now also have the origin `auto.aws.boto3` instead of `auto.http.boto3`. SQS messaging calls are the exception: `SendMessage` and `SendMessageBatch` are named `send <queue name>`, `ReceiveMessage` is named `receive <queue name>`, and `DeleteMessage` and `DeleteMessageBatch` are named `settle <queue name>`. Update any alerts, dashboards, or `ignore_spans` rules that match on the old names or origin. | | 50 | +- The boto3 integration now names spans `<ServiceID>.<Operation>` (for example, `S3.GetObject` or `DynamoDB.GetItem`), following OpenTelemetry semantic conventions. Previously, spans were named `aws.<service-id>.<Operation>` (for example, `aws.s3.GetObject`). Its spans, including the span for reading a streamed response body, now also have the origin `auto.aws.boto3` instead of `auto.http.boto3`. SQS and SNS messaging calls are the exception: SQS `SendMessage` and `SendMessageBatch` are named `send <queue name>`, `ReceiveMessage` is named `receive <queue name>`, and `DeleteMessage` and `DeleteMessageBatch` are named `settle <queue name>`; SNS `Publish` and `PublishBatch` are named `send <topic name>`. Update any alerts, dashboards, or `ignore_spans` rules that match on the old names or origin. | 51 | 51 | - Enabling an AI agent integration (LangChain, OpenAI Agents, or Pydantic AI) no longer disables the OpenAI, Anthropic, and Google GenAI integrations. Instead, those integrations don't create chat spans for requests made from within an agent framework's chat span, but still create other spans, such as embedding spans. The LangChain integration no longer instruments embeddings directly; embedding spans are now created by the integration of the underlying client library. | 52 | 52 | - Whether the MCP integration records tool arguments, tool results, and prompt contents is now controlled by `data_collection`'s new `mcp` setting (`{"inputs": bool, "outputs": bool}`, both `True` by default) instead of the `gen_ai` setting. | 53 | 53 | - With `handled_tool_call_exceptions=True` (the default), the Pydantic AI integration now reports an error for every failed tool argument validation attempt, including retries. Previously, only one error was reported per tool call. | @@ -77,7 +77,7 @@ Span ops are now a smaller, framework-neutral set. | 77 | 77 | - The Django integration now emits `view.render` instead of `view.response.render`, `view` instead of `view.authenticate`, and `function` instead of `event.django`. | 78 | 78 | - The Django, Starlette, Starlite, and Litestar integrations now emit `middleware` instead of `middleware.django`, `middleware.starlette`, `middleware.starlette.receive`, `middleware.starlette.send`, `middleware.starlite`, `middleware.starlite.receive`, `middleware.starlite.send`, `middleware.litestar`, `middleware.litestar.receive`, and `middleware.litestar.send`. | 79 | 79 | - The ARQ, Celery, Django, Dramatiq, Huey, RQ, and Ray integrations now emit `queue.publish` and `queue.process` instead of `queue.submit.arq`, `queue.task.arq`, `queue.submit.celery`, `queue.task.celery`, `queue.submit.django`, `queue.task.dramatiq`, `queue.submit.huey`, `queue.task.huey`, `queue.task.rq`, `queue.submit.ray`, and `queue.task.ray`. | 80 | | -- The Boto3 integration now emits `rpc` instead of `http.client` for AWS API calls. SQS messaging calls emit `queue.publish` (`SendMessage`, `SendMessageBatch`), `queue.receive` (`ReceiveMessage`), and `queue.settle` (`DeleteMessage`, `DeleteMessageBatch`) instead. | | 80 | +- The Boto3 integration now emits `rpc` instead of `http.client` for AWS API calls. SQS messaging calls emit `queue.publish` (`SendMessage`, `SendMessageBatch`), `queue.receive` (`ReceiveMessage`), and `queue.settle` (`DeleteMessage`, `DeleteMessageBatch`) instead, and SNS `Publish` and `PublishBatch` emit `queue.publish`. | 81 | 81 |
| 82 | 82 | #### GenAI span attributes | 83 | 83 |
| |
Collapse file Copy file name to clipboardExpand all lines: sentry_sdk/consts.py +6Lines changed: 6 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -489,6 +489,12 @@ class SPANDATA: | 489 | 489 | Example: "https://sqs.us-east-1.amazonaws.com/123456789012/orders" | 490 | 490 | """ | 491 | 491 |
| | 492 | + AWS_SNS_TOPIC_ARN = "aws.sns.topic.arn" | | 493 | + """ | | 494 | + The ARN of the AWS SNS topic the request refers to. | | 495 | + Example: "arn:aws:sns:us-east-1:123456789012:alerts" | | 496 | + """ | | 497 | + | 492 | 498 | CACHE_HIT = "cache.hit" | 493 | 499 | """ | 494 | 500 | A boolean indicating whether the requested data was found in the cache. | |
Collapse file Copy file name to clipboard +107-12Lines changed: 107 additions & 12 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | from typing import TYPE_CHECKING | 2 | 2 |
| | 3 | +import sentry_sdk | | 4 | +from sentry_sdk.consts import OP, SPANDATA | 3 | 5 | from sentry_sdk.integrations.boto3._services.base import _ServiceExtension | 4 | | -from sentry_sdk.integrations.boto3._utils import _get_aws_arn_attributes | | 6 | +from sentry_sdk.integrations.boto3._utils import ( | | 7 | + _get_aws_arn_attributes, | | 8 | + _get_batch_error_type, | | 9 | +) | | 10 | +from sentry_sdk.traces import SpanStatus | 5 | 11 | from sentry_sdk.utils import capture_internal_exceptions | 6 | 12 |
| 7 | 13 | if TYPE_CHECKING: | 8 | | - from typing import Any | | 14 | + from typing import Any, Optional | 9 | 15 |
| 10 | 16 | from sentry_sdk._types import Attributes | 11 | 17 | from sentry_sdk.integrations.boto3._context import AwsCallContext | 12 | 18 | from sentry_sdk.traces import Span | 13 | 19 |
| | 20 | +_PUBLISH_OPERATIONS = ("Publish", "PublishBatch") | | 21 | + | 14 | 22 |
| 15 | 23 | class _SNSExtension(_ServiceExtension): | | 24 | + def get_span_op(self, ctx: "AwsCallContext") -> "Optional[str]": | | 25 | + if ctx.operation_name in _PUBLISH_OPERATIONS: | | 26 | + return OP.QUEUE_PUBLISH | | 27 | + # defaults to `rpc` for other operations. | | 28 | + return None | | 29 | + | | 30 | + def get_span_name( | | 31 | + self, ctx: "AwsCallContext", attributes: "Attributes" | | 32 | + ) -> "Optional[str]": | | 33 | + if ctx.operation_name not in _PUBLISH_OPERATIONS: | | 34 | + return None | | 35 | + | | 36 | + destination = attributes.get(SPANDATA.MESSAGING_DESTINATION_NAME) | | 37 | + if destination is None and SPANDATA.SERVER_ADDRESS in attributes: | | 38 | + destination = f"{attributes[SPANDATA.SERVER_ADDRESS]}:{attributes[SPANDATA.SERVER_PORT]}" | | 39 | + | | 40 | + # for example: `send <topic name>` when `messaging.operation.type=send` (always send). | | 41 | + # https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/#span-name | | 42 | + return f"send {destination}" if destination else "send" | | 43 | + | 16 | 44 | def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes": | 17 | 45 | attributes: "Attributes" = {} | | 46 | + | | 47 | + # botocore validates params after this hook, so a malformed value must only | | 48 | + # lose the attributes read from it, not the span. | | 49 | + target_arn = ctx.params.get("TargetArn") | | 50 | + endpoint_arn = ctx.params.get("EndpointArn") | | 51 | + topic_arn = None | 18 | 52 | with capture_internal_exceptions(): | | 53 | + # `TargetArn` can be either a topic or a platform endpoint; a topic ARN | | 54 | + # should never contain ":endpoint/". | | 55 | + if target_arn and ":endpoint/" in target_arn: | | 56 | + endpoint_arn, target_arn = target_arn, None | | 57 | + | | 58 | + # `TopicArn` and `ResourceArn` are always topic ARNs. | | 59 | + topic_arn = ( | | 60 | + ctx.params.get("TopicArn") | | 61 | + or ctx.params.get("ResourceArn") | | 62 | + or target_arn | | 63 | + ) | | 64 | + | | 65 | + if topic_arn: | | 66 | + attributes[SPANDATA.AWS_SNS_TOPIC_ARN] = topic_arn | | 67 | + # `arn:aws:sns:eu-north-1:123456789012:alerts` -> `alerts` | | 68 | + topic_name = topic_arn.split(":")[-1] | | 69 | + attributes[SPANDATA.MESSAGING_DESTINATION_NAME] = topic_name | | 70 | + elif ctx.operation_name == "CreateTopic" and "Name" in ctx.params: | | 71 | + attributes[SPANDATA.MESSAGING_DESTINATION_NAME] = ctx.params["Name"] | | 72 | + | | 73 | + if ctx.operation_name in _PUBLISH_OPERATIONS: | 19 | 74 | attributes.update( | 20 | | - _get_aws_arn_attributes( | 21 | | - ( | 22 | | - ctx.params.get("EndpointArn"), | 23 | | - ctx.params.get("PlatformApplicationArn"), | 24 | | - ctx.params.get("SubscriptionArn"), | 25 | | - ctx.params.get("TargetArn"), | 26 | | - ctx.params.get("TopicArn"), | 27 | | - ), | 28 | | - ("sns",), | 29 | | - ) | | 75 | + { | | 76 | + SPANDATA.SENTRY_KIND: "producer", | | 77 | + SPANDATA.MESSAGING_SYSTEM: "aws.sns", | | 78 | + SPANDATA.MESSAGING_OPERATION_TYPE: "send", | | 79 | + SPANDATA.MESSAGING_OPERATION_NAME: "publish", | | 80 | + } | 30 | 81 | ) | | 82 | + | | 83 | + # phone numbers and device endpoints are PII. | | 84 | + if "PhoneNumber" in ctx.params or endpoint_arn: | | 85 | + attributes[SPANDATA.MESSAGING_DESTINATION_NAME] = "[Filtered]" | | 86 | + if "PublishBatchRequestEntries" in ctx.params: | | 87 | + with capture_internal_exceptions(): | | 88 | + attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] = len( | | 89 | + ctx.params["PublishBatchRequestEntries"] | | 90 | + ) | | 91 | + | | 92 | + # if scrubbing fails, drop all ARN attributes rather than send a raw | | 93 | + # endpoint or subscription ARN. | | 94 | + with capture_internal_exceptions(): | | 95 | + # endpoints and subscriptions identify a single device or subscriber. | | 96 | + subscription_arn = ctx.params.get("SubscriptionArn") | | 97 | + if not sentry_sdk.get_client().options["data_collection"]["user_info"]: | | 98 | + if endpoint_arn: | | 99 | + endpoint_arn = endpoint_arn.rsplit("/", 1)[0].replace( | | 100 | + ":endpoint/", ":app/", 1 | | 101 | + ) | | 102 | + if subscription_arn: | | 103 | + subscription_arn = subscription_arn.rsplit(":", 1)[0] | | 104 | + | | 105 | + arns = ( | | 106 | + ctx.params.get("PlatformApplicationArn"), | | 107 | + topic_arn, | | 108 | + endpoint_arn, | | 109 | + subscription_arn, | | 110 | + ) | | 111 | + attributes.update(_get_aws_arn_attributes(arns, ("sns",))) | 31 | 112 | return attributes | 32 | 113 |
| 33 | 114 | def on_success_response( | 34 | 115 | self, ctx: "AwsCallContext", response: "Any", span: "Span" | 35 | 116 | ) -> None: | | 117 | + attributes: "Attributes" = {} | | 118 | + | | 119 | + # the call already succeeded; an unexpected response shape, e.g. from a | | 120 | + # `Stubber` or an SNS-compatible service, must only lose these attributes. | | 121 | + with capture_internal_exceptions(): | | 122 | + if ctx.operation_name == "Publish" and "MessageId" in response: | | 123 | + attributes[SPANDATA.MESSAGING_MESSAGE_ID] = response["MessageId"] | | 124 | + | | 125 | + error_type = _get_batch_error_type(response) | | 126 | + if error_type is not None: | | 127 | + span.status = SpanStatus.ERROR | | 128 | + attributes[SPANDATA.ERROR_TYPE] = error_type | | 129 | + | | 130 | + span.set_attributes(attributes) | 36 | 131 | span.end() |
|