From 9cfe5559ce6c015eea04f66fb9a4ece596f775b3 Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Tue, 15 Dec 2020 20:38:24 -0800 Subject: [PATCH] Adding support for array attributes to Zipkin exporter (#1285) --- CHANGELOG.md | 2 + .../opentelemetry/exporter/zipkin/__init__.py | 47 +++- .../tests/test_zipkin_exporter.py | 205 +++++++++++++++++- 3 files changed, 245 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 078f02791f4..dad442d2522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `opentelemetry-sdk` package now registers an entrypoint `opentelemetry_configurator` to allow `opentelemetry-instrument` to load the configuration for the SDK ([#1420](https://github.com/open-telemetry/opentelemetry-python/pull/1420)) +- `opentelemetry-exporter-zipkin` Add support for array attributes in Span and Resource exports + ([#1285](https://github.com/open-telemetry/opentelemetry-python/pull/1285)) ## [0.16b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.16b1) - 2020-11-26 ### Added diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index 51130d60b5f..c6d3559b905 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -351,10 +351,15 @@ def _extract_tags_from_dict(self, tags_dict): if not tags_dict: return tags for attribute_key, attribute_value in tags_dict.items(): - if isinstance(attribute_value, (int, bool, float)): + if isinstance(attribute_value, (int, bool, float, str)): value = str(attribute_value) - elif isinstance(attribute_value, str): - value = attribute_value + elif isinstance(attribute_value, Sequence): + value = self._extract_tag_value_string_from_sequence( + attribute_value + ) + if not value: + logger.warning("Could not serialize tag %s", attribute_key) + continue else: logger.warning("Could not serialize tag %s", attribute_key) continue @@ -364,6 +369,42 @@ def _extract_tags_from_dict(self, tags_dict): tags[attribute_key] = value return tags + def _extract_tag_value_string_from_sequence(self, sequence: Sequence): + if self.max_tag_value_length == 1: + return None + + tag_value_elements = [] + running_string_length = ( + 2 # accounts for array brackets in output string + ) + defined_max_tag_value_length = self.max_tag_value_length > 0 + + for element in sequence: + if isinstance(element, (int, bool, float, str)): + tag_value_element = str(element) + elif element is None: + tag_value_element = None + else: + continue + + if defined_max_tag_value_length: + if tag_value_element is None: + running_string_length += 4 # null with no quotes + else: + # + 2 accounts for string quotation marks + running_string_length += len(tag_value_element) + 2 + + if tag_value_elements: + # accounts for ',' item separator + running_string_length += 1 + + if running_string_length > self.max_tag_value_length: + break + + tag_value_elements.append(tag_value_element) + + return json.dumps(tag_value_elements, separators=(",", ":")) + def _extract_tags_from_span(self, span: Span): tags = self._extract_tags_from_dict(getattr(span, "attributes", None)) if span.resource: diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index bbc6ccf7ce2..a21199659b8 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -425,8 +425,25 @@ def test_export_json_max_tag_length(self): span.start() span.resource = Resource({}) # added here to preserve order - span.set_attribute("k1", "v" * 500) - span.set_attribute("k2", "v" * 50) + span.set_attribute("string1", "v" * 500) + span.set_attribute("string2", "v" * 50) + span.set_attribute("list1", ["a"] * 25) + span.set_attribute("list2", ["a"] * 10) + span.set_attribute("list3", [2] * 25) + span.set_attribute("list4", [2] * 10) + span.set_attribute("list5", [True] * 25) + span.set_attribute("list6", [True] * 10) + span.set_attribute("tuple1", ("a",) * 25) + span.set_attribute("tuple2", ("a",) * 10) + span.set_attribute("tuple3", (2,) * 25) + span.set_attribute("tuple4", (2,) * 10) + span.set_attribute("tuple5", (True,) * 25) + span.set_attribute("tuple6", (True,) * 10) + span.set_attribute("range1", range(0, 25)) + span.set_attribute("range2", range(0, 10)) + span.set_attribute("empty_list", []) + span.set_attribute("none_list", ["hello", None, "world"]) + span.set_status(Status(StatusCode.ERROR, "Example description")) span.end() @@ -440,8 +457,66 @@ def test_export_json_max_tag_length(self): _, kwargs = mock_post.call_args # pylint: disable=E0633 tags = json.loads(kwargs["data"])[0]["tags"] - self.assertEqual(len(tags["k1"]), 128) - self.assertEqual(len(tags["k2"]), 50) + + self.assertEqual(len(tags["string1"]), 128) + self.assertEqual(len(tags["string2"]), 50) + self.assertEqual( + tags["list1"], + '["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]', + ) + self.assertEqual( + tags["list2"], '["a","a","a","a","a","a","a","a","a","a"]', + ) + self.assertEqual( + tags["list3"], + '["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"]', + ) + self.assertEqual( + tags["list4"], '["2","2","2","2","2","2","2","2","2","2"]', + ) + self.assertEqual( + tags["list5"], + '["True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True"]', + ) + self.assertEqual( + tags["list6"], + '["True","True","True","True","True","True","True","True","True","True"]', + ) + self.assertEqual( + tags["tuple1"], + '["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]', + ) + self.assertEqual( + tags["tuple2"], '["a","a","a","a","a","a","a","a","a","a"]', + ) + self.assertEqual( + tags["tuple3"], + '["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"]', + ) + self.assertEqual( + tags["tuple4"], '["2","2","2","2","2","2","2","2","2","2"]', + ) + self.assertEqual( + tags["tuple5"], + '["True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True","True"]', + ) + self.assertEqual( + tags["tuple6"], + '["True","True","True","True","True","True","True","True","True","True"]', + ) + self.assertEqual( + tags["range1"], + '["0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24"]', + ) + self.assertEqual( + tags["range2"], '["0","1","2","3","4","5","6","7","8","9"]', + ) + self.assertEqual( + tags["empty_list"], "[]", + ) + self.assertEqual( + tags["none_list"], '["hello",null,"world"]', + ) exporter = ZipkinSpanExporter(service_name, max_tag_value_length=2) mock_post = MagicMock() @@ -452,8 +527,126 @@ def test_export_json_max_tag_length(self): _, kwargs = mock_post.call_args # pylint: disable=E0633 tags = json.loads(kwargs["data"])[0]["tags"] - self.assertEqual(len(tags["k1"]), 2) - self.assertEqual(len(tags["k2"]), 2) + self.assertEqual(len(tags["string1"]), 2) + self.assertEqual(len(tags["string2"]), 2) + self.assertEqual(tags["list1"], "[]") + self.assertEqual(tags["list2"], "[]") + self.assertEqual(tags["list3"], "[]") + self.assertEqual(tags["list4"], "[]") + self.assertEqual(tags["list5"], "[]") + self.assertEqual(tags["list6"], "[]") + self.assertEqual(tags["tuple1"], "[]") + self.assertEqual(tags["tuple2"], "[]") + self.assertEqual(tags["tuple3"], "[]") + self.assertEqual(tags["tuple4"], "[]") + self.assertEqual(tags["tuple5"], "[]") + self.assertEqual(tags["tuple6"], "[]") + self.assertEqual(tags["range1"], "[]") + self.assertEqual(tags["range2"], "[]") + + exporter = ZipkinSpanExporter(service_name, max_tag_value_length=5) + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export([span]) + self.assertEqual(SpanExportResult.SUCCESS, status) + + _, kwargs = mock_post.call_args # pylint: disable=E0633 + tags = json.loads(kwargs["data"])[0]["tags"] + self.assertEqual(len(tags["string1"]), 5) + self.assertEqual(len(tags["string2"]), 5) + self.assertEqual(tags["list1"], '["a"]') + self.assertEqual(tags["list2"], '["a"]') + self.assertEqual(tags["list3"], '["2"]') + self.assertEqual(tags["list4"], '["2"]') + self.assertEqual(tags["list5"], "[]") + self.assertEqual(tags["list6"], "[]") + self.assertEqual(tags["tuple1"], '["a"]') + self.assertEqual(tags["tuple2"], '["a"]') + self.assertEqual(tags["tuple3"], '["2"]') + self.assertEqual(tags["tuple4"], '["2"]') + self.assertEqual(tags["tuple5"], "[]") + self.assertEqual(tags["tuple6"], "[]") + self.assertEqual(tags["range1"], '["0"]') + self.assertEqual(tags["range2"], '["0"]') + + exporter = ZipkinSpanExporter(service_name, max_tag_value_length=9) + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export([span]) + self.assertEqual(SpanExportResult.SUCCESS, status) + + _, kwargs = mock_post.call_args # pylint: disable=E0633 + tags = json.loads(kwargs["data"])[0]["tags"] + self.assertEqual(len(tags["string1"]), 9) + self.assertEqual(len(tags["string2"]), 9) + self.assertEqual(tags["list1"], '["a","a"]') + self.assertEqual(tags["list2"], '["a","a"]') + self.assertEqual(tags["list3"], '["2","2"]') + self.assertEqual(tags["list4"], '["2","2"]') + self.assertEqual(tags["list5"], '["True"]') + self.assertEqual(tags["list6"], '["True"]') + self.assertEqual(tags["tuple1"], '["a","a"]') + self.assertEqual(tags["tuple2"], '["a","a"]') + self.assertEqual(tags["tuple3"], '["2","2"]') + self.assertEqual(tags["tuple4"], '["2","2"]') + self.assertEqual(tags["tuple5"], '["True"]') + self.assertEqual(tags["tuple6"], '["True"]') + self.assertEqual(tags["range1"], '["0","1"]') + self.assertEqual(tags["range2"], '["0","1"]') + + exporter = ZipkinSpanExporter(service_name, max_tag_value_length=10) + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export([span]) + self.assertEqual(SpanExportResult.SUCCESS, status) + + _, kwargs = mock_post.call_args # pylint: disable=E0633 + tags = json.loads(kwargs["data"])[0]["tags"] + self.assertEqual(len(tags["string1"]), 10) + self.assertEqual(len(tags["string2"]), 10) + self.assertEqual(tags["list1"], '["a","a"]') + self.assertEqual(tags["list2"], '["a","a"]') + self.assertEqual(tags["list3"], '["2","2"]') + self.assertEqual(tags["list4"], '["2","2"]') + self.assertEqual(tags["list5"], '["True"]') + self.assertEqual(tags["list6"], '["True"]') + self.assertEqual(tags["tuple1"], '["a","a"]') + self.assertEqual(tags["tuple2"], '["a","a"]') + self.assertEqual(tags["tuple3"], '["2","2"]') + self.assertEqual(tags["tuple4"], '["2","2"]') + self.assertEqual(tags["tuple5"], '["True"]') + self.assertEqual(tags["tuple6"], '["True"]') + self.assertEqual(tags["range1"], '["0","1"]') + self.assertEqual(tags["range2"], '["0","1"]') + + exporter = ZipkinSpanExporter(service_name, max_tag_value_length=11) + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export([span]) + self.assertEqual(SpanExportResult.SUCCESS, status) + + _, kwargs = mock_post.call_args # pylint: disable=E0633 + tags = json.loads(kwargs["data"])[0]["tags"] + self.assertEqual(len(tags["string1"]), 11) + self.assertEqual(len(tags["string2"]), 11) + self.assertEqual(tags["list1"], '["a","a"]') + self.assertEqual(tags["list2"], '["a","a"]') + self.assertEqual(tags["list3"], '["2","2"]') + self.assertEqual(tags["list4"], '["2","2"]') + self.assertEqual(tags["list5"], '["True"]') + self.assertEqual(tags["list6"], '["True"]') + self.assertEqual(tags["tuple1"], '["a","a"]') + self.assertEqual(tags["tuple2"], '["a","a"]') + self.assertEqual(tags["tuple3"], '["2","2"]') + self.assertEqual(tags["tuple4"], '["2","2"]') + self.assertEqual(tags["tuple5"], '["True"]') + self.assertEqual(tags["tuple6"], '["True"]') + self.assertEqual(tags["range1"], '["0","1"]') + self.assertEqual(tags["range2"], '["0","1"]') # pylint: disable=too-many-locals,too-many-statements def test_export_protobuf(self):