Fixing w3c baggage support in opentelemetry-instrumentation-aws-lambda (#2589)

* Fixing w3c baggage support in opentelemetry-instrumentation-aws-lambda

* Changelog update

* Passing context not needed

* Fixing unit test after rebase
This commit is contained in:
Daniel Torok
2024-08-01 22:24:18 +01:00
committed by GitHub
parent fa6a36b8ef
commit 4ea9e5a99a
4 changed files with 116 additions and 61 deletions

View File

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
## Added
## Breaking changes
## Fixed
- `opentelemetry-instrumentation-aws-lambda` Avoid exception when a handler is not present.
@ -17,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#2483](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2484))
- `opentelemetry-instrumentation-fastapi` Fix fastapi-slim support
([#2756](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2756))
- `opentelemetry-instrumentation-aws-lambda` Fixing w3c baggage support
([#2589](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2589))
## Version 1.26.0/0.47b0 (2024-07-23)
@ -113,7 +119,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610))
- `opentelemetry-instrumentation-asgi` Bugfix: Middleware did not set status code attribute on duration metrics for non-recording spans.
([#2627](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2627))
<<<<<<< HEAD
- `opentelemetry-instrumentation-mysql` Add support for `mysql-connector-python` v9 ([#2751](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2751))
=======
>>>>>>> 5a623233 (Changelog update)
## Version 1.25.0/0.46b0 (2024-05-31)

View File

@ -76,6 +76,7 @@ from urllib.parse import urlencode
from wrapt import wrap_function_wrapper
from opentelemetry import context as context_api
from opentelemetry.context.context import Context
from opentelemetry.instrumentation.aws_lambda.package import _instruments
from opentelemetry.instrumentation.aws_lambda.version import __version__
@ -303,65 +304,74 @@ def _instrument(
schema_url="https://opentelemetry.io/schemas/1.11.0",
)
with tracer.start_as_current_span(
name=orig_handler_name,
context=parent_context,
kind=span_kind,
) as span:
if span.is_recording():
lambda_context = args[1]
# NOTE: The specs mention an exception here, allowing the
# `SpanAttributes.CLOUD_RESOURCE_ID` attribute to be set as a span
# attribute instead of a resource attribute.
#
# See more:
# https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#resource-detector
span.set_attribute(
SpanAttributes.CLOUD_RESOURCE_ID,
lambda_context.invoked_function_arn,
)
span.set_attribute(
SpanAttributes.FAAS_INVOCATION_ID,
lambda_context.aws_request_id,
)
# NOTE: `cloud.account.id` can be parsed from the ARN as the fifth item when splitting on `:`
#
# See more:
# https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#all-triggers
account_id = lambda_context.invoked_function_arn.split(":")[4]
span.set_attribute(
ResourceAttributes.CLOUD_ACCOUNT_ID,
account_id,
)
exception = None
result = None
try:
result = call_wrapped(*args, **kwargs)
except Exception as exc: # pylint: disable=W0703
exception = exc
span.set_status(Status(StatusCode.ERROR))
span.record_exception(exception)
# If the request came from an API Gateway, extract http attributes from the event
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#api-gateway
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-server-semantic-conventions
if isinstance(lambda_event, dict) and lambda_event.get(
"requestContext"
):
span.set_attribute(SpanAttributes.FAAS_TRIGGER, "http")
if lambda_event.get("version") == "2.0":
_set_api_gateway_v2_proxy_attributes(lambda_event, span)
else:
_set_api_gateway_v1_proxy_attributes(lambda_event, span)
if isinstance(result, dict) and result.get("statusCode"):
token = context_api.attach(parent_context)
try:
with tracer.start_as_current_span(
name=orig_handler_name,
kind=span_kind,
) as span:
if span.is_recording():
lambda_context = args[1]
# NOTE: The specs mention an exception here, allowing the
# `SpanAttributes.CLOUD_RESOURCE_ID` attribute to be set as a span
# attribute instead of a resource attribute.
#
# See more:
# https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#resource-detector
span.set_attribute(
SpanAttributes.HTTP_STATUS_CODE,
result.get("statusCode"),
SpanAttributes.CLOUD_RESOURCE_ID,
lambda_context.invoked_function_arn,
)
span.set_attribute(
SpanAttributes.FAAS_INVOCATION_ID,
lambda_context.aws_request_id,
)
# NOTE: `cloud.account.id` can be parsed from the ARN as the fifth item when splitting on `:`
#
# See more:
# https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#all-triggers
account_id = lambda_context.invoked_function_arn.split(
":"
)[4]
span.set_attribute(
ResourceAttributes.CLOUD_ACCOUNT_ID,
account_id,
)
exception = None
result = None
try:
result = call_wrapped(*args, **kwargs)
except Exception as exc: # pylint: disable=W0703
exception = exc
span.set_status(Status(StatusCode.ERROR))
span.record_exception(exception)
# If the request came from an API Gateway, extract http attributes from the event
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#api-gateway
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-server-semantic-conventions
if isinstance(lambda_event, dict) and lambda_event.get(
"requestContext"
):
span.set_attribute(SpanAttributes.FAAS_TRIGGER, "http")
if lambda_event.get("version") == "2.0":
_set_api_gateway_v2_proxy_attributes(
lambda_event, span
)
else:
_set_api_gateway_v1_proxy_attributes(
lambda_event, span
)
if isinstance(result, dict) and result.get("statusCode"):
span.set_attribute(
SpanAttributes.HTTP_STATUS_CODE,
result.get("statusCode"),
)
finally:
context_api.detach(token)
now = time.time()
_tracer_provider = tracer_provider or get_tracer_provider()

View File

@ -12,9 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
from opentelemetry import baggage as baggage_api
def handler(event, context):
return "200 ok"
baggage_content = dict(baggage_api.get_all().items())
return json.dumps({"baggage_content": baggage_content})
def rest_api_handler(event, context):

View File

@ -11,7 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import os
from dataclasses import dataclass
from importlib import import_module, reload
@ -19,6 +19,7 @@ from typing import Any, Callable, Dict
from unittest import mock
from opentelemetry import propagate
from opentelemetry.baggage.propagation import W3CBaggagePropagator
from opentelemetry.environment_variables import OTEL_PROPAGATORS
from opentelemetry.instrumentation.aws_lambda import (
_HANDLER,
@ -79,6 +80,9 @@ MOCK_W3C_TRACE_CONTEXT_SAMPLED = (
MOCK_W3C_TRACE_STATE_KEY = "vendor_specific_key"
MOCK_W3C_TRACE_STATE_VALUE = "test_value"
MOCK_W3C_BAGGAGE_KEY = "baggage_key"
MOCK_W3C_BAGGAGE_VALUE = "baggage_value"
def mock_execute_lambda(event=None):
"""Mocks the AWS Lambda execution.
@ -97,7 +101,7 @@ def mock_execute_lambda(event=None):
module_name, handler_name = os.environ[_HANDLER].rsplit(".", 1)
handler_module = import_module(module_name.replace("/", "."))
getattr(handler_module, handler_name)(event, MOCK_LAMBDA_CONTEXT)
return getattr(handler_module, handler_name)(event, MOCK_LAMBDA_CONTEXT)
class TestAwsLambdaInstrumentor(TestBase):
@ -181,6 +185,9 @@ class TestAwsLambdaInstrumentor(TestBase):
expected_state_value: str = None
expected_trace_state_len: int = 0
propagators: str = "tracecontext"
expected_baggage: str = None
disable_aws_context_propagation: bool = False
disable_aws_context_propagation_envvar: str = ""
def custom_event_context_extractor(lambda_event):
return get_global_textmap().extract(lambda_event["foo"]["headers"])
@ -266,6 +273,24 @@ class TestAwsLambdaInstrumentor(TestBase):
expected_state_value=MOCK_W3C_TRACE_STATE_VALUE,
xray_traceid=MOCK_XRAY_TRACE_CONTEXT_SAMPLED,
),
TestCase(
name="baggage_propagation",
custom_extractor=None,
context={
"headers": {
TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED,
TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2",
W3CBaggagePropagator._BAGGAGE_HEADER_NAME: f"{MOCK_W3C_BAGGAGE_KEY}={MOCK_W3C_BAGGAGE_VALUE}",
}
},
expected_traceid=MOCK_W3C_TRACE_ID,
expected_parentid=MOCK_W3C_PARENT_SPAN_ID,
expected_trace_state_len=3,
expected_state_value=MOCK_W3C_TRACE_STATE_VALUE,
xray_traceid=MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED,
expected_baggage=MOCK_W3C_BAGGAGE_VALUE,
propagators="tracecontext,baggage",
),
]
for test in tests:
@ -284,7 +309,9 @@ class TestAwsLambdaInstrumentor(TestBase):
AwsLambdaInstrumentor().instrument(
event_context_extractor=test.custom_extractor,
)
mock_execute_lambda(test.context)
result = mock_execute_lambda(test.context)
result = json.loads(result)
spans = self.memory_exporter.get_finished_spans()
assert spans
self.assertEqual(len(spans), 1)
@ -305,6 +332,10 @@ class TestAwsLambdaInstrumentor(TestBase):
parent_context.trace_state.get(MOCK_W3C_TRACE_STATE_KEY),
test.expected_state_value,
)
self.assertEqual(
result["baggage_content"].get(MOCK_W3C_BAGGAGE_KEY),
test.expected_baggage,
)
self.assertTrue(parent_context.is_remote)
self.memory_exporter.clear()
AwsLambdaInstrumentor().uninstrument()