Skip to content

Commit 36c9a97

Browse files
committed
Capture server attributes for botocore API calls
1 parent c6cdbeb commit 36c9a97

File tree

2 files changed

+68
-0
lines changed

2 files changed

+68
-0
lines changed

instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def response_hook(span, service_name, operation_name, result):
8282

8383
import logging
8484
from typing import Any, Callable, Collection, Dict, Optional, Tuple
85+
from urllib.parse import urlparse
8586

8687
from botocore.client import BaseClient
8788
from botocore.endpoint import Endpoint
@@ -277,6 +278,7 @@ def _patched_api_call(self, original_func, instance, args, kwargs):
277278
SpanAttributes.RPC_METHOD: call_context.operation,
278279
# TODO: update when semantic conventions exist
279280
"aws.region": call_context.region,
281+
**_get_server_attributes(call_context.endpoint_url),
280282
}
281283

282284
_safe_invoke(extension.extract_attributes, attributes)
@@ -403,3 +405,13 @@ def _safe_invoke(function: Callable, *args):
403405
logger.error(
404406
"Error when invoking function '%s'", function_name, exc_info=ex
405407
)
408+
409+
410+
def _get_server_attributes(endpoint_url: str) -> dict[str, str]:
411+
"""Extract server.* attributes from AWS endpoint URL."""
412+
parsed = urlparse(endpoint_url)
413+
attributes = {}
414+
if parsed.hostname:
415+
attributes[SpanAttributes.SERVER_ADDRESS] = parsed.hostname
416+
attributes[SpanAttributes.SERVER_PORT] = parsed.port or 443
417+
return attributes

instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import json
15+
import os
1516
from unittest.mock import ANY, Mock, patch
1617

1718
import botocore.session
@@ -63,6 +64,9 @@ def _default_span_attributes(self, service: str, operation: str):
6364
"aws.region": self.region,
6465
"retry_attempts": 0,
6566
SpanAttributes.HTTP_STATUS_CODE: 200,
67+
# Some services like IAM or STS have a global endpoint and exclude specified region.
68+
SpanAttributes.SERVER_ADDRESS: f"{service.lower()}.{'' if self.region == 'aws-global' else self.region + '.'}amazonaws.com",
69+
SpanAttributes.SERVER_PORT: 443,
6670
}
6771

6872
def assert_only_span(self):
@@ -330,6 +334,7 @@ def test_sts_client(self):
330334
span = self.assert_only_span()
331335
expected = self._default_span_attributes("STS", "GetCallerIdentity")
332336
expected["aws.request_id"] = ANY
337+
expected[SpanAttributes.SERVER_ADDRESS] = "sts.amazonaws.com"
333338
# check for exact attribute set to make sure not to leak any sts secrets
334339
self.assertEqual(expected, dict(span.attributes))
335340

@@ -497,3 +502,54 @@ def response_hook(span, service_name, operation_name, result):
497502
response_hook_result_attribute_name: 0,
498503
},
499504
)
505+
506+
@mock_aws
507+
def test_server_attributes(self):
508+
# Test regional endpoint
509+
ec2 = self._make_client("ec2")
510+
ec2.describe_instances()
511+
self.assert_span(
512+
"EC2",
513+
"DescribeInstances",
514+
attributes={
515+
SpanAttributes.SERVER_ADDRESS: f"ec2.{self.region}.amazonaws.com",
516+
SpanAttributes.SERVER_PORT: 443,
517+
},
518+
)
519+
self.memory_exporter.clear()
520+
521+
# Test global endpoint
522+
iam_global = self._make_client("iam")
523+
iam_global.list_users()
524+
self.assert_span(
525+
"IAM",
526+
"ListUsers",
527+
attributes={
528+
SpanAttributes.SERVER_ADDRESS: "iam.amazonaws.com",
529+
SpanAttributes.SERVER_PORT: 443,
530+
"aws.region": "aws-global",
531+
},
532+
)
533+
534+
@mock_aws
535+
def test_server_attributes_with_custom_endpoint(self):
536+
with patch.dict(
537+
os.environ,
538+
{"MOTO_S3_CUSTOM_ENDPOINTS": "https://proxy.amazon.org:2025"},
539+
):
540+
s3 = self.session.create_client(
541+
"s3",
542+
region_name=self.region,
543+
endpoint_url="https://proxy.amazon.org:2025",
544+
)
545+
546+
s3.list_buckets()
547+
548+
self.assert_span(
549+
"S3",
550+
"ListBuckets",
551+
attributes={
552+
SpanAttributes.SERVER_ADDRESS: "proxy.amazon.org",
553+
SpanAttributes.SERVER_PORT: 2025,
554+
},
555+
)

0 commit comments

Comments
 (0)