Skip to content

Commit 52744ef

Browse files
authored
Merge branch 'main' into bedrock-agentcore-resource-support
2 parents 98d567a + 39083ff commit 52744ef

File tree

22 files changed

+2445
-240
lines changed

22 files changed

+2445
-240
lines changed

.pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ extension-pkg-whitelist=cassandra
77

88
# Add list of files or directories to be excluded. They should be base names, not
99
# paths.
10-
ignore=CVS,gen,Dockerfile,docker-compose.yml,README.md,requirements.txt,mock_collector_service_pb2.py,mock_collector_service_pb2.pyi,mock_collector_service_pb2_grpc.py
10+
ignore=CVS,gen,Dockerfile,docker-compose.yml,README.md,requirements.txt,mock_collector_service_pb2.py,mock_collector_service_pb2.pyi,mock_collector_service_pb2_grpc.py,pyproject.toml,db.sqlite3
1111

1212
# Add files or directories matching the regex patterns to be excluded. The
1313
# regex matches against base names, not paths.

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ def _is_application_signals_runtime_enabled():
616616
)
617617

618618

619-
def _get_code_correlation_enabled_status() -> Optional[bool]:
619+
def get_code_correlation_enabled_status() -> Optional[bool]:
620620
"""
621621
Get the code correlation enabled status from environment variable.
622622

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/__init__.py

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
This module provides functionality for correlating code execution with telemetry data.
88
"""
99

10+
import inspect
1011
from functools import wraps
1112
from typing import Any, Callable
1213

@@ -21,7 +22,7 @@
2122
CODE_LINE_NUMBER = "code.line.number"
2223

2324

