2019-10-09 08:33:19 +02:00
|
|
|
// Copyright 2019, OpenTelemetry Authors
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
package stackdriver
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"math"
|
|
|
|
"strconv"
|
|
|
|
"time"
|
|
|
|
"unicode/utf8"
|
|
|
|
|
|
|
|
timestamppb "github.com/golang/protobuf/ptypes/timestamp"
|
|
|
|
wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
|
|
|
|
tracepb "google.golang.org/genproto/googleapis/devtools/cloudtrace/v2"
|
|
|
|
statuspb "google.golang.org/genproto/googleapis/rpc/status"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
|
|
|
2019-11-01 20:40:29 +02:00
|
|
|
"go.opentelemetry.io/otel/api/core"
|
|
|
|
opentelemetry "go.opentelemetry.io/otel/sdk"
|
2019-11-05 23:08:55 +02:00
|
|
|
export "go.opentelemetry.io/otel/sdk/export/trace"
|
2019-10-09 08:33:19 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
maxAnnotationEventsPerSpan = 32
|
|
|
|
// TODO(ymotongpoo): uncomment this after gRPC trace get supported.
|
|
|
|
// maxMessageEventsPerSpan = 128
|
|
|
|
maxAttributeStringValue = 256
|
|
|
|
agentLabel = "g.co/agent"
|
|
|
|
|
|
|
|
// Attributes recorded on the span for the requests.
|
|
|
|
// Only trace exporters will need them.
|
|
|
|
HostAttribute = "http.host"
|
|
|
|
MethodAttribute = "http.method"
|
|
|
|
PathAttribute = "http.path"
|
|
|
|
URLAttribute = "http.url"
|
|
|
|
UserAgentAttribute = "http.user_agent"
|
|
|
|
StatusCodeAttribute = "http.status_code"
|
|
|
|
|
|
|
|
labelHTTPHost = `/http/host`
|
|
|
|
labelHTTPMethod = `/http/method`
|
|
|
|
labelHTTPStatusCode = `/http/status_code`
|
|
|
|
labelHTTPPath = `/http/path`
|
|
|
|
labelHTTPUserAgent = `/http/user_agent`
|
|
|
|
|
|
|
|
version = "0.1.0"
|
|
|
|
)
|
|
|
|
|
|
|
|
var userAgent = fmt.Sprintf("opentelemetry-go %s; stackdriver-exporter %s", opentelemetry.Version(), version)
|
|
|
|
|
|
|
|
func protoFromSpanData(s *export.SpanData, projectID string) *tracepb.Span {
|
|
|
|
if s == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
traceIDString := s.SpanContext.TraceIDString()
|
|
|
|
spanIDString := s.SpanContext.SpanIDString()
|
|
|
|
|
|
|
|
name := s.Name
|
|
|
|
switch s.SpanKind {
|
|
|
|
// TODO(ymotongpoo): add cases for "Send" and "Recv".
|
|
|
|
default:
|
2019-10-24 01:25:14 +02:00
|
|
|
name = fmt.Sprintf("Span.%s-%s", s.SpanKind, name)
|
2019-10-09 08:33:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sp := &tracepb.Span{
|
|
|
|
Name: "projects/" + projectID + "/traces/" + traceIDString + "/spans/" + spanIDString,
|
|
|
|
SpanId: spanIDString,
|
|
|
|
DisplayName: trunc(name, 128),
|
|
|
|
StartTime: timestampProto(s.StartTime),
|
|
|
|
EndTime: timestampProto(s.EndTime),
|
|
|
|
SameProcessAsParentSpan: &wrapperspb.BoolValue{Value: !s.HasRemoteParent},
|
|
|
|
}
|
2019-10-28 19:05:06 +02:00
|
|
|
if s.ParentSpanID != s.SpanContext.SpanID && s.ParentSpanID.IsValid() {
|
2019-10-09 08:33:19 +02:00
|
|
|
sp.ParentSpanId = fmt.Sprintf("%.16x", s.ParentSpanID)
|
|
|
|
}
|
|
|
|
if s.Status != codes.OK {
|
|
|
|
sp.Status = &statuspb.Status{Code: int32(s.Status)}
|
|
|
|
}
|
|
|
|
|
|
|
|
copyAttributes(&sp.Attributes, s.Attributes)
|
|
|
|
// NOTE(ymotongpoo): omitting copyMonitoringReesourceAttributes()
|
|
|
|
|
|
|
|
var annotations, droppedAnnotationsCount int
|
|
|
|
es := s.MessageEvents
|
|
|
|
for i, e := range es {
|
|
|
|
if annotations >= maxAnnotationEventsPerSpan {
|
|
|
|
droppedAnnotationsCount = len(es) - i
|
|
|
|
break
|
|
|
|
}
|
|
|
|
annotation := &tracepb.Span_TimeEvent_Annotation{Description: trunc(e.Message, maxAttributeStringValue)}
|
|
|
|
copyAttributes(&annotation.Attributes, e.Attributes)
|
|
|
|
event := &tracepb.Span_TimeEvent{
|
|
|
|
Time: timestampProto(e.Time),
|
|
|
|
Value: &tracepb.Span_TimeEvent_Annotation_{Annotation: annotation},
|
|
|
|
}
|
|
|
|
annotations++
|
|
|
|
if sp.TimeEvents == nil {
|
|
|
|
sp.TimeEvents = &tracepb.Span_TimeEvents{}
|
|
|
|
}
|
|
|
|
sp.TimeEvents.TimeEvent = append(sp.TimeEvents.TimeEvent, event)
|
|
|
|
}
|
|
|
|
|
|
|
|
if sp.Attributes == nil {
|
|
|
|
sp.Attributes = &tracepb.Span_Attributes{
|
|
|
|
AttributeMap: make(map[string]*tracepb.AttributeValue),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only set the agent label if it is not already set. That enables the
|
|
|
|
// OpenTelemery service/collector to set the agent label based on the library that
|
|
|
|
// sent the span to the service.
|
|
|
|
if _, hasAgent := sp.Attributes.AttributeMap[agentLabel]; !hasAgent {
|
|
|
|
sp.Attributes.AttributeMap[agentLabel] = &tracepb.AttributeValue{
|
|
|
|
Value: &tracepb.AttributeValue_StringValue{
|
|
|
|
StringValue: trunc(userAgent, maxAttributeStringValue),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO(ymotongpoo): add implementations for Span_TimeEvent_MessageEvent_
|
|
|
|
// once OTel finish implementations for gRPC.
|
|
|
|
|
|
|
|
if droppedAnnotationsCount != 0 {
|
|
|
|
if sp.TimeEvents == nil {
|
|
|
|
sp.TimeEvents = &tracepb.Span_TimeEvents{}
|
|
|
|
}
|
|
|
|
sp.TimeEvents.DroppedAnnotationsCount = clip32(droppedAnnotationsCount)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO(ymotongpoo): add implementations for Links
|
|
|
|
|
|
|
|
return sp
|
|
|
|
}
|
|
|
|
|
|
|
|
// timestampProto creates a timestamp proto for a time.Time.
|
|
|
|
func timestampProto(t time.Time) *timestamppb.Timestamp {
|
|
|
|
return ×tamppb.Timestamp{
|
|
|
|
Seconds: t.Unix(),
|
|
|
|
Nanos: int32(t.Nanosecond()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// copyAttributes copies a map of attributes to a proto map field.
|
|
|
|
// It creates the map if it is nil.
|
|
|
|
func copyAttributes(out **tracepb.Span_Attributes, in []core.KeyValue) {
|
|
|
|
if len(in) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if *out == nil {
|
|
|
|
*out = &tracepb.Span_Attributes{}
|
|
|
|
}
|
|
|
|
if (*out).AttributeMap == nil {
|
|
|
|
(*out).AttributeMap = make(map[string]*tracepb.AttributeValue)
|
|
|
|
}
|
|
|
|
var dropped int32
|
|
|
|
for _, kv := range in {
|
|
|
|
av := attributeValue(kv)
|
|
|
|
if av == nil {
|
|
|
|
continue
|
|
|
|
}
|
2019-10-17 07:49:58 +02:00
|
|
|
switch kv.Key {
|
2019-10-09 08:33:19 +02:00
|
|
|
case PathAttribute:
|
|
|
|
(*out).AttributeMap[labelHTTPPath] = av
|
|
|
|
case HostAttribute:
|
|
|
|
(*out).AttributeMap[labelHTTPHost] = av
|
|
|
|
case MethodAttribute:
|
|
|
|
(*out).AttributeMap[labelHTTPMethod] = av
|
|
|
|
case UserAgentAttribute:
|
|
|
|
(*out).AttributeMap[labelHTTPUserAgent] = av
|
|
|
|
case StatusCodeAttribute:
|
|
|
|
(*out).AttributeMap[labelHTTPStatusCode] = av
|
|
|
|
default:
|
2019-10-17 07:49:58 +02:00
|
|
|
if len(kv.Key) > 128 {
|
2019-10-09 08:33:19 +02:00
|
|
|
dropped++
|
|
|
|
continue
|
|
|
|
}
|
2019-10-17 07:49:58 +02:00
|
|
|
(*out).AttributeMap[string(kv.Key)] = av
|
2019-10-09 08:33:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
(*out).DroppedAttributesCount = dropped
|
|
|
|
}
|
|
|
|
|
|
|
|
func attributeValue(kv core.KeyValue) *tracepb.AttributeValue {
|
|
|
|
value := kv.Value
|
2019-10-31 00:01:19 +02:00
|
|
|
switch value.Type() {
|
2019-10-09 08:33:19 +02:00
|
|
|
case core.BOOL:
|
|
|
|
return &tracepb.AttributeValue{
|
2019-10-31 00:01:19 +02:00
|
|
|
Value: &tracepb.AttributeValue_BoolValue{BoolValue: value.AsBool()},
|
2019-10-09 08:33:19 +02:00
|
|
|
}
|
|
|
|
case core.INT64:
|
|
|
|
return &tracepb.AttributeValue{
|
2019-10-31 00:01:19 +02:00
|
|
|
Value: &tracepb.AttributeValue_IntValue{IntValue: value.AsInt64()},
|
2019-10-09 08:33:19 +02:00
|
|
|
}
|
|
|
|
case core.FLOAT64:
|
|
|
|
// TODO: set double value if Stackdriver Trace support it in the future.
|
|
|
|
return &tracepb.AttributeValue{
|
|
|
|
Value: &tracepb.AttributeValue_StringValue{
|
2019-10-31 00:01:19 +02:00
|
|
|
StringValue: trunc(strconv.FormatFloat(value.AsFloat64(), 'f', -1, 64),
|
2019-10-09 08:33:19 +02:00
|
|
|
maxAttributeStringValue)},
|
|
|
|
}
|
|
|
|
case core.STRING:
|
|
|
|
return &tracepb.AttributeValue{
|
2019-10-31 00:01:19 +02:00
|
|
|
Value: &tracepb.AttributeValue_StringValue{StringValue: trunc(value.AsString(), maxAttributeStringValue)},
|
2019-10-09 08:33:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// trunc returns a TruncatableString truncated to the given limit.
|
|
|
|
func trunc(s string, limit int) *tracepb.TruncatableString {
|
|
|
|
if len(s) > limit {
|
|
|
|
b := []byte(s[:limit])
|
|
|
|
for {
|
|
|
|
r, size := utf8.DecodeLastRune(b)
|
|
|
|
if r == utf8.RuneError && size == 1 {
|
|
|
|
b = b[:len(b)-1]
|
|
|
|
} else {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return &tracepb.TruncatableString{
|
|
|
|
Value: string(b),
|
|
|
|
TruncatedByteCount: clip32(len(s) - len(b)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return &tracepb.TruncatableString{
|
|
|
|
Value: s,
|
|
|
|
TruncatedByteCount: 0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// clip32 clips an int to the range of an int32.
|
|
|
|
func clip32(x int) int32 {
|
|
|
|
if x < math.MinInt32 {
|
|
|
|
return math.MinInt32
|
|
|
|
}
|
|
|
|
if x > math.MaxInt32 {
|
|
|
|
return math.MaxInt32
|
|
|
|
}
|
|
|
|
return int32(x)
|
|
|
|
}
|