Skip to content

Commit 1d0aedd

Browse files
authored
Add OTLP UDP exporter (#232)
#### Description Implementing a UDP exporter for OTLP spans and metrics. Few notes: - If AppSignals is enabled in Lambda, the OTel spans and Application Signals metrics are always exported via this UdpExporter. Currently it can't be configured to use any other exporter. - Custom OTel metrics can still be exported (if enabled and requires using ADOT Collector layer) via user-specified exporter. - OTel spans and Appsignals metrics are first protobuf encoded, then base64 encoded, then prefixed with `T1` or `M1` respectively, and finally appended to the `'{"format":"json","version":1}\n'` string before being exported to the address provided in the `AWS_XRAY_DAEMON_ADDRESS` or default `127.0.0.1:2000`. #### Testing - Test script `test_lambda.py` to generate 2 nested spans: ``` python if __name__ == "__main__": tracer = trace.get_tracer("test-tracer") with tracer.start_as_current_span("parent"): # Attach a new child and update the current span with tracer.start_as_current_span("child"): # sleep for 1s sleep(1) # Close child span, set parent as current # Close parent span, set default span as current ``` - Command to run the script with auto-instrumentation: ```shell OTEL_TRACES_EXPORTER=otlp \ OTEL_METRICS_EXPORTER=none \ OTEL_LOGS_EXPORTER=none \ OTEL_AWS_APPLICATION_SIGNALS_ENABLED=true \ OTEL_PYTHON_DISTRO=aws_distro \ OTEL_PYTHON_CONFIGURATOR=aws_configurator \ OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \ # this config will be ignored since appsignals is enabled in lambda OTEL_TRACES_SAMPLER=always_on \ OTEL_RESOURCE_ATTRIBUTES="service.name=lambda_test" \ AWS_LAMBDA_FUNCTION_NAME="lambda_func_name" \ opentelemetry-instrument python test_lambda.py ``` - Printed spans in JSON format Raw spans: ```json { "name": "child", "context": { "trace_id": "0x66bbf01064511ad55e9fe435a1326167", "span_id": "0x0915f345ead6d194", "trace_state": "[]" }, "kind": "SpanKind.INTERNAL", "parent_id": "0xbd128e94b0caa171", "start_time": "2024-08-13T23:45:20.930139Z", "end_time": "2024-08-13T23:45:21.935234Z", "status": { "status_code": "UNSET" }, "attributes": { "aws.local.operation": "InternalOperation" }, "events": [], "links": [], "resource": { "attributes": { "telemetry.sdk.language": "python", "telemetry.sdk.name": "opentelemetry", "telemetry.sdk.version": "1.25.0", "service.name": "lambda_test", "telemetry.auto.version": "0.3.0.dev0-aws" }, "schema_url": "" } } { "name": "parent", "context": { "trace_id": "0x66bbf01064511ad55e9fe435a1326167", "span_id": "0xbd128e94b0caa171", "trace_state": "[]" }, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": "2024-08-13T23:45:20.930107Z", "end_time": "2024-08-13T23:45:21.935353Z", "status": { "status_code": "UNSET" }, "attributes": { "aws.local.operation": "InternalOperation", "aws.local.service": "lambda_test", "aws.span.kind": "LOCAL_ROOT" }, "events": [], "links": [], "resource": { "attributes": { "telemetry.sdk.language": "python", "telemetry.sdk.name": "opentelemetry", "telemetry.sdk.version": "1.25.0", "service.name": "lambda_test", "telemetry.auto.version": "0.3.0.dev0-aws" }, "schema_url": "" } } ``` Encoded trace data from SDK: ``` "T1CvcDCrkBCiIKFnRlbGVtZXRyeS5zZGsubGFuZ3VhZ2USCAoGcHl0aG9uCiUKEnRlbGVtZXRyeS5zZGsubmFtZRIPCg1vcGVudGVsZW1ldHJ5CiEKFXRlbGVtZXRyeS5zZGsudmVyc2lvbhIICgYxLjI1LjAKHQoMc2VydmljZS5uYW1lEg0KC2xhbWJkYV90ZXN0CioKFnRlbGVtZXRyeS5hdXRvLnZlcnNpb24SEAoOMC4zLjAuZGV2MC1hd3MSuAIKDQoLdGVzdC10cmFjZXISdQoQZrvwEGRRGtVen+Q1oTJhZxIICRXzRerW0ZQiCL0SjpSwyqFxKgVjaGlsZDABOXhrfZxybusXQdDzZdhybusXSioKE2F3cy5sb2NhbC5vcGVyYXRpb24SEwoRSW50ZXJuYWxPcGVyYXRpb256AIUBAAEAABKvAQoQZrvwEGRRGtVen+Q1oTJhZxIIvRKOlLDKoXEqBnBhcmVudDABOXjufJxybusXQajEZ9hybusXSioKE2F3cy5sb2NhbC5vcGVyYXRpb24SEwoRSW50ZXJuYWxPcGVyYXRpb25KIgoRYXdzLmxvY2FsLnNlcnZpY2USDQoLbGFtYmRhX3Rlc3RKHQoNYXdzLnNwYW4ua2luZBIMCgpMT0NBTF9ST09UegCFAQABAAA=" ``` 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 1304b13 commit 1d0aedd

File tree

4 files changed

+284
-0
lines changed

4 files changed

+284
-0
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
AwsMetricAttributesSpanExporterBuilder,
1717
)
1818
from amazon.opentelemetry.distro.aws_span_metrics_processor_builder import AwsSpanMetricsProcessorBuilder
19+
from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpMetricExporter, OTLPUdpSpanExporter
1920
from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler
2021
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as OTLPHttpOTLPMetricExporter
2122
from opentelemetry.sdk._configuration import (
@@ -62,6 +63,8 @@
6263
APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT"
6364
METRIC_EXPORT_INTERVAL_CONFIG = "OTEL_METRIC_EXPORT_INTERVAL"
6465
DEFAULT_METRIC_EXPORT_INTERVAL = 60000.0
66+
AWS_LAMBDA_FUNCTION_NAME_CONFIG = "AWS_LAMBDA_FUNCTION_NAME"
67+
AWS_XRAY_DAEMON_ADDRESS_CONFIG = "AWS_XRAY_DAEMON_ADDRESS"
6568

6669
_logger: Logger = getLogger(__name__)
6770

@@ -219,6 +222,8 @@ def _customize_sampler(sampler: Sampler) -> Sampler:
219222
def _customize_exporter(span_exporter: SpanExporter, resource: Resource) -> SpanExporter:
220223
if not _is_application_signals_enabled():
221224
return span_exporter
225+
if _is_lambda_environment():
226+
return AwsMetricAttributesSpanExporterBuilder(OTLPUdpSpanExporter(), resource).build()
222227
return AwsMetricAttributesSpanExporterBuilder(span_exporter, resource).build()
223228

224229

@@ -262,6 +267,11 @@ def _is_application_signals_enabled():
262267
)
263268

