Skip to content
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
4 changes: 2 additions & 2 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh
- 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.
- Whether the Dramatiq integration attaches message bodies to events is now controlled by `data_collection`'s `queues` setting instead of `http_bodies`.
- 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`).
- 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`. Update any alerts, dashboards, or `ignore_spans` rules that match on the old names or origin.
- 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.
- 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.
- 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.
- 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.
Expand Down Expand Up @@ -77,7 +77,7 @@ Span ops are now a smaller, framework-neutral set.
- The Django integration now emits `view.render` instead of `view.response.render`, `view` instead of `view.authenticate`, and `function` instead of `event.django`.
- 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`.
- 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`.
- The Boto3 integration now emits `rpc` instead of `http.client` for AWS API calls.
- 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.

#### GenAI span attributes

Expand Down
20 changes: 20 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,12 @@ class SPANDATA:
Example: "db-password"
"""

AWS_SQS_QUEUE_URL = "aws.sqs.queue.url"
"""
The URL of the AWS SQS queue the request refers to.
Example: "https://sqs.us-east-1.amazonaws.com/123456789012/orders"
"""

CACHE_HIT = "cache.hit"
"""
A boolean indicating whether the requested data was found in the cache.
Expand Down Expand Up @@ -1120,6 +1126,18 @@ class SPANDATA:
Number of retries/attempts to process a message.
"""

MESSAGING_OPERATION_NAME = "messaging.operation.name"
"""
The system-specific name of the messaging operation.
Example: "publish"
"""

MESSAGING_OPERATION_TYPE = "messaging.operation.type"
"""
The type of the messaging operation: `create`, `send`, `receive`, `process` or `settle`.
Example: "send"
"""

MESSAGING_SYSTEM = "messaging.system"
"""
The messaging system's name, e.g. `kafka`, `aws_sqs`
Expand Down Expand Up @@ -1519,6 +1537,8 @@ class OP:
OBJECT_UPLOAD_PART = "object.upload_part"
QUEUE_PROCESS = "queue.process"
QUEUE_PUBLISH = "queue.publish"
QUEUE_RECEIVE = "queue.receive"
QUEUE_SETTLE = "queue.settle"
QUEUE_SUBMIT_ARQ = "queue.submit.arq"
QUEUE_TASK_ARQ = "queue.task.arq"
QUEUE_SUBMIT_CELERY = "queue.submit.celery"
Expand Down
5 changes: 5 additions & 0 deletions sentry_sdk/integrations/boto3/_instrumentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ def _start_client_span(

attributes.update(service_ext.get_request_attributes(ctx))

with capture_internal_exceptions():
service_span_name = service_ext.get_span_name(ctx, attributes)
if service_span_name is not None:
span_name = service_span_name

# `start_span()` evaluates `ignore_spans` against the initial attributes.
# https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/#rpc-client-span
attributes.update(
Expand Down
6 changes: 6 additions & 0 deletions sentry_sdk/integrations/boto3/_services/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ def get_span_op(self, ctx: "AwsCallContext") -> "Optional[str]":
"""Return an optional `sentry.op` override, or `None` to keep the default."""
return None

def get_span_name(
self, ctx: "AwsCallContext", attributes: "Attributes"
) -> "Optional[str]":
"""Return an optional span name override, or `None` to keep the default."""
return None

def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes":
"""Return request attributes to add before the AWS request is made."""
return {}
Expand Down
109 changes: 96 additions & 13 deletions sentry_sdk/integrations/boto3/_services/sqs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,127 @@
from typing import TYPE_CHECKING
from urllib.parse import unquote, urlsplit

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 _get_aws_arn_attributes
from sentry_sdk.integrations.boto3._utils import (
_get_aws_arn_attributes,
_get_batch_error_type,
)
from sentry_sdk.traces import SpanStatus
from sentry_sdk.utils import capture_internal_exceptions

if TYPE_CHECKING:
from typing import Any
from typing import Any, Optional

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

# operation -> (`sentry.op`, `messaging.operation.type`)
_MESSAGING_OPERATIONS = {
"SendMessage": (OP.QUEUE_PUBLISH, "send"),
"SendMessageBatch": (OP.QUEUE_PUBLISH, "send"),
"ReceiveMessage": (OP.QUEUE_RECEIVE, "receive"),
"DeleteMessage": (OP.QUEUE_SETTLE, "settle"),
"DeleteMessageBatch": (OP.QUEUE_SETTLE, "settle"),
}


class _SQSExtension(_ServiceExtension):
def get_span_op(self, ctx: "AwsCallContext") -> "Optional[str]":
if ctx.operation_name in _MESSAGING_OPERATIONS:
return _MESSAGING_OPERATIONS[ctx.operation_name][0]
return None

def get_span_name(
self, ctx: "AwsCallContext", attributes: "Attributes"
) -> "Optional[str]":
if ctx.operation_name not in _MESSAGING_OPERATIONS:
return None
# for example: `send <queue name>` when `messaging.operation.type=send`.
# https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/#span-name
operation_type = _MESSAGING_OPERATIONS[ctx.operation_name][1]
destination_name = attributes.get(SPANDATA.MESSAGING_DESTINATION_NAME)
return (
f"{operation_type} {destination_name}"
if destination_name
else operation_type
)
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes":
attributes: "Attributes" = {}
with capture_internal_exceptions():
attributes.update(
_get_aws_arn_attributes(
(
ctx.params.get("DestinationArn"),
ctx.params.get("SourceArn"),
),
(ctx.params.get("DestinationArn"), ctx.params.get("SourceArn")),
("sqs",),
)
)

queue_url = ctx.params.get("QueueUrl")
if queue_url is None:
return attributes
if queue_url is not None:
attributes[SPANDATA.AWS_SQS_QUEUE_URL] = queue_url

with capture_internal_exceptions():
account_id = unquote(urlsplit(queue_url).path).strip("/").split("/", 1)[0]
if re.fullmatch(r"^[0-9]{12}$", account_id):
attributes[SPANDATA.CLOUD_ACCOUNT_ID] = account_id
with capture_internal_exceptions():
# `https://sqs.eu-north-1.amazonaws.com/123456789012/orders` => ['123456789012', 'orders']
path = unquote(urlsplit(queue_url).path).strip("/").split("/")

attributes[SPANDATA.MESSAGING_DESTINATION_NAME] = path[-1]
if re.fullmatch(r"[0-9]{12}", path[0]):
attributes[SPANDATA.CLOUD_ACCOUNT_ID] = path[0]
elif "QueueName" in ctx.params:
# `CreateQueue` & `GetQueueUrl` operations.
attributes[SPANDATA.MESSAGING_DESTINATION_NAME] = ctx.params["QueueName"]

if "QueueOwnerAWSAccountId" in ctx.params:
attributes[SPANDATA.CLOUD_ACCOUNT_ID] = ctx.params[
"QueueOwnerAWSAccountId"
]
elif "SourceArn" in ctx.params:
# `arn:aws:sqs:eu-north-1:123456789012:orders` => `orders`
attributes[SPANDATA.MESSAGING_DESTINATION_NAME] = ctx.params[
"SourceArn"
].split(":")[-1]

if ctx.operation_name in _MESSAGING_OPERATIONS:
operation_type = _MESSAGING_OPERATIONS[ctx.operation_name][1]
attributes.update(
{
SPANDATA.MESSAGING_SYSTEM: "aws_sqs",
SPANDATA.MESSAGING_OPERATION_TYPE: operation_type,
SPANDATA.MESSAGING_OPERATION_NAME: operation_type,
}
)
if operation_type == "send":
attributes[SPANDATA.SENTRY_KIND] = "producer"
if "Entries" in ctx.params:
with capture_internal_exceptions():
attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] = len(
ctx.params["Entries"]
)
return attributes
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

def on_success_response(
self, ctx: "AwsCallContext", response: "Any", span: "Span"
) -> None:
attributes: "Attributes" = {}

# the call already succeeded; an unexpected response shape, e.g. from a
# `Stubber` or an SQS-compatible service, must only lose these attributes.
with capture_internal_exceptions():
if ctx.operation_name == "SendMessage" and "MessageId" in response:
attributes[SPANDATA.MESSAGING_MESSAGE_ID] = response["MessageId"]
elif ctx.operation_name == "ReceiveMessage":
# `Messages` is left out when no message was received.
messages = response.get("Messages", ())
attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] = len(messages)
if len(messages) == 1 and "MessageId" in messages[0]:
attributes[SPANDATA.MESSAGING_MESSAGE_ID] = messages[0]["MessageId"]

error_type = _get_batch_error_type(response)
if error_type is not None:
span.status = SpanStatus.ERROR
attributes[SPANDATA.ERROR_TYPE] = error_type

span.set_attributes(attributes)
span.end()
15 changes: 15 additions & 0 deletions sentry_sdk/integrations/boto3/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ def _get_aws_arn_attributes(
return {}


def _get_batch_error_type(response: "Any") -> "Optional[str]":
"""
`error.type` for a batch call whose response has `Failed` entries.
Failed entries come back in a `200 OK` response; use their code when they share one.
https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_BatchResultErrorEntry.html
https://docs.aws.amazon.com/sns/latest/api/API_BatchResultErrorEntry.html
"""
error_codes = {entry["Code"] for entry in response.get("Failed", ())}
if len(error_codes) == 1:
return error_codes.pop()
if error_codes:
return "aws_batch_partial_failure"
return None


def _get_propagation_headers(
ctx: "AwsCallContext", span: "Optional[Span]"
) -> "Dict[str, str]":
Expand Down
Loading
Loading