Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zipkin Exporter: Adjust span transformation to comply with the spec #1688

Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- `SamplingResult.TraceState` is correctly propagated to a newly created
span's `SpanContext`. (#1655)
- Jaeger Exporter: Ensure mapping between OTEL and Jaeger span data complies with the specification. (#1626)
- Zipkin Exporter: Ensure mapping between OTEL and Zipkin span data complies with the specification. (#1688)
- The `otel-collector` example now correctly flushes metric events prior to shutting down the exporter. (#1678)
- Synchronization issues in global trace delegate implementation. (#1686)
- Do not set span status message in `SpanStatusFromHTTPStatusCode` if it can be inferred from `http.status_code`. (#1681)
Expand Down
117 changes: 110 additions & 7 deletions exporters/trace/zipkin/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,24 @@ import (
"encoding/binary"
"encoding/json"
"fmt"
"net"
"strconv"

zkmodel "github.com/openzipkin/zipkin-go/model"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
export "go.opentelemetry.io/otel/sdk/export/trace"
"go.opentelemetry.io/otel/semconv"
"go.opentelemetry.io/otel/trace"
)

const (
keyInstrumentationLibraryName = "otel.instrumentation_library.name"
keyInstrumentationLibraryVersion = "otel.instrumentation_library.version"
keyInstrumentationLibraryName = "otel.library.name"
keyInstrumentationLibraryVersion = "otel.library.version"

keyPeerHostname attribute.Key = "peer.hostname"
keyPeerAddress attribute.Key = "peer.address"
)

func toZipkinSpanModels(batch []*export.SpanSnapshot, serviceName string) []zkmodel.SpanModel {
Expand All @@ -50,7 +57,7 @@ func toZipkinSpanModel(data *export.SpanSnapshot, serviceName string) zkmodel.Sp
LocalEndpoint: &zkmodel.Endpoint{
ServiceName: serviceName,
},
RemoteEndpoint: nil, // *Endpoint
RemoteEndpoint: toZipkinRemoteEndpoint(data),
Annotations: toZipkinAnnotations(data.MessageEvents),
Tags: toZipkinTags(data),
}
Expand Down Expand Up @@ -140,27 +147,123 @@ func attributesToJSONMapString(attributes []attribute.KeyValue) string {
// extraZipkinTags are those that may be added to every outgoing span
var extraZipkinTags = []string{
"otel.status_code",
"otel.status_description",
keyInstrumentationLibraryName,
keyInstrumentationLibraryVersion,
}

func toZipkinTags(data *export.SpanSnapshot) map[string]string {
m := make(map[string]string, len(data.Attributes)+len(extraZipkinTags))
for _, kv := range data.Attributes {
m[(string)(kv.Key)] = kv.Value.Emit()
switch kv.Value.Type() {
// For array attributes, serialize as JSON list string.
case attribute.ARRAY:
json, _ := json.Marshal(kv.Value.AsArray())
m[(string)(kv.Key)] = (string)(json)
default:
m[(string)(kv.Key)] = kv.Value.Emit()
}
}

if data.StatusCode != codes.Unset {
m["otel.status_code"] = data.StatusCode.String()
}

if data.StatusCode == codes.Error {
m["error"] = data.StatusMessage
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
}

// If boolean with 'false' is present, should be removed.
if v, ok := m["error"]; ok && v == "false" {
delete(m, "error")
}
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
m["otel.status_code"] = data.StatusCode.String()
m["otel.status_description"] = data.StatusMessage

if il := data.InstrumentationLibrary; il.Name != "" {
m[keyInstrumentationLibraryName] = il.Name
if il.Version != "" {
m[keyInstrumentationLibraryVersion] = il.Version
}
}

if len(m) == 0 {
return nil
}

return m
}

// Rank determines selection order for remote endpoint. See the specification
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk_exporters/zipkin.md#remote-endpoint
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
var remoteEndpointKeyRank = map[attribute.Key]int{
semconv.PeerServiceKey: 0,
semconv.NetPeerNameKey: 1,
semconv.NetPeerIPKey: 2,
keyPeerHostname: 3,
keyPeerAddress: 4,
semconv.HTTPHostKey: 5,
semconv.DBNameKey: 6,
}

func toZipkinRemoteEndpoint(data *export.SpanSnapshot) *zkmodel.Endpoint {
// Should be set only for consumer or producer kind
if data.SpanKind != trace.SpanKindConsumer &&
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
data.SpanKind != trace.SpanKindProducer {
return nil
}

var endpointAttr attribute.KeyValue
for _, kv := range data.Attributes {
rank, ok := remoteEndpointKeyRank[kv.Key]
if !ok {
continue
}

currentKeyRank, ok := remoteEndpointKeyRank[endpointAttr.Key]
if !ok {
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
endpointAttr = kv
} else {
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
if rank < currentKeyRank {
endpointAttr = kv
}
}
}

if endpointAttr.Key == "" {
return nil
}

if endpointAttr.Key != semconv.NetPeerIPKey &&
endpointAttr.Value.Type() == attribute.STRING {
return &zkmodel.Endpoint{
ServiceName: endpointAttr.Value.AsString(),
}
}

return remoteEndpointPeerIPWithPort(endpointAttr.Value.AsString(), data.Attributes)
}

// Handles `net.peer.ip` remote endpoint separately (should include `net.peer.ip`
// as well, if available).
func remoteEndpointPeerIPWithPort(peerIP string, attrs []attribute.KeyValue) *zkmodel.Endpoint {
ip := net.ParseIP(peerIP)
if ip == nil {
return nil
}

endpoint := &zkmodel.Endpoint{}
// Determine if IPv4 or IPv6
if ip.To4() != nil {
endpoint.IPv4 = ip
} else {
endpoint.IPv6 = ip
}

for _, kv := range attrs {
if kv.Key == semconv.NetPeerPortKey {
port, _ := strconv.ParseUint(kv.Value.Emit(), 10, 16)
endpoint.Port = uint16(port)
return endpoint
}
}

return endpoint
}
Loading