Skip to content

Commit 9f92bd7

Browse files
committed
Make zipkin tag value length configurable (open-telemetry#1151)
Zipkin exporter truncates tag values to a maximum length of 128 characters. This commit makes this value configurable while keeping 128 as the default value.
1 parent 2b46d11 commit 9f92bd7

File tree

3 files changed

+111
-37
lines changed

3 files changed

+111
-37
lines changed

exporter/opentelemetry-exporter-zipkin/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
- Zipkin exporter now accepts a ``max_tag_value_length`` attribute to customize the
6+
maximum allowed size a tag value can have. ([#1151](https://github.com/open-telemetry/opentelemetry-python/pull/1151))
7+
- Fixed OTLP events to Zipkin annotations translation. ([#1161](https://github.com/open-telemetry/opentelemetry-python/pull/1161))
8+
59
## Version 0.13b0
610

711
Released 2020-09-17

exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py

+51-36
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474

7575
DEFAULT_RETRY = False
7676
DEFAULT_URL = "http://localhost:9411/api/v2/spans"
77+
DEFAULT_MAX_TAG_VALUE_LENGTH = 128
7778
ZIPKIN_HEADERS = {"Content-Type": "application/json"}
7879

7980
SPAN_KIND_MAP = {
@@ -108,6 +109,7 @@ def __init__(
108109
ipv4: Optional[str] = None,
109110
ipv6: Optional[str] = None,
110111
retry: Optional[str] = DEFAULT_RETRY,
112+
max_tag_value_length: Optional[int] = DEFAULT_MAX_TAG_VALUE_LENGTH,
111113
):
112114
self.service_name = service_name
113115
if url is None:
@@ -122,6 +124,7 @@ def __init__(
122124
self.ipv4 = ipv4
123125
self.ipv6 = ipv6
124126
self.retry = retry
127+
self.max_tag_value_length = max_tag_value_length
125128

126129
def export(self, spans: Sequence[Span]) -> SpanExportResult:
127130
zipkin_spans = self._translate_to_zipkin(spans)
@@ -141,6 +144,9 @@ def export(self, spans: Sequence[Span]) -> SpanExportResult:
141144
return SpanExportResult.FAILURE
142145
return SpanExportResult.SUCCESS
143146

147+
def shutdown(self) -> None:
148+
pass
149+
144150
def _translate_to_zipkin(self, spans: Sequence[Span]):
145151

146152
local_endpoint = {"serviceName": self.service_name, "port": self.port}
@@ -171,8 +177,10 @@ def _translate_to_zipkin(self, spans: Sequence[Span]):
171177
"duration": duration_mus,
172178
"localEndpoint": local_endpoint,
173179
"kind": SPAN_KIND_MAP[span.kind],
174-
"tags": _extract_tags_from_span(span),
175-
"annotations": _extract_annotations_from_events(span.events),
180+
"tags": self._extract_tags_from_span(span),
181+
"annotations": self._extract_annotations_from_events(
182+
span.events
183+
),
176184
}
177185

178186
if span.instrumentation_info is not None:
@@ -205,42 +213,49 @@ def _translate_to_zipkin(self, spans: Sequence[Span]):
205213
zipkin_spans.append(zipkin_span)
206214
return zipkin_spans
207215

208-
def shutdown(self) -> None:
209-
pass
210-
216+
def _extract_tags_from_dict(self, tags_dict):
217+
tags = {}
218+
if not tags_dict:
219+
return tags
220+
for attribute_key, attribute_value in tags_dict.items():
221+
if isinstance(attribute_value, (int, bool, float)):
222+
value = str(attribute_value)
223+
elif isinstance(attribute_value, str):
224+
value = attribute_value
225+
else:
226+
logger.warning("Could not serialize tag %s", attribute_key)
227+
continue
228+
229+
if self.max_tag_value_length > 0:
230+
value = value[: self.max_tag_value_length]
231+
tags[attribute_key] = value
232+
return tags
211233

212-
def _extract_tags_from_dict(tags_dict):
213-
tags = {}
214-
if not tags_dict:
234+
def _extract_tags_from_span(self, span: Span):
235+
tags = self._extract_tags_from_dict(getattr(span, "attributes", None))
236+
if span.resource:
237+
tags.update(self._extract_tags_from_dict(span.resource.attributes))
215238
return tags
216-
for attribute_key, attribute_value in tags_dict.items():
217-
if isinstance(attribute_value, (int, bool, float)):
218-
value = str(attribute_value)
219-
elif isinstance(attribute_value, str):
220-
value = attribute_value[:128]
221-
else:
222-
logger.warning("Could not serialize tag %s", attribute_key)
223-
continue
224-
tags[attribute_key] = value
225-
return tags
226-
227-
228-
def _extract_tags_from_span(span: Span):
229-
tags = _extract_tags_from_dict(getattr(span, "attributes", None))
230-
if span.resource:
231-
tags.update(_extract_tags_from_dict(span.resource.attributes))
232-
return tags
233-
234-
235-
def _extract_annotations_from_events(events):
236-
return (
237-
[
238-
{"timestamp": _nsec_to_usec_round(e.timestamp), "value": e.name}
239-
for e in events
240-
]
241-
if events
242-
else None
243-
)
239+
240+
def _extract_annotations_from_events(self, events):
241+
if not events:
242+
return None
243+
244+
annotations = []
245+
for event in events:
246+
attrs = {}
247+
for key, val in event.attributes.items():
248+
if isinstance(val, str):
249+
val = val[: self.max_tag_value_length]
250+
attrs[key] = val
251+
252+
annotations.append(
253+
{
254+
"timestamp": _nsec_to_usec_round(event.timestamp),
255+
"value": json.dumps({event.name: attrs}),
256+
}
257+
)
258+
return annotations
244259

245260

246261
def _nsec_to_usec_round(nsec):

exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py

+56-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,15 @@ def test_export(self):
223223
"annotations": [
224224
{
225225
"timestamp": event_timestamp // 10 ** 3,
226-
"value": "event0",
226+
"value": json.dumps(
227+
{
228+
"event0": {
229+
"annotation_bool": True,
230+
"annotation_string": "annotation_test",
231+
"key_float": 0.3,
232+
}
233+
}
234+
),
227235
}
228236
],
229237
"debug": True,
@@ -361,3 +369,50 @@ def test_invalid_response(self, mock_post):
361369
exporter = ZipkinSpanExporter("test-service")
362370
status = exporter.export(spans)
363371
self.assertEqual(SpanExportResult.FAILURE, status)
372+
373+
def test_max_tag_length(self):
374+
service_name = "test-service"
375+
376+
span_context = trace_api.SpanContext(
377+
0x0E0C63257DE34C926F9EFCD03927272E,
378+
0x04BF92DEEFC58C92,
379+
is_remote=False,
380+
trace_flags=TraceFlags(TraceFlags.SAMPLED),
381+
)
382+
383+
span = trace.Span(name="test-span", context=span_context,)
384+
385+
span.start()
386+
span.resource = Resource({})
387+
# added here to preserve order
388+
span.set_attribute("k1", "v" * 500)
389+
span.set_attribute("k2", "v" * 50)
390+
span.set_status(
391+
Status(StatusCanonicalCode.UNKNOWN, "Example description")
392+
)
393+
span.end()
394+
395+
exporter = ZipkinSpanExporter(service_name)
396+
mock_post = MagicMock()
397+
with patch("requests.post", mock_post):
398+
mock_post.return_value = MockResponse(200)
399+
status = exporter.export([span])
400+
self.assertEqual(SpanExportResult.SUCCESS, status)
401+
402+
_, kwargs = mock_post.call_args # pylint: disable=E0633
403+
404+
tags = json.loads(kwargs["data"])[0]["tags"]
405+
self.assertEqual(len(tags["k1"]), 128)
406+
self.assertEqual(len(tags["k2"]), 50)
407+
408+
exporter = ZipkinSpanExporter(service_name, max_tag_value_length=2)
409+
mock_post = MagicMock()
410+
with patch("requests.post", mock_post):
411+
mock_post.return_value = MockResponse(200)
412+
status = exporter.export([span])
413+
self.assertEqual(SpanExportResult.SUCCESS, status)
414+
415+
_, kwargs = mock_post.call_args # pylint: disable=E0633
416+
tags = json.loads(kwargs["data"])[0]["tags"]
417+
self.assertEqual(len(tags["k1"]), 2)
418+
self.assertEqual(len(tags["k2"]), 2)

0 commit comments

Comments
 (0)