Skip to content

Commit f003a1f

Browse files
authored
Setup Tracer for AWS Lambda (#257)
*Issue #, if available:* When AppSignals is enabled: * disable AppSignals metrics * enable unsampled span processor and exporter When AppSignals is disabled: * still using UDP exporter by default *Description of changes:* 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 f4f4636 commit f003a1f

File tree

7 files changed

+90
-33
lines changed

7 files changed

+90
-33
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
LOCAL_ROOT: str = "LOCAL_ROOT"
2121

2222
# Useful constants
23+
_AWS_LAMBDA_FUNCTION_NAME: str = "AWS_LAMBDA_FUNCTION_NAME"
2324
_BOTO3SQS_INSTRUMENTATION_SCOPE: str = "opentelemetry.instrumentation.boto3sqs"
2425

2526
# Max keyword length supported by parsing into remote_operation from DB_STATEMENT
@@ -50,7 +51,9 @@ def get_ingress_operation(__, span: ReadableSpan) -> str:
5051
with the first API path parameter" if the default span name is None, UnknownOperation or http.method value.
5152
"""
5253
operation: str = span.name
53-
if should_use_internal_operation(span):
54+
if _AWS_LAMBDA_FUNCTION_NAME in os.environ:
55+
operation = os.environ.get(_AWS_LAMBDA_FUNCTION_NAME) + "/Handler"
56+
elif should_use_internal_operation(span):
5457
operation = INTERNAL_OPERATION
5558
elif not _is_valid_operation(span, operation):
5659
operation = _generate_ingress_operation(span)

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

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
AwsMetricAttributesSpanExporterBuilder,
1818
)
1919
from amazon.opentelemetry.distro.aws_span_metrics_processor_builder import AwsSpanMetricsProcessorBuilder
20-
from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpMetricExporter, OTLPUdpSpanExporter
20+
from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpSpanExporter
2121
from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler
2222
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as OTLPHttpOTLPMetricExporter
23+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
2324
from opentelemetry.sdk._configuration import (
2425
_get_exporter_names,
2526
_get_id_generator,
@@ -67,6 +68,9 @@
6768
AWS_LAMBDA_FUNCTION_NAME_CONFIG = "AWS_LAMBDA_FUNCTION_NAME"
6869
AWS_XRAY_DAEMON_ADDRESS_CONFIG = "AWS_XRAY_DAEMON_ADDRESS"
6970
OTEL_AWS_PYTHON_DEFER_TO_WORKERS_ENABLED_CONFIG = "OTEL_AWS_PYTHON_DEFER_TO_WORKERS_ENABLED"
71+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"
72+
# UDP package size is not larger than 64KB
73+
LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10
7074

7175
_logger: Logger = getLogger(__name__)
7276

@@ -112,13 +116,18 @@ def _initialize_components():
112116

113117
auto_resource: Dict[str, any] = {}
114118
auto_resource = _customize_versions(auto_resource)
115-
resource = get_aggregated_resources(
119+
120+
resource_detectors = (
116121
[
117122
AwsEc2ResourceDetector(),
118123
AwsEksResourceDetector(),
119124
AwsEcsResourceDetector(),
120125
]
121-
).merge(Resource.create(auto_resource))
126+
if not _is_lambda_environment()
127+
else []
128+
)
129+
130+
resource = get_aggregated_resources(resource_detectors).merge(Resource.create(auto_resource))
122131

123132
sampler_name = _get_sampler()
124133
sampler = _custom_import_sampler(sampler_name, resource)
@@ -153,7 +162,9 @@ def _init_tracing(
153162
exporter_args: Dict[str, any] = {}
154163
span_exporter: SpanExporter = exporter_class(**exporter_args)
155164
span_exporter = _customize_exporter(span_exporter, resource)
156-
trace_provider.add_span_processor(BatchSpanProcessor(span_exporter))
165+
trace_provider.add_span_processor(
166+
BatchSpanProcessor(span_exporter=span_exporter, max_export_batch_size=_span_export_batch_size())
167+
)
157168

158169
_customize_span_processors(trace_provider, resource)
159170

@@ -175,7 +186,9 @@ def _export_unsampled_span_for_lambda(trace_provider: TracerProvider, resource:
175186
OTLPUdpSpanExporter(endpoint=traces_endpoint, sampled=False), resource
176187
).build()
177188

178-
trace_provider.add_span_processor(BatchUnsampledSpanProcessor(span_exporter))
189+
trace_provider.add_span_processor(
190+
BatchUnsampledSpanProcessor(span_exporter=span_exporter, max_export_batch_size=LAMBDA_SPAN_EXPORT_BATCH_SIZE)
191+
)
179192

180193

181194
def _is_defer_to_workers_enabled():
@@ -263,23 +276,30 @@ def _customize_sampler(sampler: Sampler) -> Sampler:
263276

264277

265278
def _customize_exporter(span_exporter: SpanExporter, resource: Resource) -> SpanExporter:
279+
if _is_lambda_environment():
280+
# Override OTLP http default endpoint to UDP
281+
if isinstance(span_exporter, OTLPSpanExporter) and os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) is None:
282+
traces_endpoint = os.environ.get(AWS_XRAY_DAEMON_ADDRESS_CONFIG, "127.0.0.1:2000")
283+
span_exporter = OTLPUdpSpanExporter(endpoint=traces_endpoint)
284+
266285
if not _is_application_signals_enabled():
267286
return span_exporter
268-
if _is_lambda_environment():
269-
traces_endpoint = os.environ.get(AWS_XRAY_DAEMON_ADDRESS_CONFIG, "127.0.0.1:2000")
270-
return AwsMetricAttributesSpanExporterBuilder(OTLPUdpSpanExporter(endpoint=traces_endpoint), resource).build()
287+
271288
return AwsMetricAttributesSpanExporterBuilder(span_exporter, resource).build()
272289

273290

274291
def _customize_span_processors(provider: TracerProvider, resource: Resource) -> None:
275292
if not _is_application_signals_enabled():
276293
return
277294

278-
_export_unsampled_span_for_lambda(provider, resource)
279-
280295
# Construct and set local and remote attributes span processor
281296
provider.add_span_processor(AttributePropagatingSpanProcessorBuilder().build())
282297

298+
# Export 100% spans and not export Application-Signals metrics if on Lambda.
299+
if _is_lambda_environment():
300+
_export_unsampled_span_for_lambda(provider, resource)
301+
return
302+
283303
# Construct meterProvider
284304
_logger.info("AWS Application Signals enabled")
285305
otel_metric_exporter = ApplicationSignalsExporterProvider().create_exporter()
@@ -318,6 +338,10 @@ def _is_lambda_environment():
318338
return AWS_LAMBDA_FUNCTION_NAME_CONFIG in os.environ
319339

320340

341+
def _span_export_batch_size():
342+
return LAMBDA_SPAN_EXPORT_BATCH_SIZE if _is_lambda_environment() else None
343+
344+
321345
class ApplicationSignalsExporterProvider:
322346
_instance: ClassVar["ApplicationSignalsExporterProvider"] = None
323347

@@ -345,11 +369,6 @@ def create_exporter(self):
345369
]:
346370
temporality_dict[typ] = AggregationTemporality.DELTA
347371

348-
if _is_lambda_environment():
349-
# When running in Lambda, export Application Signals metrics over UDP
350-
application_signals_endpoint = os.environ.get(AWS_XRAY_DAEMON_ADDRESS_CONFIG, "127.0.0.1:2000")
351-
return OTLPUdpMetricExporter(endpoint=application_signals_endpoint, preferred_temporality=temporality_dict)
352-
353372
if protocol == "http/protobuf":
354373
application_signals_endpoint = os.environ.get(
355374
APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG,

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@
2121
PROTOCOL_HEADER = '{"format":"json","version":1}\n'
2222
FORMAT_OTEL_METRICS_BINARY_PREFIX = "M1"
2323

24-
# TODO: update sampled and unsampled prefix later
25-
FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX = "T1"
26-
FORMAT_OTEL_UNSAMPLED_TRACES_BINARY_PREFIX = "T1"
24+
FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX = "T1S"
25+
FORMAT_OTEL_UNSAMPLED_TRACES_BINARY_PREFIX = "T1U"
2726

2827
_logger: Logger = getLogger(__name__)
2928

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

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from amazon.opentelemetry.distro.aws_batch_unsampled_span_processor import BatchUnsampledSpanProcessor
1111
from amazon.opentelemetry.distro.aws_metric_attributes_span_exporter import AwsMetricAttributesSpanExporter
1212
from amazon.opentelemetry.distro.aws_opentelemetry_configurator import (
13+
LAMBDA_SPAN_EXPORT_BATCH_SIZE,
1314
ApplicationSignalsExporterProvider,
1415
AwsOpenTelemetryConfigurator,
1516
_custom_import_sampler,
@@ -23,15 +24,15 @@
2324
)
2425
from amazon.opentelemetry.distro.aws_opentelemetry_distro import AwsOpenTelemetryDistro
2526
from amazon.opentelemetry.distro.aws_span_metrics_processor import AwsSpanMetricsProcessor
26-
from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpMetricExporter, OTLPUdpSpanExporter
27+
from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpSpanExporter
2728
from amazon.opentelemetry.distro.sampler._aws_xray_sampling_client import _AwsXRaySamplingClient
2829
from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler
2930
from opentelemetry.environment_variables import OTEL_LOGS_EXPORTER, OTEL_METRICS_EXPORTER, OTEL_TRACES_EXPORTER
3031
from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import OTLPMetricExporterMixin
3132
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter as OTLPGrpcOTLPMetricExporter
3233
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as OTLPHttpOTLPMetricExporter
34+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
3335
from opentelemetry.sdk.environment_variables import OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG
34-
from opentelemetry.sdk.metrics._internal.export import MetricExporter
3536
from opentelemetry.sdk.resources import Resource
3637
from opentelemetry.sdk.trace import Span, SpanProcessor, Tracer, TracerProvider
3738
from opentelemetry.sdk.trace.export import SpanExporter
@@ -250,7 +251,7 @@ def test_customize_sampler(self):
250251
os.environ.pop("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", None)
251252

252253
def test_customize_exporter(self):
253-
mock_exporter: SpanExporter = MagicMock()
254+
mock_exporter: SpanExporter = MagicMock(spec=OTLPSpanExporter)
254255
customized_exporter: SpanExporter = _customize_exporter(mock_exporter, Resource.get_empty())
255256
self.assertEqual(mock_exporter, customized_exporter)
256257

@@ -285,6 +286,23 @@ def test_customize_span_processors(self):
285286
self.assertIsInstance(second_processor, AwsSpanMetricsProcessor)
286287
os.environ.pop("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", None)
287288

289+
def test_customize_span_processors_lambda(self):
290+
mock_tracer_provider: TracerProvider = MagicMock()
291+
_customize_span_processors(mock_tracer_provider, Resource.get_empty())
292+
self.assertEqual(mock_tracer_provider.add_span_processor.call_count, 0)
293+
294+
os.environ.setdefault("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", "True")
295+
os.environ.setdefault("AWS_LAMBDA_FUNCTION_NAME", "myLambdaFunc")
296+
_customize_span_processors(mock_tracer_provider, Resource.get_empty())
297+
self.assertEqual(mock_tracer_provider.add_span_processor.call_count, 2)
298+
first_processor: SpanProcessor = mock_tracer_provider.add_span_processor.call_args_list[0].args[0]
299+
self.assertIsInstance(first_processor, AttributePropagatingSpanProcessor)
300+
second_processor: SpanProcessor = mock_tracer_provider.add_span_processor.call_args_list[1].args[0]
301+
self.assertIsInstance(second_processor, BatchUnsampledSpanProcessor)
302+
self.assertEqual(second_processor.max_export_batch_size, LAMBDA_SPAN_EXPORT_BATCH_SIZE)
303+
os.environ.pop("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", None)
304+
os.environ.pop("AWS_LAMBDA_FUNCTION_NAME", None)
305+
288306
def test_application_signals_exporter_provider(self):
289307
# Check default protocol - HTTP, as specified by AwsOpenTelemetryDistro.
290308
exporter: OTLPMetricExporterMixin = ApplicationSignalsExporterProvider().create_exporter()
@@ -303,13 +321,6 @@ def test_application_signals_exporter_provider(self):
303321
self.assertIsInstance(exporter, OTLPHttpOTLPMetricExporter)
304322
self.assertEqual("http://localhost:4316/v1/metrics", exporter._endpoint)
305323

306-
# When in Lambda, exporter should be UDP.
307-
os.environ.setdefault("AWS_LAMBDA_FUNCTION_NAME", "myLambdaFunc")
308-
exporter: MetricExporter = ApplicationSignalsExporterProvider().create_exporter()
309-
self.assertIsInstance(exporter, OTLPUdpMetricExporter)
310-
self.assertEqual("127.0.0.1:2000", exporter._udp_exporter._endpoint)
311-
os.environ.pop("AWS_LAMBDA_FUNCTION_NAME", None)
312-
313324
def test_is_defer_to_workers_enabled(self):
314325
os.environ.setdefault("OTEL_AWS_PYTHON_DEFER_TO_WORKERS_ENABLED", "True")
315326
self.assertTrue(_is_defer_to_workers_enabled())
@@ -366,6 +377,7 @@ def test_export_unsampled_span_for_lambda(self):
366377
first_processor: SpanProcessor = mock_tracer_provider.add_span_processor.call_args_list[0].args[0]
367378
self.assertIsInstance(first_processor, BatchUnsampledSpanProcessor)
368379
os.environ.pop("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", None)
380+
os.environ.pop("AWS_LAMBDA_FUNCTION_NAME", None)
369381

370382

371383
def validate_distro_environ():

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
3+
import os
34
from typing import List
45
from unittest import TestCase
5-
from unittest.mock import MagicMock
6+
from unittest.mock import MagicMock, patch
67

78
from amazon.opentelemetry.distro._aws_attribute_keys import AWS_CONSUMER_PARENT_SPAN_KIND, AWS_LOCAL_OPERATION
89
from amazon.opentelemetry.distro._aws_span_processing_util import (
10+
_AWS_LAMBDA_FUNCTION_NAME,
911
MAX_KEYWORD_LENGTH,
1012
_get_dialect_keywords,
1113
extract_api_path_value,
@@ -54,6 +56,14 @@ def test_get_ingress_operation_with_not_server(self):
5456
actual_operation: str = get_ingress_operation(self, self.span_data_mock)
5557
self.assertEqual(actual_operation, _INTERNAL_OPERATION)
5658

59+
@patch.dict(os.environ, {_AWS_LAMBDA_FUNCTION_NAME: "MyLambda"})
60+
def test_get_ingress_operation_in_lambda(self):
61+
valid_name: str = "ValidName"
62+
self.span_data_mock.name = valid_name
63+
self.span_data_mock.kind = SpanKind.SERVER
64+
actual_operation: str = get_ingress_operation(self, self.span_data_mock)
65+
self.assertEqual(actual_operation, "MyLambda/Handler")
66+
5767
def test_get_ingress_operation_http_method_name_and_no_fallback(self):
5868
invalid_name: str = "GET"
5969
self.span_data_mock.name = invalid_name

lambda-layer/src/otel-instrument

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,27 @@ fi
108108

109109
export LAMBDA_RESOURCE_ATTRIBUTES="cloud.region=$AWS_REGION,cloud.provider=aws,faas.name=$AWS_LAMBDA_FUNCTION_NAME,faas.version=$AWS_LAMBDA_FUNCTION_VERSION,faas.instance=$AWS_LAMBDA_LOG_STREAM_NAME,aws.log.group.names=$AWS_LAMBDA_LOG_GROUP_NAME";
110110

111+
112+
if [ -z "${OTEL_AWS_APPLICATION_SIGNALS_ENABLED}" ]; then
113+
export OTEL_AWS_APPLICATION_SIGNALS_ENABLED="true";
114+
fi
115+
111116
# - If Application Signals is enabled
112117

113118
if [ "${OTEL_AWS_APPLICATION_SIGNALS_ENABLED}" = "true" ]; then
114119
export OTEL_PYTHON_DISTRO="aws_distro";
115120
export OTEL_PYTHON_CONFIGURATOR="aws_configurator";
116-
export OTEL_METRICS_EXPORTER="none";
117-
export OTEL_LOGS_EXPORTER="none";
121+
if [ -z "${OTEL_METRICS_EXPORTER}" ]; then
122+
export OTEL_METRICS_EXPORTER="none";
123+
fi
124+
fi
125+
126+
# - If Application Signals is disabled
127+
128+
if [ "${OTEL_AWS_APPLICATION_SIGNALS_ENABLED}" = "false" ]; then
129+
if [ -z "${OTEL_METRICS_EXPORTER}" ]; then
130+
export OTEL_METRICS_EXPORTER="none";
131+
fi
118132
fi
119133

120134
if [ -z "${OTEL_RESOURCE_ATTRIBUTES}" ]; then
@@ -128,6 +142,7 @@ fi
128142
if [ -z ${OTEL_PYTHON_DISABLED_INSTRUMENTATIONS} ]; then
129143
export OTEL_PYTHON_DISABLED_INSTRUMENTATIONS="aio-pika,aiohttp-client,aiohttp-server,aiopg,asgi,asyncio,asyncpg,boto,boto3,cassandra,celery,confluent_kafka,dbapi,django,elasticsearch,falcon,fastapi,flask,grpc_client,grpc_server,grpc_aio_client,grpc_aio_server,httpx,jinja2,kafka,logging,mysql,mysqlclient,pika,psycopg,psycopg2,pymemcache,pymongo,pymysql,pyramid,redis,remoulade,requests,sklearn,sqlalchemy,sqlite3,starlette,system_metrics,threading,tornado,tortoiseorm,urllib,urllib3,wsgi"
130144
fi
145+
export OTEL_PYTHON_DISABLED_INSTRUMENTATIONS="$OTEL_PYTHON_DISABLED_INSTRUMENTATIONS,aws-lambda";
131146

132147
# - Use a wrapper because AWS Lambda's `python3 /var/runtime/bootstrap.py` will
133148
# use `imp.load_module` to load the function from the `_HANDLER` environment

lambda-layer/terraform/lambda/main.tf

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ module "test-function" {
2828

2929
environment_variables = {
3030
AWS_LAMBDA_EXEC_WRAPPER = "/opt/otel-instrument"
31-
OTEL_AWS_APPLICATION_SIGNALS_ENABLED = "true"
3231
}
3332

3433
tracing_mode = var.tracing_mode

0 commit comments

Comments
 (0)