264269

270+
def _is_lambda_environment():
271+
# detect if running in AWS Lambda environment
272+
return AWS_LAMBDA_FUNCTION_NAME_CONFIG in os.environ
273+
274+
265275
class ApplicationSignalsExporterProvider:
266276
_instance: ClassVar["ApplicationSignalsExporterProvider"] = None
267277

@@ -289,6 +299,11 @@ def create_exporter(self):
289299
]:
290300
temporality_dict[typ] = AggregationTemporality.DELTA
291301

302+
if _is_lambda_environment():
303+
# When running in Lambda, export Application Signals metrics over UDP
304+
application_signals_endpoint = os.environ.get(AWS_XRAY_DAEMON_ADDRESS_CONFIG, "127.0.0.1:2000")
305+
return OTLPUdpMetricExporter(endpoint=application_signals_endpoint, preferred_temporality=temporality_dict)
306+
292307
if protocol == "http/protobuf":
293308
application_signals_endpoint = os.environ.get(
294309
APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG,
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
import base64
4+
import socket
5+
from logging import Logger, getLogger
6+
from typing import Dict, Optional, Sequence, Tuple
7+
8+
from typing_extensions import override
9+
10+
from opentelemetry.exporter.otlp.proto.common.metrics_encoder import encode_metrics
11+
from opentelemetry.exporter.otlp.proto.common.trace_encoder import encode_spans
12+
from opentelemetry.sdk.metrics._internal.aggregation import AggregationTemporality
13+
from opentelemetry.sdk.metrics._internal.export import MetricExportResult
14+
from opentelemetry.sdk.metrics._internal.point import MetricsData
15+
from opentelemetry.sdk.metrics.export import MetricExporter
16+
from opentelemetry.sdk.metrics.view import Aggregation
17+
from opentelemetry.sdk.trace import ReadableSpan
18+
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
19+
20+
DEFAULT_ENDPOINT = "127.0.0.1:2000"
21+
PROTOCOL_HEADER = '{"format":"json","version":1}\n'
22+
FORMAT_OTEL_METRICS_BINARY_PREFIX = "M1"
23+
FORMAT_OTEL_TRACES_BINARY_PREFIX = "T1"
24+
25+
_logger: Logger = getLogger(__name__)
26+
27+
28+
class UdpExporter:
29+
def __init__(self, endpoint: Optional[str] = None):
30+
self._endpoint = endpoint or DEFAULT_ENDPOINT
31+
self._host, self._port = self._parse_endpoint(self._endpoint)
32+
self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
33+
self._socket.setblocking(False)
34+
35+
def send_data(self, data: bytes, signal_format_prefix: str):
36+
# base64 encoding and then converting to string with utf-8
37+
base64_encoded_string: str = base64.b64encode(data).decode("utf-8")
38+
message = f"{PROTOCOL_HEADER}{signal_format_prefix}{base64_encoded_string}"
39+
40+
try:
41+
_logger.debug("Sending UDP data: %s", message)
42+
self._socket.sendto(message.encode("utf-8"), (self._host, int(self._port)))
43+
except Exception as exc: # pylint: disable=broad-except
44+
_logger.error("Error sending UDP data: %s", exc)
45+
raise
46+
47+
def shutdown(self):
48+
self._socket.close()
49+
50+
# pylint: disable=no-self-use
51+
def _parse_endpoint(self, endpoint: str) -> Tuple[str, int]:
52+
try:
53+
vals = endpoint.split(":")
54+
host = vals[0]
55+
port = int(vals[1])
56+
except Exception as exc: # pylint: disable=broad-except
57+
raise ValueError(f"Invalid endpoint: {endpoint}") from exc
58+
59+
return host, port
60+
61+
62+
class OTLPUdpMetricExporter(MetricExporter):
63+
def __init__(
64+
self,
65+
endpoint: Optional[str] = None,
66+
preferred_temporality: Dict[type, AggregationTemporality] = None,
67+
preferred_aggregation: Dict[type, Aggregation] = None,
68+
):
69+
super().__init__(
70+
preferred_temporality=preferred_temporality,
71+
preferred_aggregation=preferred_aggregation,
72+
)
73+
self._udp_exporter = UdpExporter(endpoint=endpoint)
74+
75+
@override
76+
def export(
77+
self,
78+
metrics_data: MetricsData,
79+
timeout_millis: float = 10_000,
80+
**kwargs,
81+
) -> MetricExportResult:
82+
serialized_data = encode_metrics(metrics_data).SerializeToString()
83+
84+
try:
85+
self._udp_exporter.send_data(data=serialized_data, signal_format_prefix=FORMAT_OTEL_METRICS_BINARY_PREFIX)
86+
return MetricExportResult.SUCCESS
87+
except Exception as exc: # pylint: disable=broad-except
88+
_logger.error("Error exporting metrics: %s", exc)
89+
return MetricExportResult.FAILURE
90+
91+
# pylint: disable=no-self-use
92+
def force_flush(self, timeout_millis: float = 10_000) -> bool:
93+
# TODO: implement force flush
94+
return True
95+
96+
def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:
97+
self._udp_exporter.shutdown()
98+
99+
100+
class OTLPUdpSpanExporter(SpanExporter):
101+
def __init__(self, endpoint: Optional[str] = None):
102+
self._udp_exporter = UdpExporter(endpoint=endpoint)
103+
104+
@override
105+
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
106+
serialized_data = encode_spans(spans).SerializeToString()
107+
108+
try:
109+
self._udp_exporter.send_data(data=serialized_data, signal_format_prefix=FORMAT_OTEL_TRACES_BINARY_PREFIX)
110+
return SpanExportResult.SUCCESS
111+
except Exception as exc: # pylint: disable=broad-except
112+
_logger.error("Error exporting spans: %s", exc)
113+
return SpanExportResult.FAILURE
114+
115+
# pylint: disable=no-self-use
116+
@override
117+
def force_flush(self, timeout_millis: int = 30000) -> bool:
118+
# TODO: implement force flush
119+
return True
120+
121+
@override
122+
def shutdown(self) -> None:
123+
self._udp_exporter.shutdown()

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
)
2020
from amazon.opentelemetry.distro.aws_opentelemetry_distro import AwsOpenTelemetryDistro
2121
from amazon.opentelemetry.distro.aws_span_metrics_processor import AwsSpanMetricsProcessor
22+
from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpMetricExporter, OTLPUdpSpanExporter
2223
from amazon.opentelemetry.distro.sampler._aws_xray_sampling_client import _AwsXRaySamplingClient
2324
from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler
2425
from opentelemetry.environment_variables import OTEL_LOGS_EXPORTER, OTEL_METRICS_EXPORTER, OTEL_TRACES_EXPORTER
2526
from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import OTLPMetricExporterMixin
2627
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter as OTLPGrpcOTLPMetricExporter
2728
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as OTLPHttpOTLPMetricExporter
2829
from opentelemetry.sdk.environment_variables import OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG
30+
from opentelemetry.sdk.metrics._internal.export import MetricExporter
2931
from opentelemetry.sdk.resources import Resource
3032
from opentelemetry.sdk.trace import Span, SpanProcessor, Tracer, TracerProvider
3133
from opentelemetry.sdk.trace.export import SpanExporter
@@ -254,6 +256,16 @@ def test_customize_exporter(self):
254256
self.assertEqual(mock_exporter, customized_exporter._delegate)
255257
os.environ.pop("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", None)
256258

