Skip to content

Commit 08a9c1a

Browse files
authored
Added new Lambda Multiple Span Flag (#334)
*Description of changes:* Added a new lambda span processor that 1. Overwrites the INTERNAL API Span (example: `opentelemetry.instrumentation.flask`) to SERVER 2. adds a new flag to Lambda Spans if there are multiple server spans in the trace. Also updated the Operation Name logic so that if the span is an `opentelemetry.instrumentation.flask` span, the operation name is the API Route instead of the FunctionHandler operation name. This makes it easier to identify that the Span is an API span in the Application Signals console. *Testing:* Added unit test to test processor. Also tested by creating a lambda layer. The lambda span does have the new flag and the operation name for the API Span is the API Route. 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 7aa1f25 commit 08a9c1a

File tree

6 files changed

+139
-2
lines changed

6 files changed

+139
-2
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
AWS_SDK_DESCENDANT: str = "aws.sdk.descendant"
1414
AWS_CONSUMER_PARENT_SPAN_KIND: str = "aws.consumer.parent.span.kind"
1515
AWS_TRACE_FLAG_SAMPLED: str = "aws.trace.flag.sampled"
16+
AWS_TRACE_LAMBDA_FLAG_MULTIPLE_SERVER: str = "aws.trace.lambda.multiple-server"
1617
AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER: str = "aws.remote.resource.cfn.primary.identifier"
1718

1819
# AWS_#_NAME attributes are not supported in python as they are not part of the Semantic Conventions.

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ def get_ingress_operation(__, span: ReadableSpan) -> str:
5757
with the first API path parameter" if the default span name is None, UnknownOperation or http.method value.
5858
"""
5959
operation: str = span.name
60-
if _AWS_LAMBDA_FUNCTION_NAME in os.environ:
60+
scope = getattr(span, "instrumentation_scope", None)
61+
if _AWS_LAMBDA_FUNCTION_NAME in os.environ and scope.name != "opentelemetry.instrumentation.flask":
6162
operation = os.environ.get(_AWS_LAMBDA_FUNCTION_NAME) + "/FunctionHandler"
6263
elif should_use_internal_operation(span):
6364
operation = INTERNAL_OPERATION
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from typing import Optional
4+
5+
from typing_extensions import override
6+
7+
from amazon.opentelemetry.distro._aws_attribute_keys import AWS_TRACE_LAMBDA_FLAG_MULTIPLE_SERVER
8+
from opentelemetry.context import Context, get_value
9+
from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
10+
from opentelemetry.trace import SpanKind
11+
from opentelemetry.trace.propagation import _SPAN_KEY
12+
13+
14+
class AwsLambdaSpanProcessor(SpanProcessor):
15+
def __init__(self, instrumentation_names=None):
16+
self.instrumentation_names = set(instrumentation_names or ["opentelemetry.instrumentation.flask"])
17+
18+
@override
19+
def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None:
20+
21+
scope = getattr(span, "instrumentation_scope", None)
22+
if scope.name in self.instrumentation_names:
23+
parent_span = get_value(_SPAN_KEY, context=parent_context)
24+
25+
if parent_span is None:
26+
return
27+
28+
parent_scope = getattr(parent_span, "instrumentation_scope", None)
29+
if parent_scope.name == "opentelemetry.instrumentation.aws_lambda":
30+
span._kind = SpanKind.SERVER
31+
parent_span.set_attribute(AWS_TRACE_LAMBDA_FLAG_MULTIPLE_SERVER, True)
32+
33+
return
34+
35+
# pylint: disable=no-self-use
36+
@override
37+
def on_end(self, span: ReadableSpan) -> None:
38+
return
39+
40+
@override
41+
def shutdown(self) -> None:
42+
self.force_flush()
43+
44+
# pylint: disable=no-self-use
45+
@override
46+
def force_flush(self, timeout_millis: int = None) -> bool:
47+
return True

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
AttributePropagatingSpanProcessorBuilder,
1717
)
1818
from amazon.opentelemetry.distro.aws_batch_unsampled_span_processor import BatchUnsampledSpanProcessor
19+
from amazon.opentelemetry.distro.aws_lambda_span_processor import AwsLambdaSpanProcessor
1920
from amazon.opentelemetry.distro.aws_metric_attributes_span_exporter_builder import (
2021
AwsMetricAttributesSpanExporterBuilder,
2122
)
@@ -343,6 +344,7 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource) ->
343344
# Export 100% spans and not export Application-Signals metrics if on Lambda.
344345
if _is_lambda_environment():
345346
_export_unsampled_span_for_lambda(provider, resource)
347+
provider.add_span_processor(AwsLambdaSpanProcessor())
346348
return
347349

348350
# Construct meterProvider
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from unittest import TestCase
4+
from unittest.mock import MagicMock, patch
5+
6+
from amazon.opentelemetry.distro._aws_attribute_keys import AWS_TRACE_LAMBDA_FLAG_MULTIPLE_SERVER
7+
from amazon.opentelemetry.distro.aws_lambda_span_processor import AwsLambdaSpanProcessor
8+
from opentelemetry.context import Context, set_value
9+
from opentelemetry.trace import Span, SpanContext, SpanKind
10+
from opentelemetry.trace.propagation import _SPAN_KEY
11+
12+
13+
class TestAwsLambdaSpanProcessor(TestCase):
14+
15+
def setUp(self):
16+
self.processor = AwsLambdaSpanProcessor()
17+
self.lambda_span: Span = MagicMock()
18+
self.lambda_span.instrumentation_scope.name = "opentelemetry.instrumentation.aws_lambda"
19+
self.lambda_span.kind = SpanKind.SERVER
20+
21+
self.lambda_span_context: SpanContext = MagicMock()
22+
self.lambda_span_context.trace_id = "ABC"
23+
self.lambda_span_context.span_id = "lambda_id"
24+
25+
self.lambda_context: Context = set_value(_SPAN_KEY, self.lambda_span)
26+
27+
self.lambda_span.get_span_context.return_value = self.lambda_span_context
28+
self.processor.on_start(self.lambda_span)
29+
30+
def tearDown(self):
31+
self.processor.on_end(self.lambda_span)
32+
self.processor.shutdown()
33+
34+
@patch("opentelemetry.sdk.trace.Span")
35+
def test_lambda_span_multiple_server_flag_internal_api(self, mock_span_class):
36+
37+
flask_span = mock_span_class.return_value
38+
flask_span.instrumentation_scope.name = "opentelemetry.instrumentation.flask"
39+
flask_span.kind = SpanKind.INTERNAL
40+
flask_span.parent = self.lambda_span_context
41+
42+
self.processor.on_start(flask_span, self.lambda_context)
43+
44+
self.assertEqual(flask_span._kind, SpanKind.SERVER)
45+
self.assertIn(AWS_TRACE_LAMBDA_FLAG_MULTIPLE_SERVER, self.lambda_span.set_attribute.call_args_list[0][0][0])
46+
47+
self.processor.on_end(flask_span)
48+
self.processor.on_end(self.lambda_span)
49+
50+
self.processor.shutdown()
51+
52+
@patch("opentelemetry.sdk.trace.Span")
53+
def test_lambda_span_multiple_server_flag_server_api(self, mock_span_class):
54+
55+
flask_span = mock_span_class.return_value
56+
flask_span.instrumentation_scope.name = "opentelemetry.instrumentation.flask"
57+
flask_span.kind = SpanKind.SERVER
58+
flask_span.parent = self.lambda_span_context
59+
60+
self.processor.on_start(flask_span, self.lambda_context)
61+
62+
self.assertEqual(flask_span.kind, SpanKind.SERVER)
63+
self.assertIn(AWS_TRACE_LAMBDA_FLAG_MULTIPLE_SERVER, self.lambda_span.set_attribute.call_args_list[0][0][0])
64+
65+
self.processor.on_end(flask_span)
66+
self.processor.on_end(self.lambda_span)
67+
68+
self.processor.shutdown()
69+
70+
@patch("opentelemetry.sdk.trace.Span")
71+
def test_lambda_span_single_server_span(self, mock_span_class):
72+
73+
flask_span = mock_span_class.return_value
74+
flask_span.instrumentation_scope.name = "opentelemetry.instrumentation.http"
75+
flask_span.kind = SpanKind.CLIENT
76+
flask_span.parent = self.lambda_span_context
77+
78+
self.processor.on_start(flask_span, self.lambda_context)
79+
80+
self.assertEqual(flask_span.kind, SpanKind.CLIENT)
81+
flask_span.set_attribute.assert_not_called()
82+
83+
self.processor.on_end(flask_span)
84+
self.processor.on_end(self.lambda_span)
85+
86+
self.processor.shutdown()

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ def test_customize_span_processors_lambda(self):
318318
os.environ.setdefault("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", "True")
319319
os.environ.setdefault("AWS_LAMBDA_FUNCTION_NAME", "myLambdaFunc")
320320
_customize_span_processors(mock_tracer_provider, Resource.get_empty())
321-
self.assertEqual(mock_tracer_provider.add_span_processor.call_count, 2)
321+
self.assertEqual(mock_tracer_provider.add_span_processor.call_count, 3)
322322
first_processor: SpanProcessor = mock_tracer_provider.add_span_processor.call_args_list[0].args[0]
323323
self.assertIsInstance(first_processor, AttributePropagatingSpanProcessor)
324324
second_processor: SpanProcessor = mock_tracer_provider.add_span_processor.call_args_list[1].args[0]

0 commit comments

Comments
 (0)