Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment thread
MrAlias marked this conversation as resolved.
}

// If boolean with 'false' is present, should be removed.
if v, ok := m["error"]; ok && v == "false" {
delete(m, "error")
}
Comment thread
MrAlias marked this conversation as resolved.
Outdated
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
Comment thread
MrAlias marked this conversation as resolved.
Outdated
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 &&
Comment thread
MrAlias marked this conversation as resolved.
Outdated
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 {
Comment thread
MrAlias marked this conversation as resolved.
Outdated
endpointAttr = kv
} else {
Comment thread
MrAlias marked this conversation as resolved.
Outdated
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