Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#4755](https://github.com/open-telemetry/opentelemetry-python/pull/4755))
- logs: extend Logger.emit to accept separated keyword arguments
([#4737](https://github.com/open-telemetry/opentelemetry-python/pull/4737))
- logs: add warnings for classes that would be deprecated and renamed in 1.39.0
([#4771](https://github.com/open-telemetry/opentelemetry-python/pull/4771))

## Version 1.37.0/0.58b0 (2025-09-11)

Expand Down
38 changes: 25 additions & 13 deletions opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import warnings
from time import time_ns
from typing import Optional

Expand All @@ -20,7 +22,12 @@
from opentelemetry._events import EventLogger as APIEventLogger
from opentelemetry._events import EventLoggerProvider as APIEventLoggerProvider
from opentelemetry._logs import NoOpLogger, SeverityNumber, get_logger_provider
from opentelemetry.sdk._logs import Logger, LoggerProvider, LogRecord
from opentelemetry.sdk._logs import (
LogDeprecatedInitWarning,
Logger,
LoggerProvider,
LogRecord,
)
from opentelemetry.util.types import _ExtendedAttributes

_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -50,18 +57,23 @@ def emit(self, event: Event) -> None:
# Do nothing if SDK is disabled
return
span_context = trace.get_current_span().get_span_context()
log_record = LogRecord(
timestamp=event.timestamp or time_ns(),
observed_timestamp=None,
trace_id=event.trace_id or span_context.trace_id,
span_id=event.span_id or span_context.span_id,
trace_flags=event.trace_flags or span_context.trace_flags,
severity_text=None,
severity_number=event.severity_number or SeverityNumber.INFO,
body=event.body,
resource=getattr(self._logger, "resource", None),
attributes=event.attributes,
)

# silence deprecation warnings from internal users
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=LogDeprecatedInitWarning)

log_record = LogRecord(
timestamp=event.timestamp or time_ns(),
observed_timestamp=None,
trace_id=event.trace_id or span_context.trace_id,
span_id=event.span_id or span_context.span_id,
trace_flags=event.trace_flags or span_context.trace_flags,
severity_text=None,
severity_number=event.severity_number or SeverityNumber.INFO,
body=event.body,
resource=getattr(self._logger, "resource", None),
attributes=event.attributes,
)
self._logger.emit(log_record)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class LogDroppedAttributesWarning(UserWarning):


class LogDeprecatedInitWarning(UserWarning):
"""Custom warning to indicate deprecated LogRecord init was used.
"""Custom warning to indicate that deprecated and soon to be deprecated Log classes was used.

This class is used to filter and handle these specific warnings separately
from other warnings, ensuring that they are only shown once without
Expand Down Expand Up @@ -234,6 +234,11 @@ def __init__( # pylint:disable=too-many-locals
limits: LogLimits | None = None,
event_name: str | None = None,
):
warnings.warn(
"LogRecord will be removed in 1.39.0 and replaced by ReadWriteLogRecord and ReadableLogRecord",
LogDeprecatedInitWarning,
stacklevel=2,
)
if not context:
context = get_current()

Expand Down Expand Up @@ -358,6 +363,11 @@ def __init__(
log_record: LogRecord,
instrumentation_scope: InstrumentationScope,
):
warnings.warn(
"LogData will be removed in 1.39.0 and replaced by ReadWriteLogRecord and ReadableLogRecord",
LogDeprecatedInitWarning,
stacklevel=2,
)
self.log_record = log_record
self.instrumentation_scope = instrumentation_scope

Expand Down Expand Up @@ -727,25 +737,28 @@ def emit(
and instrumentation info.
"""

if not record:
record = LogRecord(
timestamp=timestamp,
observed_timestamp=observed_timestamp,
context=context,
severity_text=severity_text,
severity_number=severity_number,
body=body,
attributes=attributes,
event_name=event_name,
resource=self._resource,
)
elif not isinstance(record, LogRecord):
# pylint:disable=protected-access
record = LogRecord._from_api_log_record(
record=record, resource=self._resource
)
# silence deprecation warnings from internal users
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=LogDeprecatedInitWarning)
if not record:
record = LogRecord(
timestamp=timestamp,
observed_timestamp=observed_timestamp,
context=context,
severity_text=severity_text,
severity_number=severity_number,
body=body,
attributes=attributes,
event_name=event_name,
resource=self._resource,
)
elif not isinstance(record, LogRecord):
# pylint:disable=protected-access
record = LogRecord._from_api_log_record(
record=record, resource=self._resource
)

log_data = LogData(record, self._instrumentation_scope)
log_data = LogData(record, self._instrumentation_scope)

self._multi_log_record_processor.on_emit(log_data)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@
detach,
set_value,
)
from opentelemetry.sdk._logs import LogData, LogRecord, LogRecordProcessor
from opentelemetry.sdk._logs import (
LogData,
LogRecord,
LogRecordProcessor,
)
from opentelemetry.sdk._shared_internal import BatchProcessor, DuplicateFilter
from opentelemetry.sdk.environment_variables import (
OTEL_BLRP_EXPORT_TIMEOUT,
Expand Down
114 changes: 103 additions & 11 deletions opentelemetry-sdk/tests/logs/test_log_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
from opentelemetry.attributes import BoundedAttributes
from opentelemetry.context import get_current
from opentelemetry.sdk._logs import (
LogData,
LogDeprecatedInitWarning,
LogDroppedAttributesWarning,
LogLimits,
LogRecord,
)
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
from opentelemetry.trace.span import TraceFlags


Expand Down Expand Up @@ -142,11 +144,22 @@ def test_log_record_dropped_attributes_set_limits_warning_once(self):
attributes=attr,
limits=limits,
)
self.assertEqual(len(cw), 1)
self.assertIsInstance(cw[-1].message, LogDroppedAttributesWarning)

# Check that at least one LogDroppedAttributesWarning was emitted
dropped_attributes_warnings = [
w for w in cw if isinstance(w.message, LogDroppedAttributesWarning)
]
self.assertEqual(
len(dropped_attributes_warnings),
1,
"Expected exactly one LogDroppedAttributesWarning due to simplefilter('once')",
)

# Check the message content of the LogDroppedAttributesWarning
warning_message = str(dropped_attributes_warnings[0].message)
self.assertIn(
"Log record attributes were dropped due to limits",
str(cw[-1].message),
warning_message,
)

def test_log_record_dropped_attributes_unset_limits(self):
Expand All @@ -159,7 +172,7 @@ def test_log_record_dropped_attributes_unset_limits(self):
self.assertTrue(result.dropped_attributes == 0)
self.assertEqual(attr, result.attributes)

def test_log_record_deprecated_init_warning(self):
def test_log_record_context_deprecated_init_warning(self):
test_cases = [
{"trace_id": 123},
{"span_id": 123},
Expand All @@ -172,17 +185,66 @@ def test_log_record_deprecated_init_warning(self):
for _ in range(10):
LogRecord(**params)

self.assertEqual(len(cw), 1)
self.assertIsInstance(cw[-1].message, LogDeprecatedInitWarning)
self.assertIn(
"LogRecord init with `trace_id`, `span_id`, and/or `trace_flags` is deprecated since 1.35.0. Use `context` instead.",
str(cw[-1].message),
)
# Check that the LogDeprecatedInitWarning was emitted
context_deprecated_warnings = [
w
for w in cw
if isinstance(w.message, LogDeprecatedInitWarning)
]
self.assertEqual(len(context_deprecated_warnings), 2)

# Check we have the expected message once
log_record_context_warning = [
w.message
for w in cw
if "LogRecord init with `trace_id`, `span_id`, and/or `trace_flags` is deprecated since 1.35.0. Use `context` instead."
in str(w.message)
]

self.assertEqual(len(log_record_context_warning), 1)

with warnings.catch_warnings(record=True) as cw:
for _ in range(10):
LogRecord(context=get_current())
self.assertEqual(len(cw), 0)

# Check that no LogDeprecatedInitWarning was emitted when using context
context_deprecated_warnings = [
w for w in cw if isinstance(w.message, LogDeprecatedInitWarning)
]
self.assertEqual(len(context_deprecated_warnings), 1)

# Check we have no message
log_record_context_warning = [
w.message
for w in cw
if "LogRecord init with `trace_id`, `span_id`, and/or `trace_flags` is deprecated since 1.35.0. Use `context` instead."
in str(w.message)
]

self.assertEqual(len(log_record_context_warning), 0)

def test_log_record_init_deprecated_warning(self):
"""Test that LogRecord initialization emits a LogDeprecatedInitWarning."""
with warnings.catch_warnings(record=True) as cw:
warnings.simplefilter("always")
LogRecord()

# Check that at least one LogDeprecatedInitWarning was emitted
log_record_init_warnings = [
w for w in cw if isinstance(w.message, LogDeprecatedInitWarning)
]
self.assertGreater(
len(log_record_init_warnings),
0,
"Expected at least one LogDeprecatedInitWarning",
)

# Check the message content of the LogDeprecatedInitWarning
warning_message = str(log_record_init_warnings[0].message)
self.assertIn(
"LogRecord will be removed in 1.39.0 and replaced by ReadWriteLogRecord and ReadableLogRecord",
warning_message,
)

# pylint:disable=protected-access
def test_log_record_from_api_log_record(self):
Expand Down Expand Up @@ -217,3 +279,33 @@ def test_log_record_from_api_log_record(self):
self.assertEqual(record.attributes, {"a": "b"})
self.assertEqual(record.event_name, "an.event")
self.assertEqual(record.resource, resource)


class TestLogData(unittest.TestCase):
def test_init_deprecated_warning(self):
"""Test that LogData initialization emits a LogDeprecatedInitWarning."""
log_record = LogRecord()

with warnings.catch_warnings(record=True) as cw:
warnings.simplefilter("always")
LogData(
log_record=log_record,
instrumentation_scope=InstrumentationScope("foo", "bar"),
)

# Check that at least one LogDeprecatedInitWarning was emitted
init_warnings = [
w for w in cw if isinstance(w.message, LogDeprecatedInitWarning)
]
self.assertGreater(
len(init_warnings),
0,
"Expected at least one LogDeprecatedInitWarning",
)

# Check the message content of the LogDeprecatedInitWarning
warning_message = str(init_warnings[0].message)
self.assertIn(
"LogData will be removed in 1.39.0 and replaced by ReadWriteLogRecord and ReadableLogRecord",
warning_message,
)