Skip to content

Commit b232ac8

Browse files
authored
Merge branch 'main' into hectorhdzg/removelogdata
2 parents 34d2a1b + 22d1fd1 commit b232ac8

File tree

6 files changed

+188
-2
lines changed

6 files changed

+188
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ inject a `requests.Session` or `grpc.ChannelCredentials` object into OTLP export
2626
([#4731](https://github.com/open-telemetry/opentelemetry-python/pull/4731))
2727
- Performance: Cache `importlib_metadata.entry_points`
2828
([#4735](https://github.com/open-telemetry/opentelemetry-python/pull/4735))
29+
- opentelemetry-sdk: fix calling Logger.emit with an API LogRecord instance
30+
([#4741](https://github.com/open-telemetry/opentelemetry-python/pull/4741))
2931

3032
## Version 1.36.0/0.57b0 (2025-07-29)
3133

exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ def _encode_log(readable_log_record: ReadableLogRecord) -> PB2LogRecord:
6363
readable_log_record.log_record.attributes, allow_null=True
6464
),
6565
dropped_attributes_count=readable_log_record.dropped_attributes,
66+
severity_number=getattr(
67+
readable_log_record.log_record.severity_number, "value", None
68+
),
6669
severity_number=readable_log_record.log_record.severity_number.value,
6770
event_name=readable_log_record.log_record.event_name,
6871
)

exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def test_dropped_attributes_count(self):
8888

8989
@staticmethod
9090
def _get_sdk_log_data() -> List[ReadWriteLogRecord]:
91+
# pylint:disable=too-many-locals
9192
ctx_log1 = set_span_in_context(
9293
NonRecordingSpan(
9394
SpanContext(
@@ -303,7 +304,29 @@ def _get_sdk_log_data() -> List[ReadWriteLogRecord]:
303304
"extended_name", "extended_version"
304305
),
305306
)
306-
return [log1, log2, log3, log4, log5, log6, log7, log8]
307+
308+
ctx_log9 = set_span_in_context(
309+
NonRecordingSpan(
310+
SpanContext(
311+
212592107417388365804938480559624925566,
312+
6077757853989569466,
313+
False,
314+
TraceFlags(0x01),
315+
)
316+
)
317+
)
318+
log9 = LogData(
319+
log_record=SDKLogRecord(
320+
# these are otherwise set by default
321+
observed_timestamp=1644650584292683045,
322+
context=ctx_log9,
323+
resource=SDKResource({}),
324+
),
325+
instrumentation_scope=InstrumentationScope(
326+
"empty_log_record_name", "empty_log_record_version"
327+
),
328+
)
329+
return [log1, log2, log3, log4, log5, log6, log7, log8, log9]
307330

308331
def get_test_logs(
309332
self,
@@ -592,6 +615,29 @@ def get_test_logs(
592615
),
593616
],
594617
),
618+
PB2ScopeLogs(
619+
scope=PB2InstrumentationScope(
620+
name="empty_log_record_name",
621+
version="empty_log_record_version",
622+
),
623+
log_records=[
624+
PB2LogRecord(
625+
time_unix_nano=None,
626+
observed_time_unix_nano=1644650584292683045,
627+
trace_id=_encode_trace_id(
628+
212592107417388365804938480559624925566
629+
),
630+
span_id=_encode_span_id(
631+
6077757853989569466,
632+
),
633+
flags=int(TraceFlags(0x01)),
634+
severity_text=None,
635+
severity_number=None,
636+
body=None,
637+
attributes=None,
638+
),
639+
],
640+
),
595641
],
596642
),
597643
]

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ def to_json(self, indent: int | None = 4) -> str:
248248
),
249249
"dropped_attributes": self.dropped_attributes,
250250
"timestamp": ns_to_iso_str(self.log_record.timestamp),
251+
if self.log_record.timestamp is not None
252+
else None,
251253
"observed_timestamp": ns_to_iso_str(
252254
self.log_record.observed_timestamp
253255
),
@@ -275,6 +277,25 @@ def dropped_attributes(self) -> int:
275277
return self.log_record.attributes.dropped
276278
return 0
277279

280+
@classmethod
281+
def _from_api_log_record(
282+
cls, *, record: APILogRecord, resource: Resource
283+
) -> LogRecord:
284+
return cls(
285+
timestamp=record.timestamp,
286+
observed_timestamp=record.observed_timestamp,
287+
context=record.context,
288+
trace_id=record.trace_id,
289+
span_id=record.span_id,
290+
trace_flags=record.trace_flags,
291+
severity_text=record.severity_text,
292+
severity_number=record.severity_number,
293+
body=record.body,
294+
attributes=record.attributes,
295+
event_name=record.event_name,
296+
resource=resource,
297+
)
298+
278299