259+
# when Application Signals is enabled and running in lambda
260+
os.environ.setdefault("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", "True")
261+
os.environ.setdefault("AWS_LAMBDA_FUNCTION_NAME", "myLambdaFunc")
262+
customized_exporter = _customize_exporter(mock_exporter, Resource.get_empty())
263+
self.assertNotEqual(mock_exporter, customized_exporter)
264+
self.assertIsInstance(customized_exporter, AwsMetricAttributesSpanExporter)
265+
self.assertIsInstance(customized_exporter._delegate, OTLPUdpSpanExporter)
266+
os.environ.pop("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", None)
267+
os.environ.pop("AWS_LAMBDA_FUNCTION_NAME", None)
268+
257269
def test_customize_span_processors(self):
258270
mock_tracer_provider: TracerProvider = MagicMock()
259271
_customize_span_processors(mock_tracer_provider, Resource.get_empty())
@@ -286,6 +298,13 @@ def test_application_signals_exporter_provider(self):
286298
self.assertIsInstance(exporter, OTLPHttpOTLPMetricExporter)
287299
self.assertEqual("http://localhost:4316/v1/metrics", exporter._endpoint)
288300

301+
# When in Lambda, exporter should be UDP.
302+
os.environ.setdefault("AWS_LAMBDA_FUNCTION_NAME", "myLambdaFunc")
303+
exporter: MetricExporter = ApplicationSignalsExporterProvider().create_exporter()
304+
self.assertIsInstance(exporter, OTLPUdpMetricExporter)
305+
self.assertEqual("127.0.0.1:2000", exporter._udp_exporter._endpoint)
306+
os.environ.pop("AWS_LAMBDA_FUNCTION_NAME", None)
307+
289308

290309
def validate_distro_environ():
291310
tc: TestCase = TestCase()
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
import base64
4+
import socket
5+
import unittest
6+
from unittest import TestCase
7+
from unittest.mock import MagicMock, patch
8+
9+
from amazon.opentelemetry.distro.otlp_udp_exporter import (
10+
DEFAULT_ENDPOINT,
11+
PROTOCOL_HEADER,
12+
OTLPUdpMetricExporter,
13+
OTLPUdpSpanExporter,
14+
UdpExporter,
15+
)
16+
from opentelemetry.sdk.metrics._internal.export import MetricExportResult
17+
from opentelemetry.sdk.trace.export import SpanExportResult
18+
19+
20+
class TestUdpExporter(TestCase):
21+
22+
@patch("amazon.opentelemetry.distro.otlp_udp_exporter.socket.socket")
23+
def test_udp_exporter_init_default(self, mock_socket):
24+
exporter = UdpExporter()
25+
self.assertEqual(exporter._endpoint, DEFAULT_ENDPOINT)
26+
self.assertEqual(exporter._host, "127.0.0.1")
27+
self.assertEqual(exporter._port, 2000)
28+
mock_socket.assert_called_once_with(socket.AF_INET, socket.SOCK_DGRAM)
29+
mock_socket().setblocking.assert_called_once_with(False)
30+
31+
@patch("amazon.opentelemetry.distro.otlp_udp_exporter.socket.socket")
32+
def test_udp_exporter_init_with_endpoint(self, mock_socket):
33+
exporter = UdpExporter(endpoint="localhost:5000")
34+
self.assertNotEqual(exporter._endpoint, DEFAULT_ENDPOINT)
35+
self.assertEqual(exporter._host, "localhost")
36+
self.assertEqual(exporter._port, 5000)
37+
mock_socket.assert_called_once_with(socket.AF_INET, socket.SOCK_DGRAM)
38+
mock_socket().setblocking.assert_called_once_with(False)
39+
40+
@patch("amazon.opentelemetry.distro.otlp_udp_exporter.socket.socket")
41+
def test_udp_exporter_init_invalid_endpoint(self, mock_socket):
42+
with self.assertRaises(ValueError):
43+
UdpExporter(endpoint="invalidEndpoint:port")
44+
45+
# pylint: disable=no-self-use
46+
@patch("amazon.opentelemetry.distro.otlp_udp_exporter.socket.socket")
47+
def test_send_data(self, mock_socket):
48+
mock_socket_instance = mock_socket.return_value
49+
exporter = UdpExporter()
50+
input_bytes: bytes = b"hello"
51+
encoded_bytes: bytes = base64.b64encode(input_bytes)
52+
exporter.send_data(input_bytes, "signal_prefix")
53+
expected_message = PROTOCOL_HEADER + "signal_prefix" + encoded_bytes.decode("utf-8")
54+
mock_socket_instance.sendto.assert_called_once_with(expected_message.encode("utf-8"), ("127.0.0.1", 2000))
55+
56+
@patch("amazon.opentelemetry.distro.otlp_udp_exporter.socket.socket")
57+
def test_shutdown(self, mock_socket):
58+
mock_socket_instance = mock_socket.return_value
59+
exporter = UdpExporter()
60+
exporter.shutdown()
61+
mock_socket_instance.close.assert_called_once()
62+
63+
64+
class TestOTLPUdpMetricExporter(unittest.TestCase):
65+
66+
@patch("amazon.opentelemetry.distro.otlp_udp_exporter.encode_metrics")
67+
@patch("amazon.opentelemetry.distro.otlp_udp_exporter.UdpExporter")
68+
def test_export(self, mock_udp_exporter, mock_encode_metrics):
69+
mock_udp_exporter_instance = mock_udp_exporter.return_value
70+
mock_encoded_data = MagicMock()
71+
mock_encode_metrics.return_value.SerializeToString.return_value = mock_encoded_data
72+
exporter = OTLPUdpMetricExporter()
73+
result = exporter.export(MagicMock())
74+
mock_udp_exporter_instance.send_data.assert_called_once_with(data=mock_encoded_data, signal_format_prefix="M1")
75+
self.assertEqual(result, MetricExportResult.SUCCESS)
76+
77+
@patch("amazon.opentelemetry.distro.otlp_udp_exporter.encode_metrics")
78+
@patch("amazon.opentelemetry.distro.otlp_udp_exporter.UdpExporter")
79+
def test_export_with_exception(self, mock_udp_exporter, mock_encode_metrics):
80+
mock_udp_exporter_instance = mock_udp_exporter.return_value
81+
mock_encoded_data = MagicMock()
82+
mock_encode_metrics.return_value.SerializeToString.return_value = mock_encoded_data
83+
mock_udp_exporter_instance.send_data.side_effect = Exception("Something went wrong")
84+
exporter = OTLPUdpMetricExporter()
85+
result = exporter.export(MagicMock())
86+
self.assertEqual(result, MetricExportResult.FAILURE)
87+
88+
# pylint: disable=no-self-use
89+
@patch("amazon.opentelemetry.distro.otlp_udp_exporter.UdpExporter")
90+
def test_shutdown(self, mock_udp_exporter):
91+
mock_udp_exporter_instance = mock_udp_exporter.return_value
92+
exporter = OTLPUdpMetricExporter()
93+
exporter.shutdown()
94+
mock_udp_exporter_instance.shutdown.assert_called_once()
95+
96+
97+
class TestOTLPUdpSpanExporter(unittest.TestCase):
98+
99+
@patch("amazon.opentelemetry.distro.otlp_udp_exporter.encode_spans")
100+
@patch("amazon.opentelemetry.distro.otlp_udp_exporter.UdpExporter")
101+
def test_export(self, mock_udp_exporter, mock_encode_spans):
102+
mock_udp_exporter_instance = mock_udp_exporter.return_value
103+
mock_encoded_data = MagicMock()
104+
mock_encode_spans.return_value.SerializeToString.return_value = mock_encoded_data
105+
exporter = OTLPUdpSpanExporter()
106+
result = exporter.export(MagicMock())
107+
mock_udp_exporter_instance.send_data.assert_called_once_with(data=mock_encoded_data, signal_format_prefix="T1")
108+
self.assertEqual(result, SpanExportResult.SUCCESS)
109+
110+
@patch("amazon.opentelemetry.distro.otlp_udp_exporter.encode_spans")
111+
@patch("amazon.opentelemetry.distro.otlp_udp_exporter.UdpExporter")
112+
def test_export_with_exception(self, mock_udp_exporter, mock_encode_spans):
113+
mock_udp_exporter_instance = mock_udp_exporter.return_value
114+
mock_encoded_data = MagicMock()
115+
mock_encode_spans.return_value.SerializeToString.return_value = mock_encoded_data
116+
mock_udp_exporter_instance.send_data.side_effect = Exception("Something went wrong")
117+
exporter = OTLPUdpSpanExporter()
118+
result = exporter.export(MagicMock())
119+
self.assertEqual(result, SpanExportResult.FAILURE)
120+
121+
# pylint: disable=no-self-use
122+
@patch("amazon.opentelemetry.distro.otlp_udp_exporter.UdpExporter")
123+
def test_shutdown(self, mock_udp_exporter):
124+
mock_udp_exporter_instance = mock_udp_exporter.return_value
125+
exporter = OTLPUdpSpanExporter()
126+
exporter.shutdown()
127+
mock_udp_exporter_instance.shutdown.assert_called_once()

0 commit comments

Comments
 (0)