Skip to content

Commit 2388f5f

Browse files
authored
Add Compact Console Log Exporter for Lambda Environment (#442)
*Issue #, if available:* *Description of changes:* *Testing:* Lambda code: ``` import json import logging from logging import Logger, getLogger _logger: Logger = getLogger("__name__") def lambda_handler(event, context): _logger.error("helloooooo1") _logger.error("helloooooo2") _logger.error("helloooooo3") _logger.error("helloooooo4") _logger.error("helloooooo5") return { 'statusCode': 200, 'body': json.dumps('Hello from Lambda!') } ``` Env vars: ``` AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument OTEL_LOGS_EXPORTER=otlp OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true ``` See that one log is in one CWLog line. <img width="2344" height="748" alt="image" src="https://github.com/user-attachments/assets/3f09900e-fad6-4f90-b9a4-a8006a5b259d" /> 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 b6fb638 commit 2388f5f

File tree

4 files changed

+145
-2
lines changed

4 files changed

+145
-2
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
AwsMetricAttributesSpanExporterBuilder,
2424
)
2525
from amazon.opentelemetry.distro.aws_span_metrics_processor_builder import AwsSpanMetricsProcessorBuilder
26+
from amazon.opentelemetry.distro.exporter.console.logs.compact_console_log_exporter import CompactConsoleLogExporter
2627
from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpSpanExporter
2728
from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler
2829
from amazon.opentelemetry.distro.scope_based_exporter import ScopeBasedPeriodicExportingMetricReader
@@ -46,7 +47,7 @@
4647
)
4748
from opentelemetry.sdk._events import EventLoggerProvider
4849
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
49-
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, LogExporter
50+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogExporter, LogExporter
5051
from opentelemetry.sdk.environment_variables import (
5152
_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED,
5253
OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
@@ -216,6 +217,9 @@ def _init_logging(
216217
set_logger_provider(provider)
217218

218219
for _, exporter_class in exporters.items():
220+
if exporter_class is ConsoleLogExporter and _is_lambda_environment():
221+
exporter_class = CompactConsoleLogExporter
222+
_logger.debug("Lambda environment detected, using CompactConsoleLogExporter instead of ConsoleLogExporter")
219223
exporter_args = {}
220224
_customize_log_record_processor(
221225
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 CompactConsoleLogExporter(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,70 @@
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.compact_console_log_exporter import CompactConsoleLogExporter
7+
from opentelemetry.sdk._logs.export import LogExportResult
8+
9+
10+
class TestCompactConsoleLogExporter(unittest.TestCase):
11+
12+
def setUp(self):
13+
self.exporter = CompactConsoleLogExporter()
14+
15+
@patch("builtins.print")
16+
def test_export_compresses_json(self, mock_print):
17+
# Mock log data
18+
mock_log_data = Mock()
19+
mock_log_record = Mock()
20+
mock_log_data.log_record = mock_log_record
21+
22+
# Mock formatted JSON with whitespace
23+
formatted_json = '{\n "body": "test message",\n "severity_number": 9,\n "attributes": {\n "key": "value"\n }\n}' # noqa: E501
24+
self.exporter.formatter = Mock(return_value=formatted_json)
25+
26+
# Call export
27+
result = self.exporter.export([mock_log_data])
28+
29+
# Verify result
30+
self.assertEqual(result, LogExportResult.SUCCESS)
31+
32+
# Verify print calls
33+
self.assertEqual(mock_print.call_count, 1)
34+
mock_print.assert_called_with(
35+
'{"body":"test message","severity_number":9,"attributes":{"key":"value"}}', flush=True
36+
)
37+
38+
@patch("builtins.print")
39+
def test_export_multiple_records(self, mock_print):
40+
# Mock multiple log data
41+
mock_log_data1 = Mock()
42+
mock_log_data2 = Mock()
43+
mock_log_data1.log_record = Mock()
44+
mock_log_data2.log_record = Mock()
45+
46+
formatted_json = '{\n "body": "test"\n}'
47+
self.exporter.formatter = Mock(return_value=formatted_json)
48+
49+
# Call export
50+
result = self.exporter.export([mock_log_data1, mock_log_data2])
51+
52+
# Verify result
53+
self.assertEqual(result, LogExportResult.SUCCESS)
54+
55+
# Verify print calls
56+
self.assertEqual(mock_print.call_count, 2) # 2 records
57+
# Each record should print compact JSON
58+
expected_calls = [unittest.mock.call('{"body":"test"}', flush=True)] * 2
59+
mock_print.assert_has_calls(expected_calls)
60+
61+
@patch("builtins.print")
62+
def test_export_empty_batch(self, mock_print):
63+
# Call export with empty batch
64+
result = self.exporter.export([])
65+
66+
# Verify result
67+
self.assertEqual(result, LogExportResult.SUCCESS)
68+
69+
# Verify print calls
70+
mock_print.assert_not_called() # No records, no prints

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

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
)
4848
from amazon.opentelemetry.distro.aws_opentelemetry_distro import AwsOpenTelemetryDistro
4949
from amazon.opentelemetry.distro.aws_span_metrics_processor import AwsSpanMetricsProcessor
50+
from amazon.opentelemetry.distro.exporter.console.logs.compact_console_log_exporter import CompactConsoleLogExporter
5051
from amazon.opentelemetry.distro.exporter.otlp.aws.common.aws_auth_session import AwsAuthSession
5152

5253
# pylint: disable=line-too-long
@@ -70,7 +71,7 @@
7071
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
7172
from opentelemetry.metrics import get_meter_provider
7273
from opentelemetry.processor.baggage import BaggageSpanProcessor
73-
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
74+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogExporter
7475
from opentelemetry.sdk.environment_variables import OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG
7576
from opentelemetry.sdk.metrics._internal.export import PeriodicExportingMetricReader
7677
from opentelemetry.sdk.resources import Resource
@@ -680,6 +681,58 @@ def capture_exporter(*args, **kwargs):
680681

681682
os.environ.pop(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT)
682683

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

0 commit comments

Comments
 (0)