diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c693841fc7..a5d201f663a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - docs: Added sqlcommenter example ([#4734](https://github.com/open-telemetry/opentelemetry-python/pull/4734)) +- [BREAKING] Remove LogData and extend SDK LogRecord to have instrumentation scope + ([#4676](https://github.com/open-telemetry/opentelemetry-python/pull/4676)) ## Version 1.38.0/0.59b0 (2025-10-16) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py index 52cdd71aaed..e04ee95fd38 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py @@ -30,51 +30,55 @@ ResourceLogs, ScopeLogs, ) -from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs import ReadableLogRecord -def encode_logs(batch: Sequence[LogData]) -> ExportLogsServiceRequest: +def encode_logs( + batch: Sequence[ReadableLogRecord], +) -> ExportLogsServiceRequest: return ExportLogsServiceRequest(resource_logs=_encode_resource_logs(batch)) -def _encode_log(log_data: LogData) -> PB2LogRecord: +def _encode_log(readable_log_record: ReadableLogRecord) -> PB2LogRecord: span_id = ( None - if log_data.log_record.span_id == 0 - else _encode_span_id(log_data.log_record.span_id) + if readable_log_record.log_record.span_id == 0 + else _encode_span_id(readable_log_record.log_record.span_id) ) trace_id = ( None - if log_data.log_record.trace_id == 0 - else _encode_trace_id(log_data.log_record.trace_id) + if readable_log_record.log_record.trace_id == 0 + else _encode_trace_id(readable_log_record.log_record.trace_id) ) - body = log_data.log_record.body + body = readable_log_record.log_record.body return PB2LogRecord( - time_unix_nano=log_data.log_record.timestamp, - observed_time_unix_nano=log_data.log_record.observed_timestamp, + time_unix_nano=readable_log_record.log_record.timestamp, + observed_time_unix_nano=readable_log_record.log_record.observed_timestamp, span_id=span_id, trace_id=trace_id, - flags=int(log_data.log_record.trace_flags), + flags=int(readable_log_record.log_record.trace_flags), body=_encode_value(body, allow_null=True), - severity_text=log_data.log_record.severity_text, + severity_text=readable_log_record.log_record.severity_text, attributes=_encode_attributes( - log_data.log_record.attributes, allow_null=True + readable_log_record.log_record.attributes, allow_null=True ), - dropped_attributes_count=log_data.log_record.dropped_attributes, + dropped_attributes_count=readable_log_record.dropped_attributes, severity_number=getattr( - log_data.log_record.severity_number, "value", None + readable_log_record.log_record.severity_number, "value", None ), - event_name=log_data.log_record.event_name, + event_name=readable_log_record.log_record.event_name, ) -def _encode_resource_logs(batch: Sequence[LogData]) -> List[ResourceLogs]: +def _encode_resource_logs( + batch: Sequence[ReadableLogRecord], +) -> List[ResourceLogs]: sdk_resource_logs = defaultdict(lambda: defaultdict(list)) - for sdk_log in batch: - sdk_resource = sdk_log.log_record.resource - sdk_instrumentation = sdk_log.instrumentation_scope or None - pb2_log = _encode_log(sdk_log) + for readable_log in batch: + sdk_resource = readable_log.resource + sdk_instrumentation = readable_log.instrumentation_scope or None + pb2_log = _encode_log(readable_log) sdk_resource_logs[sdk_resource][sdk_instrumentation].append(pb2_log) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py index 44a859ba7b5..06ff06a9e02 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py @@ -15,7 +15,7 @@ import unittest from typing import List, Tuple -from opentelemetry._logs import SeverityNumber +from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.exporter.otlp.proto.common._internal import ( _encode_attributes, _encode_span_id, @@ -45,8 +45,7 @@ from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as PB2Resource, ) -from opentelemetry.sdk._logs import LogData, LogLimits -from opentelemetry.sdk._logs import LogRecord as SDKLogRecord +from opentelemetry.sdk._logs import LogLimits, ReadWriteLogRecord from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import ( @@ -77,7 +76,7 @@ def test_encode_no_body(self): def test_dropped_attributes_count(self): sdk_logs = self._get_test_logs_dropped_attributes() encoded_logs = encode_logs(sdk_logs) - self.assertTrue(hasattr(sdk_logs[0].log_record, "dropped_attributes")) + self.assertTrue(hasattr(sdk_logs[0], "dropped_attributes")) self.assertEqual( # pylint:disable=no-member encoded_logs.resource_logs[0] @@ -88,7 +87,7 @@ def test_dropped_attributes_count(self): ) @staticmethod - def _get_sdk_log_data() -> List[LogData]: + def _get_sdk_log_data() -> List[ReadWriteLogRecord]: # pylint:disable=too-many-locals ctx_log1 = set_span_in_context( NonRecordingSpan( @@ -100,35 +99,35 @@ def _get_sdk_log_data() -> List[LogData]: ) ) ) - log1 = LogData( - log_record=SDKLogRecord( + log1 = ReadWriteLogRecord( + LogRecord( timestamp=1644650195189786880, observed_timestamp=1644650195189786881, context=ctx_log1, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Do not go gentle into that good night. Rage, rage against the dying of the light", - resource=SDKResource( - {"first_resource": "value"}, - "resource_schema_url", - ), attributes={"a": 1, "b": "c"}, ), + resource=SDKResource( + {"first_resource": "value"}, + "resource_schema_url", + ), instrumentation_scope=InstrumentationScope( "first_name", "first_version" ), ) - log2 = LogData( - log_record=SDKLogRecord( + log2 = ReadWriteLogRecord( + LogRecord( timestamp=1644650249738562048, observed_timestamp=1644650249738562049, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Cooper, this is no time for caution!", - resource=SDKResource({"second_resource": "CASE"}), attributes={}, ), + resource=SDKResource({"second_resource": "CASE"}), instrumentation_scope=InstrumentationScope( "second_name", "second_version" ), @@ -144,17 +143,17 @@ def _get_sdk_log_data() -> List[LogData]: ) ) ) - log3 = LogData( - log_record=SDKLogRecord( + log3 = ReadWriteLogRecord( + LogRecord( timestamp=1644650427658989056, observed_timestamp=1644650427658989057, context=ctx_log3, severity_text="DEBUG", severity_number=SeverityNumber.DEBUG, body="To our galaxy", - resource=SDKResource({"second_resource": "CASE"}), attributes={"a": 1, "b": "c"}, ), + resource=SDKResource({"second_resource": "CASE"}), instrumentation_scope=None, ) @@ -168,20 +167,20 @@ def _get_sdk_log_data() -> List[LogData]: ) ) ) - log4 = LogData( - log_record=SDKLogRecord( + log4 = ReadWriteLogRecord( + LogRecord( timestamp=1644650584292683008, observed_timestamp=1644650584292683009, context=ctx_log4, severity_text="INFO", severity_number=SeverityNumber.INFO, body="Love is the one thing that transcends time and space", - resource=SDKResource( - {"first_resource": "value"}, - "resource_schema_url", - ), attributes={"filename": "model.py", "func_name": "run_method"}, ), + resource=SDKResource( + {"first_resource": "value"}, + "resource_schema_url", + ), instrumentation_scope=InstrumentationScope( "another_name", "another_version" ), @@ -197,17 +196,17 @@ def _get_sdk_log_data() -> List[LogData]: ) ) ) - log5 = LogData( - log_record=SDKLogRecord( + log5 = ReadWriteLogRecord( + LogRecord( timestamp=1644650584292683009, observed_timestamp=1644650584292683010, context=ctx_log5, severity_text="INFO", severity_number=SeverityNumber.INFO, body={"error": None, "array_with_nones": [1, None, 2]}, - resource=SDKResource({}), attributes={}, ), + resource=SDKResource({}), instrumentation_scope=InstrumentationScope( "last_name", "last_version" ), @@ -223,20 +222,20 @@ def _get_sdk_log_data() -> List[LogData]: ) ) ) - log6 = LogData( - log_record=SDKLogRecord( + log6 = ReadWriteLogRecord( + LogRecord( timestamp=1644650584292683022, observed_timestamp=1644650584292683022, context=ctx_log6, severity_text="ERROR", severity_number=SeverityNumber.ERROR, body="This instrumentation scope has a schema url", - resource=SDKResource( - {"first_resource": "value"}, - "resource_schema_url", - ), attributes={"filename": "model.py", "func_name": "run_method"}, ), + resource=SDKResource( + {"first_resource": "value"}, + "resource_schema_url", + ), instrumentation_scope=InstrumentationScope( "scope_with_url", "scope_with_url_version", @@ -254,20 +253,20 @@ def _get_sdk_log_data() -> List[LogData]: ) ) ) - log7 = LogData( - log_record=SDKLogRecord( + log7 = ReadWriteLogRecord( + LogRecord( timestamp=1644650584292683033, observed_timestamp=1644650584292683033, context=ctx_log7, severity_text="FATAL", severity_number=SeverityNumber.FATAL, body="This instrumentation scope has a schema url and attributes", - resource=SDKResource( - {"first_resource": "value"}, - "resource_schema_url", - ), attributes={"filename": "model.py", "func_name": "run_method"}, ), + resource=SDKResource( + {"first_resource": "value"}, + "resource_schema_url", + ), instrumentation_scope=InstrumentationScope( "scope_with_attributes", "scope_with_attributes_version", @@ -286,21 +285,21 @@ def _get_sdk_log_data() -> List[LogData]: ) ) ) - log8 = LogData( - log_record=SDKLogRecord( + log8 = ReadWriteLogRecord( + LogRecord( timestamp=1644650584292683044, observed_timestamp=1644650584292683044, context=ctx_log8, severity_text="INFO", severity_number=SeverityNumber.INFO, body="Test export of extended attributes", - resource=SDKResource({}), attributes={ "extended": { "sequence": [{"inner": "mapping", "none": None}] } }, ), + resource=SDKResource({}), instrumentation_scope=InstrumentationScope( "extended_name", "extended_version" ), @@ -316,13 +315,13 @@ def _get_sdk_log_data() -> List[LogData]: ) ) ) - log9 = LogData( - log_record=SDKLogRecord( + log9 = ReadWriteLogRecord( + LogRecord( # these are otherwise set by default observed_timestamp=1644650584292683045, context=ctx_log9, - resource=SDKResource({}), ), + resource=SDKResource({}), instrumentation_scope=InstrumentationScope( "empty_log_record_name", "empty_log_record_version" ), @@ -331,7 +330,7 @@ def _get_sdk_log_data() -> List[LogData]: def get_test_logs( self, - ) -> Tuple[List[SDKLogRecord], ExportLogsServiceRequest]: + ) -> Tuple[List[ReadWriteLogRecord], ExportLogsServiceRequest]: sdk_logs = self._get_sdk_log_data() pb2_service_request = ExportLogsServiceRequest( @@ -647,7 +646,7 @@ def get_test_logs( return sdk_logs, pb2_service_request @staticmethod - def _get_test_logs_dropped_attributes() -> List[LogData]: + def _get_test_logs_dropped_attributes() -> List[ReadWriteLogRecord]: ctx_log1 = set_span_in_context( NonRecordingSpan( SpanContext( @@ -658,17 +657,17 @@ def _get_test_logs_dropped_attributes() -> List[LogData]: ) ) ) - log1 = LogData( - log_record=SDKLogRecord( + log1 = ReadWriteLogRecord( + LogRecord( timestamp=1644650195189786880, context=ctx_log1, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Do not go gentle into that good night. Rage, rage against the dying of the light", - resource=SDKResource({"first_resource": "value"}), attributes={"a": 1, "b": "c", "user_id": "B121092"}, - limits=LogLimits(max_attributes=1), ), + resource=SDKResource({"first_resource": "value"}), + limits=LogLimits(max_attributes=1), instrumentation_scope=InstrumentationScope( "first_name", "first_version" ), @@ -676,16 +675,16 @@ def _get_test_logs_dropped_attributes() -> List[LogData]: ctx_log2 = set_span_in_context( NonRecordingSpan(SpanContext(0, 0, False)) ) - log2 = LogData( - log_record=SDKLogRecord( + log2 = ReadWriteLogRecord( + LogRecord( timestamp=1644650249738562048, context=ctx_log2, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Cooper, this is no time for caution!", - resource=SDKResource({"second_resource": "CASE"}), attributes={}, ), + resource=SDKResource({"second_resource": "CASE"}), instrumentation_scope=InstrumentationScope( "second_name", "second_version" ), diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index 646aa56cbda..fd0009b1528 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -28,7 +28,7 @@ from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import ( LogsServiceStub, ) -from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs import ReadableLogRecord from opentelemetry.sdk._logs.export import LogExporter, LogExportResult from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_GRPC_LOGS_CREDENTIAL_PROVIDER, @@ -46,7 +46,7 @@ class OTLPLogExporter( LogExporter, OTLPExporterMixin[ - Sequence[LogData], + Sequence[ReadableLogRecord], ExportLogsServiceRequest, LogExportResult, LogsServiceStub, @@ -105,13 +105,13 @@ def __init__( ) def _translate_data( - self, data: Sequence[LogData] + self, data: Sequence[ReadableLogRecord] ) -> ExportLogsServiceRequest: return encode_logs(data) def export( # type: ignore [reportIncompatibleMethodOverride] self, - batch: Sequence[LogData], + batch: Sequence[ReadableLogRecord], ) -> Literal[LogExportResult.SUCCESS, LogExportResult.FAILURE]: return OTLPExporterMixin._export(self, batch) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 47e99f72291..461ea0aee74 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -80,7 +80,7 @@ KeyValue, ) from opentelemetry.proto.resource.v1.resource_pb2 import Resource # noqa: F401 -from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs import ReadableLogRecord from opentelemetry.sdk._logs.export import LogExportResult from opentelemetry.sdk._shared_internal import DuplicateFilter from opentelemetry.sdk.environment_variables import ( @@ -118,7 +118,7 @@ logger.addFilter(DuplicateFilter()) SDKDataT = TypeVar( "SDKDataT", - TypingSequence[LogData], + TypingSequence[ReadableLogRecord], MetricsData, TypingSequence[ReadableSpan], ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index a8e015e8216..94e8cc944c3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -22,7 +22,7 @@ from google.protobuf.json_format import MessageToDict from grpc import ChannelCredentials, Compression -from opentelemetry._logs import SeverityNumber +from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.exporter.otlp.proto.common._internal import _encode_value from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( OTLPLogExporter, @@ -39,7 +39,7 @@ from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as OTLPResource, ) -from opentelemetry.sdk._logs import LogData, LogRecord +from opentelemetry.sdk._logs import ReadWriteLogRecord from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, @@ -74,16 +74,16 @@ def setUp(self): ) ) ) - self.log_data_1 = LogData( - log_record=LogRecord( + self.log_data_1 = ReadWriteLogRecord( + LogRecord( timestamp=int(time.time() * 1e9), context=ctx_log_data_1, severity_text="WARNING", severity_number=SeverityNumber.WARN, body="Zhengzhou, We have a heaviest rains in 1000 years", - resource=SDKResource({"key": "value"}), attributes={"a": 1, "b": "c"}, ), + resource=SDKResource({"key": "value"}), instrumentation_scope=InstrumentationScope( "first_name", "first_version" ), @@ -98,16 +98,16 @@ def setUp(self): ) ) ) - self.log_data_2 = LogData( - log_record=LogRecord( + self.log_data_2 = ReadWriteLogRecord( + LogRecord( timestamp=int(time.time() * 1e9), context=ctx_log_data_2, severity_text="INFO", severity_number=SeverityNumber.INFO2, body="Sydney, Opera House is closed", - resource=SDKResource({"key": "value"}), attributes={"custom_attr": [1, 2, 3]}, ), + resource=SDKResource({"key": "value"}), instrumentation_scope=InstrumentationScope( "second_name", "second_version" ), @@ -122,15 +122,15 @@ def setUp(self): ) ) ) - self.log_data_3 = LogData( - log_record=LogRecord( + self.log_data_3 = ReadWriteLogRecord( + LogRecord( timestamp=int(time.time() * 1e9), context=ctx_log_data_3, severity_text="ERROR", severity_number=SeverityNumber.WARN, body="Mumbai, Boil water before drinking", - resource=SDKResource({"service": "myapp"}), ), + resource=SDKResource({"service": "myapp"}), instrumentation_scope=InstrumentationScope( "third_name", "third_version" ), @@ -140,15 +140,15 @@ def setUp(self): SpanContext(0, 5213367945872657629, False, TraceFlags(0x01)) ) ) - self.log_data_4 = LogData( - log_record=LogRecord( + self.log_data_4 = ReadWriteLogRecord( + LogRecord( timestamp=int(time.time() * 1e9), context=ctx_log_data_4, severity_text="ERROR", severity_number=SeverityNumber.WARN, body="Invalid trace id check", - resource=SDKResource({"service": "myapp"}), ), + resource=SDKResource({"service": "myapp"}), instrumentation_scope=InstrumentationScope( "fourth_name", "fourth_version" ), @@ -163,15 +163,15 @@ def setUp(self): ) ) ) - self.log_data_5 = LogData( - log_record=LogRecord( + self.log_data_5 = ReadWriteLogRecord( + LogRecord( timestamp=int(time.time() * 1e9), context=ctx_log_data_5, severity_text="ERROR", severity_number=SeverityNumber.WARN, body="Invalid span id check", - resource=SDKResource({"service": "myapp"}), ), + resource=SDKResource({"service": "myapp"}), instrumentation_scope=InstrumentationScope( "fifth_name", "fifth_version" ), diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 2afdf660025..93b28d4a5c5 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -34,7 +34,7 @@ _is_retryable, _load_session_from_envvar, ) -from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs import ReadableLogRecord from opentelemetry.sdk._logs.export import ( LogExporter, LogExportResult, @@ -176,7 +176,7 @@ def _export( ) return resp - def export(self, batch: Sequence[LogData]) -> LogExportResult: + def export(self, batch: Sequence[ReadableLogRecord]) -> LogExportResult: if self._shutdown: _logger.warning("Exporter already shutdown, ignoring batch") return LogExportResult.FAILURE diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index d136e09ffdc..c9c91ebe483 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -26,7 +26,7 @@ from requests import Session from requests.models import Response -from opentelemetry._logs import SeverityNumber +from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http._log_exporter import ( DEFAULT_COMPRESSION, @@ -39,8 +39,7 @@ from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, ) -from opentelemetry.sdk._logs import LogData -from opentelemetry.sdk._logs import LogRecord as SDKLogRecord +from opentelemetry.sdk._logs import ReadWriteLogRecord from opentelemetry.sdk._logs.export import LogExportResult from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, @@ -290,16 +289,16 @@ def test_exported_log_without_trace_id(self): ) ) ) - log = LogData( - log_record=SDKLogRecord( + log = ReadWriteLogRecord( + LogRecord( timestamp=1644650195189786182, context=ctx, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Invalid trace id check", - resource=SDKResource({"first_resource": "value"}), attributes={"a": 1, "b": "c"}, ), + resource=SDKResource({"first_resource": "value"}), instrumentation_scope=InstrumentationScope("name", "version"), ) log_records = TestOTLPHTTPLogExporter.export_log_and_deserialize(log) @@ -326,16 +325,16 @@ def test_exported_log_without_span_id(self): ) ) - log = LogData( - log_record=SDKLogRecord( + log = ReadWriteLogRecord( + LogRecord( timestamp=1644650195189786360, context=ctx, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Invalid span id check", - resource=SDKResource({"first_resource": "value"}), attributes={"a": 1, "b": "c"}, ), + resource=SDKResource({"first_resource": "value"}), instrumentation_scope=InstrumentationScope("name", "version"), ) log_records = TestOTLPHTTPLogExporter.export_log_and_deserialize(log) @@ -351,7 +350,7 @@ def test_exported_log_without_span_id(self): self.fail("No log records found") @staticmethod - def _get_sdk_log_data() -> List[LogData]: + def _get_sdk_log_data() -> List[ReadWriteLogRecord]: ctx_log1 = set_span_in_context( NonRecordingSpan( SpanContext( @@ -362,16 +361,16 @@ def _get_sdk_log_data() -> List[LogData]: ) ) ) - log1 = LogData( - log_record=SDKLogRecord( + log1 = ReadWriteLogRecord( + LogRecord( timestamp=1644650195189786880, context=ctx_log1, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Do not go gentle into that good night. Rage, rage against the dying of the light", - resource=SDKResource({"first_resource": "value"}), attributes={"a": 1, "b": "c"}, ), + resource=SDKResource({"first_resource": "value"}), instrumentation_scope=InstrumentationScope( "first_name", "first_version" ), @@ -386,16 +385,16 @@ def _get_sdk_log_data() -> List[LogData]: ) ) ) - log2 = LogData( - log_record=SDKLogRecord( + log2 = ReadWriteLogRecord( + LogRecord( timestamp=1644650249738562048, context=ctx_log2, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Cooper, this is no time for caution!", - resource=SDKResource({"second_resource": "CASE"}), attributes={}, ), + resource=SDKResource({"second_resource": "CASE"}), instrumentation_scope=InstrumentationScope( "second_name", "second_version" ), @@ -410,16 +409,16 @@ def _get_sdk_log_data() -> List[LogData]: ) ) ) - log3 = LogData( - log_record=SDKLogRecord( + log3 = ReadWriteLogRecord( + LogRecord( timestamp=1644650427658989056, context=ctx_log3, severity_text="DEBUG", severity_number=SeverityNumber.DEBUG, body="To our galaxy", - resource=SDKResource({"second_resource": "CASE"}), attributes={"a": 1, "b": "c"}, ), + resource=SDKResource({"second_resource": "CASE"}), instrumentation_scope=None, ) ctx_log4 = set_span_in_context( @@ -432,16 +431,16 @@ def _get_sdk_log_data() -> List[LogData]: ) ) ) - log4 = LogData( - log_record=SDKLogRecord( + log4 = ReadWriteLogRecord( + LogRecord( timestamp=1644650584292683008, context=ctx_log4, severity_text="INFO", severity_number=SeverityNumber.INFO, body="Love is the one thing that transcends time and space", - resource=SDKResource({"first_resource": "value"}), attributes={"filename": "model.py", "func_name": "run_method"}, ), + resource=SDKResource({"first_resource": "value"}), instrumentation_scope=InstrumentationScope( "another_name", "another_version" ), diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py index 0a88936ea11..12ddb48c757 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_events/__init__.py @@ -13,7 +13,6 @@ # limitations under the License. import logging -import warnings from time import time_ns from typing import Optional @@ -21,13 +20,13 @@ from opentelemetry._events import Event 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 ( - LogDeprecatedInitWarning, - Logger, - LoggerProvider, +from opentelemetry._logs import ( LogRecord, + NoOpLogger, + SeverityNumber, + get_logger_provider, ) +from opentelemetry.sdk._logs import Logger, LoggerProvider from opentelemetry.util.types import _ExtendedAttributes _logger = logging.getLogger(__name__) @@ -58,22 +57,17 @@ def emit(self, event: Event) -> None: return span_context = trace.get_current_span().get_span_context() - # 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, - ) + 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, + attributes=event.attributes, + ) self._logger.emit(log_record) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py index dbb108b7dba..db15568842d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py @@ -12,27 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. - from opentelemetry.sdk._logs._internal import ( - LogData, - LogDeprecatedInitWarning, LogDroppedAttributesWarning, Logger, LoggerProvider, LoggingHandler, LogLimits, - LogRecord, LogRecordProcessor, + ReadableLogRecord, + ReadWriteLogRecord, ) __all__ = [ - "LogData", + "LogDroppedAttributesWarning", "Logger", "LoggerProvider", "LoggingHandler", "LogLimits", - "LogRecord", "LogRecordProcessor", - "LogDeprecatedInitWarning", - "LogDroppedAttributesWarning", + "ReadableLogRecord", + "ReadWriteLogRecord", ] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 9e2d3f7d7f3..a12de66f460 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -22,17 +22,16 @@ import threading import traceback import warnings +from dataclasses import dataclass, field from os import environ from threading import Lock from time import time_ns from typing import Any, Callable, Tuple, Union, cast, overload # noqa -from typing_extensions import deprecated - from opentelemetry._logs import Logger as APILogger from opentelemetry._logs import LoggerProvider as APILoggerProvider -from opentelemetry._logs import LogRecord as APILogRecord from opentelemetry._logs import ( + LogRecord, NoOpLogger, SeverityNumber, get_logger, @@ -40,7 +39,6 @@ ) 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, @@ -54,12 +52,8 @@ from opentelemetry.trace import ( format_span_id, format_trace_id, - get_current_span, ) -from opentelemetry.trace.span import TraceFlags -from opentelemetry.util.types import AnyValue, _ExtendedAttributes - -_logger = logging.getLogger(__name__) +from opentelemetry.util.types import _ExtendedAttributes _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128 _ENV_VALUE_UNSET = "" @@ -84,18 +78,6 @@ class LogDroppedAttributesWarning(UserWarning): warnings.simplefilter("once", LogDroppedAttributesWarning) -class LogDeprecatedInitWarning(UserWarning): - """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 - interfering with default user warnings. - """ - - -warnings.simplefilter("once", LogDeprecatedInitWarning) - - class LogLimits: """This class is based on a SpanLimits class in the Tracing module. @@ -176,109 +158,44 @@ def _from_env_if_absent( return value -class LogRecord(APILogRecord): - """A LogRecord instance represents an event being logged. - - LogRecord instances are created and emitted via `Logger` - every time something is logged. They contain all the information - 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 = None, - event_name: str | None = None, - ): ... +@dataclass(frozen=True) +class ReadableLogRecord: + """Readable LogRecord should be kept exactly in-sync with ReadWriteLogRecord, only difference is the frozen=True param.""" - @overload - @deprecated( - "LogRecord init with `trace_id`, `span_id`, and/or `trace_flags` is deprecated since 1.35.0. Use `context` instead." # noqa: E501 - ) - def __init__( - self, - timestamp: int | None = None, - observed_timestamp: int | 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 = None, - ): ... + log_record: LogRecord + resource: Resource + instrumentation_scope: InstrumentationScope | None = None + limits: LogLimits | None = None - def __init__( # pylint:disable=too-many-locals - 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 = 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() - - if trace_id or span_id or trace_flags: - warnings.warn( - "LogRecord init with `trace_id`, `span_id`, and/or `trace_flags` is deprecated since 1.35.0. Use `context` instead.", - LogDeprecatedInitWarning, - stacklevel=2, - ) + @property + def dropped_attributes(self) -> int: + if isinstance(self.log_record.attributes, BoundedAttributes): + return self.log_record.attributes.dropped + return 0 - span = get_current_span(context) - span_context = span.get_span_context() - # Use default LogLimits if none provided - if limits is None: - limits = LogLimits() +@dataclass +class ReadWriteLogRecord: + """A ReadWriteLogRecord instance represents an event being logged. + ReadWriteLogRecord instances are created and emitted via `Logger` + every time something is logged. They contain all the information + pertinent to the event being logged. + """ - super().__init__( - **{ - "timestamp": timestamp, - "observed_timestamp": observed_timestamp, - "context": context, - "trace_id": trace_id or span_context.trace_id, - "span_id": span_id or span_context.span_id, - "trace_flags": trace_flags or span_context.trace_flags, - "severity_text": severity_text, - "severity_number": severity_number, - "body": body, - "attributes": BoundedAttributes( - maxlen=limits.max_attributes, - attributes=attributes if bool(attributes) else None, - immutable=False, - max_value_len=limits.max_attribute_length, - extended_attributes=True, - ), - "event_name": event_name, - } - ) - self.resource = ( - resource if isinstance(resource, Resource) else Resource.create({}) + log_record: LogRecord + resource: Resource | None = Resource.create({}) + instrumentation_scope: InstrumentationScope | None = None + limits: LogLimits = field(default_factory=LogLimits) + + def __post_init__(self): + self.log_record.attributes = BoundedAttributes( + maxlen=self.limits.max_attributes, + attributes=self.log_record.attributes + if self.log_record.attributes + else None, + immutable=False, + max_value_len=self.limits.max_attribute_length, + extended_attributes=True, ) if self.dropped_attributes > 0: warnings.warn( @@ -288,39 +205,45 @@ def __init__( # pylint:disable=too-many-locals ) def __eq__(self, other: object) -> bool: - if not isinstance(other, LogRecord): + if not isinstance(other, ReadWriteLogRecord): return NotImplemented return self.__dict__ == other.__dict__ def to_json(self, indent: int | None = 4) -> str: return json.dumps( { - "body": self.body, - "severity_number": self.severity_number.value - if self.severity_number is not None + "body": self.log_record.body, + "severity_number": self.log_record.severity_number.value + if self.log_record.severity_number is not None else None, - "severity_text": self.severity_text, + "severity_text": self.log_record.severity_text, "attributes": ( - dict(self.attributes) if bool(self.attributes) else None + dict(self.log_record.attributes) + if bool(self.log_record.attributes) + else None ), "dropped_attributes": self.dropped_attributes, - "timestamp": ns_to_iso_str(self.timestamp) - if self.timestamp is not None + "timestamp": ns_to_iso_str(self.log_record.timestamp) + if self.log_record.timestamp is not None else None, - "observed_timestamp": ns_to_iso_str(self.observed_timestamp), + "observed_timestamp": ns_to_iso_str( + self.log_record.observed_timestamp + ), "trace_id": ( - f"0x{format_trace_id(self.trace_id)}" - if self.trace_id is not None + f"0x{format_trace_id(self.log_record.trace_id)}" + if self.log_record.trace_id is not None else "" ), "span_id": ( - f"0x{format_span_id(self.span_id)}" - if self.span_id is not None + f"0x{format_span_id(self.log_record.span_id)}" + if self.log_record.span_id is not None else "" ), - "trace_flags": self.trace_flags, + "trace_flags": self.log_record.trace_flags, "resource": json.loads(self.resource.to_json()), - "event_name": self.event_name if self.event_name else "", + "event_name": self.log_record.event_name + if self.log_record.event_name + else "", }, indent=indent, cls=BytesEncoder, @@ -328,50 +251,25 @@ def to_json(self, indent: int | None = 4) -> str: @property def dropped_attributes(self) -> int: - attributes: BoundedAttributes = cast( - BoundedAttributes, self.attributes - ) - if attributes: - return attributes.dropped + if isinstance(self.log_record.attributes, BoundedAttributes): + return self.log_record.attributes.dropped return 0 @classmethod def _from_api_log_record( - cls, *, record: APILogRecord, resource: Resource - ) -> LogRecord: + cls, + *, + record: LogRecord, + resource: Resource, + instrumentation_scope: InstrumentationScope | None = None, + ) -> ReadWriteLogRecord: 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, + log_record=record, resource=resource, + instrumentation_scope=instrumentation_scope, ) -class LogData: - """Readable LogRecord data plus associated InstrumentationLibrary.""" - - def __init__( - self, - 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 - - class LogRecordProcessor(abc.ABC): """Interface to hook the log record emitting action. @@ -381,15 +279,15 @@ class LogRecordProcessor(abc.ABC): """ @abc.abstractmethod - def on_emit(self, log_data: LogData): - """Emits the `LogData`""" + def on_emit(self, log_record: ReadWriteLogRecord): + """Emits the `ReadWriteLogRecord`""" @abc.abstractmethod def shutdown(self): """Called when a :class:`opentelemetry.sdk._logs.Logger` is shutdown""" @abc.abstractmethod - def force_flush(self, timeout_millis: int = 30000): + def force_flush(self, timeout_millis: int = 30000) -> bool: """Export all the received logs to the configured Exporter that have not yet been exported. @@ -425,9 +323,9 @@ def add_log_record_processor( with self._lock: self._log_record_processors += (log_record_processor,) - def on_emit(self, log_data: LogData) -> None: + def on_emit(self, log_record: ReadWriteLogRecord) -> None: for lp in self._log_record_processors: - lp.on_emit(log_data) + lp.on_emit(log_record) def shutdown(self) -> None: """Shutdown the log processors one by one""" @@ -499,8 +397,8 @@ def _submit_and_wait( for future in futures: future.result() - def on_emit(self, log_data: LogData): - self._submit_and_wait(lambda lp: lp.on_emit, log_data) + def on_emit(self, log_record: ReadWriteLogRecord): + self._submit_and_wait(lambda lp: lp.on_emit, log_record) def shutdown(self): self._submit_and_wait(lambda lp: lp.shutdown) @@ -609,7 +507,7 @@ def _get_attributes(record: logging.LogRecord) -> _ExtendedAttributes: ) return attributes - def _translate(self, record: logging.LogRecord) -> dict: + def _translate(self, record: logging.LogRecord) -> LogRecord: timestamp = int(record.created * 1e9) observered_timestamp = time_ns() attributes = self._get_attributes(record) @@ -643,15 +541,15 @@ def _translate(self, record: logging.LogRecord) -> dict: "WARN" if record.levelname == "WARNING" else record.levelname ) - return { - "timestamp": timestamp, - "observed_timestamp": observered_timestamp, - "context": get_current() or None, - "severity_text": level_name, - "severity_number": severity_number, - "body": body, - "attributes": attributes, - } + return LogRecord( + timestamp=timestamp, + observed_timestamp=observered_timestamp, + context=get_current() or None, + severity_text=level_name, + severity_number=severity_number, + body=body, + attributes=attributes, + ) def emit(self, record: logging.LogRecord) -> None: """ @@ -661,7 +559,7 @@ def emit(self, record: logging.LogRecord) -> None: """ logger = get_logger(record.name, logger_provider=self._logger_provider) if not isinstance(logger, NoOpLogger): - logger.emit(**self._translate(record)) + logger.emit(self._translate(record)) def flush(self) -> None: """ @@ -700,67 +598,19 @@ def __init__( def resource(self): return self._resource - @overload - def emit( - self, - *, - timestamp: int | None = None, - observed_timestamp: int | None = None, - context: Context | None = None, - severity_number: SeverityNumber | None = None, - severity_text: str | None = None, - body: AnyValue | None = None, - attributes: _ExtendedAttributes | None = None, - event_name: str | None = None, - ) -> None: ... - - @overload - def emit( # pylint:disable=arguments-differ - self, - record: APILogRecord, - ) -> None: ... - - def emit( - self, - record: APILogRecord | None = None, - *, - 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, - attributes: _ExtendedAttributes | None = None, - event_name: str | None = None, - ): - """Emits the :class:`LogData` by associating :class:`LogRecord` - and instrumentation info. + # pylint: disable=arguments-differ + def emit(self, record: LogRecord): + """Emits the :class:`ReadWriteLogRecord` by setting instrumentation scope + and forwarding to the processor. """ - - # 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) - - self._multi_log_record_processor.on_emit(log_data) + if not isinstance(record, ReadWriteLogRecord): + # pylint:disable=protected-access + record = ReadWriteLogRecord._from_api_log_record( + record=record, + resource=self._resource, + instrumentation_scope=self._instrumentation_scope, + ) + self._multi_log_record_processor.on_emit(record) class LoggerProvider(APILoggerProvider): 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 e632800c8cf..bb0ffcafd88 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -27,9 +27,9 @@ set_value, ) from opentelemetry.sdk._logs import ( - LogData, - LogRecord, LogRecordProcessor, + ReadableLogRecord, + ReadWriteLogRecord, ) from opentelemetry.sdk._shared_internal import BatchProcessor, DuplicateFilter from opentelemetry.sdk.environment_variables import ( @@ -64,10 +64,10 @@ class LogExporter(abc.ABC): """ @abc.abstractmethod - def export(self, batch: Sequence[LogData]): + def export(self, batch: Sequence[ReadableLogRecord]) -> LogExportResult: """Exports a batch of logs. Args: - batch: The list of `LogData` objects to be exported + batch: The list of `ReadableLogRecord` objects to be exported Returns: The result of the export """ @@ -91,15 +91,16 @@ class ConsoleLogExporter(LogExporter): def __init__( self, out: IO = sys.stdout, - formatter: Callable[[LogRecord], str] = lambda record: record.to_json() - + linesep, + formatter: Callable[ + [ReadableLogRecord], str + ] = lambda record: record.to_json() + linesep, ): self.out = out self.formatter = formatter - def export(self, batch: Sequence[LogData]): - for data in batch: - self.out.write(self.formatter(data.log_record)) + def export(self, batch: Sequence[ReadableLogRecord]): + for log_record in batch: + self.out.write(self.formatter(log_record)) self.out.flush() return LogExportResult.SUCCESS @@ -109,21 +110,27 @@ def shutdown(self): class SimpleLogRecordProcessor(LogRecordProcessor): """This is an implementation of LogRecordProcessor which passes - received logs in the export-friendly LogData representation to the - configured LogExporter, as soon as they are emitted. + received logs directly to the configured LogExporter, as soon as they are emitted. """ def __init__(self, exporter: LogExporter): self._exporter = exporter self._shutdown = False - def on_emit(self, log_data: LogData): + def on_emit(self, log_record: ReadWriteLogRecord): if self._shutdown: _logger.warning("Processor is already shutdown, ignoring call") return token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) try: - self._exporter.export((log_data,)) + # Convert ReadWriteLogRecord to ReadableLogRecord before exporting + readable_log_record = ReadableLogRecord( + log_record=log_record.log_record, + resource=log_record.resource, + instrumentation_scope=log_record.instrumentation_scope, + limits=log_record.limits, + ) + self._exporter.export((readable_log_record,)) except Exception: # pylint: disable=broad-exception-caught _logger.exception("Exception while exporting logs.") detach(token) @@ -138,8 +145,7 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: # pylint: disable=n class BatchLogRecordProcessor(LogRecordProcessor): """This is an implementation of LogRecordProcessor which creates batches of - received logs in the export-friendly LogData representation and - send to the configured LogExporter, as soon as they are emitted. + received logs and sends them to the configured LogExporter. `BatchLogRecordProcessor` is configurable with the following environment variables which correspond to constructor parameters: @@ -191,8 +197,15 @@ def __init__( "Log", ) - def on_emit(self, log_data: LogData) -> None: - return self._batch_processor.emit(log_data) + def on_emit(self, log_record: ReadWriteLogRecord) -> None: + # Convert ReadWriteLogRecord to ReadableLogRecord before passing to BatchProcessor + readable_log_record = ReadableLogRecord( + log_record=log_record.log_record, + resource=log_record.resource, + instrumentation_scope=log_record.instrumentation_scope, + limits=log_record.limits, + ) + return self._batch_processor.emit(readable_log_record) def shutdown(self): return self._batch_processor.shutdown() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/in_memory_log_exporter.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/in_memory_log_exporter.py index 68cb6b7389a..0911a500909 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/in_memory_log_exporter.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/in_memory_log_exporter.py @@ -15,7 +15,7 @@ import threading import typing -from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs._internal import ReadableLogRecord from opentelemetry.sdk._logs.export import LogExporter, LogExportResult @@ -36,11 +36,13 @@ def clear(self) -> None: with self._lock: self._logs.clear() - def get_finished_logs(self) -> typing.Tuple[LogData, ...]: + def get_finished_logs(self) -> typing.Tuple[ReadableLogRecord, ...]: with self._lock: return tuple(self._logs) - def export(self, batch: typing.Sequence[LogData]) -> LogExportResult: + def export( + self, batch: typing.Sequence[ReadableLogRecord] + ) -> LogExportResult: if self._stopped: return LogExportResult.FAILURE with self._lock: diff --git a/opentelemetry-sdk/tests/events/test_events.py b/opentelemetry-sdk/tests/events/test_events.py index 7b8d42ff316..df770109b8c 100644 --- a/opentelemetry-sdk/tests/events/test_events.py +++ b/opentelemetry-sdk/tests/events/test_events.py @@ -152,7 +152,6 @@ def test_event_logger_emit(self, logger_mock, log_record_mock): severity_text=None, severity_number=SeverityNumber.ERROR, body="test body", - resource=event_logger._logger.resource, attributes={ "key": "val", "foo": "bar", diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 4b8d98693c5..be9f2430dce 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -23,13 +23,12 @@ from pytest import mark -from opentelemetry._logs import SeverityNumber +from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.sdk import trace from opentelemetry.sdk._logs import ( - LogData, LoggerProvider, LoggingHandler, - LogRecord, + ReadWriteLogRecord, ) from opentelemetry.sdk._logs._internal.export import _logger from opentelemetry.sdk._logs.export import ( @@ -54,7 +53,7 @@ ) from opentelemetry.trace.span import INVALID_SPAN_CONTEXT -EMPTY_LOG = LogData( +EMPTY_LOG = ReadWriteLogRecord( log_record=LogRecord(), instrumentation_scope=InstrumentationScope("example", "example"), ) @@ -76,11 +75,13 @@ def test_simple_log_record_processor_default_level(self): logger.warning("Something is wrong") finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 1) - warning_log_record = finished_logs[0].log_record - self.assertEqual(warning_log_record.body, "Something is wrong") - self.assertEqual(warning_log_record.severity_text, "WARN") + warning_log_record = finished_logs[0] self.assertEqual( - warning_log_record.severity_number, SeverityNumber.WARN + warning_log_record.log_record.body, "Something is wrong" + ) + self.assertEqual(warning_log_record.log_record.severity_text, "WARN") + self.assertEqual( + warning_log_record.log_record.severity_number, SeverityNumber.WARN ) self.assertEqual( finished_logs[0].instrumentation_scope.name, "default_level" @@ -106,17 +107,18 @@ def test_simple_log_record_processor_custom_level(self): finished_logs = exporter.get_finished_logs() # Make sure only level >= logging.CRITICAL logs are recorded self.assertEqual(len(finished_logs), 2) - critical_log_record = finished_logs[0].log_record - fatal_log_record = finished_logs[1].log_record - self.assertEqual(critical_log_record.body, "Error message") - self.assertEqual(critical_log_record.severity_text, "ERROR") + critical_log_record = finished_logs[0] + fatal_log_record = finished_logs[1] + self.assertEqual(critical_log_record.log_record.body, "Error message") + self.assertEqual(critical_log_record.log_record.severity_text, "ERROR") self.assertEqual( - critical_log_record.severity_number, SeverityNumber.ERROR + critical_log_record.log_record.severity_number, + SeverityNumber.ERROR, ) - self.assertEqual(fatal_log_record.body, "Critical message") - self.assertEqual(fatal_log_record.severity_text, "CRITICAL") + self.assertEqual(fatal_log_record.log_record.body, "Critical message") + self.assertEqual(fatal_log_record.log_record.severity_text, "CRITICAL") self.assertEqual( - fatal_log_record.severity_number, SeverityNumber.FATAL + fatal_log_record.log_record.severity_number, SeverityNumber.FATAL ) self.assertEqual( finished_logs[0].instrumentation_scope.name, "custom_level" @@ -140,14 +142,20 @@ def test_simple_log_record_processor_trace_correlation(self): logger.warning("Warning message") finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 1) - log_record = finished_logs[0].log_record - self.assertEqual(log_record.body, "Warning message") - self.assertEqual(log_record.severity_text, "WARN") - self.assertEqual(log_record.severity_number, SeverityNumber.WARN) - self.assertEqual(log_record.trace_id, INVALID_SPAN_CONTEXT.trace_id) - self.assertEqual(log_record.span_id, INVALID_SPAN_CONTEXT.span_id) + sdk_record = finished_logs[0] + self.assertEqual(sdk_record.log_record.body, "Warning message") + self.assertEqual(sdk_record.log_record.severity_text, "WARN") + self.assertEqual( + sdk_record.log_record.severity_number, SeverityNumber.WARN + ) + self.assertEqual( + sdk_record.log_record.trace_id, INVALID_SPAN_CONTEXT.trace_id + ) + self.assertEqual( + sdk_record.log_record.span_id, INVALID_SPAN_CONTEXT.span_id + ) self.assertEqual( - log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags + sdk_record.log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags ) self.assertEqual( finished_logs[0].instrumentation_scope.name, "trace_correlation" @@ -159,18 +167,28 @@ def test_simple_log_record_processor_trace_correlation(self): logger.critical("Critical message within span") finished_logs = exporter.get_finished_logs() - log_record = finished_logs[0].log_record - self.assertEqual(log_record.body, "Critical message within span") - self.assertEqual(log_record.severity_text, "CRITICAL") - self.assertEqual(log_record.severity_number, SeverityNumber.FATAL) + sdk_record = finished_logs[0] + self.assertEqual( + sdk_record.log_record.body, "Critical message within span" + ) + self.assertEqual(sdk_record.log_record.severity_text, "CRITICAL") + self.assertEqual( + sdk_record.log_record.severity_number, SeverityNumber.FATAL + ) self.assertEqual( finished_logs[0].instrumentation_scope.name, "trace_correlation", ) 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) + self.assertEqual( + sdk_record.log_record.trace_id, span_context.trace_id + ) + self.assertEqual( + sdk_record.log_record.span_id, span_context.span_id + ) + self.assertEqual( + sdk_record.log_record.trace_flags, span_context.trace_flags + ) def test_simple_log_record_processor_shutdown(self): exporter = InMemoryLogExporter() @@ -187,11 +205,13 @@ def test_simple_log_record_processor_shutdown(self): logger.warning("Something is wrong") finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 1) - warning_log_record = finished_logs[0].log_record - self.assertEqual(warning_log_record.body, "Something is wrong") - self.assertEqual(warning_log_record.severity_text, "WARN") + warning_log_record = finished_logs[0] + self.assertEqual( + warning_log_record.log_record.body, "Something is wrong" + ) + self.assertEqual(warning_log_record.log_record.severity_text, "WARN") self.assertEqual( - warning_log_record.severity_number, SeverityNumber.WARN + warning_log_record.log_record.severity_number, SeverityNumber.WARN ) self.assertEqual( finished_logs[0].instrumentation_scope.name, "shutdown" @@ -608,16 +628,16 @@ def test_export(self): # pylint: disable=no-self-use ) ) ) - log_data = LogData( - log_record=LogRecord( + log_record = ReadWriteLogRecord( + LogRecord( timestamp=int(time.time() * 1e9), context=ctx, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Zhengzhou, We have a heaviest rains in 1000 years", - resource=SDKResource({"key": "value"}), attributes={"a": 1, "b": "c"}, ), + resource=SDKResource({"key": "value"}), instrumentation_scope=InstrumentationScope( "first_name", "first_version" ), @@ -627,9 +647,9 @@ def test_export(self): # pylint: disable=no-self-use # the exporter instance instead. with patch.object(exporter, "out") as mock_stdout: - exporter.export([log_data]) + exporter.export([log_record]) mock_stdout.write.assert_called_once_with( - log_data.log_record.to_json() + os.linesep + log_record.to_json() + os.linesep ) self.assertEqual(mock_stdout.write.call_count, 1) diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 86fbc491bab..c5db462bb9d 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -22,10 +22,10 @@ from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk import trace from opentelemetry.sdk._logs import ( - LogData, LoggerProvider, LoggingHandler, LogRecordProcessor, + ReadableLogRecord, ) from opentelemetry.sdk.environment_variables import OTEL_ATTRIBUTE_COUNT_LIMIT from opentelemetry.semconv._incubating.attributes import code_attributes @@ -101,13 +101,18 @@ def test_log_record_no_span_context(self): with self.assertLogs(level=logging.WARNING): logger.warning("Warning message") - log_record = processor.get_log_record(0) + record = processor.get_log_record(0) - self.assertIsNotNone(log_record) - self.assertEqual(log_record.trace_id, INVALID_SPAN_CONTEXT.trace_id) - self.assertEqual(log_record.span_id, INVALID_SPAN_CONTEXT.span_id) + self.assertIsNotNone(record) self.assertEqual( - log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags + record.log_record.trace_id, INVALID_SPAN_CONTEXT.trace_id + ) + self.assertEqual( + record.log_record.span_id, INVALID_SPAN_CONTEXT.span_id + ) + self.assertEqual( + record.log_record.trace_flags, + INVALID_SPAN_CONTEXT.trace_flags, ) def test_log_record_observed_timestamp(self): @@ -116,37 +121,39 @@ def test_log_record_observed_timestamp(self): with self.assertLogs(level=logging.WARNING): logger.warning("Warning message") - log_record = processor.get_log_record(0) - self.assertIsNotNone(log_record.observed_timestamp) + record = processor.get_log_record(0) + self.assertIsNotNone(record.log_record.observed_timestamp) def test_log_record_user_attributes(self): - """Attributes can be injected into logs by adding them to the LogRecord""" + """Attributes can be injected into logs by adding them to the ReadWriteLogRecord""" processor, logger = set_up_test_logging(logging.WARNING) # Assert emit gets called for warning message with self.assertLogs(level=logging.WARNING): logger.warning("Warning message", extra={"http.status_code": 200}) - log_record = processor.get_log_record(0) + record = processor.get_log_record(0) - self.assertIsNotNone(log_record) - self.assertEqual(len(log_record.attributes), 4) - self.assertEqual(log_record.attributes["http.status_code"], 200) + self.assertIsNotNone(record) + self.assertEqual(len(record.log_record.attributes), 4) + self.assertEqual(record.log_record.attributes["http.status_code"], 200) self.assertTrue( - log_record.attributes[code_attributes.CODE_FILE_PATH].endswith( - "test_handler.py" - ) + record.log_record.attributes[ + code_attributes.CODE_FILE_PATH + ].endswith("test_handler.py") ) self.assertEqual( - log_record.attributes[code_attributes.CODE_FUNCTION_NAME], + record.log_record.attributes[code_attributes.CODE_FUNCTION_NAME], "test_log_record_user_attributes", ) # The line of the log statement is not a constant (changing tests may change that), # so only check that the attribute is present. self.assertTrue( - code_attributes.CODE_LINE_NUMBER in log_record.attributes + code_attributes.CODE_LINE_NUMBER in record.log_record.attributes + ) + self.assertTrue( + isinstance(record.log_record.attributes, BoundedAttributes) ) - self.assertTrue(isinstance(log_record.attributes, BoundedAttributes)) def test_log_record_exception(self): """Exception information will be included in attributes""" @@ -158,20 +165,22 @@ def test_log_record_exception(self): with self.assertLogs(level=logging.ERROR): logger.exception("Zero Division Error") - log_record = processor.get_log_record(0) + record = processor.get_log_record(0) - self.assertIsNotNone(log_record) - self.assertTrue(isinstance(log_record.body, str)) - self.assertEqual(log_record.body, "Zero Division Error") + self.assertIsNotNone(record) + self.assertTrue(isinstance(record.log_record.body, str)) + self.assertEqual(record.log_record.body, "Zero Division Error") self.assertEqual( - log_record.attributes[exception_attributes.EXCEPTION_TYPE], + record.log_record.attributes[exception_attributes.EXCEPTION_TYPE], ZeroDivisionError.__name__, ) self.assertEqual( - log_record.attributes[exception_attributes.EXCEPTION_MESSAGE], + record.log_record.attributes[ + exception_attributes.EXCEPTION_MESSAGE + ], "division by zero", ) - stack_trace = log_record.attributes[ + stack_trace = record.log_record.attributes[ exception_attributes.EXCEPTION_STACKTRACE ] self.assertIsInstance(stack_trace, str) @@ -192,19 +201,21 @@ def test_log_record_recursive_exception(self): with self.assertLogs(level=logging.ERROR): logger.exception("Zero Division Error") - log_record = processor.get_log_record(0) + record = processor.get_log_record(0) - self.assertIsNotNone(log_record) - self.assertEqual(log_record.body, "Zero Division Error") + self.assertIsNotNone(record) + self.assertEqual(record.log_record.body, "Zero Division Error") self.assertEqual( - log_record.attributes[exception_attributes.EXCEPTION_TYPE], + record.log_record.attributes[exception_attributes.EXCEPTION_TYPE], ZeroDivisionError.__name__, ) self.assertEqual( - log_record.attributes[exception_attributes.EXCEPTION_MESSAGE], + record.log_record.attributes[ + exception_attributes.EXCEPTION_MESSAGE + ], "division by zero", ) - stack_trace = log_record.attributes[ + stack_trace = record.log_record.attributes[ exception_attributes.EXCEPTION_STACKTRACE ] self.assertIsInstance(stack_trace, str) @@ -223,18 +234,21 @@ def test_log_exc_info_false(self): with self.assertLogs(level=logging.ERROR): logger.error("Zero Division Error", exc_info=False) - log_record = processor.get_log_record(0) + record = processor.get_log_record(0) - self.assertIsNotNone(log_record) - self.assertEqual(log_record.body, "Zero Division Error") + self.assertIsNotNone(record) + self.assertEqual(record.log_record.body, "Zero Division Error") self.assertNotIn( - exception_attributes.EXCEPTION_TYPE, log_record.attributes + exception_attributes.EXCEPTION_TYPE, + record.log_record.attributes, ) self.assertNotIn( - exception_attributes.EXCEPTION_MESSAGE, log_record.attributes + exception_attributes.EXCEPTION_MESSAGE, + record.log_record.attributes, ) self.assertNotIn( - exception_attributes.EXCEPTION_STACKTRACE, log_record.attributes + exception_attributes.EXCEPTION_STACKTRACE, + record.log_record.attributes, ) def test_log_record_exception_with_object_payload(self): @@ -250,20 +264,22 @@ def __str__(self): with self.assertLogs(level=logging.ERROR): logger.exception(exception) - log_record = processor.get_log_record(0) + record = processor.get_log_record(0) - self.assertIsNotNone(log_record) - self.assertTrue(isinstance(log_record.body, str)) - self.assertEqual(log_record.body, "CustomException stringified") + self.assertIsNotNone(record) + self.assertTrue(isinstance(record.log_record.body, str)) + self.assertEqual(record.log_record.body, "CustomException stringified") self.assertEqual( - log_record.attributes[exception_attributes.EXCEPTION_TYPE], + record.log_record.attributes[exception_attributes.EXCEPTION_TYPE], CustomException.__name__, ) self.assertEqual( - log_record.attributes[exception_attributes.EXCEPTION_MESSAGE], + record.log_record.attributes[ + exception_attributes.EXCEPTION_MESSAGE + ], "CustomException message", ) - stack_trace = log_record.attributes[ + stack_trace = record.log_record.attributes[ exception_attributes.EXCEPTION_STACKTRACE ] self.assertIsInstance(stack_trace, str) @@ -285,21 +301,28 @@ def test_log_record_trace_correlation(self): with self.assertLogs(level=logging.CRITICAL): logger.critical("Critical message within span") - log_record = processor.get_log_record(0) + record = processor.get_log_record(0) self.assertEqual( - log_record.body, "Critical message within span" + record.log_record.body, + "Critical message within span", ) - self.assertEqual(log_record.severity_text, "CRITICAL") + self.assertEqual(record.log_record.severity_text, "CRITICAL") self.assertEqual( - log_record.severity_number, SeverityNumber.FATAL + record.log_record.severity_number, + SeverityNumber.FATAL, ) - self.assertEqual(log_record.context, mock_context) + self.assertEqual(record.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 + record.log_record.trace_id, span_context.trace_id + ) + self.assertEqual( + record.log_record.span_id, span_context.span_id + ) + self.assertEqual( + record.log_record.trace_flags, + span_context.trace_flags, ) def test_log_record_trace_correlation_deprecated(self): @@ -310,29 +333,35 @@ def test_log_record_trace_correlation_deprecated(self): with self.assertLogs(level=logging.CRITICAL): logger.critical("Critical message within span") - log_record = processor.get_log_record(0) + 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( + record.log_record.body, "Critical message within span" + ) + self.assertEqual(record.log_record.severity_text, "CRITICAL") + self.assertEqual( + record.log_record.severity_number, SeverityNumber.FATAL + ) 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) + self.assertEqual(record.log_record.trace_id, span_context.trace_id) + self.assertEqual(record.log_record.span_id, span_context.span_id) + self.assertEqual( + record.log_record.trace_flags, span_context.trace_flags + ) def test_warning_without_formatter(self): processor, logger = set_up_test_logging(logging.WARNING) logger.warning("Test message") - log_record = processor.get_log_record(0) - self.assertEqual(log_record.body, "Test message") + record = processor.get_log_record(0) + self.assertEqual(record.log_record.body, "Test message") def test_exception_without_formatter(self): processor, logger = set_up_test_logging(logging.WARNING) logger.exception("Test exception") - log_record = processor.get_log_record(0) - self.assertEqual(log_record.body, "Test exception") + record = processor.get_log_record(0) + self.assertEqual(record.log_record.body, "Test exception") def test_warning_with_formatter(self): processor, logger = set_up_test_logging( @@ -343,8 +372,10 @@ def test_warning_with_formatter(self): ) logger.warning("Test message") - log_record = processor.get_log_record(0) - self.assertEqual(log_record.body, "foo - WARNING - Test message") + record = processor.get_log_record(0) + self.assertEqual( + record.log_record.body, "foo - WARNING - Test message" + ) def test_log_body_is_always_string_with_formatter(self): processor, logger = set_up_test_logging( @@ -355,8 +386,8 @@ def test_log_body_is_always_string_with_formatter(self): ) logger.warning(["something", "of", "note"]) - log_record = processor.get_log_record(0) - self.assertIsInstance(log_record.body, str) + record = processor.get_log_record(0) + self.assertIsInstance(record.log_record.body, str) @patch.dict(os.environ, {"OTEL_SDK_DISABLED": "true"}) def test_handler_root_logger_with_disabled_sdk_does_not_go_into_recursion_error( @@ -391,10 +422,10 @@ def test_otel_attribute_count_limit_respected_in_logging_handler(self): "Test message with many attributes", extra=extra_attrs ) - log_record = processor.get_log_record(0) + record = processor.get_log_record(0) # With OTEL_ATTRIBUTE_COUNT_LIMIT=3, should have exactly 3 attributes - total_attrs = len(log_record.attributes) + total_attrs = len(record.log_record.attributes) self.assertEqual( total_attrs, 3, @@ -403,9 +434,9 @@ def test_otel_attribute_count_limit_respected_in_logging_handler(self): # Should have 10 dropped attributes (10 custom + 3 code - 3 kept = 10 dropped) self.assertEqual( - log_record.dropped_attributes, + record.dropped_attributes, 10, - f"Should have 10 dropped attributes, got {log_record.dropped_attributes}", + f"Should have 10 dropped attributes, got {record.dropped_attributes}", ) @patch.dict(os.environ, {OTEL_ATTRIBUTE_COUNT_LIMIT: "5"}) @@ -428,10 +459,10 @@ def test_otel_attribute_count_limit_includes_code_attributes(self): with self.assertLogs(level=logging.WARNING): logger.warning("Test message", extra=extra_attrs) - log_record = processor.get_log_record(0) + record = processor.get_log_record(0) # With OTEL_ATTRIBUTE_COUNT_LIMIT=5, should have exactly 5 attributes - total_attrs = len(log_record.attributes) + total_attrs = len(record.log_record.attributes) self.assertEqual( total_attrs, 5, @@ -440,9 +471,9 @@ def test_otel_attribute_count_limit_includes_code_attributes(self): # Should have 6 dropped attributes (8 user + 3 code - 5 kept = 6 dropped) self.assertEqual( - log_record.dropped_attributes, + record.dropped_attributes, 6, - f"Should have 6 dropped attributes, got {log_record.dropped_attributes}", + f"Should have 6 dropped attributes, got {record.dropped_attributes}", ) def test_logging_handler_without_env_var_uses_default_limit(self): @@ -457,10 +488,10 @@ def test_logging_handler_without_env_var_uses_default_limit(self): "Test message with many attributes", extra=extra_attrs ) - log_record = processor.get_log_record(0) + record = processor.get_log_record(0) # Should be limited to default limit (128) total attributes - total_attrs = len(log_record.attributes) + total_attrs = len(record.log_record.attributes) self.assertEqual( total_attrs, 128, @@ -469,9 +500,9 @@ def test_logging_handler_without_env_var_uses_default_limit(self): # Should have 25 dropped attributes (150 user + 3 code - 128 kept = 25 dropped) self.assertEqual( - log_record.dropped_attributes, + record.dropped_attributes, 25, - f"Should have 25 dropped attributes, got {log_record.dropped_attributes}", + f"Should have 25 dropped attributes, got {record.dropped_attributes}", ) @@ -491,8 +522,8 @@ class FakeProcessor(LogRecordProcessor): def __init__(self): self.log_data_emitted = [] - def on_emit(self, log_data: LogData): - self.log_data_emitted.append(log_data) + def on_emit(self, log_record: ReadableLogRecord): + self.log_data_emitted.append(log_record) def shutdown(self): pass @@ -504,4 +535,4 @@ def emit_count(self): return len(self.log_data_emitted) def get_log_record(self, i): - return self.log_data_emitted[i].log_record + return self.log_data_emitted[i] diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index 49c9c549393..fa5a4f85204 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -16,36 +16,34 @@ import unittest import warnings -from opentelemetry._logs import LogRecord as APILogRecord -from opentelemetry._logs.severity import SeverityNumber +from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.attributes import BoundedAttributes from opentelemetry.context import get_current from opentelemetry.sdk._logs import ( - LogData, - LogDeprecatedInitWarning, LogDroppedAttributesWarning, LogLimits, - LogRecord, + ReadWriteLogRecord, ) from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace.span import TraceFlags class TestLogRecord(unittest.TestCase): def test_log_record_to_json(self): - log_record = LogRecord( - timestamp=0, - observed_timestamp=0, - body={"key": "logLine", "bytes": b"123"}, + log_record = ReadWriteLogRecord( + LogRecord( + timestamp=0, + observed_timestamp=0, + body={"key": "logLine", "bytes": b"123"}, + attributes={ + "mapping": {"key": "value"}, + "none": None, + "sequence": [1, 2], + "str": "string", + }, + event_name="a.event", + ), resource=Resource({"service.name": "foo"}), - attributes={ - "mapping": {"key": "value"}, - "none": None, - "sequence": [1, 2], - "str": "string", - }, - event_name="a.event", ) self.assertEqual( @@ -54,11 +52,13 @@ def test_log_record_to_json(self): ) def test_log_record_to_json_serializes_severity_number_as_int(self): - actual = LogRecord( - timestamp=0, - severity_number=SeverityNumber.WARN, - observed_timestamp=0, - body="a log line", + actual = ReadWriteLogRecord( + LogRecord( + timestamp=0, + severity_number=SeverityNumber.WARN, + observed_timestamp=0, + body="a log line", + ), resource=Resource({"service.name": "foo"}), ) @@ -66,9 +66,11 @@ def test_log_record_to_json_serializes_severity_number_as_int(self): 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", + actual = ReadWriteLogRecord( + LogRecord( + observed_timestamp=0, + body="a log line", + ), resource=Resource({"service.name": "foo"}), ) @@ -78,14 +80,20 @@ def test_log_record_to_json_serializes_null_severity_number(self): def test_log_record_bounded_attributes(self): attr = {"key": "value"} - result = LogRecord(timestamp=0, body="a log line", attributes=attr) + result = ReadWriteLogRecord( + LogRecord(timestamp=0, body="a log line", attributes=attr) + ) - self.assertTrue(isinstance(result.attributes, BoundedAttributes)) + self.assertTrue( + isinstance(result.log_record.attributes, BoundedAttributes) + ) def test_log_record_dropped_attributes_empty_limits(self): attr = {"key": "value"} - result = LogRecord(timestamp=0, body="a log line", attributes=attr) + result = ReadWriteLogRecord( + LogRecord(timestamp=0, body="a log line", attributes=attr) + ) self.assertTrue(result.dropped_attributes == 0) @@ -95,8 +103,9 @@ def test_log_record_dropped_attributes_set_limits_max_attribute(self): max_attributes=1, ) - result = LogRecord( - timestamp=0, body="a log line", attributes=attr, limits=limits + result = ReadWriteLogRecord( + LogRecord(timestamp=0, body="a log line", attributes=attr), + limits=limits, ) self.assertTrue(result.dropped_attributes == 1) @@ -109,11 +118,16 @@ def test_log_record_dropped_attributes_set_limits_max_attribute_length( max_attribute_length=1, ) - result = LogRecord( - timestamp=0, body="a log line", attributes=attr, limits=limits + result = ReadWriteLogRecord( + LogRecord( + timestamp=0, + body="a log line", + attributes=attr, + ), + limits=limits, ) self.assertTrue(result.dropped_attributes == 0) - self.assertEqual(expected, result.attributes) + self.assertEqual(expected, result.log_record.attributes) def test_log_record_dropped_attributes_set_limits(self): attr = {"key": "value", "key2": "value2"} @@ -123,11 +137,16 @@ def test_log_record_dropped_attributes_set_limits(self): max_attribute_length=1, ) - result = LogRecord( - timestamp=0, body="a log line", attributes=attr, limits=limits + result = ReadWriteLogRecord( + LogRecord( + timestamp=0, + body="a log line", + attributes=attr, + ), + limits=limits, ) self.assertTrue(result.dropped_attributes == 1) - self.assertEqual(expected, result.attributes) + self.assertEqual(expected, result.log_record.attributes) def test_log_record_dropped_attributes_set_limits_warning_once(self): attr = {"key1": "value1", "key2": "value2"} @@ -138,10 +157,12 @@ def test_log_record_dropped_attributes_set_limits_warning_once(self): with warnings.catch_warnings(record=True) as cw: for _ in range(10): - LogRecord( - timestamp=0, - body="a log line", - attributes=attr, + ReadWriteLogRecord( + LogRecord( + timestamp=0, + body="a log line", + attributes=attr, + ), limits=limits, ) @@ -166,95 +187,23 @@ def test_log_record_dropped_attributes_unset_limits(self): attr = {"key": "value", "key2": "value2"} limits = LogLimits() - result = LogRecord( - timestamp=0, body="a log line", attributes=attr, limits=limits + result = ReadWriteLogRecord( + LogRecord( + timestamp=0, + body="a log line", + attributes=attr, + ), + limits=limits, ) self.assertTrue(result.dropped_attributes == 0) - self.assertEqual(attr, result.attributes) - - def test_log_record_context_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) - - # 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()) - - # 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, - ) + self.assertEqual(attr, result.log_record.attributes) # pylint:disable=protected-access def test_log_record_from_api_log_record(self): - api_log_record = APILogRecord( + api_log_record = LogRecord( 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", @@ -263,49 +212,22 @@ def test_log_record_from_api_log_record(self): ) resource = Resource.create({}) - record = LogRecord._from_api_log_record( + record = ReadWriteLogRecord._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) - - -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, + self.assertEqual(record.log_record.timestamp, 1) + self.assertEqual(record.log_record.observed_timestamp, 2) + self.assertEqual(record.log_record.context, get_current()) + # trace_id, span_id, and trace_flags come from the context's span + self.assertEqual(record.log_record.trace_id, 0) + self.assertEqual(record.log_record.span_id, 0) + self.assertEqual(record.log_record.trace_flags, TraceFlags(0x00)) + self.assertEqual(record.log_record.severity_text, "WARN") + self.assertEqual( + record.log_record.severity_number, SeverityNumber.WARN ) + self.assertEqual(record.log_record.body, "a log line") + self.assertEqual(record.log_record.attributes, {"a": "b"}) + self.assertEqual(record.log_record.event_name, "an.event") + self.assertEqual(record.resource, resource) diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index e4849e07a2e..70811260ae4 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -17,13 +17,12 @@ import unittest from unittest.mock import Mock, patch -from opentelemetry._logs import LogRecord as APILogRecord -from opentelemetry._logs import SeverityNumber +from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.context import get_current from opentelemetry.sdk._logs import ( Logger, LoggerProvider, - LogRecord, + ReadableLogRecord, ) from opentelemetry.sdk._logs._internal import ( NoOpLogger, @@ -95,6 +94,47 @@ def test_logger_provider_init(self, resource_patch): self.assertIsNotNone(logger_provider._at_exit_handler) +class TestReadableLogRecord(unittest.TestCase): + def setUp(self): + self.log_record = LogRecord( + timestamp=1234567890, + observed_timestamp=1234567891, + body="Test log message", + attributes={"key": "value"}, + severity_number=SeverityNumber.INFO, + severity_text="INFO", + ) + self.resource = Resource({"service.name": "test-service"}) + self.readable_log_record = ReadableLogRecord( + log_record=self.log_record, + resource=self.resource, + instrumentation_scope=None, + ) + + def test_readable_log_record_is_frozen(self): + """Test that ReadableLogRecord is frozen and cannot be modified.""" + with self.assertRaises((AttributeError, TypeError)): + self.readable_log_record.log_record = LogRecord( + timestamp=999, body="Modified" + ) + + def test_readable_log_record_can_read_attributes(self): + """Test that ReadableLogRecord provides read access to all fields.""" + self.assertEqual( + self.readable_log_record.log_record.timestamp, 1234567890 + ) + self.assertEqual( + self.readable_log_record.log_record.body, "Test log message" + ) + self.assertEqual( + self.readable_log_record.log_record.attributes["key"], "value" + ) + self.assertEqual( + self.readable_log_record.resource.attributes["service.name"], + "test-service", + ) + + class TestLogger(unittest.TestCase): @staticmethod def _get_logger(): @@ -126,7 +166,7 @@ def test_can_emit_logrecord(self): def test_can_emit_api_logrecord(self): logger, log_record_processor_mock = self._get_logger() - api_log_record = APILogRecord( + api_log_record = LogRecord( observed_timestamp=0, body="a log line", ) @@ -137,18 +177,18 @@ def test_can_emit_api_logrecord(self): self.assertTrue(isinstance(log_record, LogRecord)) self.assertEqual(log_record.timestamp, None) self.assertEqual(log_record.observed_timestamp, 0) - self.assertEqual(log_record.context, {}) + self.assertIsNotNone(log_record.context) self.assertEqual(log_record.severity_number, None) self.assertEqual(log_record.severity_text, None) self.assertEqual(log_record.body, "a log line") self.assertEqual(log_record.attributes, {}) self.assertEqual(log_record.event_name, None) - self.assertEqual(log_record.resource, logger.resource) + self.assertEqual(log_data.resource, logger.resource) def test_can_emit_with_keywords_arguments(self): logger, log_record_processor_mock = self._get_logger() - logger.emit( + log_record = LogRecord( timestamp=100, observed_timestamp=101, context=get_current(), @@ -158,16 +198,19 @@ def test_can_emit_with_keywords_arguments(self): attributes={"some": "attributes"}, event_name="event_name", ) + 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] - log_record = log_data.log_record - self.assertTrue(isinstance(log_record, LogRecord)) - self.assertEqual(log_record.timestamp, 100) - self.assertEqual(log_record.observed_timestamp, 101) - self.assertEqual(log_record.context, {}) - self.assertEqual(log_record.severity_number, SeverityNumber.WARN) - self.assertEqual(log_record.severity_text, "warn") - self.assertEqual(log_record.body, "a body") - self.assertEqual(log_record.attributes, {"some": "attributes"}) - self.assertEqual(log_record.event_name, "event_name") - self.assertEqual(log_record.resource, logger.resource) + result_log_record = log_data.log_record + self.assertTrue(isinstance(result_log_record, LogRecord)) + self.assertEqual(result_log_record.timestamp, 100) + self.assertEqual(result_log_record.observed_timestamp, 101) + self.assertIsNotNone(result_log_record.context) + self.assertEqual( + result_log_record.severity_number, SeverityNumber.WARN + ) + self.assertEqual(result_log_record.severity_text, "warn") + self.assertEqual(result_log_record.body, "a body") + self.assertEqual(result_log_record.attributes, {"some": "attributes"}) + self.assertEqual(result_log_record.event_name, "event_name") + self.assertEqual(log_data.resource, logger.resource) diff --git a/opentelemetry-sdk/tests/logs/test_multi_log_processor.py b/opentelemetry-sdk/tests/logs/test_multi_log_processor.py index e121f136223..c35ba120e37 100644 --- a/opentelemetry-sdk/tests/logs/test_multi_log_processor.py +++ b/opentelemetry-sdk/tests/logs/test_multi_log_processor.py @@ -21,13 +21,13 @@ from abc import ABC, abstractmethod from unittest.mock import Mock -from opentelemetry._logs import SeverityNumber +from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.sdk._logs._internal import ( ConcurrentMultiLogRecordProcessor, LoggerProvider, LoggingHandler, - LogRecord, LogRecordProcessor, + ReadWriteLogRecord, SynchronousMultiLogRecordProcessor, ) @@ -38,11 +38,14 @@ def __init__(self, exporter, logs_list): self._log_list = logs_list self._closed = False - def on_emit(self, log_data): + def on_emit(self, log_record: ReadWriteLogRecord): if self._closed: return self._log_list.append( - (log_data.log_record.body, log_data.log_record.severity_text) + ( + log_record.log_record.body, + log_record.log_record.severity_text, + ) ) def shutdown(self): @@ -105,11 +108,13 @@ def _get_multi_log_record_processor(self): pass def make_record(self): - return LogRecord( - timestamp=1622300111608942000, - severity_text="WARN", - severity_number=SeverityNumber.WARN, - body="Warning message", + return ReadWriteLogRecord( + LogRecord( + timestamp=1622300111608942000, + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Warning message", + ) ) def test_on_emit(self): diff --git a/opentelemetry-sdk/tests/shared_internal/test_batch_processor.py b/opentelemetry-sdk/tests/shared_internal/test_batch_processor.py index f07ebc5ae76..f5ed15110b9 100644 --- a/opentelemetry-sdk/tests/shared_internal/test_batch_processor.py +++ b/opentelemetry-sdk/tests/shared_internal/test_batch_processor.py @@ -27,10 +27,12 @@ import pytest -from opentelemetry.sdk._logs import ( - LogData, +from opentelemetry._logs import ( LogRecord, ) +from opentelemetry.sdk._logs import ( + ReadWriteLogRecord, +) from opentelemetry.sdk._logs.export import ( BatchLogRecordProcessor, ) @@ -41,7 +43,7 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.util.instrumentation import InstrumentationScope -EMPTY_LOG = LogData( +EMPTY_LOG = ReadWriteLogRecord( log_record=LogRecord(), instrumentation_scope=InstrumentationScope("example", "example"), )