Repository: open-telemetry/opentelemetry-python

Issue:
zipkin exporter does not work with jaeger collector for spanIds/traceIds/parentIds starting with a 0
**Describe your environment**
I am using Jaeger as backend and can only collect with the zipkin format (Don't ask me why...).  
I am using opentelemetry-ext-zipkin to do so

**Steps to reproduce**
Upload a couple of spans until randomly one has an Id with a zero as first hex digit, and you will get an error like this:
```
Traces cannot be uploaded; status code: 400, message Unable to process request body: validation failure list:
id in body should be at least 16 chars long
traceId in body should be at least 16 chars long
```

**Cause of the problem and proposed fix**
This is due to the `format(traceId, "x")` in the code which does not do any zero padding, which can be replaced with `format(traceId, "032x")` and `format(spanId, "016x")`.
It fixes the problem on jaeger-collector for sure, but I will let you check it for the real zipkin collector, I suppose it will not have issues with zero-padded hex.

I am currently writing a PR, which I will link in the next few minutes.
I will let you decide if it is legitimate, I think it is, or instead push a more permissive acceptance on the jaeger-collector side.




Discussion:

Resolving patch:
diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py
--- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py
+++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py
@@ -168,8 +168,9 @@ def _translate_to_zipkin(self, spans: Sequence[Span]):
             duration_mus = _nsec_to_usec_round(span.end_time - span.start_time)
 
             zipkin_span = {
-                "traceId": format(trace_id, "x"),
-                "id": format(span_id, "x"),
+                # Ensure left-zero-padding of traceId, spanId, parentId
+                "traceId": format(trace_id, "032x"),
+                "id": format(span_id, "016x"),
                 "name": span.name,
                 "timestamp": start_timestamp_mus,
                 "duration": duration_mus,
@@ -184,10 +185,10 @@ def _translate_to_zipkin(self, spans: Sequence[Span]):
 
             if isinstance(span.parent, Span):
                 zipkin_span["parentId"] = format(
-                    span.parent.get_context().span_id, "x"
+                    span.parent.get_context().span_id, "016x"
                 )
             elif isinstance(span.parent, SpanContext):
-                zipkin_span["parentId"] = format(span.parent.span_id, "x")
+                zipkin_span["parentId"] = format(span.parent.span_id, "016x")
 
             zipkin_spans.append(zipkin_span)
         return zipkin_spans


Test patch:
diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py
--- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py
+++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py
@@ -264,6 +264,72 @@ def test_export(self):
             headers={"Content-Type": "application/json"},
         )
 
+    # pylint: disable=too-many-locals
+    def test_zero_padding(self):
+        """test that hex ids starting with 0
+        are properly padded to 16 or 32 hex chars
+        when exported
+        """
+
+        span_names = "testZeroes"
+        trace_id = 0x0E0C63257DE34C926F9EFCD03927272E
+        span_id = 0x04BF92DEEFC58C92
+        parent_id = 0x0AAAAAAAAAAAAAAA
+
+        start_time = 683647322 * 10 ** 9  # in ns
+        duration = 50 * 10 ** 6
+        end_time = start_time + duration
+
+        span_context = trace_api.SpanContext(
+            trace_id,
+            span_id,
+            is_remote=False,
+            trace_flags=TraceFlags(TraceFlags.SAMPLED),
+        )
+        parent_context = trace_api.SpanContext(
+            trace_id, parent_id, is_remote=False
+        )
+
+        otel_span = trace.Span(
+            name=span_names[0], context=span_context, parent=parent_context,
+        )
+
+        otel_span.start(start_time=start_time)
+        otel_span.end(end_time=end_time)
+
+        service_name = "test-service"
+        local_endpoint = {"serviceName": service_name, "port": 9411}
+
+        exporter = ZipkinSpanExporter(service_name)
+        # Check traceId are properly lowercase 16 or 32 hex
+        expected = [
+            {
+                "traceId": "0e0c63257de34c926f9efcd03927272e",
+                "id": "04bf92deefc58c92",
+                "name": span_names[0],
+                "timestamp": start_time // 10 ** 3,
+                "duration": duration // 10 ** 3,
+                "localEndpoint": local_endpoint,
+                "kind": None,
+                "tags": {},
+                "annotations": None,
+                "debug": True,
+                "parentId": "0aaaaaaaaaaaaaaa",
+            }
+        ]
+
+        mock_post = MagicMock()
+        with patch("requests.post", mock_post):
+            mock_post.return_value = MockResponse(200)
+            status = exporter.export([otel_span])
+            self.assertEqual(SpanExportResult.SUCCESS, status)
+
+        mock_post.assert_called_with(
+            url="http://localhost:9411/api/v2/spans",
+            data=json.dumps(expected),
+            headers={"Content-Type": "application/json"},
+        )
+
     @patch("requests.post")
     def test_invalid_response(self, mock_post):
         mock_post.return_value = MockResponse(404)