Skip to content

Commit 9baabb1

Browse files
authored
support code attributes for starlette, fastapi, flask and aws lambda (#488)
*Description of changes:* If set environment variable `OTEL_AWS_CODE_CORRELATION_ENABLED=true`, server spans of starlette, fastapi, flask and aws lambda have code-level information. ``` "attributes": { "code.function.name": "home", "code.file.path": "/Volumes/workplace/extension/aws-otel-python-instrumentation/samples/fastapi/app.py", "code.line.number": 87, }, ``` By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 6eaf909 commit 9baabb1

File tree

19 files changed

+1930
-91
lines changed

19 files changed

+1930
-91
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: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
CODE_LINE_NUMBER = "code.line.number"
2222

2323

24-
def _add_code_attributes_to_span(span, func: Callable[..., Any]) -> None:
24+
def add_code_attributes_to_span(span, func: Callable[..., Any]) -> None:
2525
"""
2626
Add code-related attributes to a span based on a Python function.
2727
@@ -68,7 +68,7 @@ def _add_code_attributes_to_span(span, func: Callable[..., Any]) -> None:
6868
pass
6969

7070

71-
def add_code_attributes_to_span(func: Callable[..., Any]) -> Callable[..., Any]:
71+
def record_code_attributes(func: Callable[..., Any]) -> Callable[..., Any]:
7272
"""
7373
Decorator to automatically add code attributes to the current OpenTelemetry span.
7474
@@ -81,12 +81,12 @@ def add_code_attributes_to_span(func: Callable[..., Any]) -> Callable[..., Any]:
8181
This decorator supports both synchronous and asynchronous functions.
8282
8383
Usage:
84-
@add_code_attributes_to_span
84+
@record_code_attributes
8585
def my_sync_function():
8686
# Sync function implementation
8787
pass
8888
89-
@add_code_attributes_to_span
89+
@record_code_attributes
9090
async def my_async_function():
9191
# Async function implementation
9292
pass
@@ -109,7 +109,7 @@ async def async_wrapper(*args, **kwargs):
109109
try:
110110
current_span = trace.get_current_span()
111111
if current_span:
112-
_add_code_attributes_to_span(current_span, func)
112+
add_code_attributes_to_span(current_span, func)
113113
except Exception: # pylint: disable=broad-exception-caught
114114
# Silently handle any unexpected errors
115115
pass
@@ -126,7 +126,7 @@ def sync_wrapper(*args, **kwargs):
126126
try:
127127
current_span = trace.get_current_span()
128128
if current_span:
129-
_add_code_attributes_to_span(current_span, func)
129+
add_code_attributes_to_span(current_span, func)
130130
except Exception: # pylint: disable=broad-exception-caught
131131
# Silently handle any unexpected errors
132132
pass
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)
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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_flask_instrumentation_patches() -> None:
13+
"""Flask instrumentation patches
14+
15+
Applies patches to provide code attributes support for Flask instrumentation.
16+
This patches the Flask 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_flask_code_attributes_patch()
21+
22+
23+
def _apply_flask_code_attributes_patch() -> None: # pylint: disable=too-many-statements
24+
"""Flask instrumentation patch for code attributes
25+
26+
This patch modifies the Flask instrumentation to automatically apply
27+
the current_span_code_attributes decorator to all view functions when
28+
the Flask app is instrumented.
29+
30+
The patch:
31+
1. Imports current_span_code_attributes decorator from AWS distro utils
32+
2. Hooks Flask's add_url_rule method during _instrument by patching Flask class
33+
3. Hooks Flask's dispatch_request method to handle deferred view function binding
34+
4. Automatically decorates view functions as they are registered or at request time
35+
5. Adds code.function.name, code.file.path, and code.line.number to spans
36+
6. Provides cleanup during _uninstrument
37+
"""
38+
try:
39+
# Import Flask instrumentation classes and AWS decorator
40+
import flask # pylint: disable=import-outside-toplevel
41+
42+
from amazon.opentelemetry.distro.code_correlation import ( # pylint: disable=import-outside-toplevel
43+
record_code_attributes,
44+
)
45+
from opentelemetry.instrumentation.flask import FlaskInstrumentor # pylint: disable=import-outside-toplevel
46+
47+
# Store the original _instrument and _uninstrument methods
48+
original_instrument = FlaskInstrumentor._instrument
49+
original_uninstrument = FlaskInstrumentor._uninstrument
50+
51+
# Store reference to original Flask methods
52+
original_flask_add_url_rule = flask.Flask.add_url_rule
53+
original_flask_dispatch_request = flask.Flask.dispatch_request
54+
55+
def _decorate_view_func(view_func, endpoint=None):
56+
"""Helper function to decorate a view function with code attributes."""
57+
try:
58+
if view_func and callable(view_func):
59+
# Check if function is already decorated (avoid double decoration)
60+
if not hasattr(view_func, "_current_span_code_attributes_decorated"):
61+
# Apply decorator
62+
decorated_view_func = record_code_attributes(view_func)
63+
# Mark as decorated to avoid double decoration
64+
decorated_view_func._current_span_code_attributes_decorated = True
65+
decorated_view_func._original_view_func = view_func
66+
return decorated_view_func
67+
return view_func
68+
except Exception as exc: # pylint: disable=broad-exception-caught
69+
_logger.warning("Failed to apply code attributes decorator to view function %s: %s", endpoint, exc)
70+
return view_func
71+
72+
def _wrapped_add_url_rule(self, rule, endpoint=None, view_func=None, **options):
73+
"""Wrapped Flask.add_url_rule method with code attributes decoration."""
74+
# Apply decorator to view function if available
75+
if view_func:
76+
view_func = _decorate_view_func(view_func, endpoint)
77+
78+
return original_flask_add_url_rule(self, rule, endpoint, view_func, **options)
79+
80+
def _wrapped_dispatch_request(self):
81+
"""Wrapped Flask.dispatch_request method to handle deferred view function binding."""
82+
try:
83+
# Get the current request context
84+
from flask import request # pylint: disable=import-outside-toplevel
85+
86+
# Check if there's an endpoint for this request
87+
endpoint = request.endpoint
88+
if endpoint and endpoint in self.view_functions:
89+
view_func = self.view_functions[endpoint]
90+
91+
# Check if the view function needs decoration
92+
if view_func and callable(view_func):
93+
if not hasattr(view_func, "_current_span_code_attributes_decorated"):
94+
# Decorate the view function and replace it in view_functions
95+
decorated_view_func = _decorate_view_func(view_func, endpoint)
96+
if decorated_view_func != view_func:
97+
self.view_functions[endpoint] = decorated_view_func
98+
_logger.debug(
99+
"Applied code attributes decorator to deferred view function for endpoint: %s",
100+
endpoint,
101+
)
102+
103+
except Exception as exc: # pylint: disable=broad-exception-caught
104+
_logger.warning("Failed to process deferred view function decoration: %s", exc)
105+
106+
# Call the original dispatch_request method
107+
return original_flask_dispatch_request(self)
108+
109+
def patched_instrument(self, **kwargs):
110+
"""Patched _instrument method with Flask method wrapping"""
111+
# Store original methods if not already stored
112+
if not hasattr(self, "_original_flask_add_url_rule"):
113+
self._original_flask_add_url_rule = flask.Flask.add_url_rule
114+
self._original_flask_dispatch_request = flask.Flask.dispatch_request
115+
116+
# Wrap Flask methods with code attributes decoration
117+
flask.Flask.add_url_rule = _wrapped_add_url_rule
118+
flask.Flask.dispatch_request = _wrapped_dispatch_request
119+
120+
# Call the original _instrument method
121+
original_instrument(self, **kwargs)
122+
123+
def patched_uninstrument(self, **kwargs):
124+
"""Patched _uninstrument method with Flask method restoration"""
125+
# Call the original _uninstrument method first
126+
original_uninstrument(self, **kwargs)
127+
128+
# Restore original Flask methods if they exist
129+
if hasattr(self, "_original_flask_add_url_rule"):
130+
try:
131+
flask.Flask.add_url_rule = self._original_flask_add_url_rule
132+
flask.Flask.dispatch_request = self._original_flask_dispatch_request
133+
delattr(self, "_original_flask_add_url_rule")
134+
delattr(self, "_original_flask_dispatch_request")
135+
except Exception as exc: # pylint: disable=broad-exception-caught
136+
_logger.warning("Failed to restore original Flask methods: %s", exc)
137+
138+
# Apply the patches to FlaskInstrumentor
139+
FlaskInstrumentor._instrument = patched_instrument
140+
FlaskInstrumentor._uninstrument = patched_uninstrument
141+
142+
_logger.debug("Flask instrumentation code attributes patch applied successfully")
143+
144+
except Exception as exc: # pylint: disable=broad-exception-caught
145+
_logger.warning("Failed to apply Flask code attributes patch: %s", exc)

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ def apply_instrumentation_patches() -> None:
7171
# TODO: Remove patch after syncing with upstream v1.34.0 or later
7272
_apply_starlette_instrumentation_patches()
7373

74+
if is_installed("flask"):
75+
# pylint: disable=import-outside-toplevel
76+
# Delay import to only occur if patches is safe to apply (e.g. the instrumented library is installed).
77+
from amazon.opentelemetry.distro.patches._flask_patches import _apply_flask_instrumentation_patches
78+
79+
_apply_flask_instrumentation_patches()
80+
81+
if is_installed("fastapi"):
82+
# pylint: disable=import-outside-toplevel
83+
# Delay import to only occur if patches is safe to apply (e.g. the instrumented library is installed).
84+
from amazon.opentelemetry.distro.patches._fastapi_patches import _apply_fastapi_instrumentation_patches
85+
86+
_apply_fastapi_instrumentation_patches()
87+
7488
# No need to check if library is installed as this patches opentelemetry.sdk,
7589
# which must be installed for the distro to work at all.
7690
_apply_resource_detector_patches()

0 commit comments

Comments
 (0)