279300
class LogRecordProcessor(abc.ABC):
280301
"""Interface to hook the log record emitting action.
@@ -608,6 +629,12 @@ def emit(self, record: LogRecord):
608629
"""Emits the :class:`ReadWriteLogRecord` by setting instrumentation scope
609630
and forwarding to the processor.
610631
"""
632+
if not isinstance(record, ReadWriteLogRecord):
633+
# pylint:disable=protected-access
634+
record = LogRecord._from_api_log_record(
635+
record=record, resource=self._resource
636+
)
637+
611638
log_record = ReadWriteLogRecord(
612639
log_record=record,
613640
resource=self._resource,

opentelemetry-sdk/tests/logs/test_log_record.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ def test_log_record_to_json_serializes_severity_number_as_int(self):
6363
decoded = json.loads(actual.to_json())
6464
self.assertEqual(SeverityNumber.WARN.value, decoded["severity_number"])
6565

66+
def test_log_record_to_json_serializes_null_severity_number(self):
67+
actual = LogRecord(
68+
observed_timestamp=0,
69+
body="a log line",
70+
resource=Resource({"service.name": "foo"}),
71+
)
72+
73+
decoded = json.loads(actual.to_json())
74+
self.assertEqual(None, decoded["timestamp"])
75+
6676
def test_log_record_bounded_attributes(self):
6777
attr = {"key": "value"}
6878

@@ -172,3 +182,62 @@ def test_log_record_dropped_attributes_unset_limits(self):
172182
)
173183
self.assertTrue(result.dropped_attributes == 0)
174184
self.assertEqual(attr, result.log_record.attributes)
185+
186+
def test_log_record_deprecated_init_warning(self):
187+
test_cases = [
188+
{"trace_id": 123},
189+
{"span_id": 123},
190+
{"trace_flags": TraceFlags(0x01)},
191+
]
192+
193+
for params in test_cases:
194+
with self.subTest(params=params):
195+
with warnings.catch_warnings(record=True) as cw:
196+
for _ in range(10):
197+
LogRecord(**params)
198+
199+
self.assertEqual(len(cw), 1)
200+
self.assertIsInstance(cw[-1].message, LogDeprecatedInitWarning)
201+
self.assertIn(
202+
"LogRecord init with `trace_id`, `span_id`, and/or `trace_flags` is deprecated since 1.35.0. Use `context` instead.",
203+
str(cw[-1].message),
204+
)
205+
206+
with warnings.catch_warnings(record=True) as cw:
207+
for _ in range(10):
208+
LogRecord(context=get_current())
209+
self.assertEqual(len(cw), 0)
210+
211+
# pylint:disable=protected-access
212+
def test_log_record_from_api_log_record(self):
213+
api_log_record = APILogRecord(
214+
timestamp=1,
215+
observed_timestamp=2,
216+
context=get_current(),
217+
trace_id=123,
218+
span_id=456,
219+
trace_flags=TraceFlags(0x01),
220+
severity_text="WARN",
221+
severity_number=SeverityNumber.WARN,
222+
body="a log line",
223+
attributes={"a": "b"},
224+
event_name="an.event",
225+
)
226+
227+
resource = Resource.create({})
228+
record = LogRecord._from_api_log_record(
229+
record=api_log_record, resource=resource
230+
)
231+
232+
self.assertEqual(record.timestamp, 1)
233+
self.assertEqual(record.observed_timestamp, 2)
234+
self.assertEqual(record.context, get_current())
235+
self.assertEqual(record.trace_id, 123)
236+
self.assertEqual(record.span_id, 456)
237+
self.assertEqual(record.trace_flags, TraceFlags(0x01))
238+
self.assertEqual(record.severity_text, "WARN")
239+
self.assertEqual(record.severity_number, SeverityNumber.WARN)
240+
self.assertEqual(record.body, "a log line")
241+
self.assertEqual(record.attributes, {"a": "b"})
242+
self.assertEqual(record.event_name, "an.event")
243+
self.assertEqual(record.resource, resource)

opentelemetry-sdk/tests/logs/test_logs.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
)
2626
from opentelemetry.sdk.environment_variables import OTEL_SDK_DISABLED
2727
from opentelemetry.sdk.resources import Resource
28+
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
2829

2930

3031
class TestLoggerProvider(unittest.TestCase):
@@ -87,7 +88,6 @@ def test_logger_provider_init(self, resource_patch):
8788
)
8889
self.assertIsNotNone(logger_provider._at_exit_handler)
8990

90-
9191
class TestReadableLogRecord(unittest.TestCase):
9292
def setUp(self):
9393
self.log_record = LogRecord(
@@ -127,3 +127,42 @@ def test_readable_log_record_can_read_attributes(self):
127127
self.readable_log_record.resource.attributes["service.name"],
128128
"test-service",
129129
)
130+
131+
class TestLogger(unittest.TestCase):
132+
@staticmethod
133+
def _get_logger():
134+
log_record_processor_mock = Mock()
135+
logger = Logger(
136+
resource=Resource.create({}),
137+
multi_log_record_processor=log_record_processor_mock,
138+
instrumentation_scope=InstrumentationScope(
139+
"name",
140+
"version",
141+
"schema_url",
142+
{"an": "attribute"},
143+
),
144+
)
145+
return logger, log_record_processor_mock
146+
147+
def test_can_emit_logrecord(self):
148+
logger, log_record_processor_mock = self._get_logger()
149+
log_record = LogRecord(
150+
observed_timestamp=0,
151+
body="a log line",
152+
)
153+
154+
logger.emit(log_record)
155+
log_record_processor_mock.on_emit.assert_called_once()
156+
log_data = log_record_processor_mock.on_emit.call_args.args[0]
157+
self.assertTrue(isinstance(log_data.log_record, LogRecord))
158+
159+
def test_can_emit_api_logrecord(self):
160+
logger, log_record_processor_mock = self._get_logger()
161+
api_log_record = APILogRecord(
162+
observed_timestamp=0,
163+
body="a log line",
164+
)
165+
logger.emit(api_log_record)
166+
log_record_processor_mock.on_emit.assert_called_once()
167+
log_data = log_record_processor_mock.on_emit.call_args.args[0]
168+
self.assertTrue(isinstance(log_data.log_record, LogRecord))

0 commit comments

Comments
 (0)