Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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 .codespellrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[codespell]
# skipping auto generated folders
skip = ./.tox,./.mypy_cache,./target,*/LICENSE,./venv,*/sql_dialect_keywords.json
ignore-words-list = afterall,assertIn, crate
skip = ./.tox,./.mypy_cache,./target,*/LICENSE,./venv,*/sql_dialect_keywords.json,*/3rd.txt
ignore-words-list = afterall,assertIn, crate
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,13 @@ def _customize_logs_exporter(log_exporter: LogExporter) -> LogExporter:


def _customize_span_processors(provider: TracerProvider, resource: Resource) -> None:

if get_code_correlation_enabled_status() is True:
# pylint: disable=import-outside-toplevel
from amazon.opentelemetry.distro.code_correlation import CodeAttributesSpanProcessor

provider.add_span_processor(CodeAttributesSpanProcessor())

# Add LambdaSpanProcessor to list of processors regardless of application signals.
if _is_lambda_environment():
provider.add_span_processor(AwsLambdaSpanProcessor())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,123 +6,39 @@

This module provides functionality for correlating code execution with telemetry data.
"""

import inspect
from functools import wraps
from typing import Any, Callable

from opentelemetry import trace

# Import code attributes span processor
from .code_attributes_span_processor import CodeAttributesSpanProcessor

# Import main utilities to maintain API compatibility
from .utils import (
add_code_attributes_to_span,
add_code_attributes_to_span_from_frame,
get_callable_fullname,
get_function_fullname_from_frame,
record_code_attributes,
)

# Version information
__version__ = "1.0.0"


# Code correlation attribute constants
CODE_FUNCTION_NAME = "code.function.name"
CODE_FILE_PATH = "code.file.path"
CODE_LINE_NUMBER = "code.line.number"


def add_code_attributes_to_span(span, func_or_class: Callable[..., Any]) -> None:
"""
Add code-related attributes to a span based on a Python function.

This utility method extracts function metadata and adds the following
span attributes:
- CODE_FUNCTION_NAME: The name of the function
- CODE_FILE_PATH: The file path where the function is defined
- CODE_LINE_NUMBER: The line number where the function is defined

Args:
span: The OpenTelemetry span to add attributes to
func: The Python function to extract metadata from
"""
if not span.is_recording():
return

try:
# Check if it's a class first, with proper exception handling
try:
is_class = inspect.isclass(func_or_class)
except Exception: # pylint: disable=broad-exception-caught
# If inspect.isclass fails, we can't safely determine the type, so return early
return

if is_class:
span.set_attribute(CODE_FUNCTION_NAME, f"{func_or_class.__module__}.{func_or_class.__qualname__}")
span.set_attribute(CODE_FILE_PATH, inspect.getfile(func_or_class))
else:
code = getattr(func_or_class, "__code__", None)
if code:
span.set_attribute(CODE_FUNCTION_NAME, f"{func_or_class.__module__}.{func_or_class.__qualname__}")
span.set_attribute(CODE_FILE_PATH, code.co_filename)
span.set_attribute(CODE_LINE_NUMBER, code.co_firstlineno)
except Exception: # pylint: disable=broad-exception-caught
pass


def record_code_attributes(func: Callable[..., Any]) -> Callable[..., Any]:
"""
Decorator to automatically add code attributes to the current OpenTelemetry span.

This decorator extracts metadata from the decorated function and adds it as
attributes to the current active span. The attributes added are:
- code.function.name: The name of the function
- code.file.path: The file path where the function is defined
- code.line.number: The line number where the function is defined

This decorator supports both synchronous and asynchronous functions.

Usage:
@record_code_attributes
def my_sync_function():
# Sync function implementation
pass

@record_code_attributes
async def my_async_function():
# Async function implementation
pass

Args:
func: The function to be decorated

