Skip to content

Commit 9a68c94

Browse files
Logging API accepts optional Context at LogRecord init else get_current; deprecates trace_id, span_id, trace_flags (#4597)
* Logs API/SDK accepts additional otel context * Changelog * LoggingHandler translates to LogRecord with current Otel context * Add LogRecord init priority for context's span over old span info * Add LogRecord serialized_context for to_json of arbitrary objects * Add test coverage * Changelog * lint * Fix tests * Changelog * Rm Context inclusion from to_json of LogRecord * Revision: logs SDK does get_current, overload init and deprecate trace_id etc * Simplify test * Changelog * Use typing_extensions deprecated, not custom * Update LogRecord API; simplify test * Force logrecord api kwarg-only to avoid param order issues * Add special LogDeprecatedInitWarning that logs once * Rm deprecated decorator * api too * catch_warnings instead of assertLogs in test * changelog * Rm with assertLogs for py3.13 ubuntu test --------- Co-authored-by: Emídio Neto <[email protected]>
1 parent aaee549 commit 9a68c94

File tree

7 files changed

+167
-6
lines changed

7 files changed

+167
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
([#4621](https://github.com/open-telemetry/opentelemetry-python/pull/4621))
2020
- Fix license field in pyproject.toml files
2121
([#4625](https://github.com/open-telemetry/opentelemetry-python/pull/4625))
22+
- Logging API accepts optional `context`; deprecates `trace_id`, `span_id`, `trace_flags`.
23+
([#4597](https://github.com/open-telemetry/opentelemetry-python/pull/4597))
2224

2325
## Version 1.34.0/0.55b0 (2025-06-04)
2426

opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@
3737
from logging import getLogger
3838
from os import environ
3939
from time import time_ns
40-
from typing import Optional, cast
40+
from typing import Optional, cast, overload
4141

4242
from opentelemetry._logs.severity import SeverityNumber
43+
from opentelemetry.context.context import Context
4344
from opentelemetry.environment_variables import _OTEL_PYTHON_LOGGER_PROVIDER
4445
from opentelemetry.trace.span import TraceFlags
4546
from opentelemetry.util._once import Once
@@ -57,8 +58,23 @@ class LogRecord(ABC):
5758
pertinent to the event being logged.
5859
"""
5960

61+
@overload
6062
def __init__(
6163
self,
64+
*,
65+
timestamp: Optional[int] = None,
66+
observed_timestamp: Optional[int] = None,
67+
context: Optional[Context] = None,
68+
severity_text: Optional[str] = None,
69+
severity_number: Optional[SeverityNumber] = None,
70+
body: AnyValue = None,
71+
attributes: Optional[_ExtendedAttributes] = None,
72+
) -> None: ...
73+
74+
@overload
75+
def __init__(
76+
self,
77+
*,
6278
timestamp: Optional[int] = None,
6379
observed_timestamp: Optional[int] = None,
6480
trace_id: Optional[int] = None,
@@ -68,11 +84,27 @@ def __init__(
6884
severity_number: Optional[SeverityNumber] = None,
6985
body: AnyValue = None,
7086
attributes: Optional[_ExtendedAttributes] = None,
71-
):
87+
) -> None: ...
88+
89+
def __init__(
90+
self,
91+
*,
92+
timestamp: Optional[int] = None,
93+
observed_timestamp: Optional[int] = None,
94+
context: Optional[Context] = None,
95+
trace_id: Optional[int] = None,
96+
span_id: Optional[int] = None,
97+
trace_flags: Optional["TraceFlags"] = None,
98+
severity_text: Optional[str] = None,
99+
severity_number: Optional[SeverityNumber] = None,
100+
body: AnyValue = None,
101+
attributes: Optional[_ExtendedAttributes] = None,
102+
) -> None:
72103
self.timestamp = timestamp
73104
if observed_timestamp is None:
74105
observed_timestamp = time_ns()
75106
self.observed_timestamp = observed_timestamp
107+
self.context = context
76108
self.trace_id = trace_id
77109
self.span_id = span_id
78110
self.trace_flags = trace_flags

opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from opentelemetry.sdk._logs._internal import (
1717
LogData,
18+
LogDeprecatedInitWarning,
1819
LogDroppedAttributesWarning,
1920
Logger,
2021
LoggerProvider,
@@ -32,5 +33,6 @@
3233
"LogLimits",
3334
"LogRecord",
3435
"LogRecordProcessor",
36+
"LogDeprecatedInitWarning",
3537
"LogDroppedAttributesWarning",
3638
]

opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from os import environ
2626
from threading import Lock
2727
from time import time_ns
28-
from typing import Any, Callable, Tuple, Union, cast # noqa
28+
from typing import Any, Callable, Tuple, Union, cast, overload # noqa
2929

3030
from opentelemetry._logs import Logger as APILogger
3131
from opentelemetry._logs import LoggerProvider as APILoggerProvider
@@ -38,6 +38,8 @@
3838
std_to_otel,
3939
)
4040
from opentelemetry.attributes import _VALID_ANY_VALUE_TYPES, BoundedAttributes
41+
from opentelemetry.context import get_current
42+
from opentelemetry.context.context import Context
4143
from opentelemetry.sdk.environment_variables import (
4244
OTEL_ATTRIBUTE_COUNT_LIMIT,
4345
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
@@ -81,6 +83,18 @@ class LogDroppedAttributesWarning(UserWarning):
8183
warnings.simplefilter("once", LogDroppedAttributesWarning)
8284

8385

86+
class LogDeprecatedInitWarning(UserWarning):
87+
"""Custom warning to indicate deprecated LogRecord init was used.
88+
89+
This class is used to filter and handle these specific warnings separately
90+
from other warnings, ensuring that they are only shown once without
91+
interfering with default user warnings.
92+
"""
93+
94+
95+
warnings.simplefilter("once", LogDeprecatedInitWarning)
96+
97+
8498
class LogLimits:
8599
"""This class is based on a SpanLimits class in the Tracing module.
86100
@@ -180,6 +194,21 @@ class LogRecord(APILogRecord):
180194
pertinent to the event being logged.
181195
"""
182196

197+
@overload
198+
def __init__(
199+
self,
200+
timestamp: int | None = None,
201+
observed_timestamp: int | None = None,
202+
context: Context | None = None,
203+
severity_text: str | None = None,
204+
severity_number: SeverityNumber | None = None,
205+
body: AnyValue | None = None,
206+
resource: Resource | None = None,
207+
attributes: _ExtendedAttributes | None = None,
208+
limits: LogLimits | None = _UnsetLogLimits,
209+
): ...
210+
211+
@overload
183212
def __init__(
184213
self,
185214
timestamp: int | None = None,
@@ -193,11 +222,46 @@ def __init__(
193222
resource: Resource | None = None,
194223
attributes: _ExtendedAttributes | None = None,
195224
limits: LogLimits | None = _UnsetLogLimits,
225+
): ...
226+
227+
def __init__(
228+
self,
229+
timestamp: int | None = None,
230+
observed_timestamp: int | None = None,
231+
context: Context | None = None,
232+
trace_id: int | None = None,
233+
span_id: int | None = None,
234+
trace_flags: TraceFlags | None = None,
235+
severity_text: str | None = None,
236+
severity_number: SeverityNumber | None = None,
237+
body: AnyValue | None = None,
238+
resource: Resource | None = None,
239+
attributes: _ExtendedAttributes | None = None,
240+
limits: LogLimits | None = _UnsetLogLimits,
196241
):
242+
if trace_id or span_id or trace_flags:
243+
warnings.warn(
244+
"LogRecord init with `trace_id`, `span_id`, and/or `trace_flags` is deprecated. Use `context` instead.",
245+
LogDeprecatedInitWarning,
246+
stacklevel=2,
247+
)
248+
249+
if not context:
250+
context = get_current()
251+
252+
if context is not None:
253+
span = get_current_span(context)
254+
span_context = span.get_span_context()
255+
if span_context.is_valid:
256+
trace_id = span_context.trace_id
257+
span_id = span_context.span_id
258+
trace_flags = span_context.trace_flags
259+
197260
super().__init__(
198261
**{
199262
"timestamp": timestamp,
200263
"observed_timestamp": observed_timestamp,
264+
"context": context,
201265
"trace_id": trace_id,
202266
"span_id": span_id,
203267
"trace_flags": trace_flags,

opentelemetry-sdk/tests/logs/test_export.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,7 @@ def test_simple_log_record_processor_shutdown(self):
193193
)
194194
exporter.clear()
195195
logger_provider.shutdown()
196-
with self.assertLogs(level=logging.WARNING):
197-
logger.warning("Log after shutdown")
196+
logger.warning("Log after shutdown")
198197
finished_logs = exporter.get_finished_logs()
199198
self.assertEqual(len(finished_logs), 0)
200199

opentelemetry-sdk/tests/logs/test_handler.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@
2929
)
3030
from opentelemetry.semconv._incubating.attributes import code_attributes
3131
from opentelemetry.semconv.attributes import exception_attributes
32-
from opentelemetry.trace import INVALID_SPAN_CONTEXT
32+
from opentelemetry.trace import (
33+
INVALID_SPAN_CONTEXT,
34+
set_span_in_context,
35+
)
3336

3437

3538
class TestLoggingHandler(unittest.TestCase):
@@ -269,6 +272,37 @@ def __str__(self):
269272
def test_log_record_trace_correlation(self):
270273
processor, logger = set_up_test_logging(logging.WARNING)
271274

275+
tracer = trace.TracerProvider().get_tracer(__name__)
276+
with tracer.start_as_current_span("test") as span:
277+
mock_context = set_span_in_context(span)
278+
279+
with patch(
280+
"opentelemetry.sdk._logs._internal.get_current",
281+
return_value=mock_context,
282+
):
283+
with self.assertLogs(level=logging.CRITICAL):
284+
logger.critical("Critical message within span")
285+
286+
log_record = processor.get_log_record(0)
287+
288+
self.assertEqual(
289+
log_record.body, "Critical message within span"
290+
)
291+
self.assertEqual(log_record.severity_text, "CRITICAL")
292+
self.assertEqual(
293+
log_record.severity_number, SeverityNumber.FATAL
294+
)
295+
self.assertEqual(log_record.context, mock_context)
296+
span_context = span.get_span_context()
297+
self.assertEqual(log_record.trace_id, span_context.trace_id)
298+
self.assertEqual(log_record.span_id, span_context.span_id)
299+
self.assertEqual(
300+
log_record.trace_flags, span_context.trace_flags
301+
)
302+
303+
def test_log_record_trace_correlation_deprecated(self):
304+
processor, logger = set_up_test_logging(logging.WARNING)
305+
272306
tracer = trace.TracerProvider().get_tracer(__name__)
273307
with tracer.start_as_current_span("test") as span:
274308
with self.assertLogs(level=logging.CRITICAL):

opentelemetry-sdk/tests/logs/test_log_record.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818

1919
from opentelemetry._logs.severity import SeverityNumber
2020
from opentelemetry.attributes import BoundedAttributes
21+
from opentelemetry.context import get_current
2122
from opentelemetry.sdk._logs import (
23+
LogDeprecatedInitWarning,
2224
LogDroppedAttributesWarning,
2325
LogLimits,
2426
LogRecord,
2527
)
2628
from opentelemetry.sdk.resources import Resource
29+
from opentelemetry.trace.span import TraceFlags
2730

2831

2932
class TestLogRecord(unittest.TestCase):
@@ -143,3 +146,28 @@ def test_log_record_dropped_attributes_unset_limits(self):
143146
)
144147
self.assertTrue(result.dropped_attributes == 0)
145148
self.assertEqual(attr, result.attributes)
149+
150+
def test_log_record_deprecated_init_warning(self):
151+
test_cases = [
152+
{"trace_id": 123},
153+
{"span_id": 123},
154+
{"trace_flags": TraceFlags(0x01)},
155+
]
156+
157+
for params in test_cases:
158+
with self.subTest(params=params):
159+
with warnings.catch_warnings(record=True) as cw:
160+
for _ in range(10):
161+
LogRecord(**params)
162+
163+
self.assertEqual(len(cw), 1)
164+
self.assertIsInstance(cw[-1].message, LogDeprecatedInitWarning)
165+
self.assertIn(
166+
"LogRecord init with `trace_id`, `span_id`, and/or `trace_flags` is deprecated. Use `context` instead.",
167+
str(cw[-1].message),
168+
)
169+
170+
with warnings.catch_warnings(record=True) as cw:
171+
for _ in range(10):
172+
LogRecord(context=get_current())
173+
self.assertEqual(len(cw), 0)

0 commit comments

Comments
 (0)