diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformer.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformer.java index acb76c242ab..8175315851d 100644 --- a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformer.java +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformer.java @@ -6,11 +6,15 @@ package io.opentelemetry.exporter.zipkin; import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_PORT; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_PEER_ADDR; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.PEER_SERVICE; import static java.util.concurrent.TimeUnit.NANOSECONDS; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributeType; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.resources.Resource; @@ -72,8 +76,6 @@ private OtelToZipkinSpanTransformer(Supplier ipAddressSupplier) { * @return a new Zipkin Span */ Span generateSpan(SpanData spanData) { - Endpoint endpoint = getEndpoint(spanData); - long startTimestamp = toEpochMicros(spanData.getStartEpochNanos()); long endTimestamp = toEpochMicros(spanData.getEndEpochNanos()); @@ -85,7 +87,8 @@ Span generateSpan(SpanData spanData) { .name(spanData.getName()) .timestamp(toEpochMicros(spanData.getStartEpochNanos())) .duration(Math.max(1, endTimestamp - startTimestamp)) - .localEndpoint(endpoint); + .localEndpoint(getLocalEndpoint(spanData)) + .remoteEndpoint(getRemoteEndpoint(spanData)); if (spanData.getParentSpanContext().isValid()) { spanBuilder.parentId(spanData.getParentSpanId()); @@ -140,7 +143,7 @@ private static String nullToEmpty(String value) { return value != null ? value : ""; } - private Endpoint getEndpoint(SpanData spanData) { + private Endpoint getLocalEndpoint(SpanData spanData) { Attributes resourceAttributes = spanData.getResource().getAttributes(); Endpoint.Builder endpoint = Endpoint.newBuilder(); @@ -158,6 +161,30 @@ private Endpoint getEndpoint(SpanData spanData) { return endpoint.build(); } + @Nullable + private static Endpoint getRemoteEndpoint(SpanData spanData) { + if (spanData.getKind() == SpanKind.CLIENT || spanData.getKind() == SpanKind.PRODUCER) { + // TODO: Implement fallback mechanism: + // https://opentelemetry.io/docs/reference/specification/trace/sdk_exporters/zipkin/#otlp---zipkin + Attributes attributes = spanData.getAttributes(); + String serviceName = attributes.get(PEER_SERVICE); + + if (serviceName != null) { + Endpoint.Builder endpoint = Endpoint.newBuilder(); + endpoint.serviceName(serviceName); + endpoint.ip(attributes.get(NET_SOCK_PEER_ADDR)); + Long port = attributes.get(NET_PEER_PORT); + if (port != null) { + endpoint.port(port.intValue()); + } + + return endpoint.build(); + } + } + + return null; + } + @Nullable private static Span.Kind toSpanKind(SpanData spanData) { switch (spanData.getKind()) { diff --git a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformerTest.java b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformerTest.java index b47b23e5d2d..230ab38894c 100644 --- a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformerTest.java +++ b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/OtelToZipkinSpanTransformerTest.java @@ -16,6 +16,9 @@ import static io.opentelemetry.exporter.zipkin.ZipkinTestUtil.spanBuilder; import static io.opentelemetry.exporter.zipkin.ZipkinTestUtil.zipkinSpan; import static io.opentelemetry.exporter.zipkin.ZipkinTestUtil.zipkinSpanBuilder; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_PORT; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_SOCK_PEER_ADDR; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.PEER_SERVICE; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -31,8 +34,11 @@ import java.net.InetAddress; import java.util.Arrays; import java.util.Collections; +import javax.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import zipkin2.Endpoint; import zipkin2.Span; @@ -135,11 +141,11 @@ void generateSpan_ResourceServiceNameMapping() { Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "super-zipkin-service")); SpanData data = spanBuilder().setResource(resource).build(); - Endpoint expectedEndpoint = + Endpoint expectedLocalEndpoint = Endpoint.newBuilder().serviceName("super-zipkin-service").ip(localIp).build(); Span expectedZipkinSpan = zipkinSpan(Span.Kind.SERVER, localIp).toBuilder() - .localEndpoint(expectedEndpoint) + .localEndpoint(expectedLocalEndpoint) .putTag(OtelToZipkinSpanTransformer.OTEL_STATUS_CODE, "OK") .build(); assertThat(transformer.generateSpan(data)).isEqualTo(expectedZipkinSpan); @@ -149,19 +155,204 @@ void generateSpan_ResourceServiceNameMapping() { void generateSpan_defaultResourceServiceName() { SpanData data = spanBuilder().setResource(Resource.empty()).build(); - Endpoint expectedEndpoint = + Endpoint expectedLocalEndpoint = Endpoint.newBuilder() .serviceName(Resource.getDefault().getAttribute(ResourceAttributes.SERVICE_NAME)) .ip(localIp) .build(); Span expectedZipkinSpan = zipkinSpan(Span.Kind.SERVER, localIp).toBuilder() - .localEndpoint(expectedEndpoint) + .localEndpoint(expectedLocalEndpoint) .putTag(OtelToZipkinSpanTransformer.OTEL_STATUS_CODE, "OK") .build(); assertThat(transformer.generateSpan(data)).isEqualTo(expectedZipkinSpan); } + @ParameterizedTest + @EnumSource( + value = SpanKind.class, + names = {"CLIENT", "PRODUCER"}) + void generateSpan_RemoteEndpointMapping(SpanKind spanKind) { + Attributes attributes = + Attributes.builder() + .put(PEER_SERVICE, "remote-test-service") + .put(NET_SOCK_PEER_ADDR, "8.8.8.8") + .put(NET_PEER_PORT, 42L) + .build(); + + SpanData spanData = + spanBuilder() + .setKind(spanKind) + .setResource(Resource.empty()) + .setAttributes(attributes) + .build(); + + Endpoint expectedLocalEndpoint = + Endpoint.newBuilder() + .serviceName(Resource.getDefault().getAttribute(ResourceAttributes.SERVICE_NAME)) + .ip(localIp) + .build(); + + Endpoint expectedRemoteEndpoint = + Endpoint.newBuilder().serviceName("remote-test-service").ip("8.8.8.8").port(42).build(); + + Span expectedSpan = + zipkinSpan(toZipkinSpanKind(spanKind), localIp).toBuilder() + .localEndpoint(expectedLocalEndpoint) + .remoteEndpoint(expectedRemoteEndpoint) + .putTag(PEER_SERVICE.getKey(), "remote-test-service") + .putTag(NET_SOCK_PEER_ADDR.getKey(), "8.8.8.8") + .putTag(NET_PEER_PORT.getKey(), "42") + .putTag(OtelToZipkinSpanTransformer.OTEL_STATUS_CODE, "OK") + .build(); + + assertThat(transformer.generateSpan(spanData)).isEqualTo(expectedSpan); + } + + @ParameterizedTest + @EnumSource( + value = SpanKind.class, + names = {"SERVER", "CONSUMER", "INTERNAL"}) + void generateSpan_RemoteEndpointMappingWhenKindIsNotClientOrProducer(SpanKind spanKind) { + Attributes attributes = + Attributes.builder() + .put(PEER_SERVICE, "remote-test-service") + .put(NET_SOCK_PEER_ADDR, "8.8.8.8") + .put(NET_PEER_PORT, 42L) + .build(); + + SpanData spanData = + spanBuilder() + .setKind(spanKind) + .setResource(Resource.empty()) + .setAttributes(attributes) + .build(); + + Endpoint expectedLocalEndpoint = + Endpoint.newBuilder() + .serviceName(Resource.getDefault().getAttribute(ResourceAttributes.SERVICE_NAME)) + .ip(localIp) + .build(); + + Span expectedSpan = + zipkinSpan(toZipkinSpanKind(spanKind), localIp).toBuilder() + .localEndpoint(expectedLocalEndpoint) + .remoteEndpoint(null) + .putTag(PEER_SERVICE.getKey(), "remote-test-service") + .putTag(NET_SOCK_PEER_ADDR.getKey(), "8.8.8.8") + .putTag(NET_PEER_PORT.getKey(), "42") + .putTag(OtelToZipkinSpanTransformer.OTEL_STATUS_CODE, "OK") + .build(); + + assertThat(transformer.generateSpan(spanData)).isEqualTo(expectedSpan); + } + + @ParameterizedTest + @EnumSource( + value = SpanKind.class, + names = {"CLIENT", "PRODUCER"}) + void generateSpan_RemoteEndpointMappingWhenServiceNameIsMissing(SpanKind spanKind) { + Attributes attributes = + Attributes.builder().put(NET_SOCK_PEER_ADDR, "8.8.8.8").put(NET_PEER_PORT, 42L).build(); + + SpanData spanData = + spanBuilder() + .setKind(spanKind) + .setResource(Resource.empty()) + .setAttributes(attributes) + .build(); + + Endpoint expectedLocalEndpoint = + Endpoint.newBuilder() + .serviceName(Resource.getDefault().getAttribute(ResourceAttributes.SERVICE_NAME)) + .ip(localIp) + .build(); + + Span expectedSpan = + zipkinSpan(toZipkinSpanKind(spanKind), localIp).toBuilder() + .localEndpoint(expectedLocalEndpoint) + .remoteEndpoint(null) + .putTag(NET_SOCK_PEER_ADDR.getKey(), "8.8.8.8") + .putTag(NET_PEER_PORT.getKey(), "42") + .putTag(OtelToZipkinSpanTransformer.OTEL_STATUS_CODE, "OK") + .build(); + + assertThat(transformer.generateSpan(spanData)).isEqualTo(expectedSpan); + } + + @ParameterizedTest + @EnumSource( + value = SpanKind.class, + names = {"CLIENT", "PRODUCER"}) + void generateSpan_RemoteEndpointMappingWhenPortIsMissing(SpanKind spanKind) { + Attributes attributes = + Attributes.builder() + .put(PEER_SERVICE, "remote-test-service") + .put(NET_SOCK_PEER_ADDR, "8.8.8.8") + .build(); + + SpanData spanData = + spanBuilder() + .setKind(spanKind) + .setResource(Resource.empty()) + .setAttributes(attributes) + .build(); + + Endpoint expectedLocalEndpoint = + Endpoint.newBuilder() + .serviceName(Resource.getDefault().getAttribute(ResourceAttributes.SERVICE_NAME)) + .ip(localIp) + .build(); + + Endpoint expectedRemoteEndpoint = + Endpoint.newBuilder().serviceName("remote-test-service").ip("8.8.8.8").build(); + + Span expectedSpan = + zipkinSpan(toZipkinSpanKind(spanKind), localIp).toBuilder() + .localEndpoint(expectedLocalEndpoint) + .remoteEndpoint(expectedRemoteEndpoint) + .putTag(PEER_SERVICE.getKey(), "remote-test-service") + .putTag(NET_SOCK_PEER_ADDR.getKey(), "8.8.8.8") + .putTag(OtelToZipkinSpanTransformer.OTEL_STATUS_CODE, "OK") + .build(); + + assertThat(transformer.generateSpan(spanData)).isEqualTo(expectedSpan); + } + + @ParameterizedTest + @EnumSource( + value = SpanKind.class, + names = {"CLIENT", "PRODUCER"}) + void generateSpan_RemoteEndpointMappingWhenIpAndPortAreMissing(SpanKind spanKind) { + Attributes attributes = Attributes.builder().put(PEER_SERVICE, "remote-test-service").build(); + + SpanData spanData = + spanBuilder() + .setKind(spanKind) + .setResource(Resource.empty()) + .setAttributes(attributes) + .build(); + + Endpoint expectedLocalEndpoint = + Endpoint.newBuilder() + .serviceName(Resource.getDefault().getAttribute(ResourceAttributes.SERVICE_NAME)) + .ip(localIp) + .build(); + + Endpoint expectedRemoteEndpoint = + Endpoint.newBuilder().serviceName("remote-test-service").build(); + + Span expectedSpan = + zipkinSpan(toZipkinSpanKind(spanKind), localIp).toBuilder() + .localEndpoint(expectedLocalEndpoint) + .remoteEndpoint(expectedRemoteEndpoint) + .putTag(PEER_SERVICE.getKey(), "remote-test-service") + .putTag(OtelToZipkinSpanTransformer.OTEL_STATUS_CODE, "OK") + .build(); + + assertThat(transformer.generateSpan(spanData)).isEqualTo(expectedSpan); + } + @Test void generateSpan_WithAttributes() { Attributes attributes = @@ -304,4 +495,9 @@ void generateSpan_WithRpcUnsetStatus() { .putTag(SemanticAttributes.RPC_SERVICE.getKey(), "my service name") .build()); } + + @Nullable + private static Span.Kind toZipkinSpanKind(SpanKind spanKind) { + return spanKind != SpanKind.INTERNAL ? Span.Kind.valueOf(spanKind.name()) : null; + } } diff --git a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinTestUtil.java b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinTestUtil.java index 1c96e034eeb..ef2f0aac6e3 100644 --- a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinTestUtil.java +++ b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinTestUtil.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import javax.annotation.Nullable; import zipkin2.Endpoint; import zipkin2.Span; @@ -60,11 +61,11 @@ static TestSpanData.Builder spanBuilder() { .setHasEnded(true); } - static Span zipkinSpan(Span.Kind kind, InetAddress localIp) { + static Span zipkinSpan(@Nullable Span.Kind kind, InetAddress localIp) { return zipkinSpanBuilder(kind, localIp).build(); } - static Span.Builder zipkinSpanBuilder(Span.Kind kind, InetAddress localIp) { + static Span.Builder zipkinSpanBuilder(@Nullable Span.Kind kind, InetAddress localIp) { return Span.newBuilder() .traceId(TRACE_ID) .parentId(PARENT_SPAN_ID)