Skip to content

Commit 83edee5

Browse files
committed
function decorator for adding code level attributes to span
1 parent 93d317b commit 83edee5

File tree

4 files changed

+624
-0
lines changed

4 files changed

+624
-0
lines changed

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"
9696
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"
9797
OTEL_EXPORTER_OTLP_LOGS_HEADERS = "OTEL_EXPORTER_OTLP_LOGS_HEADERS"
98+
CODE_CORRELATION_ENABLED_CONFIG = "OTEL_AWS_CODE_CORRELATION_ENABLED"
9899

99100
XRAY_SERVICE = "xray"
100101
LOGS_SERIVCE = "logs"
@@ -615,6 +616,32 @@ def _is_application_signals_runtime_enabled():
615616
)
616617

617618

619+
def _get_code_correlation_enabled_status() -> Optional[bool]:
620+
"""
621+
Get the code correlation enabled status from environment variable.
622+
623+
Returns:
624+
True if OTEL_AWS_CODE_CORRELATION_ENABLED is set to 'true'
625+
False if OTEL_AWS_CODE_CORRELATION_ENABLED is set to 'false'
626+
None if OTEL_AWS_CODE_CORRELATION_ENABLED is not set (default state)
627+
"""
628+
env_value = os.environ.get(CODE_CORRELATION_ENABLED_CONFIG)
629+
630+
if env_value is None:
631+
return None # Default state - environment variable not set
632+
633+
env_value_lower = env_value.strip().lower()
634+
if env_value_lower == "true":
635+
return True
636+
elif env_value_lower == "false":
637+
return False
638+
else:
639+
# Invalid value, treat as default and log warning
640+
_logger.warning("Invalid value for %s: %s. Expected 'true' or 'false'. Using default.",
641+
CODE_CORRELATION_ENABLED_CONFIG, env_value)
642+
return None
643+
644+
618645
def _is_lambda_environment():
619646
# detect if running in AWS Lambda environment
620647
return AWS_LAMBDA_FUNCTION_NAME_CONFIG in os.environ
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""
5+
Code correlation module for AWS OpenTelemetry Python Instrumentation.
6+
7+
This module provides functionality for correlating code execution with telemetry data.
8+
"""
9+
10+
__version__ = "1.0.0"
11+
12+
13+
"""
14+
Utility functions for adding code information to OpenTelemetry spans.
15+
"""
16+
17+
from typing import Any, Callable
18+
from functools import wraps
19+
from opentelemetry import trace
20+
21+
22+
# Code correlation attribute constants
23+
CODE_FUNCTION_NAME = "code.function.name"
24+
CODE_FILE_PATH = "code.file.path"
25+
CODE_LINE_NUMBER = "code.line.number"
26+
27+
28+
def _add_code_attributes_to_span(span, func: Callable[..., Any]) -> None:
29+
"""
30+
Add code-related attributes to a span based on a Python function.
31+
32+
This utility method extracts function metadata and adds the following
33+
span attributes:
34+
- CODE_FUNCTION_NAME: The name of the function
35+
- CODE_FILE_PATH: The file path where the function is defined
36+
- CODE_LINE_NUMBER: The line number where the function is defined
37+
38+
Args:
39+
span: The OpenTelemetry span to add attributes to
40+
func: The Python function to extract metadata from
41+
"""
42+
if not span.is_recording():
43+
return
44+
45+
try:
46+
# Get function name
47+
function_name = getattr(func, '__name__', str(func))
48+
span.set_attribute(CODE_FUNCTION_NAME, function_name)
49+
50+
# Get function source file from code object
51+
try:
52+
if hasattr(func, '__code__'):
53+
source_file = func.__code__.co_filename
54+
span.set_attribute(CODE_FILE_PATH, source_file)
55+
except (AttributeError, TypeError):
56+
# Handle cases where code object is not available
57+
# (e.g., built-in functions, C extensions)
58+
pass
59+
60+
# Get function line number from code object
61+
try:
62+
if hasattr(func, '__code__'):
63+
line_number = func.__code__.co_firstlineno
64+
span.set_attribute(CODE_LINE_NUMBER, line_number)
65+
except (AttributeError, TypeError):
66+
# Handle cases where code object is not available
67+
pass
68+
69+
except Exception:
70+
# Silently handle any unexpected errors to avoid breaking
71+
# the instrumentation flow
72+
pass
73+
74+
75+
def add_code_attributes_to_span(func: Callable[..., Any]) -> Callable[..., Any]:
76+
"""
77+
Decorator to automatically add code attributes to the current OpenTelemetry span.
78+
79+
This decorator extracts metadata from the decorated function and adds it as
80+
attributes to the current active span. The attributes added are:
81+
- code.function.name: The name of the function
82+
- code.file.path: The file path where the function is defined
83+
- code.line.number: The line number where the function is defined
84+
85+
This decorator supports both synchronous and asynchronous functions.
86+
87+
Usage:
88+
@add_code_attributes_to_span
89+
def my_sync_function():
90+
# Sync function implementation
91+
pass
92+
93+
@add_code_attributes_to_span
94+
async def my_async_function():
95+
# Async function implementation
96+
pass
97+
98+
Args:
99+
func: The function to be decorated
100+
101+
Returns:
102+
The wrapped function with current span code attributes tracing
103+
"""
104+
# Detect async functions: check function code object flags or special attributes
105+
# CO_ITERABLE_COROUTINE = 0x80, async functions will have this flag set
106+
is_async = (hasattr(func, '__code__') and
107+
func.__code__.co_flags & 0x80) or hasattr(func, '_is_coroutine')
108+
109+
if is_async:
110+
# Async function wrapper
111+
@wraps(func)
112+
async def async_wrapper(*args, **kwargs):
113+
# Add code attributes to current span
114+
try:
115+
current_span = trace.get_current_span()
116+
if current_span:
117+
_add_code_attributes_to_span(current_span, func)
118+
except Exception:
119+
# Silently handle any unexpected errors
120+
pass
121+
122+
# Call and await the original async function
123+
return await func(*args, **kwargs)
124+
125+
return async_wrapper
126+
else:
127+
# Sync function wrapper
128+
@wraps(func)
129+
def sync_wrapper(*args, **kwargs):
130+
# Add code attributes to current span
131+
try:
132+
current_span = trace.get_current_span()
133+
if current_span:
134+
_add_code_attributes_to_span(current_span, func)
135+
except Exception:
136+
# Silently handle any unexpected errors
137+
pass
138+
139+
# Call the original sync function
140+
return func(*args, **kwargs)
141+
142+
return sync_wrapper

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
_export_unsampled_span_for_agent_observability,
4040
_export_unsampled_span_for_lambda,
4141
_fetch_logs_header,
42+
_get_code_correlation_enabled_status,
4243
_init_logging,
4344
_is_application_signals_enabled,
4445
_is_application_signals_runtime_enabled,
@@ -1425,6 +1426,72 @@ def test_create_emf_exporter_cloudwatch_exporter_import_error(
14251426
self.assertIsNone(result)
14261427
mock_logger.error.assert_called_once()
14271428

1429+
def test_get_code_correlation_enabled_status(self):
1430+
"""Test _get_code_correlation_enabled_status function with various environment variable values"""
1431+
# Import the constant we need
1432+
from amazon.opentelemetry.distro.aws_opentelemetry_configurator import CODE_CORRELATION_ENABLED_CONFIG
1433+
1434+
# Test when environment variable is not set (default state)
1435+
os.environ.pop(CODE_CORRELATION_ENABLED_CONFIG, None)
1436+
result = _get_code_correlation_enabled_status()
1437+
self.assertIsNone(result)
1438+
1439+
# Test when environment variable is set to 'true' (case insensitive)
1440+
os.environ[CODE_CORRELATION_ENABLED_CONFIG] = "true"
1441+
result = _get_code_correlation_enabled_status()
1442+
self.assertTrue(result)
1443+
1444+
os.environ[CODE_CORRELATION_ENABLED_CONFIG] = "TRUE"
1445+
result = _get_code_correlation_enabled_status()
1446+
self.assertTrue(result)
1447+
1448+
os.environ[CODE_CORRELATION_ENABLED_CONFIG] = "True"
1449+
result = _get_code_correlation_enabled_status()
1450+
self.assertTrue(result)
1451+
1452+
# Test when environment variable is set to 'false' (case insensitive)
1453+
os.environ[CODE_CORRELATION_ENABLED_CONFIG] = "false"
1454+
result = _get_code_correlation_enabled_status()
1455+
self.assertFalse(result)
1456+
1457+
os.environ[CODE_CORRELATION_ENABLED_CONFIG] = "FALSE"
1458+
result = _get_code_correlation_enabled_status()
1459+
self.assertFalse(result)
1460+
1461+
os.environ[CODE_CORRELATION_ENABLED_CONFIG] = "False"
1462+
result = _get_code_correlation_enabled_status()
1463+
self.assertFalse(result)
1464+
1465+
# Test with leading/trailing whitespace
1466+
os.environ[CODE_CORRELATION_ENABLED_CONFIG] = " true "
1467+
result = _get_code_correlation_enabled_status()
1468+
self.assertTrue(result)
1469+
1470+
os.environ[CODE_CORRELATION_ENABLED_CONFIG] = " false "
1471+
result = _get_code_correlation_enabled_status()
1472+
self.assertFalse(result)
1473+
1474+
# Test invalid values (should return None and log warning)
1475+
# We'll use caplog to capture log messages instead of mocking
1476+
import logging
1477+
1478+
os.environ[CODE_CORRELATION_ENABLED_CONFIG] = "invalid"
1479+
result = _get_code_correlation_enabled_status()
1480+
self.assertIsNone(result)
1481+
1482+
# Test another invalid value
1483+
os.environ[CODE_CORRELATION_ENABLED_CONFIG] = "yes"
1484+
result = _get_code_correlation_enabled_status()
1485+
self.assertIsNone(result)
1486+
1487+
# Test empty string (invalid)
1488+
os.environ[CODE_CORRELATION_ENABLED_CONFIG] = ""
1489+
result = _get_code_correlation_enabled_status()
1490+
self.assertIsNone(result)
1491+
1492+
# Clean up
1493+
os.environ.pop(CODE_CORRELATION_ENABLED_CONFIG, None)
1494+
14281495

14291496
def validate_distro_environ():
14301497
tc: TestCase = TestCase()

0 commit comments

Comments
 (0)