Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#3765](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3765))
- Add `rstcheck` to pre-commit to stop introducing invalid RST
([#3777](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3777))
- `opentelemetry-exporter-credential-provider-gcp`: create this package which provides support for supplying your machine's Application Default Credentials (https://cloud.google.com/docs/authentication/application-default-credentials) to the OTLP Exporters created automatically by OpenTelemetry Python's auto instrumentation. These credentials authorize OTLP traces to be sent to `telemetry.googleapis.com`.
[#3766](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3766).
Comment on lines +27 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `opentelemetry-exporter-credential-provider-gcp`: create this package which provides support for supplying your machine's Application Default Credentials (https://cloud.google.com/docs/authentication/application-default-credentials) to the OTLP Exporters created automatically by OpenTelemetry Python's auto instrumentation. These credentials authorize OTLP traces to be sent to `telemetry.googleapis.com`.
[#3766](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3766).

- `opentelemetry-instrumentation-botocore`: botocore: Add AWS_LAMBDA_RESOURCE_MAPPING_ID Semantic Convention Support for AWS Lambda SDK
([#3800](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3800))
- `opentelemetry-exporter-credential-provider-gcp`: create this package which provides support for supplying your machine's Application Default
Credentials (https://cloud.google.com/docs/authentication/application-default-credentials) to the OTLP Exporters created automatically by OpenTelemetry Python's auto instrumentation. These credentials authorize OTLP traces to be sent to `telemetry.googleapis.com`. [#3766](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3766).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,27 @@
import inspect
import json
import re
from typing import Dict
from typing import Dict, Final

from opentelemetry.instrumentation.botocore.extensions.types import (
_AttributeMapT,
_AwsSdkCallContext,
_AwsSdkExtension,
_BotocoreInstrumentorContext,
_BotoResultT,
)
from opentelemetry.propagate import inject
from opentelemetry.semconv._incubating.attributes.aws_attributes import (
AWS_LAMBDA_RESOURCE_MAPPING_ID,
)
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace.span import Span

# Work is underway to add these two keys to the SemConv AWS registry, in line with other AWS resources.
# https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/aws.md#amazon-lambda-attributes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually don't add attributes before they're added to semconv, do you want to wait for these to get there or move the usage of these attributes to another PR?

AWS_LAMBDA_FUNCTION_ARN: Final = "aws.lambda.function.arn"
AWS_LAMBDA_FUNCTION_NAME: Final = "aws.lambda.function.name"


class _LambdaOperation(abc.ABC):
@classmethod
Expand Down Expand Up @@ -68,12 +77,16 @@ def extract_attributes(
)
attributes[SpanAttributes.FAAS_INVOKED_REGION] = call_context.region

@classmethod
def _parse_function_name(cls, call_context: _AwsSdkCallContext):
@staticmethod
def _parse_function_name(call_context: _AwsSdkCallContext):
function_name_or_arn = call_context.params.get("FunctionName")
matches = cls.ARN_LAMBDA_PATTERN.match(function_name_or_arn)
function_name = matches.group(1)
return function_name_or_arn if function_name is None else function_name
if function_name_or_arn is None:
return None
matches = _OpInvoke.ARN_LAMBDA_PATTERN.match(function_name_or_arn)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why you are moving to a staticmethod while still referencing class attributes but not an issue for me 😅

if matches:
function_name = matches.group(1)
return function_name if function_name else function_name_or_arn
return function_name_or_arn

@classmethod
def before_service_call(cls, call_context: _AwsSdkCallContext, span: Span):
Expand Down Expand Up @@ -115,6 +128,13 @@ def __init__(self, call_context: _AwsSdkCallContext):
self._op = _OPERATION_MAPPING.get(call_context.operation)

def extract_attributes(self, attributes: _AttributeMapT):
function_name = _OpInvoke._parse_function_name(self._call_context)
if function_name:
attributes[AWS_LAMBDA_FUNCTION_NAME] = function_name
resource_mapping_id = self._call_context.params.get("UUID")
if resource_mapping_id:
attributes[AWS_LAMBDA_RESOURCE_MAPPING_ID] = resource_mapping_id

if self._op is None:
return

Expand All @@ -127,3 +147,20 @@ def before_service_call(
return

self._op.before_service_call(self._call_context, span)

def on_success(
self,
span: Span,
result: _BotoResultT,
instrumentor_context: _BotocoreInstrumentorContext,
):
resource_mapping_id = result.get("UUID")
if resource_mapping_id:
span.set_attribute(
AWS_LAMBDA_RESOURCE_MAPPING_ID, resource_mapping_id
)

lambda_configuration = result.get("Configuration", {})
function_arn = lambda_configuration.get("FunctionArn")
if function_arn:
span.set_attribute(AWS_LAMBDA_FUNCTION_ARN, function_arn)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import json
import sys
import zipfile
from typing import Final
from unittest import mock

import botocore.session
Expand All @@ -27,11 +28,17 @@
_LambdaExtension,
)
from opentelemetry.propagate import get_global_textmap, set_global_textmap
from opentelemetry.semconv._incubating.attributes.aws_attributes import (
AWS_LAMBDA_RESOURCE_MAPPING_ID,
)
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.test.mock_textmap import MockTextMapPropagator
from opentelemetry.test.test_base import TestBase
from opentelemetry.trace.span import Span

AWS_LAMBDA_FUNCTION_ARN: Final = "aws.lambda.function.arn"
AWS_LAMBDA_FUNCTION_NAME: Final = "aws.lambda.function.name"


def get_as_zip_file(file_name, content):
zip_output = io.BytesIO()
Expand Down Expand Up @@ -126,6 +133,18 @@ def _create_lambda_function(self, function_name: str, function_code: str):
Publish=True,
)

def _create_sqs_queue_and_get_arn(self) -> str:
"""Helper method to create SQS queue and return ARN"""
session = botocore.session.get_session()
session.set_credentials(
access_key="access-key", secret_key="secret-key"
)
sqs_client = session.create_client("sqs", region_name=self.region)
sqs_client.create_queue(
QueueName="MyTestQueue.fifo", Attributes={"FifoQueue": "true"}
)
return f"arn:aws:sqs:{self.region}:123456789012:MyTestQueue.fifo"

@mark.skip(reason="Docker error, unblocking builds for now.")
@mark.skipif(
sys.platform == "win32",
Expand Down Expand Up @@ -185,3 +204,94 @@ def test_invoke_parse_arn(self):
self.assertEqual(
function_name, attributes[SpanAttributes.FAAS_INVOKED_NAME]
)

@mock_aws
def test_get_function(self):
function_name = "lambda-function-name-foo"
self._create_lambda_function(
function_name, return_headers_lambda_str()
)

self.memory_exporter.clear()
self.client.get_function(FunctionName=function_name)
spans = self.memory_exporter.get_finished_spans()
self.assertEqual(1, len(spans))
span = spans[0]
self.assertEqual(
"GetFunction", span.attributes[SpanAttributes.RPC_METHOD]
)
self.assertEqual(
"lambda-function-name-foo",
span.attributes[AWS_LAMBDA_FUNCTION_NAME],
)

function_arn = span.attributes.get(AWS_LAMBDA_FUNCTION_ARN)
self.assertIsNotNone(function_arn)
self.assertIn("lambda-function-name-foo", function_arn)
self.assertIsNone(span.attributes.get(AWS_LAMBDA_RESOURCE_MAPPING_ID))

@mock_aws
def test_create_event_source_mapping(self):
function_name = "MyLambdaFnFoo"
self._create_lambda_function(
function_name, return_headers_lambda_str()
)

queue_arn = self._create_sqs_queue_and_get_arn()
self.memory_exporter.clear()
response = self.client.create_event_source_mapping(
EventSourceArn=queue_arn, FunctionName=function_name, BatchSize=10
)
expected_uuid = response["UUID"]
self.assertIsNotNone(expected_uuid)
self.assertTrue(expected_uuid)

spans = self.memory_exporter.get_finished_spans()
self.assertEqual(1, len(spans))
span = spans[0]
self.assertEqual(
"CreateEventSourceMapping",
span.attributes[SpanAttributes.RPC_METHOD],
)
self.assertEqual(
"MyLambdaFnFoo", span.attributes[AWS_LAMBDA_FUNCTION_NAME]
)

uuid = span.attributes.get(AWS_LAMBDA_RESOURCE_MAPPING_ID)
self.assertIsNotNone(uuid)
self.assertEqual(expected_uuid, uuid)

@mock_aws
def test_get_event_source_mapping(self):
function_name = "MyLambdaFnBar"
self._create_lambda_function(
function_name, return_headers_lambda_str()
)

queue_arn = self._create_sqs_queue_and_get_arn()

# Create event source mapping first
create_response = self.client.create_event_source_mapping(
EventSourceArn=queue_arn, FunctionName=function_name, BatchSize=10
)
mapping_uuid = create_response["UUID"]

self.memory_exporter.clear()
response = self.client.get_event_source_mapping(UUID=mapping_uuid)
expected_uuid = response["UUID"]
self.assertIsNotNone(expected_uuid)
self.assertTrue(expected_uuid)

spans = self.memory_exporter.get_finished_spans()
self.assertEqual(1, len(spans))
span = spans[0]
self.assertEqual(
"GetEventSourceMapping",
span.attributes[SpanAttributes.RPC_METHOD],
)

uuid = span.attributes.get(AWS_LAMBDA_RESOURCE_MAPPING_ID)
self.assertIsNotNone(uuid)
self.assertEqual(expected_uuid, uuid)
self.assertIsNone(span.attributes.get(AWS_LAMBDA_FUNCTION_ARN))
self.assertIsNone(span.attributes.get(AWS_LAMBDA_FUNCTION_NAME))