diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f0ac8a0395..61035474f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4621](https://github.com/open-telemetry/opentelemetry-python/pull/4621)) - Fix license field in pyproject.toml files ([#4625](https://github.com/open-telemetry/opentelemetry-python/pull/4625)) +- Logging API accepts optional `context`; deprecates `trace_id`, `span_id`, `trace_flags`. + ([#4597](https://github.com/open-telemetry/opentelemetry-python/pull/4597)) ## Version 1.34.0/0.55b0 (2025-06-04) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index f969f7db730..e2d416d5992 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -37,9 +37,10 @@ from logging import getLogger from os import environ from time import time_ns -from typing import Optional, cast +from typing import Optional, cast, overload from opentelemetry._logs.severity import SeverityNumber +from opentelemetry.context.context import Context from opentelemetry.environment_variables import _OTEL_PYTHON_LOGGER_PROVIDER from opentelemetry.trace.span import TraceFlags from opentelemetry.util._once import Once @@ -57,8 +58,23 @@ class LogRecord(ABC): pertinent to the event being logged. """ + @overload def __init__( self, + *, + timestamp: Optional[int] = None, + observed_timestamp: Optional[int] = None, + context: Optional[Context] = None, + severity_text: Optional[str] = None, + severity_number: Optional[SeverityNumber] = None, + body: AnyValue = None, + attributes: Optional[_ExtendedAttributes] = None, + ) -> None: ... + + @overload + def __init__( + self, + *, timestamp: Optional[int] = None, observed_timestamp: Optional[int] = None, trace_id: Optional[int] = None, @@ -68,11 +84,27 @@ def __init__( severity_number: Optional[SeverityNumber] = None, body: AnyValue = None, attributes: Optional[_ExtendedAttributes] = None, - ): + ) -> None: ... + + def __init__( + self, + *, + timestamp: Optional[int] = None, + observed_timestamp: Optional[int] = None, + context: Optional[Context] = None, + trace_id: Optional[int] = None, + span_id: Optional[int] = None, + trace_flags: Optional["TraceFlags"] = None, + severity_text: Optional[str] = None, + severity_number: Optional[SeverityNumber] = None, + body: AnyValue = None, + attributes: Optional[_ExtendedAttributes] = None, + ) -> None: self.timestamp = timestamp if observed_timestamp is None: observed_timestamp = time_ns() self.observed_timestamp = observed_timestamp + self.context = context self.trace_id = trace_id self.span_id = span_id self.trace_flags = trace_flags diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index 0254c135e84..dbb108b7dba 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -15,6 +15,7 @@ from opentelemetry.sdk._logs._internal import ( LogData, + LogDeprecatedInitWarning, LogDroppedAttributesWarning, Logger, LoggerProvider, @@ -32,5 +33,6 @@ "LogLimits", "LogRecord", "LogRecordProcessor", + "LogDeprecatedInitWarning", "LogDroppedAttributesWarning", ] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 8c0be234508..8201b6c78f0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -25,7 +25,7 @@ from os import environ from threading import Lock from time import time_ns -from typing import Any, Callable, Tuple, Union, cast # noqa +from typing import Any, Callable, Tuple, Union, cast, overload # noqa from opentelemetry._logs import Logger as APILogger from opentelemetry._logs import LoggerProvider as APILoggerProvider @@ -38,6 +38,8 @@ std_to_otel, ) from opentelemetry.attributes import _VALID_ANY_VALUE_TYPES, BoundedAttributes +from opentelemetry.context import get_current +from opentelemetry.context.context import Context from opentelemetry.sdk.environment_variables import ( OTEL_ATTRIBUTE_COUNT_LIMIT, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, @@ -81,6 +83,18 @@ class LogDroppedAttributesWarning(UserWarning): warnings.simplefilter("once", LogDroppedAttributesWarning) +class LogDeprecatedInitWarning(UserWarning): + """Custom warning to indicate deprecated LogRecord init 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 + interfering with default user warnings. + """ + + +warnings.simplefilter("once", LogDeprecatedInitWarning) + + class LogLimits: """This class is based on a SpanLimits class in the Tracing module. @@ -180,6 +194,21 @@ class LogRecord(APILogRecord): pertinent to the event being logged. """ + @overload + def __init__( + self, + timestamp: int | None = None, + observed_timestamp: int | None = None, + context: Context | None = None, + severity_text: str | None = None, + severity_number: SeverityNumber | None = None, + body: AnyValue | None = None, + resource: Resource | None = None, + attributes: _ExtendedAttributes | None = None, + limits: LogLimits | None = _UnsetLogLimits, + ): ... + + @overload def __init__( self, timestamp: int | None = None, @@ -193,11 +222,46 @@ def __init__( resource: Resource | None = None, attributes: _ExtendedAttributes | None = None, limits: LogLimits | None = _UnsetLogLimits, + ): ... + + def __init__( + self, + timestamp: int | None = None, + observed_timestamp: int | None = None, + context: Context | None = None, + trace_id: int | None = None, + span_id: int | None = None, + trace_flags: TraceFlags | None = None, + severity_text: str | None = None, + severity_number: SeverityNumber | None = None, + body: AnyValue | None = None, + resource: Resource | None = None, + attributes: _ExtendedAttributes | None = None, + limits: LogLimits | None = _UnsetLogLimits, ): + if trace_id or span_id or trace_flags: + warnings.warn( + "LogRecord init with `trace_id`, `span_id`, and/or `trace_flags` is deprecated. Use `context` instead.", + LogDeprecatedInitWarning, + stacklevel=2, + ) + + if not context: + context = get_current() + + if context is not None: + span = get_current_span(context) + span_context = span.get_span_context() + if span_context.is_valid: + trace_id = span_context.trace_id + span_id = span_context.span_id + trace_flags = span_context.trace_flags + super().__init__( **{ "timestamp": timestamp, "observed_timestamp": observed_timestamp, + "context": context, "trace_id": trace_id, "span_id": span_id, "trace_flags": trace_flags, diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 02741acfd74..4f24d5d1639 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -193,8 +193,7 @@ def test_simple_log_record_processor_shutdown(self): ) exporter.clear() logger_provider.shutdown() - with self.assertLogs(level=logging.WARNING): - logger.warning("Log after shutdown") + logger.warning("Log after shutdown") finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 0) diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 3817c440258..ead090a77cc 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -29,7 +29,10 @@ ) from opentelemetry.semconv._incubating.attributes import code_attributes from opentelemetry.semconv.attributes import exception_attributes -from opentelemetry.trace import INVALID_SPAN_CONTEXT +from opentelemetry.trace import ( + INVALID_SPAN_CONTEXT, + set_span_in_context, +) class TestLoggingHandler(unittest.TestCase): @@ -269,6 +272,37 @@ def __str__(self): def test_log_record_trace_correlation(self): processor, logger = set_up_test_logging(logging.WARNING) + tracer = trace.TracerProvider().get_tracer(__name__) + with tracer.start_as_current_span("test") as span: + mock_context = set_span_in_context(span) + + with patch( + "opentelemetry.sdk._logs._internal.get_current", + return_value=mock_context, + ): + with self.assertLogs(level=logging.CRITICAL): + logger.critical("Critical message within span") + + log_record = processor.get_log_record(0) + + self.assertEqual( + log_record.body, "Critical message within span" + ) + self.assertEqual(log_record.severity_text, "CRITICAL") + self.assertEqual( + log_record.severity_number, SeverityNumber.FATAL + ) + self.assertEqual(log_record.context, mock_context) + span_context = span.get_span_context() + self.assertEqual(log_record.trace_id, span_context.trace_id) + self.assertEqual(log_record.span_id, span_context.span_id) + self.assertEqual( + log_record.trace_flags, span_context.trace_flags + ) + + def test_log_record_trace_correlation_deprecated(self): + processor, logger = set_up_test_logging(logging.WARNING) + tracer = trace.TracerProvider().get_tracer(__name__) with tracer.start_as_current_span("test") as span: with self.assertLogs(level=logging.CRITICAL): diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index 76cc262aabc..d7aa0faa649 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -18,12 +18,15 @@ from opentelemetry._logs.severity import SeverityNumber from opentelemetry.attributes import BoundedAttributes +from opentelemetry.context import get_current from opentelemetry.sdk._logs import ( + LogDeprecatedInitWarning, LogDroppedAttributesWarning, LogLimits, LogRecord, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.trace.span import TraceFlags class TestLogRecord(unittest.TestCase): @@ -143,3 +146,28 @@ 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): + test_cases = [ + {"trace_id": 123}, + {"span_id": 123}, + {"trace_flags": TraceFlags(0x01)}, + ] + + for params in test_cases: + with self.subTest(params=params): + with warnings.catch_warnings(record=True) as cw: + 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. Use `context` instead.", + str(cw[-1].message), + ) + + with warnings.catch_warnings(record=True) as cw: + for _ in range(10): + LogRecord(context=get_current()) + self.assertEqual(len(cw), 0)