From 5a99080d6b4b55480677227870c1c5968c58ea5a Mon Sep 17 00:00:00 2001 From: ajuijas Date: Fri, 13 Mar 2026 15:48:54 +0530 Subject: [PATCH 1/6] fix(logs): optimize LogRecord memory by removing redundant context Removed direct storage of the Context object in LogRecord to prevent memory inflation when logs are buffered. Correlation IDs (TraceId, SpanId, TraceFlags) are still preserved. Resolves #4957 --- CHANGELOG.md | 2 ++ opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py | 1 - opentelemetry-sdk/tests/logs/test_handler.py | 1 - opentelemetry-sdk/tests/logs/test_log_record.py | 1 - opentelemetry-sdk/tests/logs/test_logs.py | 2 -- 5 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a6fd6d387..1838151d7c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4979](https://github.com/open-telemetry/opentelemetry-python/pull/4979)) - `opentelemetry-sdk`: Map Python `CRITICAL` log level to OTel `FATAL` severity text per the specification ([#4984](https://github.com/open-telemetry/opentelemetry-python/issues/4984)) +- `opentelemetry-api`: Optimize `LogRecord` memory by removing direct storage of the `Context` object. + ([#4957](https://github.com/open-telemetry/opentelemetry-python/issues/4957)) - `opentelemetry-sdk`: Add file configuration support with YAML/JSON loading, environment variable substitution, and schema validation against the vendored OTel config JSON schema ([#4898](https://github.com/open-telemetry/opentelemetry-python/pull/4898)) - Fix intermittent CI failures in `getting-started` and `tracecontext` jobs caused by GitHub git CDN SHA propagation lag by installing contrib packages from the already-checked-out local copy instead of a second git clone diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index 780bcb4843d..bff369f3997 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -118,7 +118,6 @@ def __init__( if observed_timestamp is None: observed_timestamp = time_ns() self.observed_timestamp = observed_timestamp - self.context = context self.trace_id = trace_id or span_context.trace_id self.span_id = span_id or span_context.span_id self.trace_flags = trace_flags or span_context.trace_flags diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index b3b8b47d16a..fd24a310c84 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -346,7 +346,6 @@ def test_log_record_trace_correlation(self): record.log_record.severity_number, SeverityNumber.FATAL, ) - self.assertEqual(record.log_record.context, mock_context) span_context = span.get_span_context() self.assertEqual( record.log_record.trace_id, span_context.trace_id diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index 3f9c4763018..de54fab4b60 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -221,7 +221,6 @@ def test_log_record_from_api_log_record(self): self.assertEqual(record.log_record.timestamp, 1) self.assertEqual(record.log_record.observed_timestamp, 2) - self.assertEqual(record.log_record.context, get_current()) # trace_id, span_id, and trace_flags come from the context's span self.assertEqual(record.log_record.trace_id, 0) self.assertEqual(record.log_record.span_id, 0) diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index 6a9c95685cd..71fb0c89f66 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -180,7 +180,6 @@ def test_can_emit_api_logrecord(self): self.assertTrue(isinstance(log_record, LogRecord)) self.assertEqual(log_record.timestamp, None) self.assertEqual(log_record.observed_timestamp, 0) - self.assertIsNotNone(log_record.context) self.assertEqual(log_record.severity_number, None) self.assertEqual(log_record.severity_text, None) self.assertEqual(log_record.body, "a log line") @@ -208,7 +207,6 @@ def test_can_emit_with_keywords_arguments(self): self.assertTrue(isinstance(result_log_record, LogRecord)) self.assertEqual(result_log_record.timestamp, 100) self.assertEqual(result_log_record.observed_timestamp, 101) - self.assertIsNotNone(result_log_record.context) self.assertEqual( result_log_record.severity_number, SeverityNumber.WARN ) From ef5598f968e79ce9b9cb050b1731e4229521e5ea Mon Sep 17 00:00:00 2001 From: ajuijas Date: Sat, 14 Mar 2026 20:24:23 +0530 Subject: [PATCH 2/6] fix(logs): reduce memory usage in BatchLogRecordProcessor (#4957) Clear LogRecord context references before buffering in the SDK to prevent memory inflation while maintaining context access for synchronous processors. --- CHANGELOG.md | 2 +- .../opentelemetry/sdk/_logs/_internal/export/__init__.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1838151d7c8..e64bef1366f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4979](https://github.com/open-telemetry/opentelemetry-python/pull/4979)) - `opentelemetry-sdk`: Map Python `CRITICAL` log level to OTel `FATAL` severity text per the specification ([#4984](https://github.com/open-telemetry/opentelemetry-python/issues/4984)) -- `opentelemetry-api`: Optimize `LogRecord` memory by removing direct storage of the `Context` object. +- `opentelemetry-sdk`: Optimize `LogRecord` memory in `BatchLogRecordProcessor` by clearing the `Context` object before buffering. ([#4957](https://github.com/open-telemetry/opentelemetry-python/issues/4957)) - `opentelemetry-sdk`: Add file configuration support with YAML/JSON loading, environment variable substitution, and schema validation against the vendored OTel config JSON schema ([#4898](https://github.com/open-telemetry/opentelemetry-python/pull/4898)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py index 1c0f82ac055..da8c41d8bf6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -14,6 +14,7 @@ from __future__ import annotations import abc +import copy import enum import logging import sys @@ -315,8 +316,13 @@ def on_emit(self, log_record: ReadWriteLogRecord) -> None: if log_record.resource is not None else Resource.create({}) ) + # Shallow copy the API log record to break the reference to the potentially large context + # while keeping the original context intact for other processors. + api_log_record = copy.copy(log_record.log_record) + api_log_record.context = None + readable_log_record = ReadableLogRecord( - log_record=log_record.log_record, + log_record=api_log_record, resource=resource, instrumentation_scope=log_record.instrumentation_scope, limits=log_record.limits, From 4d07cc056dca57d6deaa53d4f9a2aa63efd6fbf8 Mon Sep 17 00:00:00 2001 From: ajuijas Date: Sat, 14 Mar 2026 20:31:11 +0530 Subject: [PATCH 3/6] Revert "fix(logs): optimize LogRecord memory by removing redundant context" This reverts commit c77ecd0bbec1de1025fb330242bac6a2246bbd09. --- opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py | 1 + opentelemetry-sdk/tests/logs/test_handler.py | 1 + opentelemetry-sdk/tests/logs/test_log_record.py | 1 + opentelemetry-sdk/tests/logs/test_logs.py | 2 ++ 4 files changed, 5 insertions(+) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index bff369f3997..780bcb4843d 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -118,6 +118,7 @@ def __init__( if observed_timestamp is None: observed_timestamp = time_ns() self.observed_timestamp = observed_timestamp + self.context = context self.trace_id = trace_id or span_context.trace_id self.span_id = span_id or span_context.span_id self.trace_flags = trace_flags or span_context.trace_flags diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index fd24a310c84..b3b8b47d16a 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -346,6 +346,7 @@ def test_log_record_trace_correlation(self): record.log_record.severity_number, SeverityNumber.FATAL, ) + self.assertEqual(record.log_record.context, mock_context) span_context = span.get_span_context() self.assertEqual( record.log_record.trace_id, span_context.trace_id diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index de54fab4b60..3f9c4763018 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -221,6 +221,7 @@ def test_log_record_from_api_log_record(self): self.assertEqual(record.log_record.timestamp, 1) self.assertEqual(record.log_record.observed_timestamp, 2) + self.assertEqual(record.log_record.context, get_current()) # trace_id, span_id, and trace_flags come from the context's span self.assertEqual(record.log_record.trace_id, 0) self.assertEqual(record.log_record.span_id, 0) diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index 71fb0c89f66..6a9c95685cd 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -180,6 +180,7 @@ def test_can_emit_api_logrecord(self): self.assertTrue(isinstance(log_record, LogRecord)) self.assertEqual(log_record.timestamp, None) self.assertEqual(log_record.observed_timestamp, 0) + self.assertIsNotNone(log_record.context) self.assertEqual(log_record.severity_number, None) self.assertEqual(log_record.severity_text, None) self.assertEqual(log_record.body, "a log line") @@ -207,6 +208,7 @@ def test_can_emit_with_keywords_arguments(self): self.assertTrue(isinstance(result_log_record, LogRecord)) self.assertEqual(result_log_record.timestamp, 100) self.assertEqual(result_log_record.observed_timestamp, 101) + self.assertIsNotNone(result_log_record.context) self.assertEqual( result_log_record.severity_number, SeverityNumber.WARN ) From 22a4a5abd44b552dda61f8b8e622f8931acbcb48 Mon Sep 17 00:00:00 2001 From: Ijas Date: Wed, 18 Mar 2026 20:04:12 +0530 Subject: [PATCH 4/6] Update opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py Co-authored-by: Riccardo Magliocchetti --- .../src/opentelemetry/sdk/_logs/_internal/export/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py index da8c41d8bf6..527a80c4e3f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -319,7 +319,7 @@ def on_emit(self, log_record: ReadWriteLogRecord) -> None: # Shallow copy the API log record to break the reference to the potentially large context # while keeping the original context intact for other processors. api_log_record = copy.copy(log_record.log_record) - api_log_record.context = None + api_log_record.context = Context() readable_log_record = ReadableLogRecord( log_record=api_log_record, From 6c6591352205881cb2fe93318875b782a7b30d63 Mon Sep 17 00:00:00 2001 From: ajuijas Date: Sat, 21 Mar 2026 13:26:17 +0530 Subject: [PATCH 5/6] logs: address memory optimization review comments - Add missing Context import. - Apply memory optimization to SimpleLogRecordProcessor. - Update BatchLogRecordProcessor to use Context() instead of None. --- .../opentelemetry/sdk/_logs/_internal/export/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py index 527a80c4e3f..0f5a626042a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -26,6 +26,7 @@ from opentelemetry.context import ( _ON_EMIT_RECURSION_COUNT_KEY, _SUPPRESS_INSTRUMENTATION_KEY, + Context, attach, detach, get_value, @@ -224,8 +225,13 @@ def on_emit(self, log_record: ReadWriteLogRecord): if log_record.resource is not None else Resource.create({}) ) + # Shallow copy the API log record to break the reference to the potentially large context + # while keeping the original context intact for other processors. + api_log_record = copy.copy(log_record.log_record) + api_log_record.context = Context() + readable_log_record = ReadableLogRecord( - log_record=log_record.log_record, + log_record=api_log_record, resource=resource, instrumentation_scope=log_record.instrumentation_scope, limits=log_record.limits, From 56ffc797669a3c77c282121070f1f8f58815aaf8 Mon Sep 17 00:00:00 2001 From: ajuijas Date: Sat, 4 Apr 2026 13:46:16 +0530 Subject: [PATCH 6/6] logs: revert context clearing in SimpleLogRecordProcessor --- .../opentelemetry/sdk/_logs/_internal/export/__init__.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py index 0f5a626042a..34383e8df20 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -225,13 +225,8 @@ def on_emit(self, log_record: ReadWriteLogRecord): if log_record.resource is not None else Resource.create({}) ) - # Shallow copy the API log record to break the reference to the potentially large context - # while keeping the original context intact for other processors. - api_log_record = copy.copy(log_record.log_record) - api_log_record.context = Context() - readable_log_record = ReadableLogRecord( - log_record=api_log_record, + log_record=log_record.log_record, resource=resource, instrumentation_scope=log_record.instrumentation_scope, limits=log_record.limits,