Skip to content

Commit 80fdab5

Browse files
committed
add compressed console log exporter for lambda environment
1 parent 9b4905f commit 80fdab5

File tree

4 files changed

+151
-2
lines changed

4 files changed

+151
-2
lines changed

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
AwsMetricAttributesSpanExporterBuilder,
2424
)
2525
from amazon.opentelemetry.distro.aws_span_metrics_processor_builder import AwsSpanMetricsProcessorBuilder
26+
from amazon.opentelemetry.distro.exporter.console.logs.compressed_console_log_exporter import (
27+
CompressedConsoleLogExporter,
28+
)
2629
from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpSpanExporter
2730
from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler
2831
from amazon.opentelemetry.distro.scope_based_exporter import ScopeBasedPeriodicExportingMetricReader
@@ -46,7 +49,7 @@
4649
)
4750
from opentelemetry.sdk._events import EventLoggerProvider
4851
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
49-
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, LogExporter
52+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogExporter, LogExporter
5053
from opentelemetry.sdk.environment_variables import (
5154
_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED,
5255
OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
@@ -209,6 +212,9 @@ def _init_logging(
209212
set_logger_provider(provider)
210213

211214
for _, exporter_class in exporters.items():
215+
if exporter_class is ConsoleLogExporter and _is_lambda_environment():
216+
exporter_class = CompressedConsoleLogExporter
217+
_logger.debug("Lambda environment detected, using CompressedConsoleLogExporter")
212218
exporter_args = {}
213219
_customize_log_record_processor(
214220
logger_provider=provider, log_exporter=_customize_logs_exporter(exporter_class(**exporter_args))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
import re
4+
from typing import Sequence
5+
6+
from opentelemetry.sdk._logs import LogData
7+
from opentelemetry.sdk._logs.export import ConsoleLogExporter, LogExportResult
8+
9+
10+
class CompressedConsoleLogExporter(ConsoleLogExporter):
11+
def export(self, batch: Sequence[LogData]):
12+
for data in batch:
13+
formatted_json = self.formatter(data.log_record)
14+
print(re.sub(r"\s*([{}[\]:,])\s*", r"\1", formatted_json), flush=True)
15+
16+
return LogExportResult.SUCCESS
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
import unittest
4+
from unittest.mock import Mock, patch
5+
6+
from amazon.opentelemetry.distro.exporter.console.logs.compressed_console_log_exporter import (
7+
CompressedConsoleLogExporter,
8+
)
9+
from opentelemetry.sdk._logs.export import LogExportResult
10+
11+
12+
class TestCompressedConsoleLogExporter(unittest.TestCase):
13+
14+
def setUp(self):
15+
self.exporter = CompressedConsoleLogExporter()
16+
17+
@patch("builtins.print")
18+
def test_export_compresses_json(self, mock_print):
19+
# Mock log data
20+
mock_log_data = Mock()
21+
mock_log_record = Mock()
22+
mock_log_data.log_record = mock_log_record
23+
24+
# Mock formatted JSON with whitespace
25+
formatted_json = '{\n "body": "test message",\n "severity_number": 9,\n "attributes": {\n "key": "value"\n }\n}' # noqa: E501
26+
self.exporter.formatter = Mock(return_value=formatted_json)
27+
28+
# Call export
29+
result = self.exporter.export([mock_log_data])
30+
31+
# Verify result
32+
self.assertEqual(result, LogExportResult.SUCCESS)
33+
34+
# Verify print calls
35+
self.assertEqual(mock_print.call_count, 1)
36+
mock_print.assert_called_with(
37+
'{"body":"test message","severity_number":9,"attributes":{"key":"value"}}', flush=True
38+
)
39+
40+
@patch("builtins.print")
41+
def test_export_multiple_records(self, mock_print):
42+
# Mock multiple log data
43+
mock_log_data1 = Mock()
44+
mock_log_data2 = Mock()
45+
mock_log_data1.log_record = Mock()
46+
mock_log_data2.log_record = Mock()
47+
48+
formatted_json = '{\n "body": "test"\n}'
49+
self.exporter.formatter = Mock(return_value=formatted_json)
50+
51+
# Call export
52+
result = self.exporter.export([mock_log_data1, mock_log_data2])
53+
54+
# Verify result
55+
self.assertEqual(result, LogExportResult.SUCCESS)
56+
57+
# Verify print calls
58+
self.assertEqual(mock_print.call_count, 2) # 2 records
59+
# Each record should print compressed JSON
60+
expected_calls = [unittest.mock.call('{"body":"test"}', flush=True)] * 2
61+
mock_print.assert_has_calls(expected_calls)
62+
63+
@patch("builtins.print")
64+
def test_export_empty_batch(self, mock_print):
65+
# Call export with empty batch
66+
result = self.exporter.export([])
67+
68+
# Verify result
69+
self.assertEqual(result, LogExportResult.SUCCESS)
70+
71+
# Verify print calls
72+
mock_print.assert_not_called() # No records, no prints

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

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
from amazon.opentelemetry.distro.aws_opentelemetry_distro import AwsOpenTelemetryDistro
4848
from amazon.opentelemetry.distro.aws_span_metrics_processor import AwsSpanMetricsProcessor
4949
from amazon.opentelemetry.distro.exporter.aws.metrics.aws_cloudwatch_emf_exporter import AwsCloudWatchEmfExporter
50+
from amazon.opentelemetry.distro.exporter.console.logs.compressed_console_log_exporter import (
51+
CompressedConsoleLogExporter,
52+
)
5053
from amazon.opentelemetry.distro.exporter.otlp.aws.common.aws_auth_session import AwsAuthSession
5154

5255
# pylint: disable=line-too-long
@@ -70,7 +73,7 @@
7073
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
7174
from opentelemetry.metrics import get_meter_provider
7275
from opentelemetry.processor.baggage import BaggageSpanProcessor
73-
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
76+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogExporter
7477
from opentelemetry.sdk.environment_variables import OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG
7578
from opentelemetry.sdk.metrics._internal.export import PeriodicExportingMetricReader
7679
from opentelemetry.sdk.resources import Resource
@@ -676,6 +679,58 @@ def capture_exporter(*args, **kwargs):
676679

677680
os.environ.pop(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT)
678681

682+
@patch("amazon.opentelemetry.distro.aws_opentelemetry_configurator.LoggingHandler", return_value=MagicMock())
683+
@patch("logging.getLogger", return_value=MagicMock())
684+
@patch("amazon.opentelemetry.distro.aws_opentelemetry_configurator._customize_logs_exporter")
685+
@patch("amazon.opentelemetry.distro.aws_opentelemetry_configurator.LoggerProvider", return_value=MagicMock())
686+
@patch("amazon.opentelemetry.distro.aws_opentelemetry_configurator._customize_log_record_processor")
687+
def test_init_logging_console_exporter_replacement(
688+
self,
689+
mock_customize_processor,
690+
mock_logger_provider,
691+
mock_customize_logs_exporter,
692+
mock_get_logger,
693+
mock_logging_handler,
694+
):
695+
"""Test that ConsoleLogExporter is replaced with CompressedConsoleLogExporter when in Lambda"""
696+
697+
# Mock _is_lambda_environment to return True
698+
with patch(
699+
"amazon.opentelemetry.distro.aws_opentelemetry_configurator._is_lambda_environment", return_value=True
700+
):
701+
# Test with ConsoleLogExporter
702+
exporters = {"console": ConsoleLogExporter}
703+
_init_logging(exporters, Resource.get_empty())
704+
705+
# Verify that _customize_log_record_processor was called
706+
mock_customize_processor.assert_called_once()
707+
708+
# Get the exporter that was passed to _customize_logs_exporter
709+
call_args = mock_customize_logs_exporter.call_args
710+
exporter_instance = call_args[0][0]
711+
712+
# Verify it's a CompressedConsoleLogExporter instance
713+
self.assertIsInstance(exporter_instance, CompressedConsoleLogExporter)
714+
715+
# Reset mocks
716+
mock_customize_processor.reset_mock()
717+
mock_customize_logs_exporter.reset_mock()
718+
719+
# Test when not in Lambda environment - should not replace
720+
with patch(
721+
"amazon.opentelemetry.distro.aws_opentelemetry_configurator._is_lambda_environment", return_value=False
722+
):
723+
exporters = {"console": ConsoleLogExporter}
724+
_init_logging(exporters, Resource.get_empty())
725+
726+
# Get the exporter that was passed to _customize_logs_exporter
727+
call_args = mock_customize_logs_exporter.call_args
728+
exporter_instance = call_args[0][0]
729+
730+
# Verify it's still a regular ConsoleLogExporter
731+
self.assertIsInstance(exporter_instance, ConsoleLogExporter)
732+
self.assertNotIsInstance(exporter_instance, CompressedConsoleLogExporter)
733+
679734
def test_customize_span_processors(self):
680735
mock_tracer_provider: TracerProvider = MagicMock()
681736
os.environ.pop("AGENT_OBSERVABILITY_ENABLED", None)

0 commit comments

Comments
 (0)