diff --git a/CHANGELOG.md b/CHANGELOG.md index 43efa6ad00a..df4d319ef0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v0.18b0...HEAD) +### Added +- Add `max_attr_value_length` support to Jaeger exporter + ([#1633])(https://github.com/open-telemetry/opentelemetry-python/pull/1633) + ### Changed - Rename `IdsGenerator` to `IdGenerator` ([#1651])(https://github.com/open-telemetry/opentelemetry-python/pull/1651) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py index 20b4bda3d9f..69102db1d9a 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/__init__.py @@ -47,6 +47,7 @@ # insecure=True, # optional # credentials=xxx # optional channel creds # transport_format='protobuf' # optional + # max_tag_value_length=None # optional ) # Create a BatchExportSpanProcessor and add the exporter to it @@ -120,6 +121,7 @@ class JaegerSpanExporter(SpanExporter): insecure: True if collector has no encryption or authentication credentials: Credentials for server authentication. transport_format: Transport format for exporting spans to collector. + max_tag_value_length: Max length string attribute values can have. Set to None to disable. """ def __init__( @@ -133,8 +135,10 @@ def __init__( insecure: Optional[bool] = None, credentials: Optional[ChannelCredentials] = None, transport_format: Optional[str] = None, + max_tag_value_length: Optional[int] = None, ): self.service_name = service_name + self._max_tag_value_length = max_tag_value_length self.agent_host_name = _parameter_setter( param=agent_host_name, env_variable=environ.get(OTEL_EXPORTER_JAEGER_AGENT_HOST), @@ -220,13 +224,15 @@ def _collector_http_client(self) -> Optional[Collector]: def export(self, spans) -> SpanExportResult: translator = Translate(spans) if self.transport_format == TRANSPORT_FORMAT_PROTOBUF: - pb_translator = ProtobufTranslator(self.service_name) + pb_translator = ProtobufTranslator( + self.service_name, self._max_tag_value_length + ) jaeger_spans = translator._translate(pb_translator) batch = model_pb2.Batch(spans=jaeger_spans) request = PostSpansRequest(batch=batch) self._collector_grpc_client.PostSpans(request) else: - thrift_translator = ThriftTranslator() + thrift_translator = ThriftTranslator(self._max_tag_value_length) jaeger_spans = translator._translate(thrift_translator) batch = jaeger_thrift.Batch( spans=jaeger_spans, diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py index 853da9ac6df..c60820085ac 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. import abc +from typing import Optional from opentelemetry.trace import SpanKind @@ -41,6 +42,9 @@ def _convert_int_to_i64(val): class Translator(abc.ABC): + def __init__(self, max_tag_value_length: Optional[int] = None): + self._max_tag_value_length = max_tag_value_length + @abc.abstractmethod def _translate_span(self, span): """Translates span to jaeger format. diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py index f97977516db..011e24f17f2 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/protobuf.py @@ -76,24 +76,31 @@ def _get_binary_key_value(key: str, value: bytes) -> model_pb2.KeyValue: def _translate_attribute( - key: str, value: types.AttributeValue + key: str, value: types.AttributeValue, max_length: Optional[int] ) -> Optional[model_pb2.KeyValue]: """Convert the attributes to jaeger keyvalues.""" translated = None if isinstance(value, bool): translated = _get_bool_key_value(key, value) elif isinstance(value, str): + if max_length is not None: + value = value[:max_length] translated = _get_string_key_value(key, value) elif isinstance(value, int): translated = _get_long_key_value(key, value) elif isinstance(value, float): translated = _get_double_key_value(key, value) elif isinstance(value, tuple): - translated = _get_string_key_value(key, str(value)) + value = str(value) + if max_length is not None: + value = value[:max_length] + translated = _get_string_key_value(key, value) return translated -def _extract_resource_tags(span: ReadableSpan) -> Sequence[model_pb2.KeyValue]: +def _extract_resource_tags( + span: ReadableSpan, max_tag_value_length: Optional[int] +) -> Sequence[model_pb2.KeyValue]: """Extracts resource attributes from span and returns list of jaeger keyvalues. @@ -102,7 +109,7 @@ def _extract_resource_tags(span: ReadableSpan) -> Sequence[model_pb2.KeyValue]: """ tags = [] for key, value in span.resource.attributes.items(): - tag = _translate_attribute(key, value) + tag = _translate_attribute(key, value, max_tag_value_length) if tag: tags.append(tag) return tags @@ -140,7 +147,10 @@ def _proto_timestamp_from_epoch_nanos(nsec: int) -> Timestamp: class ProtobufTranslator(Translator): - def __init__(self, svc_name): + def __init__( + self, svc_name: str, max_tag_value_length: Optional[int] = None + ): + super().__init__(max_tag_value_length) self.svc_name = svc_name def _translate_span(self, span: ReadableSpan) -> model_pb2.Span: @@ -161,7 +171,8 @@ def _translate_span(self, span: ReadableSpan) -> model_pb2.Span: flags = int(ctx.trace_flags) process = model_pb2.Process( - service_name=self.svc_name, tags=_extract_resource_tags(span) + service_name=self.svc_name, + tags=_extract_resource_tags(span, self._max_tag_value_length), ) jaeger_span = model_pb2.Span( trace_id=trace_id, @@ -183,12 +194,16 @@ def _extract_tags( translated = [] if span.attributes: for key, value in span.attributes.items(): - key_value = _translate_attribute(key, value) + key_value = _translate_attribute( + key, value, self._max_tag_value_length + ) if key_value is not None: translated.append(key_value) if span.resource.attributes: for key, value in span.resource.attributes.items(): - key_value = _translate_attribute(key, value) + key_value = _translate_attribute( + key, value, self._max_tag_value_length + ) if key_value: translated.append(key_value) @@ -256,7 +271,9 @@ def _extract_logs( for event in span.events: fields = [] for key, value in event.attributes.items(): - tag = _translate_attribute(key, value) + tag = _translate_attribute( + key, value, self._max_tag_value_length + ) if tag: fields.append(tag) diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py index 9df9c716882..45129601ac3 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/translate/thrift.py @@ -58,19 +58,24 @@ def _get_trace_id_high(trace_id): def _translate_attribute( - key: str, value: types.AttributeValue + key: str, value: types.AttributeValue, max_length: Optional[int] ) -> Optional[TCollector.Tag]: """Convert the attributes to jaeger tags.""" if isinstance(value, bool): return _get_bool_tag(key, value) if isinstance(value, str): + if max_length is not None: + value = value[:max_length] return _get_string_tag(key, value) if isinstance(value, int): return _get_long_tag(key, value) if isinstance(value, float): return _get_double_tag(key, value) if isinstance(value, tuple): - return _get_string_tag(key, str(value)) + value = str(value) + if max_length is not None: + value = value[:max_length] + return _get_string_tag(key, value) return None @@ -111,12 +116,16 @@ def _extract_tags(self, span: ReadableSpan) -> Sequence[TCollector.Tag]: translated = [] if span.attributes: for key, value in span.attributes.items(): - tag = _translate_attribute(key, value) + tag = _translate_attribute( + key, value, self._max_tag_value_length + ) if tag: translated.append(tag) if span.resource.attributes: for key, value in span.resource.attributes.items(): - tag = _translate_attribute(key, value) + tag = _translate_attribute( + key, value, self._max_tag_value_length + ) if tag: translated.append(tag) @@ -185,7 +194,9 @@ def _extract_logs( for event in span.events: fields = [] for key, value in event.attributes.items(): - tag = _translate_attribute(key, value) + tag = _translate_attribute( + key, value, self._max_tag_value_length + ) if tag: fields.append(tag) diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py index 80cbdfb55dd..eea3e54700c 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_protobuf.py @@ -385,3 +385,61 @@ def test_translate_to_jaeger(self): ) self.assertEqual(spans, expected_spans) + + def test_max_tag_value_length(self): + span = trace._Span( + name="span", + resource=Resource( + attributes={ + "key_resource": "some_resource some_resource some_more_resource" + } + ), + context=trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + ), + ) + + span.start() + span.set_attribute("key_bool", False) + span.set_attribute("key_string", "hello_world hello_world hello_world") + span.set_attribute("key_float", 111.22) + span.set_attribute("key_int", 1100) + span.set_attribute("key_tuple", ("tuple_element", "tuple_element2")) + span.end() + + translate = Translate([span]) + + # does not truncate by default + # pylint: disable=protected-access + spans = translate._translate(pb_translator.ProtobufTranslator("svc")) + tags_by_keys = { + tag.key: tag.v_str + for tag in spans[0].tags + if tag.v_type == model_pb2.ValueType.STRING + } + self.assertEqual( + "hello_world hello_world hello_world", tags_by_keys["key_string"] + ) + self.assertEqual( + "('tuple_element', 'tuple_element2')", tags_by_keys["key_tuple"] + ) + self.assertEqual( + "some_resource some_resource some_more_resource", + tags_by_keys["key_resource"], + ) + + # truncates when max_tag_value_length is passed + # pylint: disable=protected-access + spans = translate._translate( + pb_translator.ProtobufTranslator("svc", max_tag_value_length=5) + ) + tags_by_keys = { + tag.key: tag.v_str + for tag in spans[0].tags + if tag.v_type == model_pb2.ValueType.STRING + } + self.assertEqual("hello", tags_by_keys["key_string"]) + self.assertEqual("('tup", tags_by_keys["key_tuple"]) + self.assertEqual("some_", tags_by_keys["key_resource"]) diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py index c0faafc1b6e..0bee785760d 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger_exporter_thrift.py @@ -67,6 +67,7 @@ def test_constructor_default(self): self.assertEqual(exporter.password, None) self.assertTrue(exporter._collector_http_client is None) self.assertTrue(exporter._agent_client is not None) + self.assertIsNone(exporter._max_tag_value_length) def test_constructor_explicit(self): # pylint: disable=protected-access @@ -88,6 +89,7 @@ def test_constructor_explicit(self): collector_endpoint=collector_endpoint, username=username, password=password, + max_tag_value_length=42, ) self.assertEqual(exporter.service_name, service) @@ -104,6 +106,7 @@ def test_constructor_explicit(self): exporter.password = None self.assertNotEqual(exporter._collector_http_client, collector) self.assertTrue(exporter._collector_http_client.auth is None) + self.assertEqual(exporter._max_tag_value_length, 42) def test_constructor_by_environment_variables(self): # pylint: disable=protected-access @@ -465,3 +468,55 @@ def test_agent_client(self): ) agent_client.emit(batch) + + def test_max_tag_value_length(self): + span = trace._Span( + name="span", + resource=Resource( + attributes={ + "key_resource": "some_resource some_resource some_more_resource" + } + ), + context=trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + ), + ) + + span.start() + span.set_attribute("key_bool", False) + span.set_attribute("key_string", "hello_world hello_world hello_world") + span.set_attribute("key_float", 111.22) + span.set_attribute("key_int", 1100) + span.set_attribute("key_tuple", ("tuple_element", "tuple_element2")) + span.end() + + translate = Translate([span]) + + # does not truncate by default + # pylint: disable=protected-access + spans = translate._translate(ThriftTranslator()) + tags_by_keys = { + tag.key: tag.vStr for tag in spans[0].tags if tag.vType == 0 + } + self.assertEqual( + "hello_world hello_world hello_world", tags_by_keys["key_string"] + ) + self.assertEqual( + "('tuple_element', 'tuple_element2')", tags_by_keys["key_tuple"] + ) + self.assertEqual( + "some_resource some_resource some_more_resource", + tags_by_keys["key_resource"], + ) + + # truncates when max_tag_value_length is passed + # pylint: disable=protected-access + spans = translate._translate(ThriftTranslator(max_tag_value_length=5)) + tags_by_keys = { + tag.key: tag.vStr for tag in spans[0].tags if tag.vType == 0 + } + self.assertEqual("hello", tags_by_keys["key_string"]) + self.assertEqual("('tup", tags_by_keys["key_tuple"]) + self.assertEqual("some_", tags_by_keys["key_resource"])