Returns:
The wrapped function with current span code attributes tracing
"""
# Detect async functions
is_async = inspect.iscoroutinefunction(func)

if is_async:
# Async function wrapper
@wraps(func)
async def async_wrapper(*args, **kwargs):
# Add code attributes to current span
try:
current_span = trace.get_current_span()
if current_span:
add_code_attributes_to_span(current_span, func)
except Exception: # pylint: disable=broad-exception-caught
# Silently handle any unexpected errors
pass

# Call and await the original async function
return await func(*args, **kwargs)

return async_wrapper

# Sync function wrapper
@wraps(func)
def sync_wrapper(*args, **kwargs):
# Add code attributes to current span
try:
current_span = trace.get_current_span()
if current_span:
add_code_attributes_to_span(current_span, func)
except Exception: # pylint: disable=broad-exception-caught
# Silently handle any unexpected errors
pass

# Call the original sync function
return func(*args, **kwargs)

return sync_wrapper
# Import constants from separate module to avoid circular imports
from .constants import CODE_FILE_PATH, CODE_FUNCTION_NAME, CODE_LINE_NUMBER, CODE_STACKTRACE

# Define public API
__all__ = [
# Constants
"CODE_FUNCTION_NAME",
"CODE_FILE_PATH",
"CODE_LINE_NUMBER",
"CODE_STACKTRACE",
# Functions
"add_code_attributes_to_span",
"add_code_attributes_to_span_from_frame",
"get_callable_fullname",
"get_function_fullname_from_frame",
"record_code_attributes",
# Classes
"CodeAttributesSpanProcessor",
# Version
"__version__",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

"""
Code Attributes Span Processor implementation for OpenTelemetry Python.

This processor captures stack traces and attaches them to spans as attributes.
It's based on the OpenTelemetry Java contrib StackTraceSpanProcessor.
"""

import sys
import typing as t
from types import FrameType
from typing import Optional

from opentelemetry.context import Context
from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
from opentelemetry.trace import SpanKind

from .constants import CODE_FUNCTION_NAME
from .internal.packages import _build_package_mapping, _load_third_party_packages, is_user_code
from .utils import add_code_attributes_to_span_from_frame


class CodeAttributesSpanProcessor(SpanProcessor):
"""
A SpanProcessor that captures and attaches code attributes to spans.

This processor adds stack trace information as span attributes, which can be
useful for debugging and understanding the call flow that led to span creation.
"""

# Maximum number of stack frames to examine
MAX_STACK_FRAMES = 50

@staticmethod
def _iter_stack_frames(frame: FrameType) -> t.Iterator[FrameType]:
"""Iterate through stack frames starting from the given frame."""
_frame: t.Optional[FrameType] = frame
while _frame is not None:
yield _frame
_frame = _frame.f_back

def __init__(self):
"""Initialize the CodeAttributesSpanProcessor."""
# Pre-initialize expensive operations to avoid runtime performance overhead
# These @execute_once methods are slow, so we call them during initialization
# to cache their results ahead of time
_build_package_mapping()
_load_third_party_packages()

def on_start(
self,
span: Span,
parent_context: Optional[Context] = None,
) -> None:
"""
Called when a span is started. Captures and attaches code attributes from stack trace.

Args:
span: The span that was started
parent_context: The parent context (unused)
"""
# Skip if span should not be processed
if not self._should_process_span(span):
return

# Capture code attributes from stack trace
self._capture_code_attributes(span)

@staticmethod
def _should_process_span(span: Span) -> bool:
"""
Determine if span should be processed for code attributes.

Returns False if:
- Span already has code attributes
- Span is not a client span
"""
# Skip if span already has code attributes
if span.attributes is not None and CODE_FUNCTION_NAME in span.attributes:
return False

# Only process client spans
return span.kind == SpanKind.CLIENT

def _capture_code_attributes(self, span: Span) -> None:
"""Capture and attach code attributes from current stack trace."""
try:
current_frame = sys._getframe(1)

for frame_index, frame in enumerate(self._iter_stack_frames(current_frame)):
if frame_index >= self.MAX_STACK_FRAMES:
break

code = frame.f_code

if is_user_code(code.co_filename):
add_code_attributes_to_span_from_frame(frame, span)
break # Only capture the first user code frame

except (OSError, ValueError):
# sys._getframe may not be available on all platforms
pass

def on_end(self, span: ReadableSpan) -> None:
"""
Called when a span is ended. Captures and attaches stack trace if conditions are met.
"""

def shutdown(self) -> None:
"""Called when the processor is shutdown. No cleanup needed."""
# No cleanup needed for code attributes processor

def force_flush(self, timeout_millis: int = 30000) -> bool: # pylint: disable=no-self-use,unused-argument
"""Force flush any pending spans. Always returns True as no pending work."""
return True
Loading