diff --git a/.changelog/5305.added b/.changelog/5305.added new file mode 100644 index 00000000000..507a490ac98 --- /dev/null +++ b/.changelog/5305.added @@ -0,0 +1 @@ + `opentelemetry-exporter-otlp-proto-common`, `opentelemetry-exporter-otlp-json-common`: encoders now always accept null, and encode it as an empty `AnyValue` in accordance with the spec. \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/__init__.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/__init__.py index 5a66b4dfae0..a59ab85fe47 100644 --- a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/__init__.py @@ -6,12 +6,9 @@ from __future__ import annotations import logging -from collections.abc import Collection, Mapping, Sequence +from collections.abc import Mapping, Sequence from os import environ -from typing import ( - Any, - cast, -) +from typing import Any from opentelemetry.proto_json.common.v1.common import AnyValue as JSONAnyValue from opentelemetry.proto_json.common.v1.common import ( @@ -47,7 +44,7 @@ ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.util.types import Attributes +from opentelemetry.util.types import _ExtendedAttributes _logger = logging.getLogger(__name__) @@ -71,9 +68,9 @@ def _encode_resource(resource: Resource) -> JSONResource: # pylint: disable-next=too-many-return-statements -def _encode_value(value: Any, allow_null: bool = False) -> JSONAnyValue | None: - if allow_null and value is None: - return None +def _encode_value(value: Any) -> JSONAnyValue: + if value is None: + return JSONAnyValue() if isinstance(value, bool): return JSONAnyValue(bool_value=value) if isinstance(value, str): @@ -87,45 +84,20 @@ def _encode_value(value: Any, allow_null: bool = False) -> JSONAnyValue | None: if isinstance(value, Sequence): return JSONAnyValue( array_value=JSONArrayValue( - values=cast( - list[JSONAnyValue], - _encode_array(value, allow_null=allow_null), - ) + values=[_encode_value(v) for v in value] ) ) if isinstance(value, Mapping): return JSONAnyValue( kvlist_value=JSONKeyValueList( - values=[ - _encode_key_value(str(k), v, allow_null=allow_null) - for k, v in value.items() - ] + values=[_encode_key_value(str(k), v) for k, v in value.items()] ) ) raise TypeError(f"Invalid type {type(value)} of value {value}") -def _encode_key_value( - key: str, value: Any, allow_null: bool = False -) -> JSONKeyValue: - return JSONKeyValue( - key=key, value=_encode_value(value, allow_null=allow_null) - ) - - -def _encode_array( - array: Collection[Any], allow_null: bool = False -) -> list[JSONAnyValue | None]: - if not allow_null: - # Let the exception get raised by _encode_value() - return [_encode_value(v, allow_null=allow_null) for v in array] - - return [ - _encode_value(v, allow_null=allow_null) - if v is not None - else JSONAnyValue() - for v in array - ] +def _encode_key_value(key: str, value: Any) -> JSONKeyValue: + return JSONKeyValue(key=key, value=_encode_value(value)) def _encode_span_id(span_id: int) -> bytes: @@ -137,8 +109,7 @@ def _encode_trace_id(trace_id: int) -> bytes: def _encode_attributes( - attributes: Attributes | None, - allow_null: bool = False, + attributes: _ExtendedAttributes | None, ) -> list[JSONKeyValue]: if not attributes: return [] @@ -146,9 +117,7 @@ def _encode_attributes( for key, value in attributes.items(): # pylint: disable=broad-exception-caught try: - json_attributes.append( - _encode_key_value(key, value, allow_null=allow_null) - ) + json_attributes.append(_encode_key_value(key, value)) except Exception as error: _logger.exception("Failed to encode key %s: %s", key, error) return json_attributes diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/_log_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/_log_encoder/__init__.py index 2569892bf60..104d17dbc8f 100644 --- a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/_log_encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/_log_encoder/__init__.py @@ -50,13 +50,10 @@ def _encode_log(readable_log_record: ReadableLogRecord) -> JSONLogRecord: if readable_log_record.log_record.trace_id else None, flags=int(readable_log_record.log_record.trace_flags), - body=_encode_value( - readable_log_record.log_record.body, allow_null=True - ), + body=_encode_value(readable_log_record.log_record.body), severity_text=readable_log_record.log_record.severity_text, attributes=_encode_attributes( cast(Attributes, readable_log_record.log_record.attributes), - allow_null=True, ), dropped_attributes_count=readable_log_record.dropped_attributes, severity_number=getattr( diff --git a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py index 7f845e757f2..4eff30be6ff 100644 --- a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_common_encoder.py @@ -9,7 +9,6 @@ from unittest.mock import patch from opentelemetry.exporter.otlp.json.common._internal import ( - _encode_array, _encode_attributes, _encode_instrumentation_scope, _encode_key_value, @@ -57,6 +56,11 @@ _COMMON_LOGGER_NAME = "opentelemetry.exporter.otlp.json.common._internal" +class CallingStrRaisesException: + def __str__(self): + raise ValueError("Cannot encode") + + class TestCommonEncoder(unittest.TestCase): def test_encode_value(self): cases = [ @@ -151,29 +155,23 @@ def test_encode_value_mapping(self): result_dict = result.to_dict() self.assertIn("kvlistValue", result_dict) - def test_encode_value_none_not_allowed(self): - with self.assertRaises(TypeError): - _encode_value(None) - - def test_encode_value_none_allowed(self): - result = _encode_value(None, allow_null=True) - self.assertIsNone(result) + def test_encode_null(self): + result = _encode_value(None) + self.assertEqual(result, JSONAnyValue()) def test_encode_array_with_nulls(self): - result = _encode_array([1, None, 2], allow_null=True) - self.assertEqual( - result, - [ - JSONAnyValue(int_value=1), - JSONAnyValue(), - JSONAnyValue(int_value=2), - ], + result = _encode_value([1, None, 2]) + expected = JSONAnyValue( + array_value=JSONArrayValue( + values=[ + JSONAnyValue(int_value=1), + JSONAnyValue(), + JSONAnyValue(int_value=2), + ] + ) ) - self.assertEqual(result[1].to_dict(), {}) - - def test_encode_array_none_raises(self): - with self.assertRaises(TypeError): - _encode_array([1, None, 2], allow_null=False) + self.assertEqual(result, expected) + self.assertEqual(result.array_value.values[1].to_dict(), {}) def test_encode_key_value(self): result = _encode_key_value("mykey", "myval") @@ -254,7 +252,9 @@ def test_encode_attributes_empty(self): def test_encode_attributes_error_skips_bad_key(self): with self.assertLogs(level=ERROR) as error: - result = _encode_attributes({"a": 1, "bad_key": None, "b": 2}) + result = _encode_attributes( + {"a": 1, "bad_key": CallingStrRaisesException(), "b": 2} + ) self.assertEqual(len(error.records), 1) self.assertEqual(error.records[0].msg, "Failed to encode key %s: %s") @@ -268,10 +268,14 @@ def test_encode_attributes_error_skips_bad_key(self): ], ) - def test_encode_attributes_error_list_none(self): + def test_encode_attributes_error_list_unencodable_item(self): with self.assertLogs(level=ERROR) as error: result = _encode_attributes( - {"a": 1, "bad_key": ["test", None, "test"], "b": 2} + { + "a": 1, + "bad_key": ["test", CallingStrRaisesException(), "test"], + "b": 2, + } ) self.assertEqual(len(error.records), 1) diff --git a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_log_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_log_encoder.py index 12e1dfdc617..6a4e2faa2b6 100644 --- a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_log_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_log_encoder.py @@ -1,34 +1,34 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -# pylint: disable=unsubscriptable-object - -import json import unittest from opentelemetry._logs import LogRecord, SeverityNumber -from opentelemetry.exporter.otlp.json.common._internal import ( +from opentelemetry.exporter.otlp.proto.common._internal import ( + _encode_attributes, _encode_span_id, _encode_trace_id, + _encode_value, ) -from opentelemetry.exporter.otlp.json.common._log_encoder import encode_logs -from opentelemetry.proto_json.collector.logs.v1.logs_service import ( - ExportLogsServiceRequest as JSONExportLogsServiceRequest, +from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, ) -from opentelemetry.proto_json.common.v1.common import AnyValue as JSONAnyValue -from opentelemetry.proto_json.common.v1.common import ( - ArrayValue as JSONArrayValue, +from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue +from opentelemetry.proto.common.v1.common_pb2 import ( + InstrumentationScope as PB2InstrumentationScope, ) -from opentelemetry.proto_json.common.v1.common import KeyValue as JSONKeyValue -from opentelemetry.proto_json.common.v1.common import ( - KeyValueList as JSONKeyValueList, +from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue +from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord +from opentelemetry.proto.logs.v1.logs_pb2 import ( + ResourceLogs as PB2ResourceLogs, ) -from opentelemetry.sdk._logs import ( - LogRecordLimits, - ReadableLogRecord, - ReadWriteLogRecord, +from opentelemetry.proto.logs.v1.logs_pb2 import ScopeLogs as PB2ScopeLogs +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as PB2Resource, ) -from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk._logs import LogRecordLimits, ReadWriteLogRecord +from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import ( NonRecordingSpan, @@ -36,326 +36,270 @@ TraceFlags, set_span_in_context, ) -from tests import ( - SPAN_ID, - TIME, - TRACE_ID, - assert_proto_json_equal, - make_log, - make_log_context, -) - -def _get_first_log_record(result): - return result.resource_logs[0].scope_logs[0].log_records[0] +_CONTEXT_LOG = set_span_in_context( + NonRecordingSpan( + SpanContext( + 89564621134313219400156819398935297684, + 1312458408527513268, + False, + TraceFlags(0x01), + ) + ) +) class TestOTLPLogEncoder(unittest.TestCase): - def test_encode_single_log(self): - log = make_log() - result = encode_logs([log]) - - self.assertEqual(len(result.resource_logs), 1) - self.assertEqual(len(result.resource_logs[0].scope_logs), 1) - self.assertEqual( - len(result.resource_logs[0].scope_logs[0].log_records), 1 + def test_encode_basic_log_record(self): + basic_log_record = ReadWriteLogRecord( + LogRecord( + timestamp=1644650195189786880, + observed_timestamp=1644650195189786881, + context=_CONTEXT_LOG, + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Do not go gentle into that good night. Rage, rage against the dying of the light", + attributes={"a": 1, "b": "c"}, + ), + resource=SDKResource( + {"first_resource": "value"}, + "resource_schema_url", + ), + instrumentation_scope=InstrumentationScope( + "first_name", "first_version" + ), + ) + pb2_service_request = ExportLogsServiceRequest( + resource_logs=[ + PB2ResourceLogs( + resource=PB2Resource( + attributes=[ + PB2KeyValue( + key="first_resource", + value=PB2AnyValue(string_value="value"), + ) + ] + ), + scope_logs=[ + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="first_name", version="first_version" + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650195189786880, + observed_time_unix_nano=1644650195189786881, + trace_id=_encode_trace_id( + 89564621134313219400156819398935297684 + ), + span_id=_encode_span_id( + 1312458408527513268 + ), + flags=int(TraceFlags(0x01)), + severity_text="WARN", + severity_number=SeverityNumber.WARN.value, + body=_encode_value( + "Do not go gentle into that good night. Rage, rage against the dying of the light" + ), + attributes=_encode_attributes( + {"a": 1, "b": "c"} + ), + ) + ], + ), + ], + schema_url="resource_schema_url", + ) + ] + ) + self.assertEqual(encode_logs([basic_log_record]), pb2_service_request) + + def test_encode_log_record_with_no_instrumentation_scope_and_dict_body( + self, + ): + log_record_with_no_instrumentation_scope_and_dict_body = ( + ReadWriteLogRecord( + LogRecord( + timestamp=1644650427658989056, + observed_timestamp=1644650427658989057, + context=_CONTEXT_LOG, + severity_text="DEBUG", + severity_number=SeverityNumber.DEBUG, + body={"error": None, "array_with_nones": [1, None, 2]}, + attributes={"a": 1, "b": "c"}, + ), + resource=SDKResource({"second_resource": "CASE"}), + instrumentation_scope=None, + ) + ) + pb2_resource_logs = PB2ResourceLogs( + resource=PB2Resource( + attributes=[ + PB2KeyValue( + key="second_resource", + value=PB2AnyValue(string_value="CASE"), + ) + ] + ), + scope_logs=[ + PB2ScopeLogs( + scope=PB2InstrumentationScope(), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650427658989056, + observed_time_unix_nano=1644650427658989057, + trace_id=_encode_trace_id( + 89564621134313219400156819398935297684 + ), + span_id=_encode_span_id(1312458408527513268), + flags=int(TraceFlags(0x01)), + severity_text="DEBUG", + severity_number=SeverityNumber.DEBUG.value, + body=_encode_value( + { + "error": None, + "array_with_nones": [1, None, 2], + } + ), + attributes=_encode_attributes({"a": 1, "b": "c"}), + ) + ], + ) + ], ) - - lr = _get_first_log_record(result) - self.assertEqual(lr.time_unix_nano, TIME) - self.assertEqual(lr.observed_time_unix_nano, TIME + 1000) - self.assertEqual(lr.severity_text, "INFO") - self.assertEqual(lr.severity_number, SeverityNumber.INFO.value) self.assertEqual( - lr.body, JSONAnyValue(string_value="test log message") + encode_logs( + [log_record_with_no_instrumentation_scope_and_dict_body] + ), + ExportLogsServiceRequest(resource_logs=[pb2_resource_logs]), ) - def test_encode_log_with_trace_context(self): - ctx = make_log_context() - log = make_log(context=ctx) - result = encode_logs([log]) - lr = _get_first_log_record(result) - - self.assertEqual(lr.trace_id, _encode_trace_id(TRACE_ID)) - self.assertEqual(lr.span_id, _encode_span_id(SPAN_ID)) - self.assertEqual(lr.flags, int(TraceFlags(0x01))) - - def test_encode_log_zero_span_trace_id(self): - ctx = set_span_in_context(NonRecordingSpan(SpanContext(0, 0, False))) - log = make_log(context=ctx) - result = encode_logs([log]) - lr = _get_first_log_record(result) - - self.assertIsNone(lr.span_id) - self.assertIsNone(lr.trace_id) - - lr_dict = result.to_dict()["resourceLogs"][0]["scopeLogs"][0][ - "logRecords" - ][0] - self.assertNotIn("traceId", lr_dict) - self.assertNotIn("spanId", lr_dict) - - def test_encode_log_severity_numbers(self): - cases = [ - ("WARN", SeverityNumber.WARN), - ("DEBUG", SeverityNumber.DEBUG), - ("INFO", SeverityNumber.INFO), - ("ERROR", SeverityNumber.ERROR), - ("FATAL", SeverityNumber.FATAL), - ] - for text, number in cases: - with self.subTest(severity=text): - log = make_log(severity_text=text, severity_number=number) - result = encode_logs([log]) - lr = _get_first_log_record(result) - self.assertEqual(lr.severity_text, text) - self.assertEqual(lr.severity_number, number.value) - - def test_encode_log_string_body(self): - log = make_log(body="hello world") - result = encode_logs([log]) - lr = _get_first_log_record(result) - self.assertEqual(lr.body, JSONAnyValue(string_value="hello world")) - - def test_encode_log_dict_body_with_nulls(self): - log = make_log(body={"error": None, "array_with_nones": [1, None, 2]}) - result = encode_logs([log]) - lr = _get_first_log_record(result) - - self.assertEqual( - lr.body, - JSONAnyValue( - kvlist_value=JSONKeyValueList( - values=[ - JSONKeyValue(key="error"), - JSONKeyValue( - key="array_with_nones", - value=JSONAnyValue( - array_value=JSONArrayValue( - values=[ - JSONAnyValue(int_value=1), - JSONAnyValue(), - JSONAnyValue(int_value=2), - ] - ) + def test_encode_log_record_with_empty_resource_and_dict_attribute_value( + self, + ): + log_record_with_empty_resource_and_dict_attribute_value = ReadWriteLogRecord( + LogRecord( + timestamp=1644650584292683033, + observed_timestamp=1644650584292683033, + context=_CONTEXT_LOG, + severity_text="FATAL", + severity_number=SeverityNumber.FATAL, + body="This instrumentation scope has a schema url and attributes", + attributes={ + "extended": { + "sequence": [{"inner": "mapping", "none": None}] + } + }, + ), + resource=SDKResource({}), + instrumentation_scope=InstrumentationScope( + "scope_with_attributes", + "scope_with_attributes_version", + "instrumentation_schema_url", + {"one": 1, "two": "2"}, + ), + ) + pb2_resource_logs = PB2ResourceLogs( + resource=PB2Resource(attributes=[]), + scope_logs=[ + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="scope_with_attributes", + version="scope_with_attributes_version", + attributes=_encode_attributes({"one": 1, "two": "2"}), + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650584292683033, + observed_time_unix_nano=1644650584292683033, + trace_id=_encode_trace_id( + 89564621134313219400156819398935297684 ), - ), - ] + span_id=_encode_span_id(1312458408527513268), + flags=int(TraceFlags(0x01)), + severity_text="FATAL", + severity_number=SeverityNumber.FATAL.value, + body=_encode_value( + "This instrumentation scope has a schema url and attributes" + ), + attributes=_encode_attributes( + { + "extended": { + "sequence": [ + {"inner": "mapping", "none": None} + ] + } + } + ), + ) + ], + schema_url="instrumentation_schema_url", ) + ], + ) + self.assertEqual( + encode_logs( + [log_record_with_empty_resource_and_dict_attribute_value] ), + ExportLogsServiceRequest(resource_logs=[pb2_resource_logs]), ) - def test_encode_log_no_body(self): - log = make_log(body=None) - result = encode_logs([log]) - lr = _get_first_log_record(result) - self.assertIsNone(lr.body) - - lr_dict = result.to_dict()["resourceLogs"][0]["scopeLogs"][0][ - "logRecords" - ][0] - self.assertNotIn("body", lr_dict) - - def test_encode_log_extended_attributes(self): - log = make_log( - attributes={ - "extended": {"sequence": [{"inner": "mapping", "none": None}]} - } + 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], "dropped_attributes")) + self.assertEqual( + # pylint:disable=no-member + encoded_logs.resource_logs[0] + .scope_logs[0] + .log_records[0] + .dropped_attributes_count, + 2, ) - result = encode_logs([log]) - lr = _get_first_log_record(result) - self.assertIsNotNone(lr.attributes) - self.assertEqual(len(lr.attributes), 1) - self.assertEqual(lr.attributes[0].key, "extended") - self.assertIsNotNone(lr.attributes[0].value.kvlist_value) - - def test_encode_log_empty_record(self): - ctx = make_log_context() - log = ReadableLogRecord( - LogRecord(observed_timestamp=TIME + 1000, context=ctx), - resource=Resource({}), - instrumentation_scope=InstrumentationScope("test", "1.0"), + @staticmethod + def _get_test_logs_dropped_attributes() -> list[ReadWriteLogRecord]: + ctx_log1 = set_span_in_context( + NonRecordingSpan( + SpanContext( + 89564621134313219400156819398935297684, + 1312458408527513268, + False, + TraceFlags(0x01), + ) + ) ) - result = encode_logs([log]) - lr = _get_first_log_record(result) - - self.assertIsNone(lr.time_unix_nano) - self.assertEqual(lr.observed_time_unix_nano, TIME + 1000) - self.assertIsNone(lr.severity_text) - self.assertIsNone(lr.severity_number) - self.assertIsNone(lr.body) - self.assertEqual(lr.attributes, []) - - def test_encode_log_event_name(self): - log = make_log(body="event happened", event_name="my.event") - result = encode_logs([log]) - lr = _get_first_log_record(result) - self.assertEqual(lr.event_name, "my.event") - - lr_dict = result.to_dict()["resourceLogs"][0]["scopeLogs"][0][ - "logRecords" - ][0] - self.assertEqual(lr_dict["eventName"], "my.event") - - def test_dropped_attributes_count(self): - ctx = make_log_context() - # ReadWriteLogRecord applies limits via __post_init__ - log = ReadWriteLogRecord( + log1 = ReadWriteLogRecord( LogRecord( - timestamp=TIME, - context=ctx, + timestamp=1644650195189786880, + context=ctx_log1, severity_text="WARN", severity_number=SeverityNumber.WARN, - body="test", + body="Do not go gentle into that good night. Rage, rage against the dying of the light", attributes={"a": 1, "b": "c", "user_id": "B121092"}, ), - resource=Resource({}), + resource=SDKResource({"first_resource": "value"}), limits=LogRecordLimits(max_attributes=1), - instrumentation_scope=InstrumentationScope("test", "1.0"), - ) - result = encode_logs([log]) - lr = _get_first_log_record(result) - self.assertEqual(lr.dropped_attributes_count, 2) - - def test_encode_log_grouping_by_resource(self): - r1 = Resource({"service": "svc1"}) - r2 = Resource({"service": "svc2"}) - log1 = make_log(body="r1", resource=r1) - log2 = make_log(body="r2", resource=r2) - - result = encode_logs([log1, log2]) - self.assertEqual(len(result.resource_logs), 2) - - groups = {} - # pylint: disable-next=not-an-iterable - for rl in result.resource_logs: - svc_val = rl.resource.attributes[0].value.string_value - bodies = [ - lr.body.string_value - for sl in rl.scope_logs - for lr in sl.log_records - ] - groups[svc_val] = bodies - - self.assertEqual(groups["svc1"], ["r1"]) - self.assertEqual(groups["svc2"], ["r2"]) - - def test_encode_log_grouping_by_scope(self): - resource = Resource({"svc": "test"}) - scope1 = InstrumentationScope("lib1", "1.0") - scope2 = InstrumentationScope("lib2", "2.0") - - logs = [ - make_log( - body="s1a", - resource=resource, - instrumentation_scope=scope1, - ), - make_log( - body="s1b", - resource=resource, - instrumentation_scope=scope1, - ), - make_log( - body="s2", - resource=resource, - instrumentation_scope=scope2, + instrumentation_scope=InstrumentationScope( + "first_name", "first_version" ), - ] - result = encode_logs(logs) - self.assertEqual(len(result.resource_logs), 1) - scope_logs = result.resource_logs[0].scope_logs - self.assertEqual(len(scope_logs), 2) - - groups = { - sl.scope.name: [lr.body.string_value for lr in sl.log_records] - for sl in scope_logs - } - self.assertEqual(groups["lib1"], ["s1a", "s1b"]) - self.assertEqual(groups["lib2"], ["s2"]) - self.assertEqual(scope_logs[0].scope.version, "1.0") - self.assertEqual(scope_logs[1].scope.version, "2.0") - - def test_encode_log_scope_schema_url(self): - scope = InstrumentationScope("my_scope", "1.0", "schema_url_value") - log = make_log(instrumentation_scope=scope) - result = encode_logs([log]) - self.assertEqual( - result.resource_logs[0].scope_logs[0].schema_url, - "schema_url_value", ) - - def test_encode_log_scope_attributes(self): - scope = InstrumentationScope( - "my_scope", - "1.0", - attributes={"scope_key": 42}, + ctx_log2 = set_span_in_context( + NonRecordingSpan(SpanContext(0, 0, False)) ) - log = make_log(instrumentation_scope=scope) - result = encode_logs([log]) - encoded_scope = result.resource_logs[0].scope_logs[0].scope - self.assertEqual(encoded_scope.name, "my_scope") - self.assertEqual(len(encoded_scope.attributes), 1) - self.assertEqual(encoded_scope.attributes[0].key, "scope_key") - - def test_encode_log_none_scope(self): - log = make_log(instrumentation_scope=None) - result = encode_logs([log]) - encoded_scope = result.resource_logs[0].scope_logs[0].scope - self.assertFalse(encoded_scope.name) - self.assertFalse(encoded_scope.version) - self.assertEqual(encoded_scope.to_dict(), {}) - - def test_encode_logs_to_dict(self): - ctx = make_log_context() - log = make_log(context=ctx, attributes={"key": "val"}) - result = encode_logs([log]) - result_dict = result.to_dict() - - self.assertIn("resourceLogs", result_dict) - lr = result_dict["resourceLogs"][0]["scopeLogs"][0]["logRecords"][0] - - self.assertIsInstance(lr["traceId"], str) - self.assertEqual(len(lr["traceId"]), 32) - self.assertIsInstance(lr["spanId"], str) - self.assertEqual(len(lr["spanId"]), 16) - self.assertIsInstance(lr["timeUnixNano"], str) - self.assertIsInstance(lr["observedTimeUnixNano"], str) - self.assertIn("severityText", lr) - self.assertIn("severityNumber", lr) - - def test_encode_logs_json_roundtrip(self): - ctx1 = make_log_context() - ctx2 = make_log_context(trace_id=12345678, span_id=87654321) - logs = [ - make_log( - body="log with context", - context=ctx1, - attributes={"a": 1}, - resource=Resource({"r": "v"}, "resource_schema"), - instrumentation_scope=InstrumentationScope( - "lib", - "1.0", - "scope_schema", - {"sk": "sv"}, - ), - ), - make_log( - body={"dict_body": [1, None, 2]}, - context=ctx2, - resource=Resource({}), - ), - make_log( - body=None, + log2 = ReadWriteLogRecord( + LogRecord( + timestamp=1644650249738562048, + context=ctx_log2, severity_text="WARN", severity_number=SeverityNumber.WARN, - event_name="my.event", + body="Cooper, this is no time for caution!", + attributes={}, + ), + resource=SDKResource({"second_resource": "CASE"}), + instrumentation_scope=InstrumentationScope( + "second_name", "second_version" ), - ] - result = encode_logs(logs) - json_str = result.to_json() - roundtripped = JSONExportLogsServiceRequest.from_dict( - json.loads(json_str) ) - assert_proto_json_equal(self, result, roundtripped) + + return [log1, log2] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py index 8dbbc212f74..7528b5994b8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py @@ -51,9 +51,9 @@ def _encode_resource(resource: Resource) -> PB2Resource: return PB2Resource(attributes=_encode_attributes(resource.attributes)) -def _encode_value(value: Any, allow_null: bool = False) -> PB2AnyValue | None: - if allow_null is True and value is None: - return None +def _encode_value(value: Any) -> PB2AnyValue: + if value is None: + return PB2AnyValue() if isinstance(value, bool): return PB2AnyValue(bool_value=value) if isinstance(value, str): @@ -66,45 +66,19 @@ def _encode_value(value: Any, allow_null: bool = False) -> PB2AnyValue | None: return PB2AnyValue(bytes_value=value) if isinstance(value, Sequence): return PB2AnyValue( - array_value=PB2ArrayValue( - values=_encode_array(value, allow_null=allow_null) - ) + array_value=PB2ArrayValue(values=[_encode_value(v) for v in value]) ) - elif isinstance(value, Mapping): + if isinstance(value, Mapping): return PB2AnyValue( kvlist_value=PB2KeyValueList( - values=[ - _encode_key_value(str(k), v, allow_null=allow_null) - for k, v in value.items() - ] + values=[_encode_key_value(str(k), v) for k, v in value.items()] ) ) raise Exception(f"Invalid type {type(value)} of value {value}") -def _encode_key_value( - key: str, value: Any, allow_null: bool = False -) -> PB2KeyValue: - return PB2KeyValue( - key=key, value=_encode_value(value, allow_null=allow_null) - ) - - -def _encode_array( - array: Sequence[Any], allow_null: bool = False -) -> Sequence[PB2AnyValue]: - if not allow_null: - # Let the exception get raised by _encode_value() - return [_encode_value(v, allow_null=allow_null) for v in array] - - return [ - _encode_value(v, allow_null=allow_null) - if v is not None - # Use an empty AnyValue to represent None in an array. Behavior may change pending - # https://github.com/open-telemetry/opentelemetry-specification/issues/4392 - else PB2AnyValue() - for v in array - ] +def _encode_key_value(key: str, value: Any) -> PB2KeyValue: + return PB2KeyValue(key=key, value=_encode_value(value)) def _encode_span_id(span_id: int) -> bytes: @@ -116,21 +90,17 @@ def _encode_trace_id(trace_id: int) -> bytes: def _encode_attributes( - attributes: _ExtendedAttributes, - allow_null: bool = False, -) -> list[PB2KeyValue] | None: - if attributes: - pb2_attributes = [] - for key, value in attributes.items(): - # pylint: disable=broad-exception-caught - try: - pb2_attributes.append( - _encode_key_value(key, value, allow_null=allow_null) - ) - except Exception as error: - _logger.exception("Failed to encode key %s: %s", key, error) - else: - pb2_attributes = None + attributes: _ExtendedAttributes | None, +) -> list[PB2KeyValue]: + if not attributes: + return [] + pb2_attributes = [] + for key, value in attributes.items(): + # pylint: disable=broad-exception-caught + try: + pb2_attributes.append(_encode_key_value(key, value)) + except Exception as error: + _logger.exception("Failed to encode key %s: %s", key, error) return pb2_attributes 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 453b4e79b8d..86b4380bd95 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 @@ -39,17 +39,16 @@ def _encode_log(readable_log_record: ReadableLogRecord) -> PB2LogRecord: if readable_log_record.log_record.trace_id == 0 else _encode_trace_id(readable_log_record.log_record.trace_id) ) - body = readable_log_record.log_record.body return PB2LogRecord( 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(readable_log_record.log_record.trace_flags), - body=_encode_value(body, allow_null=True), + body=_encode_value(readable_log_record.log_record.body), severity_text=readable_log_record.log_record.severity_text, attributes=_encode_attributes( - readable_log_record.log_record.attributes, allow_null=True + readable_log_record.log_record.attributes ), dropped_attributes_count=readable_log_record.dropped_attributes, severity_number=getattr( diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py index 58ce2b337e4..4c2dd7bb427 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py @@ -14,6 +14,11 @@ from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue +class CallingStrRaisesException: + def __str__(self): + raise ValueError("Cannot encode") + + class TestOTLPAttributeEncoder(unittest.TestCase): def test_encode_attributes_all_kinds(self): result = _encode_attributes( @@ -77,10 +82,10 @@ def test_encode_attributes_all_kinds(self): ], ) - def test_encode_attributes_error_list_none(self): + def test_encode_attributes_error_logs_key(self): with self.assertLogs(level=ERROR) as error: result = _encode_attributes( - {"a": 1, "bad_key": ["test", None, "test"], "b": 2} + {"a": 1, "bad_key": CallingStrRaisesException(), "b": 2} ) self.assertEqual(len(error.records), 1) @@ -94,19 +99,3 @@ def test_encode_attributes_error_list_none(self): PB2KeyValue(key="b", value=PB2AnyValue(int_value=2)), ], ) - - def test_encode_attributes_error_logs_key(self): - with self.assertLogs(level=ERROR) as error: - result = _encode_attributes({"a": 1, "bad_key": None, "b": 2}) - - self.assertEqual(len(error.records), 1) - self.assertEqual(error.records[0].msg, "Failed to encode key %s: %s") - self.assertEqual(error.records[0].args[0], "bad_key") - self.assertIsInstance(error.records[0].args[1], Exception) - self.assertEqual( - result, - [ - PB2KeyValue(key="a", value=PB2AnyValue(int_value=1)), - PB2KeyValue(key="b", value=PB2AnyValue(int_value=2)), - ], - ) 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 a84134fab70..6a4e2faa2b6 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,16 +15,10 @@ ExportLogsServiceRequest, ) from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue -from opentelemetry.proto.common.v1.common_pb2 import ( - ArrayValue as PB2ArrayValue, -) from opentelemetry.proto.common.v1.common_pb2 import ( InstrumentationScope as PB2InstrumentationScope, ) from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue -from opentelemetry.proto.common.v1.common_pb2 import ( - KeyValueList as PB2KeyValueList, -) from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord from opentelemetry.proto.logs.v1.logs_pb2 import ( ResourceLogs as PB2ResourceLogs, @@ -43,55 +37,25 @@ set_span_in_context, ) - -class TestOTLPLogEncoder(unittest.TestCase): - def test_encode(self): - sdk_logs, expected_encoding = self.get_test_logs() - self.assertEqual(encode_logs(sdk_logs), expected_encoding) - - def test_encode_no_body(self): - sdk_logs, expected_encoding = self.get_test_logs() - for log in sdk_logs: - log.log_record.body = None - - for resource_log in expected_encoding.resource_logs: - for scope_log in resource_log.scope_logs: - for log_record in scope_log.log_records: - log_record.ClearField("body") - - self.assertEqual(encode_logs(sdk_logs), expected_encoding) - - 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], "dropped_attributes")) - self.assertEqual( - # pylint:disable=no-member - encoded_logs.resource_logs[0] - .scope_logs[0] - .log_records[0] - .dropped_attributes_count, - 2, +_CONTEXT_LOG = set_span_in_context( + NonRecordingSpan( + SpanContext( + 89564621134313219400156819398935297684, + 1312458408527513268, + False, + TraceFlags(0x01), ) + ) +) - @staticmethod - def _get_sdk_log_data() -> list[ReadWriteLogRecord]: - # pylint:disable=too-many-locals - ctx_log1 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 89564621134313219400156819398935297684, - 1312458408527513268, - False, - TraceFlags(0x01), - ) - ) - ) - log1 = ReadWriteLogRecord( + +class TestOTLPLogEncoder(unittest.TestCase): + def test_encode_basic_log_record(self): + basic_log_record = ReadWriteLogRecord( LogRecord( timestamp=1644650195189786880, observed_timestamp=1644650195189786881, - context=ctx_log1, + context=_CONTEXT_LOG, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Do not go gentle into that good night. Rage, rage against the dying of the light", @@ -105,222 +69,6 @@ def _get_sdk_log_data() -> list[ReadWriteLogRecord]: "first_name", "first_version" ), ) - - log2 = ReadWriteLogRecord( - LogRecord( - timestamp=1644650249738562048, - observed_timestamp=1644650249738562049, - severity_text="WARN", - severity_number=SeverityNumber.WARN, - body="Cooper, this is no time for caution!", - attributes={}, - ), - resource=SDKResource({"second_resource": "CASE"}), - instrumentation_scope=InstrumentationScope( - "second_name", "second_version" - ), - ) - - ctx_log3 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 271615924622795969659406376515024083555, - 4242561578944770265, - False, - TraceFlags(0x01), - ) - ) - ) - log3 = ReadWriteLogRecord( - LogRecord( - timestamp=1644650427658989056, - observed_timestamp=1644650427658989057, - context=ctx_log3, - severity_text="DEBUG", - severity_number=SeverityNumber.DEBUG, - body="To our galaxy", - attributes={"a": 1, "b": "c"}, - ), - resource=SDKResource({"second_resource": "CASE"}), - instrumentation_scope=None, - ) - - ctx_log4 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 212592107417388365804938480559624925555, - 6077757853989569223, - False, - TraceFlags(0x01), - ) - ) - ) - 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", - attributes={"filename": "model.py", "func_name": "run_method"}, - ), - resource=SDKResource( - {"first_resource": "value"}, - "resource_schema_url", - ), - instrumentation_scope=InstrumentationScope( - "another_name", "another_version" - ), - ) - - ctx_log5 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 212592107417388365804938480559624925555, - 6077757853989569445, - False, - TraceFlags(0x01), - ) - ) - ) - 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]}, - attributes={}, - ), - resource=SDKResource({}), - instrumentation_scope=InstrumentationScope( - "last_name", "last_version" - ), - ) - - ctx_log6 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 212592107417388365804938480559624925522, - 6077757853989569222, - False, - TraceFlags(0x01), - ) - ) - ) - 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", - 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", - "instrumentation_schema_url", - ), - ) - - ctx_log7 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 212592107417388365804938480559624925533, - 6077757853989569233, - False, - TraceFlags(0x01), - ) - ) - ) - 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", - 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", - "instrumentation_schema_url", - {"one": 1, "two": "2"}, - ), - ) - - ctx_log8 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 212592107417388365804938480559624925566, - 6077757853989569466, - False, - TraceFlags(0x01), - ) - ) - ) - log8 = ReadWriteLogRecord( - LogRecord( - timestamp=1644650584292683044, - observed_timestamp=1644650584292683044, - context=ctx_log8, - severity_text="INFO", - severity_number=SeverityNumber.INFO, - body="Test export of extended attributes", - attributes={ - "extended": { - "sequence": [{"inner": "mapping", "none": None}] - } - }, - ), - resource=SDKResource({}), - instrumentation_scope=InstrumentationScope( - "extended_name", "extended_version" - ), - ) - - ctx_log9 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 212592107417388365804938480559624925566, - 6077757853989569466, - False, - TraceFlags(0x01), - ) - ) - ) - log9 = ReadWriteLogRecord( - LogRecord( - # 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, - ) -> tuple[list[ReadWriteLogRecord], ExportLogsServiceRequest]: - sdk_logs = self._get_sdk_log_data() - pb2_service_request = ExportLogsServiceRequest( resource_logs=[ PB2ResourceLogs( @@ -354,284 +102,160 @@ def get_test_logs( "Do not go gentle into that good night. Rage, rage against the dying of the light" ), attributes=_encode_attributes( - {"a": 1, "b": "c"}, - allow_null=True, - ), - ) - ], - ), - PB2ScopeLogs( - scope=PB2InstrumentationScope( - name="another_name", - version="another_version", - ), - log_records=[ - PB2LogRecord( - time_unix_nano=1644650584292683008, - observed_time_unix_nano=1644650584292683009, - trace_id=_encode_trace_id( - 212592107417388365804938480559624925555 - ), - span_id=_encode_span_id( - 6077757853989569223 - ), - flags=int(TraceFlags(0x01)), - severity_text="INFO", - severity_number=SeverityNumber.INFO.value, - body=_encode_value( - "Love is the one thing that transcends time and space" - ), - attributes=_encode_attributes( - { - "filename": "model.py", - "func_name": "run_method", - }, - allow_null=True, - ), - ) - ], - ), - PB2ScopeLogs( - scope=PB2InstrumentationScope( - name="scope_with_url", - version="scope_with_url_version", - ), - schema_url="instrumentation_schema_url", - log_records=[ - PB2LogRecord( - time_unix_nano=1644650584292683022, - observed_time_unix_nano=1644650584292683022, - trace_id=_encode_trace_id( - 212592107417388365804938480559624925522 - ), - span_id=_encode_span_id( - 6077757853989569222 - ), - flags=int(TraceFlags(0x01)), - severity_text="ERROR", - severity_number=SeverityNumber.ERROR.value, - body=_encode_value( - "This instrumentation scope has a schema url" - ), - attributes=_encode_attributes( - { - "filename": "model.py", - "func_name": "run_method", - }, - allow_null=True, - ), - ) - ], - ), - PB2ScopeLogs( - scope=PB2InstrumentationScope( - name="scope_with_attributes", - version="scope_with_attributes_version", - attributes=_encode_attributes( - {"one": 1, "two": "2"}, - allow_null=True, - ), - ), - schema_url="instrumentation_schema_url", - log_records=[ - PB2LogRecord( - time_unix_nano=1644650584292683033, - observed_time_unix_nano=1644650584292683033, - trace_id=_encode_trace_id( - 212592107417388365804938480559624925533 - ), - span_id=_encode_span_id( - 6077757853989569233 - ), - flags=int(TraceFlags(0x01)), - severity_text="FATAL", - severity_number=SeverityNumber.FATAL.value, - body=_encode_value( - "This instrumentation scope has a schema url and attributes" - ), - attributes=_encode_attributes( - { - "filename": "model.py", - "func_name": "run_method", - }, - allow_null=True, + {"a": 1, "b": "c"} ), ) ], ), ], schema_url="resource_schema_url", + ) + ] + ) + self.assertEqual(encode_logs([basic_log_record]), pb2_service_request) + + def test_encode_log_record_with_no_instrumentation_scope_and_dict_body( + self, + ): + log_record_with_no_instrumentation_scope_and_dict_body = ( + ReadWriteLogRecord( + LogRecord( + timestamp=1644650427658989056, + observed_timestamp=1644650427658989057, + context=_CONTEXT_LOG, + severity_text="DEBUG", + severity_number=SeverityNumber.DEBUG, + body={"error": None, "array_with_nones": [1, None, 2]}, + attributes={"a": 1, "b": "c"}, ), - PB2ResourceLogs( - resource=PB2Resource( - attributes=[ - PB2KeyValue( - key="second_resource", - value=PB2AnyValue(string_value="CASE"), - ) - ] - ), - scope_logs=[ - PB2ScopeLogs( - scope=PB2InstrumentationScope( - name="second_name", - version="second_version", + resource=SDKResource({"second_resource": "CASE"}), + instrumentation_scope=None, + ) + ) + pb2_resource_logs = PB2ResourceLogs( + resource=PB2Resource( + attributes=[ + PB2KeyValue( + key="second_resource", + value=PB2AnyValue(string_value="CASE"), + ) + ] + ), + scope_logs=[ + PB2ScopeLogs( + scope=PB2InstrumentationScope(), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650427658989056, + observed_time_unix_nano=1644650427658989057, + trace_id=_encode_trace_id( + 89564621134313219400156819398935297684 ), - log_records=[ - PB2LogRecord( - time_unix_nano=1644650249738562048, - observed_time_unix_nano=1644650249738562049, - trace_id=None, - span_id=None, - flags=int(TraceFlags.DEFAULT), - severity_text="WARN", - severity_number=SeverityNumber.WARN.value, - body=_encode_value( - "Cooper, this is no time for caution!" - ), - attributes={}, - ), - ], - ), - PB2ScopeLogs( - scope=PB2InstrumentationScope(), - log_records=[ - PB2LogRecord( - time_unix_nano=1644650427658989056, - observed_time_unix_nano=1644650427658989057, - trace_id=_encode_trace_id( - 271615924622795969659406376515024083555 - ), - span_id=_encode_span_id( - 4242561578944770265 - ), - flags=int(TraceFlags(0x01)), - severity_text="DEBUG", - severity_number=SeverityNumber.DEBUG.value, - body=_encode_value("To our galaxy"), - attributes=_encode_attributes( - {"a": 1, "b": "c"}, - allow_null=True, - ), - ), - ], - ), + span_id=_encode_span_id(1312458408527513268), + flags=int(TraceFlags(0x01)), + severity_text="DEBUG", + severity_number=SeverityNumber.DEBUG.value, + body=_encode_value( + { + "error": None, + "array_with_nones": [1, None, 2], + } + ), + attributes=_encode_attributes({"a": 1, "b": "c"}), + ) ], - ), - PB2ResourceLogs( - resource=PB2Resource(), - scope_logs=[ - PB2ScopeLogs( - scope=PB2InstrumentationScope( - name="last_name", - version="last_version", + ) + ], + ) + self.assertEqual( + encode_logs( + [log_record_with_no_instrumentation_scope_and_dict_body] + ), + ExportLogsServiceRequest(resource_logs=[pb2_resource_logs]), + ) + + def test_encode_log_record_with_empty_resource_and_dict_attribute_value( + self, + ): + log_record_with_empty_resource_and_dict_attribute_value = ReadWriteLogRecord( + LogRecord( + timestamp=1644650584292683033, + observed_timestamp=1644650584292683033, + context=_CONTEXT_LOG, + severity_text="FATAL", + severity_number=SeverityNumber.FATAL, + body="This instrumentation scope has a schema url and attributes", + attributes={ + "extended": { + "sequence": [{"inner": "mapping", "none": None}] + } + }, + ), + resource=SDKResource({}), + instrumentation_scope=InstrumentationScope( + "scope_with_attributes", + "scope_with_attributes_version", + "instrumentation_schema_url", + {"one": 1, "two": "2"}, + ), + ) + pb2_resource_logs = PB2ResourceLogs( + resource=PB2Resource(attributes=[]), + scope_logs=[ + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="scope_with_attributes", + version="scope_with_attributes_version", + attributes=_encode_attributes({"one": 1, "two": "2"}), + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650584292683033, + observed_time_unix_nano=1644650584292683033, + trace_id=_encode_trace_id( + 89564621134313219400156819398935297684 ), - log_records=[ - PB2LogRecord( - time_unix_nano=1644650584292683009, - observed_time_unix_nano=1644650584292683010, - trace_id=_encode_trace_id( - 212592107417388365804938480559624925555 - ), - span_id=_encode_span_id( - 6077757853989569445, - ), - flags=int(TraceFlags(0x01)), - severity_text="INFO", - severity_number=SeverityNumber.INFO.value, - body=PB2AnyValue( - kvlist_value=PB2KeyValueList( - values=[ - PB2KeyValue(key="error"), - PB2KeyValue( - key="array_with_nones", - value=PB2AnyValue( - array_value=PB2ArrayValue( - values=[ - PB2AnyValue( - int_value=1 - ), - PB2AnyValue(), - PB2AnyValue( - int_value=2 - ), - ] - ) - ), - ), - ] - ) - ), - attributes={}, - ), - ], - ), - PB2ScopeLogs( - scope=PB2InstrumentationScope( - name="extended_name", - version="extended_version", + span_id=_encode_span_id(1312458408527513268), + flags=int(TraceFlags(0x01)), + severity_text="FATAL", + severity_number=SeverityNumber.FATAL.value, + body=_encode_value( + "This instrumentation scope has a schema url and attributes" ), - log_records=[ - PB2LogRecord( - time_unix_nano=1644650584292683044, - observed_time_unix_nano=1644650584292683044, - trace_id=_encode_trace_id( - 212592107417388365804938480559624925566 - ), - span_id=_encode_span_id( - 6077757853989569466, - ), - flags=int(TraceFlags(0x01)), - severity_text="INFO", - severity_number=SeverityNumber.INFO.value, - body=_encode_value( - "Test export of extended attributes" - ), - attributes=_encode_attributes( - { - "extended": { - "sequence": [ - { - "inner": "mapping", - "none": None, - } - ] - } - }, - allow_null=True, - ), - ), - ], - ), - PB2ScopeLogs( - scope=PB2InstrumentationScope( - name="empty_log_record_name", - version="empty_log_record_version", + attributes=_encode_attributes( + { + "extended": { + "sequence": [ + {"inner": "mapping", "none": None} + ] + } + } ), - 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, - ), - ], - ), + ) ], - ), - ] + schema_url="instrumentation_schema_url", + ) + ], + ) + self.assertEqual( + encode_logs( + [log_record_with_empty_resource_and_dict_attribute_value] + ), + ExportLogsServiceRequest(resource_logs=[pb2_resource_logs]), ) - return sdk_logs, pb2_service_request + 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], "dropped_attributes")) + self.assertEqual( + # pylint:disable=no-member + encoded_logs.resource_logs[0] + .scope_logs[0] + .log_records[0] + .dropped_attributes_count, + 2, + ) @staticmethod def _get_test_logs_dropped_attributes() -> list[ReadWriteLogRecord]: