Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ inject a `requests.Session` or `grpc.ChannelCredentials` object into OTLP export
([#4731](https://github.com/open-telemetry/opentelemetry-python/pull/4731))
- Performance: Cache `importlib_metadata.entry_points`
([#4735](https://github.com/open-telemetry/opentelemetry-python/pull/4735))
- opentelemetry-sdk: fix calling Logger.emit with an API LogRecord instance
([#4741](https://github.com/open-telemetry/opentelemetry-python/pull/4741))

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ def _encode_log(log_data: LogData) -> PB2LogRecord:
log_data.log_record.attributes, allow_null=True
),
dropped_attributes_count=log_data.log_record.dropped_attributes,
severity_number=log_data.log_record.severity_number.value,
severity_number=getattr(
log_data.log_record.severity_number, "value", None
),
event_name=log_data.log_record.event_name,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def test_dropped_attributes_count(self):

@staticmethod
def _get_sdk_log_data() -> List[LogData]:
# pylint:disable=too-many-locals
ctx_log1 = set_span_in_context(
NonRecordingSpan(
SpanContext(
Expand Down Expand Up @@ -304,7 +305,29 @@ def _get_sdk_log_data() -> List[LogData]:
"extended_name", "extended_version"
),
)
return [log1, log2, log3, log4, log5, log6, log7, log8]

ctx_log9 = set_span_in_context(
NonRecordingSpan(
SpanContext(
212592107417388365804938480559624925566,
6077757853989569466,
False,
TraceFlags(0x01),
)
)
)
log9 = LogData(
log_record=SDKLogRecord(
# these are otherwise set by default
observed_timestamp=1644650584292683045,
context=ctx_log9,
resource=SDKResource({}),
),
instrumentation_scope=InstrumentationScope(
"empty_log_record_name", "empty_log_record_version"
),
)
return [log1, log2, log3, log4, log5, log6, log7, log8, log9]

def get_test_logs(
self,
Expand Down Expand Up @@ -593,6 +616,29 @@ def get_test_logs(
),
],
),
PB2ScopeLogs(
scope=PB2InstrumentationScope(
name="empty_log_record_name",
version="empty_log_record_version",
),
log_records=[
PB2LogRecord(
time_unix_nano=None,
observed_time_unix_nano=1644650584292683045,
trace_id=_encode_trace_id(
212592107417388365804938480559624925566
),
span_id=_encode_span_id(
6077757853989569466,
),
flags=int(TraceFlags(0x01)),
severity_text=None,
severity_number=None,
body=None,
attributes=None,
),
],
),
],
),
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,9 @@ def to_json(self, indent: int | None = 4) -> str:
dict(self.attributes) if bool(self.attributes) else None
),
"dropped_attributes": self.dropped_attributes,
"timestamp": ns_to_iso_str(self.timestamp),
"timestamp": ns_to_iso_str(self.timestamp)
if self.timestamp is not None
else None,
"observed_timestamp": ns_to_iso_str(self.observed_timestamp),
"trace_id": (
f"0x{format_trace_id(self.trace_id)}"
Expand Down Expand Up @@ -335,6 +337,25 @@ def dropped_attributes(self) -> int:
return attributes.dropped
return 0

@classmethod
def _from_api_log_record(
cls, *, record: APILogRecord, resource: Resource
) -> LogRecord:
return cls(
timestamp=record.timestamp,
observed_timestamp=record.observed_timestamp,
context=record.context,
trace_id=record.trace_id,
span_id=record.span_id,
trace_flags=record.trace_flags,
severity_text=record.severity_text,
severity_number=record.severity_number,
body=record.body,
attributes=record.attributes,
event_name=record.event_name,
resource=resource,
)


class LogData:
"""Readable LogRecord data plus associated InstrumentationLibrary."""
Expand Down Expand Up @@ -678,10 +699,15 @@ def __init__(
def resource(self):
return self._resource

def emit(self, record: LogRecord):
def emit(self, record: APILogRecord):
"""Emits the :class:`LogData` by associating :class:`LogRecord`
and instrumentation info.
"""
if 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)
self._multi_log_record_processor.on_emit(log_data)

Expand Down
45 changes: 45 additions & 0 deletions opentelemetry-sdk/tests/logs/test_log_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import unittest
import warnings

from opentelemetry._logs import LogRecord as APILogRecord
from opentelemetry._logs.severity import SeverityNumber
from opentelemetry.attributes import BoundedAttributes
from opentelemetry.context import get_current
Expand Down Expand Up @@ -62,6 +63,16 @@ def test_log_record_to_json_serializes_severity_number_as_int(self):
decoded = json.loads(actual.to_json())
self.assertEqual(SeverityNumber.WARN.value, decoded["severity_number"])

def test_log_record_to_json_serializes_null_severity_number(self):
actual = LogRecord(
observed_timestamp=0,
body="a log line",
resource=Resource({"service.name": "foo"}),
)

decoded = json.loads(actual.to_json())
self.assertEqual(None, decoded["timestamp"])

def test_log_record_bounded_attributes(self):
attr = {"key": "value"}

Expand Down Expand Up @@ -172,3 +183,37 @@ def test_log_record_deprecated_init_warning(self):
for _ in range(10):
LogRecord(context=get_current())
self.assertEqual(len(cw), 0)

# pylint:disable=protected-access
def test_log_record_from_api_log_record(self):
api_log_record = APILogRecord(
timestamp=1,
observed_timestamp=2,
context=get_current(),
trace_id=123,
span_id=456,
trace_flags=TraceFlags(0x01),
severity_text="WARN",
severity_number=SeverityNumber.WARN,
body="a log line",
attributes={"a": "b"},
event_name="an.event",
)

resource = Resource.create({})
record = LogRecord._from_api_log_record(
record=api_log_record, resource=resource
)

self.assertEqual(record.timestamp, 1)
self.assertEqual(record.observed_timestamp, 2)
self.assertEqual(record.context, get_current())
self.assertEqual(record.trace_id, 123)
self.assertEqual(record.span_id, 456)
self.assertEqual(record.trace_flags, TraceFlags(0x01))
self.assertEqual(record.severity_text, "WARN")
self.assertEqual(record.severity_number, SeverityNumber.WARN)
self.assertEqual(record.body, "a log line")
self.assertEqual(record.attributes, {"a": "b"})
self.assertEqual(record.event_name, "an.event")
self.assertEqual(record.resource, resource)
44 changes: 43 additions & 1 deletion opentelemetry-sdk/tests/logs/test_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
import unittest
from unittest.mock import Mock, patch

from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry._logs import LogRecord as APILogRecord
from opentelemetry.sdk._logs import Logger, LoggerProvider, LogRecord
from opentelemetry.sdk._logs._internal import (
NoOpLogger,
SynchronousMultiLogRecordProcessor,
)
from opentelemetry.sdk.environment_variables import OTEL_SDK_DISABLED
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.util.instrumentation import InstrumentationScope


class TestLoggerProvider(unittest.TestCase):
Expand Down Expand Up @@ -85,3 +87,43 @@ def test_logger_provider_init(self, resource_patch):
)
)
self.assertIsNotNone(logger_provider._at_exit_handler)


class TestLogger(unittest.TestCase):
@staticmethod
def _get_logger():
log_record_processor_mock = Mock()
logger = Logger(
resource=Resource.create({}),
multi_log_record_processor=log_record_processor_mock,
instrumentation_scope=InstrumentationScope(
"name",
"version",
"schema_url",
{"an": "attribute"},
),
)
return logger, log_record_processor_mock

def test_can_emit_logrecord(self):
logger, log_record_processor_mock = self._get_logger()
log_record = LogRecord(
observed_timestamp=0,
body="a log line",
)

logger.emit(log_record)
log_record_processor_mock.on_emit.assert_called_once()
log_data = log_record_processor_mock.on_emit.call_args.args[0]
self.assertTrue(isinstance(log_data.log_record, LogRecord))

def test_can_emit_api_logrecord(self):
logger, log_record_processor_mock = self._get_logger()
api_log_record = APILogRecord(
observed_timestamp=0,
body="a log line",
)
logger.emit(api_log_record)
log_record_processor_mock.on_emit.assert_called_once()
log_data = log_record_processor_mock.on_emit.call_args.args[0]
self.assertTrue(isinstance(log_data.log_record, LogRecord))
Loading