24-
def _add_code_attributes_to_span(span, func: Callable[..., Any]) -> None:
25+
def add_code_attributes_to_span(span, func_or_class: Callable[..., Any]) -> None:
2526
"""
2627
Add code-related attributes to a span based on a Python function.
2728
@@ -39,36 +40,27 @@ def _add_code_attributes_to_span(span, func: Callable[..., Any]) -> None:
3940
return
4041

4142
try:
42-
# Get function name
43-
function_name = getattr(func, "__name__", str(func))
44-
span.set_attribute(CODE_FUNCTION_NAME, function_name)
45-
46-
# Get function source file from code object
47-
try:
48-
if hasattr(func, "__code__"):
49-
source_file = func.__code__.co_filename
50-
span.set_attribute(CODE_FILE_PATH, source_file)
51-
except (AttributeError, TypeError):
52-
# Handle cases where code object is not available
53-
# (e.g., built-in functions, C extensions)
54-
pass
55-
56-
# Get function line number from code object
43+
# Check if it's a class first, with proper exception handling
5744
try:
58-
if hasattr(func, "__code__"):
59-
line_number = func.__code__.co_firstlineno
60-
span.set_attribute(CODE_LINE_NUMBER, line_number)
61-
except (AttributeError, TypeError):
62-
# Handle cases where code object is not available
63-
pass
64-
45+
is_class = inspect.isclass(func_or_class)
46+
except Exception: # pylint: disable=broad-exception-caught
47+
# If inspect.isclass fails, we can't safely determine the type, so return early
48+
return
49+
50+
if is_class:
51+
span.set_attribute(CODE_FUNCTION_NAME, f"{func_or_class.__module__}.{func_or_class.__qualname__}")
52+
span.set_attribute(CODE_FILE_PATH, inspect.getfile(func_or_class))
53+
else:
54+
code = getattr(func_or_class, "__code__", None)
55+
if code:
56+
span.set_attribute(CODE_FUNCTION_NAME, f"{func_or_class.__module__}.{func_or_class.__qualname__}")
57+
span.set_attribute(CODE_FILE_PATH, code.co_filename)
58+
span.set_attribute(CODE_LINE_NUMBER, code.co_firstlineno)
6559
except Exception: # pylint: disable=broad-exception-caught
66-
# Silently handle any unexpected errors to avoid breaking
67-
# the instrumentation flow
6860
pass
6961

7062

71-
def add_code_attributes_to_span(func: Callable[..., Any]) -> Callable[..., Any]:
63+
def record_code_attributes(func: Callable[..., Any]) -> Callable[..., Any]:
7264
"""
7365
Decorator to automatically add code attributes to the current OpenTelemetry span.
7466
@@ -81,12 +73,12 @@ def add_code_attributes_to_span(func: Callable[..., Any]) -> Callable[..., Any]:
8173
This decorator supports both synchronous and asynchronous functions.
8274
8375
Usage:
84-
@add_code_attributes_to_span
76+
@record_code_attributes
8577
def my_sync_function():
8678
# Sync function implementation
8779
pass
8880
89-
@add_code_attributes_to_span
81+
@record_code_attributes
9082
async def my_async_function():
9183
# Async function implementation
9284
pass
@@ -97,9 +89,8 @@ async def my_async_function():
9789
Returns:
9890
The wrapped function with current span code attributes tracing
9991
"""
100-
# Detect async functions: check function code object flags or special attributes
101-
# CO_ITERABLE_COROUTINE = 0x80, async functions will have this flag set
102-
is_async = (hasattr(func, "__code__") and func.__code__.co_flags & 0x80) or hasattr(func, "_is_coroutine")
92+
# Detect async functions
93+
is_async = inspect.iscoroutinefunction(func)
10394

10495
if is_async:
10596
# Async function wrapper
@@ -109,7 +100,7 @@ async def async_wrapper(*args, **kwargs):
109100
try:
110101
current_span = trace.get_current_span()
111102
if current_span:
112-
_add_code_attributes_to_span(current_span, func)
103+
add_code_attributes_to_span(current_span, func)
113104
except Exception: # pylint: disable=broad-exception-caught
114105
# Silently handle any unexpected errors
115106
pass
@@ -126,7 +117,7 @@ def sync_wrapper(*args, **kwargs):
126117
try:
127118
current_span = trace.get_current_span()
128119
if current_span:
129-
_add_code_attributes_to_span(current_span, func)
120+
add_code_attributes_to_span(current_span, func)
130121
except Exception: # pylint: disable=broad-exception-caught
131122
# Silently handle any unexpected errors
132123
pass
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
# Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License.
4+
5+
from logging import getLogger
6+
7+
from amazon.opentelemetry.distro.aws_opentelemetry_configurator import get_code_correlation_enabled_status
8+
9+
_logger = getLogger(__name__)
10+
11+
12+
def _apply_django_instrumentation_patches() -> None:
13+
"""Django instrumentation patches
14+
15+
Applies patches to provide code attributes support for Django instrumentation.
16+
This patches the Django instrumentation to automatically add code attributes
17+
to spans by modifying the process_view method of the Django middleware.
18+
Also patches Django's path/re_path functions for URL pattern instrumentation.
19+
"""
20+
if get_code_correlation_enabled_status() is True:
21+
_apply_django_code_attributes_patch()
22+
23+
24+
def _apply_django_code_attributes_patch() -> None: # pylint: disable=too-many-statements
25+
"""Django instrumentation patch for code attributes
26+
27+
This patch modifies the Django middleware's process_view method to automatically add
28+
code attributes to the current span when a view function is about to be executed.
29+
30+
The patch includes:
31+
1. Support for class-based views by extracting the actual HTTP method handler
32+
2. Automatic addition of code.function.name, code.file.path, and code.line.number
33+
3. Graceful error handling and cleanup during uninstrument
34+
"""
35+
try:
36+
# Import Django instrumentation classes and AWS code correlation function
37+
from amazon.opentelemetry.distro.code_correlation import ( # pylint: disable=import-outside-toplevel
38+
add_code_attributes_to_span,
39+
)
40+
from opentelemetry.instrumentation.django import DjangoInstrumentor # pylint: disable=import-outside-toplevel
41+
42+
# Store the original _instrument and _uninstrument methods
43+
original_instrument = DjangoInstrumentor._instrument
44+
original_uninstrument = DjangoInstrumentor._uninstrument
45+
46+
# Store reference to original Django middleware process_view method
47+
original_process_view = None
48+
49+
def _patch_django_middleware():
50+
"""Patch Django middleware's process_view method to add code attributes."""
51+
try:
52+
# Import Django middleware class
53+
# pylint: disable=import-outside-toplevel
54+
from opentelemetry.instrumentation.django.middleware.otel_middleware import _DjangoMiddleware
55+
56+
nonlocal original_process_view
57+
if original_process_view is None:
58+
original_process_view = _DjangoMiddleware.process_view
59+
60+
def patched_process_view(
61+
self, request, view_func, *args, **kwargs
62+
): # pylint: disable=too-many-locals,too-many-nested-blocks,too-many-branches
63+
"""Patched process_view method to add code attributes to the span."""
64+
# First call the original process_view method
65+
result = original_process_view(self, request, view_func, *args, **kwargs)
66+
67+
# Add code attributes if we have a span and view function
68+
try:
69+
if (
70+
self._environ_activation_key in request.META.keys()
71+
and self._environ_span_key in request.META.keys()
72+
):
73+
span = request.META[self._environ_span_key]
74+
if span and view_func and span.is_recording():
75+
# Determine the target function/method to analyze
76+
target = view_func
77+
78+
# If it's a class-based view, get the corresponding HTTP method handler
79+
view_class = getattr(view_func, "view_class", None)
80+
if view_class:
81+
method_name = request.method.lower()
82+
handler = getattr(view_class, method_name, None) or view_class
83+
target = handler
84+
85+
# Call the existing add_code_attributes_to_span function
86+
add_code_attributes_to_span(span, target)
87+
_logger.debug(
88+
"Added code attributes to span for Django view: %s",
89+
getattr(target, "__name__", str(target)),
90+
)
91+
except Exception as exc: # pylint: disable=broad-exception-caught
92+
# Don't let code attributes addition break the request processing
93+
_logger.warning("Failed to add code attributes to Django span: %s", exc)
94+
95+
return result
96+
97+
# Apply the patch
98+
_DjangoMiddleware.process_view = patched_process_view
99+
_logger.debug("Django middleware process_view patched successfully for code attributes")
100+
101+
except Exception as exc: # pylint: disable=broad-exception-caught
102+
_logger.warning("Failed to patch Django middleware process_view: %s", exc)
103+
104+
def _unpatch_django_middleware():
105+
"""Restore original Django middleware process_view method."""
106+
try:
107+
# pylint: disable=import-outside-toplevel
108+
from opentelemetry.instrumentation.django.middleware.otel_middleware import _DjangoMiddleware
109+
110+
if original_process_view is not None:
111+
_DjangoMiddleware.process_view = original_process_view
112+
_logger.debug("Django middleware process_view restored successfully")
113+
114+
except Exception as exc: # pylint: disable=broad-exception-caught
115+
_logger.warning("Failed to restore Django middleware process_view: %s", exc)
116+
117+
def patched_instrument(self, **kwargs):
118+
"""Patched _instrument method with Django middleware patching"""
119+
# Apply Django middleware patches
120+
_patch_django_middleware()
121+
122+
# Call the original _instrument method
123+
original_instrument(self, **kwargs)
124+
125+
def patched_uninstrument(self, **kwargs):
126+
"""Patched _uninstrument method with Django middleware patch restoration"""
127+
# Call the original _uninstrument method first
128+
original_uninstrument(self, **kwargs)
129+
130+
# Restore original Django middleware
131+
_unpatch_django_middleware()
132+
133+
# Apply the patches to DjangoInstrumentor
134+
DjangoInstrumentor._instrument = patched_instrument
135+
DjangoInstrumentor._uninstrument = patched_uninstrument
136+
137+
_logger.debug("Django instrumentation code attributes patch applied successfully")
138+
139+
except Exception as exc: # pylint: disable=broad-exception-caught
140+
_logger.warning("Failed to apply Django code attributes patch: %s", exc)
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
# Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License.
4+
5+
from logging import getLogger
6+
7+
from amazon.opentelemetry.distro.aws_opentelemetry_configurator import get_code_correlation_enabled_status
8+
9+
_logger = getLogger(__name__)
10+
11+
12+
def _apply_fastapi_instrumentation_patches() -> None:
13+
"""FastAPI instrumentation patches
14+
15+
Applies patches to provide code attributes support for FastAPI instrumentation.
16+
This patches the FastAPI instrumentation to automatically add code attributes
17+
to spans by decorating view functions with record_code_attributes.
18+
"""
19+
if get_code_correlation_enabled_status() is True:
20+
_apply_fastapi_code_attributes_patch()
21+
22+
23+
def _apply_fastapi_code_attributes_patch() -> None:
24+
"""FastAPI instrumentation patch for code attributes
25+
26+
This patch modifies the FastAPI instrumentation to automatically apply
27+
the current_span_code_attributes decorator to all endpoint functions when
28+
the FastAPI app is instrumented.
29+
30+
The patch:
31+
1. Imports current_span_code_attributes decorator from AWS distro utils
32+
2. Hooks FastAPI's APIRouter.add_api_route method during instrumentation
33+
3. Automatically decorates endpoint functions as they are registered
34+
4. Adds code.function.name, code.file.path, and code.line.number to spans
35+
5. Provides cleanup during uninstrumentation
36+
"""
37+
try:
38+
# Import FastAPI instrumentation classes and AWS decorator
39+
from fastapi import routing # pylint: disable=import-outside-toplevel
40+
41+
from amazon.opentelemetry.distro.code_correlation import ( # pylint: disable=import-outside-toplevel
42+
record_code_attributes,
43+
)
44+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor # pylint: disable=import-outside-toplevel
45+
46+
# Store the original _instrument and _uninstrument methods
47+
original_instrument = FastAPIInstrumentor._instrument
48+
original_uninstrument = FastAPIInstrumentor._uninstrument
49+
50+
def _wrapped_add_api_route(original_add_api_route_method):
51+
"""Wrapper for APIRouter.add_api_route method."""
52+
53+
def wrapper(self, *args, **kwargs):
54+
# Apply current_span_code_attributes decorator to endpoint function
55+
try:
56+
# Get endpoint function from args or kwargs
57+
endpoint = None
58+
if len(args) >= 2:
59+
endpoint = args[1]
60+
else:
61+
endpoint = kwargs.get("endpoint")
62+
63+
if endpoint and callable(endpoint):
64+
# Check if function is already decorated (avoid double decoration)
65+
if not hasattr(endpoint, "_current_span_code_attributes_decorated"):
66+
# Apply decorator
67+
decorated_endpoint = record_code_attributes(endpoint)
68+
# Mark as decorated to avoid double decoration
69+
decorated_endpoint._current_span_code_attributes_decorated = True
70+
decorated_endpoint._original_endpoint = endpoint
71+
72+
# Replace endpoint in args or kwargs
73+
if len(args) >= 2:
74+
args = list(args)
75+
args[1] = decorated_endpoint
76+
args = tuple(args)
77+
elif "endpoint" in kwargs:
78+
kwargs["endpoint"] = decorated_endpoint
79+
80+
except Exception as exc: # pylint: disable=broad-exception-caught
81+
_logger.warning("Failed to apply code attributes decorator to endpoint: %s", exc)
82+
83+
return original_add_api_route_method(self, *args, **kwargs)
84+
85+
return wrapper
86+
87+
def patched_instrument(self, **kwargs):
88+
"""Patched _instrument method with APIRouter.add_api_route wrapping"""
89+
# Store original add_api_route method if not already stored
90+
if not hasattr(self, "_original_apirouter"):
91+
self._original_apirouter = routing.APIRouter.add_api_route
92+
93+
# Wrap APIRouter.add_api_route with code attributes decoration
94+
routing.APIRouter.add_api_route = _wrapped_add_api_route(self._original_apirouter)
95+
96+
# Call the original _instrument method
97+
original_instrument(self, **kwargs)
98+
99+
def patched_uninstrument(self, **kwargs):
100+
"""Patched _uninstrument method with APIRouter.add_api_route restoration"""
101+
# Call the original _uninstrument method first
102+
original_uninstrument(self, **kwargs)
103+
104+
# Restore original APIRouter.add_api_route method if it exists
105+
if hasattr(self, "_original_apirouter"):
106+
try:
107+
routing.APIRouter.add_api_route = self._original_apirouter
108+
delattr(self, "_original_apirouter")
109+
except Exception as exc: # pylint: disable=broad-exception-caught
110+
_logger.warning("Failed to restore original APIRouter.add_api_route method: %s", exc)
111+
112+
# Apply the patches to FastAPIInstrumentor
113+
FastAPIInstrumentor._instrument = patched_instrument
114+
FastAPIInstrumentor._uninstrument = patched_uninstrument
115+
116+
_logger.debug("FastAPI instrumentation code attributes patch applied successfully")
117+
118+
except Exception as exc: # pylint: disable=broad-exception-caught
119+
_logger.warning("Failed to apply FastAPI code attributes patch: %s", exc)

0 commit comments

Comments
 (0)