diff --git a/CHANGELOG.md b/CHANGELOG.md index 90fcb56cb7e..9538426481f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py index c427a48e2f8..0a88936ea11 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py @@ -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 @@ -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__) @@ -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) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 6a029b8759d..9e2d3f7d7f3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -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 @@ -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() @@ -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 @@ -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) 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 411f92aec18..e632800c8cf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -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, diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index d5f2745d248..49c9c549393 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -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 @@ -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): @@ -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}, @@ -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): @@ -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, + )